5

After switching from Redux to MobX for React I'm starting to extremely like MobX. It's pretty awesome.

MobX has a certain behavior where it won't update component if the provided store observable is not used in render. I think generally that's a pretty great behavior that makes components render only when something actually changed.

But... I do encountered couple of cases where I do not want or need to use MobX observable inside render method and in such cases my this.props.store with MobX store won't get updated.

And in such cases, as a workaround, I just reference the observable in the render method, but I don't think that's a very clean approach, and I'm wondering if there's a cleaner way to do that?

This component code should explain even more what I'm exactly asking about. It's a component that changes body overflow style based on a MobX observable that I have in my store.

/*------------------------------------*\
  Imports
\*------------------------------------*/
import React from 'react';
import connectStore from 'script/connect-store';
import addClass from 'dom-helpers/class/addClass';
import removeClass from 'dom-helpers/class/removeClass';


/*------------------------------------*\
  Component
\*------------------------------------*/
class Component extends React.Component {

  constructor(props) {
    super(props);
    this.constructor.displayName = 'BodyClassSync';
  }

  componentDidMount() {
    this.checkAndUpdateMuiClass();
  }

  componentDidUpdate() {
    this.checkAndUpdateMuiClass();
  }

  checkAndUpdateMuiClass() {

    // This place is the only place I need latest MobX store... 
    if (this.props.store.muiOverlay) {
      addClass(window.document.body, 'mod-mui-overlay');
    }

    else {
      removeClass(window.document.body, 'mod-mui-overlay');
    }

  }


  render() {

    // This is my workaround so the componentDidUpdate will actually fire
    // (with latest and updated store)
    // when the 'muiOverlay' observable changes in the MobX store
    // Is there any cleaner/better way to do this?
    this.props.store.muiOverlay;

    // This component doesn't and shouldn't render anything
    return null;

  }



}



/*------------------------------------*\
  Export
\*------------------------------------*/
const ComponentWithStore = connectStore(Component);
export default ComponentWithStore;

(Note: I don't use @decorators, I connect store using ES5 syntax in the connectStore function. And it would be awesome if solution to this would be also in ES5.)

Dominik Serafin
  • 3,616
  • 3
  • 17
  • 27

2 Answers2

3

You could use an autorun in componentDidMount and dispose the listener in componentWillUnmount so that you don't have to reference it in the render method.

class Component extends React.Component {
  constructor(props) {
    super(props);
    this.constructor.displayName = 'BodyClassSync';
  }

  componentDidMount() {
    this.dispose = autorun(() => {
      const { muiOverlay } = this.props.store;

      if (muiOverlay) {
        addClass(window.document.body, 'mod-mui-overlay');
      } else {
        removeClass(window.document.body, 'mod-mui-overlay');
      }
    });
  }

  componentWillUnmount() {
    this.dispose();
  }

  render() {
    return null;
  }
}

Since you are not really doing anything in this component, you could put this autorun directly in your store as well.

Tholle
  • 108,070
  • 19
  • 198
  • 189
  • Interesting, thanks! I've upvoted this answer and will probably use this method for some of my other components where I use greater amount of MobX observables. But... I'm still wondering if there's any other way to do this? This still seems a little hackish because from my understanding of autorun it will update on every store change - not only in this case `muiOverlay` observable. – Dominik Serafin Jul 11 '18 at 11:43
  • 1
    `autorun` will only run again when the observables it referenced last time are changed, i.e. *only* when `muiOverlay` changes. – Tholle Jul 11 '18 at 11:44
  • So if I in this case reference only `muiOverlay` inside the autorun function, then that function will get executed **only** if the `muiOverlay` changes? And if other observables of `this.props.store` change, then the autorun function won't execute at all? – Dominik Serafin Jul 11 '18 at 11:49
  • 1
    @DominikSerafin That's right. [Look at this for a basic example](https://codesandbox.io/s/jjwo7w3w25). – Tholle Jul 11 '18 at 11:54
  • 1
    Thanks for explanation and the example, I appreciate that – Dominik Serafin Jul 11 '18 at 12:17
  • Thanks for the example! Do you think it will harm the performance if I'm going to use `autorun` in multiple components? Is there a better solution? – Liam Apr 04 '21 at 09:19
2

Not sure how I missed it, but thanks to @Tholle I've reread the "Reacting to observables" section in the MobX docs and found out the reaction helper was the most suitable solution for me.

While, the reaction was the most fitting for the exact problem I had. The autorun and when helpers are pretty similar in the function and I probably could use them for my use case too.

https://mobx.js.org/refguide/autorun.html

https://mobx.js.org/refguide/when.html

https://mobx.js.org/refguide/reaction.html


This is the code example that shows how to use reaction for anyone that just wants quick copy+paste:

componentDidMount(){
  this._notificationsReactionDispose = mobx.reaction(
    () => this.props.store.notifications,
    (notifications, reaction) => {
      ... the code that runs when "notifications" in my store change
    }
  );
}

componentWillUnmount() {
  this._notificationsReactionDispose();
}
Dominik Serafin
  • 3,616
  • 3
  • 17
  • 27