mobx-state-tree
mobx-state-tree copied to clipboard
How I can use React Hook & Context API with MobX-State-Tree
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
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>
);
}
Dear terrysahaidak Thanks for your reply. this is a good example and this will guide me in my projects Thanks again Best Regards
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);
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 You should use @observer
function to make your components react to store changes, more here https://mobx.js.org/refguide/observer-component.html
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
Hi @dayemsiddiqui, you have to use observer
from mobx-react version 6, or use useObserver
from mobx-react-lite.
@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 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 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 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, @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 could you please provide minimum reproducing codesandbox so I can debug it?
@terrysahaidak yes, let me try to mock up a minimal example.
@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);
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()
@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
@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.
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.
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:
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 .
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
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 .
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 (
// ...
)
});
@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.
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
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 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
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 .
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.