home
  • Blog
7.1
  • Introduction
  • Getting Started
  • Tutorial
  • Core Concepts
  • Components
  • Routing
  • Services
  • EmberData
  • In-Depth Topics
    • Autotracking In-Depth
    • Reactivity
      • Thinking in Reactivity
      • Root State
      • Derived State
      • Inputs and Outputs
    • Patterns for Components
    • Patterns for Actions
    • Making API Requests
    • Native Classes In-Depth
    • Rendering Values
  • Application Development
  • Application Concerns
  • Accessibility
  • Configuration
  • Testing
  • Addons and Dependencies
  • Using TypeScript
  • Developer Tools
  • Build Tooling
  • Ember Inspector
  • Code Editors
  • Additional Resources
  • Upgrading
  • Contributing to Ember.js
  • Glossary

Inputs and Outputs


The reactive graph - root state at the bottom, derived state above it - is deliberately self-contained: values in, values out, no side effects. But applications exist to affect the world outside that graph: paint pixels, play audio, talk to servers, listen to sockets. This guide is about the edges of the graph - how data gets in, how it gets out, and why Ember draws those edges where it does.

The shape to keep in mind:

  • Inputs write to root state: event handlers, response callbacks, subscription messages, timers.
  • Outputs read the graph and act on the world. In Ember, the output is the renderer; other side effects are managed effects, covered below.

Everything in between stays pure.

We'll start with outputs - that's where Ember differs most from other reactive systems - and work back around to inputs.

Rendering Is the Effect

In some reactive systems, you wire outputs yourself with an effect primitive: a function that re-runs whenever the reactive values it read change. If you ask "where is Ember's effect primitive?", the first answer is that you've been using it all along - it's the renderer.

A template is a declaration of effects: every {{expression}} and every attribute binding is a tiny "when this value changes, update that DOM" rule. The renderer is the exit point of the graph, the thing that actively pulls on your derivations and pushes the results into the world. You write the pure part; the framework owns the part that touches the world - batching, scheduling before paint, and updating only the DOM whose inputs actually changed.

Why There Is No Effect Primitive

