mobx-state-tree icon indicating copy to clipboard operation
mobx-state-tree copied to clipboard

How I can use React Hook & Context API with MobX-State-Tree

Open osmannassar2019 opened this issue 4 years ago • 32 comments

How I can use React Hook & Context API with MobX-State-Tree

I am using React Functional Component. So, I need to use React Hook (useContext()) to get all action from the store.

Please help me

Thanks

osmannassar2019 avatar Aug 10 '19 05:08 osmannassar2019

Here is a basic example of useMst hook I've built:

import React, { useContext, forwardRef } from 'react';

const MSTContext = React.createContext(null);

// eslint-disable-next-line prefer-destructuring
export const Provider = MSTContext.Provider;

export function useMst(mapStateToProps) {
  const store = useContext(MSTContext);

  if (typeof mapStateToProps !== 'undefined') {
    return mapStateToProps(store);
  }

  return store;
}

const RootStore = types.model({
  count: 0,
}).actions(self => ({
  inc() {
    self.count += 1;
  }
}));

const rootStore = RootStore.create({});

// in root component

return (
  <Provider value={rootStore}>
    <Counter />
  </Provider>
);

// somewhere in the app

function Counter() {
  const { count, inc } = useMst(store => ({
    count: store.count,
    inc: store.inc,
  }));

  return (
    <div>
      value: {count}
      <button onClick={inc}>Inc</button>
    </div>
  );
}

terrysahaidak avatar Aug 12 '19 05:08 terrysahaidak

Dear terrysahaidak Thanks for your reply. this is a good example and this will guide me in my projects Thanks again Best Regards

osmannassar2019 avatar Aug 12 '19 20:08 osmannassar2019

thx @terrysahaidak , I guess this should be added to docs. For Typescript , MSTContext and Provider should be declared after rootStore, and : const MSTContext = React.createContext(rootStore);

yonatanmn avatar Sep 30 '19 23:09 yonatanmn

Hi I have followed the exact same steps and my store is working i.e data is being updated in the store however it is not getting reflected in my react component. For some strange reason changes to the store is not triggering rerender of my component

dayemsiddiqui avatar Oct 23 '19 23:10 dayemsiddiqui

@dayemsiddiqui You should use @observer function to make your components react to store changes, more here https://mobx.js.org/refguide/observer-component.html

Romanior avatar Oct 24 '19 08:10 Romanior

Hey guys, I am using Typescript + React + Hooks with mobx-state-tree. I am also exclusively using functional components in my code and I don't have any class-based component. I tried a lot of things to get my component to listen to state changes but no avail. The observer does not seem to be working with functional components but I figured out a solution to fix this problem that I wanted to share:

Basically I have written a custom hook called useObserveStore that listens to store changes using the getSnapshot() method and updates the state accordingly. We can simply use this hook in any component that we want to rerender based on store changes. Here is the code:

//  useObserverTaskStore.ts
import { useState } from 'react';
import { onSnapshot } from 'mobx-state-tree';
import { taskStore } from '../models/stores';
import { TaskStoreSnapshot } from '../interfaces/TaskStoreModel.interface';

const useObserveTaskStore = (initialValue?: TaskStoreSnapshot) => {
  const [snapshot, setSnapshot] = useState<TaskStoreSnapshot>(
    initialValue || {
      waiting: [],
      inprogress: [],
      inreview: [],
      done: []
    }
  );
  onSnapshot(taskStore, newSnapshopt => {
    setSnapshot(newSnapshopt);
  });
  return {
    snapshot
  };
};

export default useObserveTaskStore;

Then in my component I simply use:

