Search code examples
reactjsreact-contextmobx-state-tree

Where is the better places for React.createContext and React.useContext? (+ mobx-state-tree useLocalObservable as provider)


I'm working on a react app with mobx-state-tree.
I want to get some advices for 2 things.

  1. Q1 - Where is the better place for React.useContext?
  2. Q2 - Which is better to use the stored things? inherit? or useContext directly?

By the app is being expanded, components are getting more depth than before.

So, there are many components like this.

    1. One of Main Menu (ex. PostList.tsx)
      1. Sub Tab of #1 Main Menu Controller (ex. TypeXPostListController.tsx)
        1. View Component of #2 (ex. TypeXPostListView)
          1. Dialog Controller(ex. AddTypeXPostDialog)
            1. Dialog Body of #4 (ex. AddTypeXPostDialogContents)
              1. (When the dialog requires complexed things) Sub Component of #5 (ex. InputAndVerificationField)

I'm confused about the best place of the context provider and useContext for them.
Actually, #1 is under the route list and my context provider wrapped the route list at the almost near the top.

  • root store provider
    • route list
      • #1

In this app, I have a root store and the post store is in it.

  • root store
    • post store
    • (and other stores which are in the same level)

Here is the first question. Q1 - Where is the better place for React.useContext?
Do I need to separate the post store from the root store and set a new provider for #1? When I searched about this, someone recommended to place it as near as can from the components which use it.

The good thing in the current structure is, there are no need to set the post store provider again for other menus (by the business reason, some other main menus require the post related features). I just call the useContext of root store and get the post store from it.

Q2 - Which place is better to use the stored things? inherit? or useContext directly?
The second question is, if I use the useContext in #5 or #6 directly again (even if I used it on the upper components already), does it impact on performance?

I'm reading some articles and React docs but the confusion is not clear yet...
If you give me any recommended search words, it also be a good help for me.

React docs provides below example. But what I wonder is (with related Q2), Button is the smallest thing in an app. Is it okay to call the context directly in the button? (as you know, theme is not only for that button and be required many other areas)

Will update my question if I get more understanding...
Thans in advance.

https://reactjs.org/docs/context.html#dynamic-context


Solution

  • I'm the maintainer of MobX-State-Tree.

    I recommend using this pattern to retrieve your store. It's essentially a singleton pattern, but can be overridden if you need to (such as for tests) with a provider. You do not need to use a provider for your app, though.

    const _rootStore = RootStoreModel.create({})
    const RootStoreContext = createContext<RootStore>(_rootStore)
    export const useStores = () => useContext(RootStoreContext)
    

    Then, in any component in your app, you can use it:

    export const AddTypeXPostDialogContents = observer((props) {
      const { postStore } = useStores()
    
      // ...
    })
    

    No need for a provider at all, as it'll just use the instantiated RootStore.

    If you want to see a more complex example, the new Ignite Maverick branch has a good one:

    https://github.com/infinitered/ignite/blob/bcd4a75e0aac11b40c8890c93ed2e5447d98775a/boilerplate/app/models/helpers/use-stores.ts

    I'll replicate it here just in case it goes away someday:

    import { createContext, useContext, useEffect, useState } from "react"
    import { setReactotronRootStore } from "../../services/reactotron"
    import { RootStore, RootStoreModel } from "../RootStore"
    import { setupRootStore } from "./setup-root-store"
    
    /**
     * Create the initial (empty) global RootStore instance here.
     *
     * Later, it will be rehydrated in app.tsx with the setupRootStore function.
     *
     * If your RootStore requires specific properties to be instantiated,
     * you can do so here.
     *
     * If your RootStore has a _ton_ of sub-stores and properties (the tree is
     * very large), you may want to use a different strategy than immediately
     * instantiating it, although that should be rare.
     */
    const _rootStore = RootStoreModel.create({})
    
    /**
     * The RootStoreContext provides a way to access
     * the RootStore in any screen or component.
     */
    const RootStoreContext = createContext<RootStore>(_rootStore)
    
    /**
     * You can use this Provider to specify a *different* RootStore
     * than the singleton version above if you need to. Generally speaking,
     * this Provider & custom RootStore instances would only be used in
     * testing scenarios.
     */
    export const RootStoreProvider = RootStoreContext.Provider
    
    /**
     * A hook that screens and other components can use to gain access to
     * our stores:
     *
     * const rootStore = useStores()
     *
     * or:
     *
     * const { someStore, someOtherStore } = useStores()
     */
    export const useStores = () => useContext(RootStoreContext)
    
    /**
     * Used only in the app.tsx file, this hook sets up the RootStore
     * and then rehydrates it. It connects everything with Reactotron
     * and then lets the app know that everything is ready to go.
     */
    export const useInitialRootStore = (callback: () => void | Promise<void>) => {
      const rootStore = useStores()
      const [rehydrated, setRehydrated] = useState(false)
    
      // Kick off initial async loading actions, like loading fonts and rehydrating RootStore
      useEffect(() => {
        let _unsubscribe
        ;(async () => {
          // set up the RootStore (returns the state restored from AsyncStorage)
          const { restoredState, unsubscribe } = await setupRootStore(rootStore)
          _unsubscribe = unsubscribe
    
          // reactotron integration with the MST root store (DEV only)
          setReactotronRootStore(rootStore, restoredState)
    
          // let the app know we've finished rehydrating
          setRehydrated(true)
    
          // invoke the callback, if provided
          if (callback) callback()
        })()
    
        return () => {
          // cleanup
          if (_unsubscribe) _unsubscribe()
        }
      }, [])
    
      return { rootStore, rehydrated }
    }