Reactions & Derivations

Computed values are values that react automatically to state changes. Reactions are side effects that react automatically to state changes. Reactions can be used to ensure that a certain side effect (mainly I/O) is automatically executed when relevant state changes, like logging, network requests, etc. The most commonly used reaction is the observer decorator for React components.

observer

  • observer(React.createClass({ ... }))
  • observer((props, context) => ReactElement)
  • observer(class MyComponent extends React.Component { ... })
  • @observer class MyComponent extends React.Component { ... })

Can be used as a higher order component around a React component. The component will then automatically re-render if any of the observables used in the render function of the component has changed.

Note:

observer is provided by the "mobx-react" package and not by "mobx" itself.

autorun

  • autorun(debugname?, () => { sideEffect })

Autorun runs the provided sideEffect and tracks which observable state is accessed while running the side effect. Whenever one of the used observables is changed in the future, the same sideEffect will be run again. Returns a disposer function to cancel the side effect.

autorunAsync

  • autorunAsync(debugname?: string, action: () => void, minimumDelay?: number, scope?)
  • autorunAsync(action: () => void, minimumDelay?: number, scope?)

Just like autorun except that the action won't be invoked synchronously, but asynchronously after the minimum amount of milliseconds has passed. The action will be run and observed. However, instead of running the action immediately when the values it observes have changed, the minimumDelay will be awaited before re-executing the action again.

If observed values are changed multiple times while waiting, the action is only triggered once, so in a sense it achieves a similar effect as a transaction. This might be useful for stuff that is expensive and doesn't need to happen synchronously; such as debouncing server communication.

autorunAsync returns a disposer to cancel the autorun.

autorunAsync(() => {
    // Assuming that profile.asJson returns an observable Json representation of profile,
    // send it to the server each time it is changed, but await at least 300 milliseconds before sending it.
    // When sent, the latest value of profile.asJson will be used.
    sendProfileToServer(profile.asJson);
}, 300);

reaction

  • reaction(debugname?, () => data, data => { sideEffect }, fireImmediately = false, delay = 0)

A variation on autorun that gives more fine-grained control on which observables that will be tracked. It takes two functions, the first one is tracked and returns data that is used as input for the second one, the side effect. Unlike autorun the side effect won't be run initially, and any observables that are accessed while executing the side effect will not be tracked. The side effect can be debounced, just like autorunAsync.

when

  • when(debugname?, predicate: () => boolean, () => void, scope?)

The condition expression will react automatically to any observables it uses. As soon as the expression returns true, the sideEffect function will be invoked, but only once. when returns a disposer to prematurely cancel the whole thing.

when observes and runs the given predicate until it returns true. Once that happens, the given effect is executed and the autorunner is disposed. The function returns a disposer to cancel the autorunner prematurely.

This function is really useful to dispose or cancel stuff in a reactive way.

class MyResource {
    constructor() {
        when(
            // once...
            () => !this.isVisible,
            // ... then
            () => this.dispose()
        );
    }

    @computed get isVisible() {
        // indicate whether this item is visible
    }

    dispose() {
        // dispose
    }
}

expr

  • expr(() => someExpression)

Just a shorthand for computed(() => someExpression).get(). expr is useful in some rare cases to optimize another computed function or reaction. In general, it is simpler and better to just split the function in multiple smaller computed's to achieve the same effect.

expr can be used to create temporarily computed values inside computed values. Nesting computed values is useful to create cheap computations to prevent expensive computations to run.

In the following example, the expression prevents the TodoView component from being re-rendered if the selection changes elsewhere. Instead, the component will only re-render when the relevant todo is (de)selected.

const TodoView = observer(({todo, editorState}) => {
    const isSelected = mobx.expr(() => editorState.selection === todo);
    return <div className={isSelected ? "todo todo-selected" : "todo"}>{todo.title}</div>;
});

onReactionError

  • extras.onReactionError(handler: (error: any, derivation) => void)

This method attaches a global error listener, which is invoked for every error that is thrown from a reaction. This can be used for monitoring or test purposes.


@observer

The observer function / decorator can be used to turn ReactJS components into reactive components. It wraps the component's render function in mobx.autorun to make sure that any data that is used during the rendering of a component forces a re-rendering upon change. It is available through the separate mobx-react package.

import {observer} from "mobx-react";

var timerData = observable({
    secondsPassed: 0
});

setInterval(() => {
    timerData.secondsPassed++;
}, 1000);

@observer class Timer extends React.Component {
    render() {
        return (<span>Seconds passed: { this.props.timerData.secondsPassed } </span> )
    }
};

React.render(<Timer timerData={timerData} />, document.body);

Tip:

When observer needs to be combined with other decorator or higher-order-components, make sure that observer is the innermost (first applied) decorator; otherwise, it might do nothing at all.

Note:

Using @observer as decorator is optional. observer(class Timer ... { }) achieves exactly the same.

Gotcha: Deference Values Inside Your Components

MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object). So it's not the values that are observable, but the properties of an object. This means that @observer actually reacts to the fact that you dereference a value. So in our above example, the Timer component would not react if it was initialized as follows:

React.render(<Timer timerData={timerData.secondsPassed} />, document.body)

In this snippet, just the current value of secondsPassed is passed to the Timer, which is the immutable value 0 (all primitives are immutable in JS). That number won't change anymore in the future, so Timer will never update. It is the property secondsPassed that will change in the future, so we need to access it in the component. Or, in other words: values need to be passed by reference and not by value.

Stateless Function Components

The above timer widget could also be written using stateless function components that are passed through observer:

import {observer} from "mobx-react";

const Timer = observer(({ timerData }) =>
    <span>Seconds passed: { timerData.secondsPassed } </span>
);

Observable Local Component State

Just like normal classes, you can introduce observable properties on a component by using the @observable decorator. This means that you can have local state in components that doesn't need to be managed by React's verbose and imperative setState mechanism, but is as powerful. The reactive state will be picked up by render but will not explicitly invoke other React lifecycle methods except componentWillUpdate and componentDidUpdate. If you need other React lifecycle methods, just use the normal React state based APIs.

The example above could also have been written as:

import {observer} from "mobx-react"
import {observable} from "mobx"

@observer class Timer extends React.Component {
    @observable secondsPassed = 0

    componentWillMount() {
        setInterval(() => {
            this.secondsPassed++
        }, 1000)
    }

    render() {
        return (<span>Seconds passed: { this.secondsPassed } </span> )
    }
})

React.render(<Timer />, document.body)

For more advantages of using observable local component state, see 3 Reasons Why I Stopped Using setState.

Connect Components to Provided Stores using inject

The mobx-react package also provides the Provider component that can be used to pass down stores using React's context mechanism. To connect to those stores, pass a list of store names to inject, which will make the stores available as props.

Example:

const colors = observable({
   foreground: '#000',
   background: '#fff'
});

const App = () =>
  <Provider colors={colors}>
     <app stuff... />
  </Provider>;

const Button = inject("colors")(observer(({ colors, label, onClick }) =>
  <button style={{
      color: colors.foreground,
      backgroundColor: colors.background
    }}
    onClick={onClick}
  >{label}<button>
));

// later..
colors.foreground = 'blue';
// all buttons updated

When to Apply observer?

The simple rule of thumb is: all components that render observable data. If you don't want to mark a component as observer, for example to reduce the dependencies of a generic component package, make sure you only pass it plain data.

With @observer there is no need to distinguish 'smart' components from 'dumb' components for the purpose of rendering. It is still a good separation of concerns for where to handle events, make requests, etc. All components become responsible for updating when their own dependencies change. Its overhead is neglectable and it makes sure that whenever you start using observable data the component will respond to it.

observer and PureRenderMixin

observer also prevents re-renderings when the props of the component have only shallowly changed, which makes a lot of sense if the data passed into the component is reactive. This behavior is similar to React's PureRenderMixin, except that state changes are still always processed. If a component provides its own shouldComponentUpdate, that one takes precedence.

componentWillReact (Lifecycle Hook)

React components usually render on a fresh stack, so that makes it often hard to figure out what caused a component to re-render. When using mobx-react, you can define a new life cycle hook, componentWillReact (pun intended) that will be triggered when a component will be scheduled to re-render because data it observes has changed. This makes it easy to trace renders back to the action that caused the rendering.

import {observer} from "mobx-react";

@observer class TodoView extends React.Component {
    componentWillReact() {
        console.log("I will re-render, since the todo has changed!");
    }

    render() {
        return <div>this.props.todo.title</div>;
    }
}
  • componentWillReact doesn't take arguments
  • componentWillReact won't fine before the initial render (use componentWillMount instead)
  • componentWillReact for mobx-react@4+, the hook will fire when receiving new props and after setState calls.

MobX-React-DevTools

In combination with @observer you can use the mobx-react-devtools. It shows exactly when your components are re-rendered and you can inspect the data dependencies of your components.

Characteristics of Observer Components

  • Observers only subscribe to the data structures that were actively used during the last render. This means that you cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at a later moment in time. This is ideal for asynchronously loading data.
  • You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and tracked in a very fine-grained manner.
  • Usually reactive components have no or little state, as it is often more convenient to encapsulate (view) state in objects that are shared with other components. But, you are still free to use state.
  • @observer implements shouldComponentUpdate in the same way as PureRenderMixin so that children are not re-rendered unnecessarily.
  • Reactive components sideways load data; parent components won't re-render unnecessarily even when child components will.
  • @observer does not depend on React's context system.
  • In mobx-react@4+, the props object and the state object of an observer component are automatically made observable to make it easier to create @computed properties that derive from props inside such a component. If you have a reaction (i.e. autorun) inside your @observer component that must not be re-evaluated when the specific props it uses don't change, be sure to de-reference those specific props for use inside your reaction (i.e. const myProp = props.myProp). Otherwise, if you reference props.myProp inside the reaction, then a change in any of the props will cause the reaction to be re-evaluated.

Autorun