// containers/Board.tsx
const Board: React.FC = () => {
  useFetchTasks();
  const { snapshot } = useObserveTaskStore();
  return (
    <Container fluid>
      <Row>
        <Col className="task-list-container" sm="3">
          <TaskList
            title="Waiting"
            tasks={snapshot.waiting}
            onPinTask={() => {}}
            onArchiveTask={() => {}}
          ></TaskList>
        </Col>
        <Col className="task-list-container" sm="3">
          <TaskList
            title="In Progress"
            tasks={snapshot.inprogress}
            onPinTask={() => {}}
            onArchiveTask={() => {}}
          ></TaskList>
        </Col>

        <Col className="task-list-container" sm="3">
          <TaskList
            title="In Review"
            tasks={snapshot.inreview}
            onPinTask={() => {}}
            onArchiveTask={() => {}}
          ></TaskList>
        </Col>

        <Col className="task-list-container" sm="3">
          <TaskList
            title="Done"
            tasks={snapshot.done}
            onPinTask={() => {}}
            onArchiveTask={() => {}}
          ></TaskList>
        </Col>
      </Row>
    </Container>
  );
};

I hope people might find it useful. Do let me know if there are any potential problems in my code

dayemsiddiqui avatar Oct 24 '19 18:10 dayemsiddiqui

Hi @dayemsiddiqui, you have to use observer from mobx-react version 6, or use useObserver from mobx-react-lite.

terrysahaidak avatar Oct 24 '19 19:10 terrysahaidak

@dayemsiddiqui thanks for providing that workaround! @terrysahaidak @Romanior Could either of you provide an example of how to use observer with a functional component properly? The docs for observer only show examples for class components. I have seen the comment to 'use observer' several times, but I have never seen an example with functional components with a more complex functional component using hooks and context and I can not figure it out either. I have been in a similar situation as @dayemsiddiqui and can not figure out how to make the store observable so that components rerender properly as the store updates itself.

My specific use case is having a rootStore that uses a useContext.Provider at the top level App. I would like a functional component to ask the rootStore for a piece of data. If the rootStore has it, then it delivers that data, which renders, otherwise it performs an async (flow) api call to get the data. I would like when that data comes back, the store updates itself. When the store updates itself it should cause a rerender of the component that initially asked for the data. I feel like this should be a very common pattern. But I can not figure out how to make it work.

So something like

rootStore.js


export const RootStore = types
    .model({
        dataMap: types.map(MyData),
    })
    .actions(self => ({
        loadData(date) {
            self.dataMap[date] = MyData.create({date: date, state: "init"});
            self.dataMap[date].loadDay(date)
        },
    }))
    .views(self => ({
        getData(date) {
            if (!(date in self.dataMap)) {
                self.loadData(date)
            }
            return self.dataMap[date]
        }

    }));

const MyData = types
    .model({
        date: types.optional(types.string, ""),
        // the data here is irrelevant
        data: types.optional(types.integer, 0),
        state: types.enumeration("State", ["init", "pending", "done", "error"])
    })
    .actions(self => ({
        // noticed that we cannot load data in afterCreate as it does not change snapshot
        //afterCreate(){
            //self.loadDay(self.date)
        //},
        loadDay: flow(function* loadDay(curDate) {
            self.date = curDate;
            self.state = "pending";
            try {
                // ... yield can be used in async/await style
                const ret = yield callApi(...)  // any async function here returning the data
                self.data = ret
                self.state = "done"
            } catch (error) {
                // ... including try/catch error handling
                console.error("Failed to fetch composition date", error)
                self.state = "error"
            }
        })
    }));

app.js

const GlobalStoreContext = React.createContext(null);


function App() {
    return (
        <GlobalStoreContext.Provider value={globalStore}>
            <MyGreatApp/>
        </GlobalStoreContext.Provider>
    );
}


export default App;

someComponent.js here is where I would like to access the rootStore and rerender as needed based on any change in the wanted data. The display here is a bit contrived. Where does the observer go in SomeComponent.js or how when pulling in the store via a useContext to we make that piece of the store observable?

function SomeComponent(props) {

    const rootstore = useContext(GlobalStoreContext)
    const wanteddata = rootstore.getData('2019-10-24')

    return(
        <div>
            {wanteddata.state}
        </div>
    )
}

