javascriptreactjsreact-nativemobxmobx-react

MobX - Why should I use `observer` when I could use `inject` when injecting data into a React component


MobX documentation suggests I should use observer on all my components. However by using inject I get more fine grained control over what data causes a re-rendering of my components.

My understanding is I that with observer, a change in all accessed observables in the last render will cause a re-render, even if the observable is nested deep in the data store, while inject only re-renders when observables accessed in the injector function change.

For example:

class Store{
  @observable data = {
    nestedData: {
      deepData: 'my_data'
    }
  }
}

const store = new Store();

... Assume the store is injected using <Provider /> component

// This will cause re-render when the data object changes
// for example: store.data = { new_data: 'new_data' }
@inject(stores => {
  return { data: stores.dataStore.data }; 
})
class MyComponent extends Component {  }

// This will re-render on change of the data object, but also
// on change of nestedData and deepData properties
@inject(stores => {
  return { data: stores.dataStore.data }; 
})
@observer
class MyComponent extends Component {  }

Could someone confirm my understanding of this?

In my opinion, it's better to use only inject as it gives you more control, and can prevent unnecessary re-renders. If the data is deeply nested you could create a computed property that gets and prepares the data from the deep structure and then inject that property in the component.

Are there other benefits/drawbacks when using one over the other


Solution

  • I believe you are correct in your assessment. Let me try to rephrase for clarity:

    @observer tracks which observables are used by render and automatically re-renders the component when one of these values changes.

    We should note that @observable values used by render might be deeply nested within a given prop, per your example:

    class Store{
      @observable data = {
        nestedData: {
          // changes to `deepData` would theoretically re-render any observer
          deepData: 'my_data' 
        }
      }
    }
    

    with observer, a change in all accessed observables in the last render will cause a re-render, even if the observable is nested deep in the data store

    Bingo!

    Although there's a quirk with observable, as you'll see in a moment...


    On the other hand you have @inject which makes available to a component (via props) specific data structures defined by a Provider.

    For example:

    @inject('title')
    class MyComponent extends React.Component {
        render() {
            return (<div>{this.props.title}</div>);
        }
    }
    
    const Container = () => (
        <Provider title="This value is passed as a prop using `inject`">
            <MyComponent />
        </Provider>
    );
    

    inject only re-renders when observables accessed in the injector function change.

    Bingo!

    inject will only spawn a re-render if the prop itself has recognized changes.


    This is effectively the same issue with shouldComponentUpdate() and a deep-comparison of props -- though observer seems to be far more efficient than shouldComponentUpdate.

    In my opinion, it's better to use only inject as it gives you more control, and can prevent unnecessary re-renders.

    I wouldn't necessarily go that far... it all depends on how you have your code structured.

    If I modify your original example as so:

    class Store{
        @observable data = {
            nestedData: {}
        };
    
        constructor() {
            this.data.nestedData.deepData = 'my_data';
        }
    }
    

    ...the addition of deepData won't actually get picked up as an observable change (i.e. re-render) because that property didn't exist when we originally tagged data as an observable value. So that's one problem.

    A different approach could be to do something like this:

    class Person {
        @observable name = 'John Doe';
    }
    
    class Store{
        @observable data = null;
    
        constructor() {
            this.data = new Person();
        }
    }
    

    This allows you to spread the observable values out across classes -- so you might still want to inject Store into a component (to access Store.data but ultimately any observable changes come from updating Person.