The second answer is that the omission is deliberate, and the reasons are instructive:

  • Effects are eager. An effect must re-run on every change to its inputs, whether or not anyone needs the result, breaking the lazy, pull-based economics that make the rest of the system cheap.
  • Effect ordering is undefined. When one change triggers several effects, the execution order is unspecified. Correctness that depends on effect order is a latent bug.
  • Effects that write state are a trap. The most common effect mistake is using one to sync state: "when X changes, update Y." Now Y is stale until the effect runs, can disagree with X, and if the effect's write triggers another effect, you have a cascade or an infinite loop. The answer is to derive instead of syncing: in Ember, the getter was the answer all along, and the backtracking assertion makes write-during-derivation a loud error rather than a quiet bug.
  • Almost every "effect" is something more specific. Look closely at real effect code and you find derived values (should be getters), DOM manipulation (should be scoped to an element), and lifecycle-bound processes like subscriptions (should be tied to an owner's lifetime, with cleanup). Ember provides each of those as a dedicated, managed construct instead of one general escape hatch.

Managed Effects: Attached to a Lifetime

Rendering is the only true output in Ember - but it isn't the only side effect. When you do need to act on the world yourself, Ember's tools all share one design: the effect is attached to something with a lifetime, runs with cleanup, and re-runs through the same autotracking as everything else.

Modifiers are effects scoped to a DOM element. They run when the element is rendered, re-run (after cleanup) when tracked state they consumed changes, and clean up when the element goes away:

app/modifiers/draw-chart.js
import { modifier } from 'ember-modifier';
import Chart from 'chart.js/auto';

export default modifier((element, [data]) => {
  let chart = new Chart(element, { type: 'bar', data });

  return () => chart.destroy();
});
import drawChart from 'my-app/modifiers/draw-chart';

<template>
  <canvas {{drawChart @chartData}}></canvas>
</template>

This is "re-run when @chartData changes" - an effect - but bounded: it cannot run before there's an element, cannot leak after the element is gone, and declares in the template exactly where its impact lands. See Template Lifecycle, DOM, and Modifiers.

Destroyables cover lifecycle-bound work with no element. Anything with an owner - components, services, helpers - can pair setup with guaranteed teardown via registerDestructor from @ember/destroyable:

app/services/clock.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';

export default class ClockService extends Service {
  @tracked now = new Date();

  constructor(...args) {
    super(...args);

    let timer = setInterval(() => {
      this.now = new Date();
    }, 1000);

    registerDestructor(this, () => clearInterval(timer));
  }
}

Any getter, anywhere in the app, can now derive from clock.now - "seconds remaining," "is the store open," a formatted timestamp - and every one of them updates each second, while the actual side effect (one interval, one cleanup) stays in one place.

This setup-plus-cleanup-plus-reactive-state package is commonly called a resource. In the Ember ecosystem, the ember-resources library offers resources as values you can use right in components and templates; a built-in equivalent is an active area of design. A service with a destructor, as above, is the no-dependencies version of the pattern.

Inputs: Writing into the Graph from Outside

The inverse direction needs no special machinery at all. Code running outside the graph - event handlers, socket callbacks, timers, promise resolutions - simply writes to root state, and the graph takes it from there:

this.socket.addEventListener('message', (event) => {
  this.lastMessage = JSON.parse(event.data); // a tracked property
});

Writes from the outside are always safe. The backtracking assertion only restricts writes during a reactive computation (inside getters and templates). An event callback runs outside any computation, so it can write as much as it likes, and all the writes coalesce into a single rerender.

The clock service above is the full input pattern in miniature: an external process (the interval) feeds the graph through one tracked write, and cleanup is bound to a lifetime. Subscriptions, ResizeObservers, BroadcastChannels - they all take the same shape. Subscribe with cleanup, write root state on each notification, and derive everything else.

Async: Tracking Stops at await

Tracking contexts are synchronous. The system records reads that happen while a template expression or cached getter is computing - and a computation, in JavaScript, ends at the first await. Code after an await (or inside setTimeout, or a .then() callback) runs later, outside the computation that started it, so nothing it reads is consumed:

// 🛑 The renderer cannot see through this
get userName() {
  return fetch(`/users/${this.userId}`).then((r) => r.json()); // a Promise, not a name
}

Derivations must be synchronous. The reactive way to handle async work follows from the input rule above: the request is a side effect, and its progress is root state. Run the effect at a lifetime boundary, and write each phase of it into tracked properties:

app/components/profile.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

class RequestState {
  @tracked status = 'pending';
  @tracked value = null;
  @tracked error = null;

  get isPending() {
    return this.status === 'pending';
  }

  constructor(promise) {
    promise.then(
      (value) => {
        this.value = value;
        this.status = 'resolved';
      },
      (error) => {
        this.error = error;
        this.status = 'rejected';
      }
    );
  }
}

export default class Profile extends Component {
  user = new RequestState(
    fetch(`/users/${this.args.userId}`).then((response) => response.json())
  );

  <template>
    {{#if this.user.isPending}}
      Loading…
    {{else if this.user.error}}
      Something went wrong.
    {{else}}
      Hello, {{this.user.value.name}}!
    {{/if}}
  </template>
}

Once async state is data, it stops being a special case: "show a spinner while pending" is just another derivation, the same {{#if}} as anything else. This "reactive promise" shape - status, value, and error as reactive fields - is available ready-made from libraries like ember-resources and WarpDrive's request state - or in a dozen lines of your own, as above.

Note one limitation of the example as written: the request is created in a field initializer, so it captures userId once and won't re-fetch if the argument changes - the construction-time snapshot trap described in Deferring Consumption. Re-running an effect when its reactive inputs change is exactly the job of the managed constructs from earlier - a modifier (if there's a sensible element) or a resource. When an effect needs to respond to the graph, give it a lifetime the framework manages; when the world needs to update the graph, write root state.

Choosing the Right Edge

| You want to… | Reach for | | --------------------------------------------------------- | ---------------------------------------------------- | | Update the page when state changes | A template expression - that's the renderer's job | | Compute a value from other values | A getter or function | | Manipulate a DOM element when state changes | A modifier | | Start a process and clean it up with its owner | registerDestructor (or a resource) | | Feed external events into the app | Write tracked state from the callback | | Track an async operation | Store its status/value/error as root state | | Run arbitrary code "whenever X changes" | Reconsider - it's almost always one of the above |

left arrow
Derived State
We've finished covering Reactivity. Next up: In-Depth Topics - Patterns for Components
right arrow
On this page

  • Rendering Is the Effect
  • Why There Is No Effect Primitive
  • Managed Effects: Attached to a Lifetime
  • Inputs: Writing into the Graph from Outside
  • Async: Tracking Stops at await
  • Choosing the Right Edge
Team Sponsors Security Legal Branding Community Guidelines
Twitter GitHub Discord Mastodon

If you want help you can contact us by email, open an issue, or get realtime help by joining the Ember Discord.

© Copyright 2026 - Tilde Inc.
Ember.js is free, open source and always will be.


Ember is generously supported by
blue Created with Sketch.