ssolari avatar Oct 24 '19 19:10 ssolari

@ssolari just wrap it with observer:

const SomeComponent = observer((props) => {
 const rootstore = useContext(GlobalStoreContext)
    const wanteddata = rootstore.getData('2019-10-24')

    return(
        <div>
            {wanteddata.state}
        </div>
    )
});

You can check out a working example here: https://codesandbox.io/s/classic-mobx-i9q9c

terrysahaidak avatar Oct 24 '19 19:10 terrysahaidak

@terrysahaidak the above code example is with a mobx store not with mobx-state-tree store. I checked the codesandbox link. It seems to be working with mobx-store but I can't get it to work with mobx-state-tree store like @ssolari has explained

dayemsiddiqui avatar Oct 24 '19 20:10 dayemsiddiqui

@dayemsiddiqui There is no difference between mobx store and mobx-state-tree store since mobx-state-tree is an abstraction over mobx. observer tracks changes in observables you're using inside your component. Each model in mobx-state-tree is observable. You can simply change my mobx model to be mst-model and it will be still working as expected.

terrysahaidak avatar Oct 24 '19 20:10 terrysahaidak

@terrysahaidak, @dayemsiddiqui is correct. Also, the point of the example is when a mobx-state-tree store updates itself in the background. When a button is added to the component I think the functional component is rerendered anyway. I can make this work because somehow the button click forces the rerender. But wrapping the component in observer as you stated does not work.

ssolari avatar Oct 24 '19 20:10 ssolari

@ssolari could you please provide minimum reproducing codesandbox so I can debug it?

terrysahaidak avatar Oct 24 '19 20:10 terrysahaidak

@terrysahaidak yes, let me try to mock up a minimal example.

ssolari avatar Oct 24 '19 20:10 ssolari

@terrysahaidak thank you for your patience and leading me to figure the problem out. As you stated, observer just works!!! It just needs to be wrapping the right component. My issue was that I was wrapping a parent component with observer but needed to wrap the sub component in observer. Hopefully this fully working example helps someone else.

In summary (referring to the below example), the problem was that I wrapped MyGreatApp with observer rather than wrapping SubComponent with observer. As soon as I wrapped SubComponent with observer things started working.

For a reference, could you maybe explain why this is the case?

all three files in same directory. index.js

import React from 'react';
import ReactDOM from 'react-dom';

import {MyGreatApp} from "./myGreatApp"
import {globalStore, GlobalStoreContext} from "./rootStore";

function App() {
    return (
        <GlobalStoreContext.Provider value={globalStore}>
            <MyGreatApp/>
        </GlobalStoreContext.Provider>
    );
}

ReactDOM.render(<App />, document.getElementById("root"));

myGreatApp.js

import React, {useContext} from 'react';
import {observer} from "mobx-react-lite";
import {GlobalStoreContext} from "./rootStore";

const SubComponent = observer((props) => {

    return (
        <div><h2>State to update: {props.dt.state}</h2></div>
    )
});


export function MyGreatApp (props) {

    const gstore = useContext(GlobalStoreContext);
    const dt = gstore.getData('2019-10-24');

    return (
        <SubComponent dt={dt}></SubComponent>
    )
};

rootStore.js

import React from "react";
import {types, flow} from "mobx-state-tree";

const sleep = (milliseconds) => {
    return new Promise(resolve => setTimeout(resolve, milliseconds))
};


const MyData = types
    .model({
        date: types.optional(types.string, ""),
        // the data here is irrelevant
        data: types.optional(types.integer, 0),
        state: types.enumeration("State", ["init", "pending", "done", "error"])
    })
    .actions(self => ({
        // noticed that we cannot load data in afterCreate as it does not change snapshot
        //afterCreate(){
        //self.loadDay(self.date)
        //},
        loadDay: flow(function* loadDay(curDate) {
            self.date = curDate;
            self.state = "pending";
            try {
                // mocking an async call that has a 2 second delay.  The state should change after that delay.
                yield sleep(2000)
                self.state = "done"
            } catch (error) {
                console.error("Failed to fetch composition date", error)
                self.state = "error"
            }
        })
    }));



