React Context

If you are using inject in your code, please refer to the migration guide first, or learn why inject is considered obsolete.

React Context replaces the Legacy context which was fairly awkward to use.

Let's make a small, but not-so-contrived example to show what this means for MobX.

Create store

Let's declare a simple store. No need to worry about observables at this point, it's just a plain object.

  1. export type TFriend = {
  2. name: string
  3. isFavorite: boolean
  4. isSingle: boolean
  5. }
  6. export function createStore() {
  7. // note the use of this which refers to observable instance of the store
  8. return {
  9. friends: [] as TFriend[],
  10. makeFriend(name, isFavorite = false, isSingle = false) {
  11. const oldFriend = this.friends.find(friend => friend.name === name)
  12. if (oldFriend) {
  13. oldFriend.isFavorite = isFavorite
  14. oldFriend.isSingle = isSingle
  15. } else {
  16. this.friends.push({ name, isFavorite, isSingle })
  17. }
  18. },
  19. get singleFriends() {
  20. return this.friends.filter(friend => friend.isSingle)
  21. },
  22. }
  23. }
  24. export type TStore = ReturnType<typeof createStore>

For TypeScript user it's important to note that this will work correctly only when noImplicitThis or strict option is enabled in tsconfig.json.

Setup context

Nothing spectacular about it really, better to read React docs if you are unsure though.

  1. import React from 'react'
  2. import { createStore, TStore } from './createStore'
  3. import { useLocalStore } from 'mobx-react' // 6.x or mobx-react-lite@1.4.0
  4. const storeContext = React.createContext<TStore | null>(null)
  5. export const StoreProvider = ({ children }) => {
  6. const store = useLocalStore(createStore)
  7. return <storeContext.Provider value={store}>{children}</storeContext.Provider>
  8. }
  9. export const useStore = () => {
  10. const store = React.useContext(storeContext)
  11. if (!store) {
  12. // this is especially useful in TypeScript so you don't need to be checking for null all the time
  13. throw new Error('useStore must be used within a StoreProvider.')
  14. }
  15. return store
  16. }

You could drop the whole Provider dance and set created store as a default value of the createContext. The reference of the store object does not need to change, so it will work in most cases. However, you might still setup a Provider for tests to battle flakiness.

Making friends

Now somewhere in the tree we have a component like this.

  1. import React from 'react'
  2. import { useStore } from '../../../store'
  3. export const FriendsMaker = observer(() => {
  4. const store = useStore()
  5. const onSubmit = ({ name, favorite, single }) =>
  6. store.makeFriend(name, favorite, single)
  7. return (
  8. <form onSubmit={onSubmit}>
  9. Total friends: {store.friends.length}
  10. <input type="text" id="name" />
  11. <input type="checkbox" id="favorite" />
  12. <input type="checkbox" id="single" />
  13. </form>
  14. )
  15. })

Explicit implementation of form logic would take up too much space and is not important for the show case.

Listing friends

In some other part of the app we want to show friends that are single and favorite.

  1. import React from 'react'
  2. import { useStore } from '../../../../store'
  3. export const MatchMaker = observer(() => {
  4. const store = useStore()
  5. // for a sake of example filtering is done here
  6. // you might as well expose it on the store directly
  7. const singleAndFavoriteFriends = store.singleFriends.filter(
  8. friend => friend.isFavorite,
  9. )
  10. return <div>{singleAndFavoriteFriends.map(renderFriend)}</div>
  11. })

Complex stores

The example above is still very contrived. Usually the app state is much more robust, but it does not differ that much in its essence. You can have a single Root store and attach every other store onto it. Or have a multiple contexts, each for own segment of the app.

Perhaps you want to consider mobx-state-tree for declaring store shape? It comes with other powerful features like snapshots and type safety out of box. Be sure to check it out.

The power of React hooks allows you to create specific hooks for abstracting how is the store structured, eg. useFriendsList or useOrderCart.

Such hooks are also great for mitigating long paths in a bigger folder structure as seen in the examples above.

Multiple global stores

In your application, you might need to have multiple global stores in order to better separate your different concerns.Example stores could be, CurrentUserStore, ShoppingCartStore, UserThemeStore, etc. With the use of React Context and Hooks,we can make this pretty simple and scalable.

In this section, we'll make a custom hook called useStores that we can use to destructure the store or stores that we need within our given application components.

Our example application will have two stores, CounterStore and ThemeStore.

  1. // src/stores/counter-store.tsx
  2. import { observable, action, computed } from 'mobx'
  3. export class CounterStore {
  4. @observable
  5. count = 0
  6. @action
  7. increment() {
  8. this.count++
  9. }
  10. @action
  11. decrement() {
  12. this.count--
  13. }
  14. @computed
  15. get doubleCount() {
  16. return this.count * 2
  17. }
  18. }
  19. // src/stores/theme-store.tsx
  20. import { observable, action } from 'mobx'
  21. export class ThemeStore {
  22. @observable
  23. theme = 'light'
  24. @action
  25. setTheme(newTheme: string) {
  26. this.theme = newTheme
  27. }
  28. }

It's important to note that we are only exporting the store classes themselves here, and not instances of them. It is considered a bad practice to keep stores globally as it will cause issues when managing unit and integrations tests in a non-trivial application.

Next we want to make a storesContext that will contain each of our stores.

  1. // src/contexts/index.tsx
  2. import React from 'react'
  3. import { CounterStore, ThemeStore } from '../stores'
  4. export const storesContext = React.createContext({
  5. counterStore: new CounterStore(),
  6. themeStore: new ThemeStore(),
  7. })

Here we are simply instantiating the classes directly, but another viable pattern is to create factory functions for each store, like createCounterStore() which hides the fact the store is a class and makes it easier to implement React.useState(createCounterStore) if such a need arises. Either approach allows for overriding your stores within tests.

In a complex app where stores might share dependencies, you might have to defer the instantiation of a store after some initialization is complete, in this case, the Provider is necessary.

Finally, let's make our custom useStores hook to access the exported storesContext value.

  1. // src/hooks/use-stores.tsx
  2. import React from 'react'
  3. import { storesContext } from '../contexts'
  4. export const useStores = () => React.useContext(storesContext)

Now we're ready to start consuming and using our stores.

  1. import React from 'react'
  2. import { observer } from 'mobx-react'
  3. import { useStores } from '../hooks/use-stores'
  4. // src/components/Counter.tsx
  5. export const Counter = observer(() => {
  6. const { counterStore } = useStores()
  7. return (
  8. <>
  9. <div>{counterStore.count}</div>
  10. <button onClick={() => counterStore.increment()}>++</button>
  11. <button onClick={() => counterStore.decrement()}>--</button>
  12. </>
  13. )
  14. })
  15. // src/components/ThemeToggler.tsx
  16. export const ThemeToggler = observer(() => {
  17. const { themeStore } = useStores()
  18. return (
  19. <>
  20. <div>{themeStore.theme}</div>
  21. <button onClick={() => themeStore.setTheme('light')}>
  22. set theme: light
  23. </button>
  24. <button onClick={() => themeStore.setTheme('dark')}>
  25. set theme: dark
  26. </button>
  27. </>
  28. )
  29. })
  30. // src/App.tsx
  31. const App = () => (
  32. <main>
  33. <Counter />
  34. <ThemeToggler />
  35. </main>
  36. )