using mobx with react functional components and without decorators

23,462

In React, functional components are not persistent. They run from top to bottom, return some JSX, rinse and repeat.

let i = 0;

const FunctionalComp = (props) => {
  const foo = props.foo.toUpperCase();
  return <span>Rendered {i++} times. {foo}</span>;
}

All this functional component will ever do is synchronously create the value foo and then return the span. When this component's parent re-renders, this component will do the same exact same, but with potentially new values.

It can never do anything else, and that is why it is powerful. That is why we can call it a functional component: Because it only depends on the values provided to it, because it does not cause side effects that would alter the direction of the rest of the application, and because given the same arguments, this function will produce the same result for the rest of eternity.

Predictable = powerful.

Now, a class component holds persistent state. It constructs, initializes its state, methods, and properties, and then renders and returns JSX. The class (object) still exists in memory, so all of the values and methods on it exist too.

The methods of class component are not so predictable.

class Foo {
  name = 'tommy';

  getUpperName() {
    return this.name.toUpperCase();
  }

  setName(name) {
    this.name = name;
  }
}

Foo.getUpperName will not produce the same result every time it is ever used with the same arguments (hint: it doesn't accept any arguments and depends on the context around it to determine its result), so other pieces of the application may change Foo.name and, essentially, control Foo.getUpperName's outcome, potentially by accident.

The class can update its own state, causing itself and all children components to re-compute their JSX returns.

In a plain arrow function, after it returns, all that exists is the return value that it produces and the function statement (declaration). Nothing in between.

All this said, the functional component has no this bound to it. (That is a no-no in functional programming.) It will never have state.

So you can not do anything with this inside of a functional component and it can not hold observable values because every time it re-renders it would re-instantiate each of those values.

In your example above, even if this did refer to Test, this is what would happen:

  1. Test would create the observable value button as false.
  2. Test would render the button, which you would then click.
  3. Test would create the observable value button as false.
  4. Test would render the button, which you would then click.
  5. Test would create the observable value button as false.
  6. Test would render the button, which you would then click.

So on and so forth.

In MobX, your observables need to live on a persistent data structure and be passed into your render functions that return UI markup.

const store = observable({
  name: 'tommy'
});

const changeName = () => store.name = store.name.split('').reverse().join('');

const Foo = observer((props) => {
  return (
    <button onClick={changeName}>{store.name}'s button</button>
  )
});

This is not a great pattern, as neither Foo nor changeName are pure, this code would work, at least.

You need to do something like so:

const store = () => {
  const self = {};

  self.actions = {
    setName: action((name) =>  self.name = name);
  }

  return extendObservable(self, { name: 'tommy' });
}

const App = (props) => {
    return <span><Foo store={store} /></span>
}

const Foo = observer((props) => {
  return (
    <button onClick={props.store.actions.setName}>
      {store.name}'s button
    </button>
  )
})

Again, this is not an ideal implementation, but it would work, and I am at work and have to get back to what they pay me to do. ;)

Share:
23,462
Marcus
Author by

Marcus

Well, what's there to say? I like to code, so I do actually like my job. I wish I would have started sooner, so I would already be better at it. Anyway, I try to improve my coding skills constantly and I very much value code that is readable and maintainable.

Updated on February 17, 2020

Comments

  • Marcus
    Marcus about 4 years

    I'm trying to get MobX to work with functional components in react. I want to do this without having to use decorators. I have set up an app with create-react-app, added MobX and MobX-react as dependencies. However, I can't seem to get observables working within functional components.

    import React from 'react';
    import { extendObservable } from 'mobx';
    import { observer } from 'mobx-react';
    
    const Test = () => {
        extendObservable(this, {
            button: false
        });
    
        const handleB1 = () => {
            this.button = false;
        }
    
        const handleB2 = () => {
            this.button = true;
        }
    
        const getButton2 = () => {
            console.log('button2');
            return (
                <button type="button" onClick={handleB2}>Button 2</button>
            );
        };
    
        const getButton1 = () => {
            console.log('button1');
            return (
                <button type="button" onClick={handleB1}>Button 1</button>
            );
        };
    
        return (
            <div>
                {this.button ? getButton1() : getButton2()}
            </div>
        )
    };
    
    export default observer(Test);
    

    Clicking the button I would expect the component to get rerendered due to the observable being changed, but I get an error:

    ×
    Error: [mobx] Invariant failed: Side effects like changing state are not 
    allowed at this point. Are you trying to modify state from, for example, the render 
    function of a React component? Tried to modify: [email protected]
    

    I have tried declaring the observable as part of a functional component or before like this:

    const buttonState = () => {
        extendObservable(this, {
            button: false
        });
    }
    

    but in both cases I could not get the component to rerender or i was not sure if the observable was actually correctly set.

    If i write the whole thing as a class like this it works perfectly

    import React from 'react';
    import { extendObservable } from 'mobx';
    import { observer } from 'mobx-react';
    
    class Test extends React.Component {
        constructor(props) {
            super();
            extendObservable(this, {
                button: false
            });
        }
    
        handleB1 = () => {
            this.button = false;
        }
    
        handleB2 = () => {
            this.button = true;
        }
    
        getButton2 = () => {
            console.log('button2');
            return (
                <button type="button" onClick={this.handleB2}>Button 2</button>
            );
        };
    
        getButton1 = () => {
            console.log('button1');
            return (
                <button type="button" onClick={this.handleB1}>Button 1</button>
            );
        };
    
        render = () => {
            return  (
            <div>
                {this.button ? this.getButton1() : this.getButton2()}
            </div>
            )
        }
    };
    
    export default observer(Test);
    
  • Pavel Poberezhnyi
    Pavel Poberezhnyi almost 4 years
    your point about FP for class components is wrong because useState acts the same way as Observable.
  • OfirD
    OfirD about 3 years
    this is not an ideal implementation - any suggestion (link, maybe) for a good one?