const RootStore = types
    .model({
        dataMap: types.map(MyData),
    })
    .actions(self => ({
        loadData(date) {
            self.dataMap[date] = MyData.create({date: date, state: "init"});
            self.dataMap[date].loadDay(date)
        },
    }))
    .views(self => ({
        getData(date) {
            if (!(date in self.dataMap)) {
                self.loadData(date)
            }
            return self.dataMap[date]
        }

    }));

export let globalStore = RootStore.create({dataMap: {}});
export const GlobalStoreContext = React.createContext(null);

ssolari avatar Oct 24 '19 21:10 ssolari

I dunno if this is just me but I wrap pretty much every single component (most of ours are functional, but that may not be relevant) in observer()

cmdcolin avatar Oct 24 '19 22:10 cmdcolin

@ssolari Damn!! Thanks, man it now works for me as well. Thank you so much for figuring this out and posting the sample code. @terrysahaidak @Romanior it would be really cool if it's possible to add some example code for functional components with hooks in the documentation so other people can easily reference it. May be @ssolari can create a PR for it as he has already figured out the problem

dayemsiddiqui avatar Oct 24 '19 23:10 dayemsiddiqui

@cmdcolin It depends on you project structure. It would be awesome to have some kind of white-paper to illustrate performance implications of wrapping "pretty much every single component" versus careful way where props selected and only reactive components wrapped.

Romanior avatar Oct 25 '19 09:10 Romanior

I don't have time to do a PR, but if it would help someone else, I'm happy if any of the code above ends up in examples in docs (no credit needed). It seems that the future of React is functional components with hooks and there are not quite as many examples to go off of yet.

ssolari avatar Oct 28 '19 18:10 ssolari

Decided to give mst a go today and experimented before finding this thread.

I've noticed that my experiment worked without using a Context Provider. Is this even required since we are using observer or will I run into any issues by doing this?

I've linked a codesandbox for feedback below:

Edit mst-hooks-typescript

ecklf avatar Oct 30 '19 10:10 ecklf

For observer to work, it doesn't matter how the component got a hold on the store, props, state, context, from some global closure, singleton. It doesn't matter at all. Context is just one way (and a very elegant one which I recommend), to give the component that pointer to the store. In other words, context is the dependency injection mechanism, observer + observable is the change tracking mechanism.

On Wed, Oct 30, 2019 at 10:57 AM impulse [email protected] wrote:

Decided to give mst a go today and experimented before finding this thread.

I've noticed that my experiment worked without using a Context Provider. Is this even required since we are using observer or will I run into any issues by doing this?

I've linked a codesandbox for feedback below:

[image: Edit mst-hooks-typescript] https://codesandbox.io/s/mst-hooks-typescript-w6vlb?fontsize=14

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-state-tree/issues/1363?email_source=notifications&email_token=AAN4NBDZCS3AVATZJUX4CFLQRFSBJA5CNFSM4IKYT4BKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECTXJ2Q#issuecomment-547845354, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBGATDXMN6JFEJVXKNDQRFSBJANCNFSM4IKYT4BA .

mweststrate avatar Oct 30 '19 11:10 mweststrate

Thanks for the explanation @mweststrate.

I've created a template project with React Hooks + MST + TypeScript:

Demo: https://react-hooks-mobx-state-tree.vercel.app GitHub: https://github.com/impulse/react-hooks-mobx-state-tree

ecklf avatar Oct 31 '19 16:10 ecklf

Looking good! I'll link it in the next version of the docs

On Thu, Oct 31, 2019 at 4:28 PM impulse [email protected] wrote:

Thanks for the explanation @mweststrate https://github.com/mweststrate.

I've created a template project with React Hooks + MST + TypeScript:

Demo: https://react-hooks-mobx-state-tree.netlify.com/ GitHub: https://github.com/impulse/react-hooks-mobx-state-tree

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-state-tree/issues/1363?email_source=notifications&email_token=AAN4NBGAAP6UGWINAC6R5XTQRMBUDA5CNFSM4IKYT4BKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECYNAJY#issuecomment-548458535, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBD2YFH3J2VQ63OBIHDQRMBUDANCNFSM4IKYT4BA .

mweststrate avatar Nov 07 '19 21:11 mweststrate

And about useEffect I'm having problems with observer and useEffect used together.

Uncaught (in promise) TypeError: Object(...) is not a function

Quite simple component

const RestaurantWaitersList = observer(({ restaurant }) => {
    useEffect(() => {
        restaurant.fetchWaiters(); // Promise never called
    });

    return (
        // ...
    )
});

image

Ridermansb avatar Nov 09 '19 12:11 Ridermansb

@Ridermansb please open a new issue, and create a minimal reproduction. But superficially, this looks totally unrelated to MST / mobx / React. Probably best checks the props you are passing in.

mweststrate avatar Nov 09 '19 12:11 mweststrate

For anyone dropping in on this, thought I'd mention a npm package I made to help with using mobx-state-tree along with React Hooks.

Figured others might be able to use it, or that it might help you with figuring out how to handle this in your own apps.

https://www.npmjs.com/package/mobx-store-provider

jonbnewman avatar Jan 25 '20 23:01 jonbnewman

What is the benefit of using React Context with MST? How is this better than just importing the Stores you need?

@mweststrate @impulse @terrysahaidak

cgradwohl avatar Mar 11 '20 15:03 cgradwohl

@cgradwohl I know you didn't tag me... but to put my $0.02 in... one of the big benefits is it lets you write more testable components.

Basically because you can 'inject' a mocked model in place of your real one (via the Provider). If you just import an instance you created in another module then you can't mock that with fake/false data during a test.

That said, I think there are other issues with direct context use, and I outlined them here: http://mobx-store-provider.overfoc.us/motivation.html#cant-i-just-use-react-context

jonbnewman avatar Mar 11 '20 16:03 jonbnewman

Yeah the most important argument is testing I'd say. With direct imports you will end up with singletons that need resetting. With context every rendered react tree can be given it's own instance of the store

On Wed, Mar 11, 2020 at 4:43 PM Jonathan Newman [email protected] wrote:

@cgradwohl https://github.com/cgradwohl I know you didn't tag me... but to put my $0.02 in... one of the big benefits is it lets you write more testable components.

Basically because you can 'inject' a mocked model in place of your real one (via the Provider). If you just import an instance your created in another module then you can't mock that with fake/false data.

That said, I think there are other issues with direct context use, and I outlined them here: http://mobx-store-provider.overfoc.us/motivation.html#cant-i-just-use-react-context

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mobxjs/mobx-state-tree/issues/1363#issuecomment-597741343, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4NBG5HAXUUX4ZDEIJQNTRG65RDANCNFSM4IKYT4BA .

mweststrate avatar Mar 11 '20 17:03 mweststrate

Context can also be very useful for scenarios where you want to provide different instances of the same model type to different parts of your component tree.

My use case is that I have an AnimationStore model where I store the running state and other info about animations in my app. Several places where I want to deal with animations as a group I create a new instance of AnimationStore and provide it via context to just that subtree. My animation hooks don't need to care which tree/store they're under, they just do useContext(AnimationStoreContext) to get the nearest one. Now they can share information and act as a nice "scene" together giving me a really clean way to deal with otherwise difficult and messy react-native animations.

MST + context works wonderfully in this case! I can't think of any other way to do it that would be as simple, elegant, and performant.

evelant avatar Jun 17 '20 23:06 evelant