mobx.autorun can be used in those cases where you want to create a reactive function that will never have observers itself. This is usually the case when you need a bridge from reactive to imperative code, for example logging, persistence, or UI-updating code. When autorun is used, the provided function will always be triggered once immediately and then again each time one of its dependencies changes. In contrast, computed(function) creates functions that only re-evaluate if it has observers on its own, otherwise its value is considered to be irrelevant. As a rule of thumb: use autorun if you have a function that should run automatically but that doesn't result in a new value. Use computed for everything else. Autoruns are about initiating effects, not about producing new values. If a string is passed as first argument to autorun, it will be used as debug name.

The function passed to autorun will receive one argument when invoked, the current reaction (autorun), which can be used to dispose the autorun during execution.

Just like the @observer decorator/function, autorun will only observe data that is used during execution of the provided function.

var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a, b) => a + b, 0));

var disposer = autorun(() => console.log(sum.get()));
// prints '6'
numbers.push(4);
// prints '10'

disposer();
numbers.push(5);
// won't print anything, nor is `sum` re-evaluated

Error Handling

Exceptions thrown in autorun and all other types reactions are caught and logged to the console, but not propagated back to the original causing code. This is to make sure that a reaction in one exception does not prevent the scheduled execution of other, possibly unrelated, reactions. This also allows reactions to recover from exceptions; throwing an exception does not break the tracking done by MobX, so a subsequent run of a reaction might complete normally again if the cause for the exception is removed.

It is possible to override the default logging behavior of Reactions by calling the onError handler on the disposer of the reaction.

const age = observable(10)
const dispose = autorun(() => {
    if (age.get() < 0)
        throw new Error("Age should not be negative")
    console.log("Age", age.get())
})

age.set(18)  // Logs: Age 18
age.set(-10) // Logs "Error in reaction .... Age should not be negative
age.set(5)   // Recovered, logs Age 5

dispose.onError(e => {
    window.alert("Please enter a valid age")
})

age.set(-5)  // Shows alert box

A global onError handler can be set as well through extras.onReactionError(handler). This can be useful in tests or for monitoring.


Reaction

A variation on autorun that gives more fine-grained control on which observable will be tracked. It takes two functions, the first one (the data function) is tracked and returns data that is used as input for the second one, the effect function. Unlike autorun, the side effect won't be run directly when created, but only after the data expression returns a new value for the first time. Any observables that are accessed while executing the side effect will not be tracked.

The side effect can be debounced, just like autorunAsync. reaction returns a disposer function. The functions passed to reaction will receive one argument when invoked, the current reaction, which can be used to dispose during execution.

It is important to notice that the side effect will only react to data that was accessed in the data expression, which might be less than the data returned by the expression has changed. In order words: reaction requires you to produce the things you need in your side effect.

Options

Reaction accepts as third argument an options object with the following options:

  • context: The this to be used in the functions passed to reaction. By default, undefined (use arrow functions instead!)
  • fireImmediately: Boolean that indicates that the effect function should immediately be triggered after the first run of the data function. false by default. If a boolean is passed as third argument to reaction, it will be interpreted as the fireImmediately option.
  • delay: Number in milliseconds that can be used to debounce the effect function. If zero (the default), no debouncing will happen.
  • compareStructural: false by default. If true, the return value of the data function is structurally compared to its previous return value, and the effect function will only be invoked if there is a structural change in the output. This can also be specified by setting equals to comparer.structural.
  • equals: comparer.default by default. If specified, the comparer function will be used to compare the previous and next values produced by the data function. The effect function will only be invoked if this function returns true. If specified, this will override compareStructural.
  • name: String that is used as name for this reaction in, for example spy events.

Example

In the following example, both reaction1, reaction2, and autorun1 will react to the addition, removal or replacement of todo's in the todos array. But only reaction2 and autorun will react to the change of a title in one of the todo items, because title is used in the data expression of reaction2, while it isn't in the data expression of reaction1. autorun tracks the complete side effect, hence it will always trigger correctly, but is also more susceptible to accidentally accessing unrelevant data.

const todos = observable([
    {
        title: "Make coffee",
        done: true,
    },
    {
        title: "Find biscuit",
        done: false
    }
]);

// wrong use of reaction: reacts to length changes, but not to title changes!
const reaction1 = reaction(
    () => todos.length,
    length => console.log("reaction 1:", todos.map(todo => todo.title).join(", "))
);

// correct use of reaction: reacts to length and title changes
const reaction2 = reaction(
    () => todos.map(todo => todo.title),
    titles => console.log("reaction 2:", titles.join(", "))
);

// autorun reacts to just everything that is used in its function
const autorun1 = autorun(
    () => console.log("autorun 1:", todos.map(todo => todo.title).join(", "))
);

todos.push({ title: "explain reactions", done: false });
// prints:
// reaction 1: Make coffee, find biscuit, explain reactions
// reaction 2: Make coffee, find biscuit, explain reactions
// autorun 1: Make coffee, find biscuit, explain reactions

todos[0].title = "Make tea"
// prints:
// reaction 2: Make tea, find biscuit, explain reactions
// autorun 1: Make tea, find biscuit, explain reactions

Reaction is roughly speaking sugar for:

  • computed(expression).observe(action(sideEffect))
  • autorun(() => action(sideEffect)(expression)

results matching ""

    No results matching ""