Split Context Files


Uncategorized

Updated May 25th, 2023

In “React for the Rest of Us” the reducer function and corresponding initial state is in the “Main.js” file yet their are two separate context files, one for Dispatch and one for State.

Why bother splitting them up?

In Brad Schiff’s video (here) he explains that dispatch doesn’t really change like state does. The children and grandchildren components can choose which one they want to subscribe to. Some components may only need one or the other. If a component only subscribes to dispatch the component will not re-render if state changes.

Note: He also speaks about the caveat of context being an object and not just a string of text or number. This could trigger unintentional re-renders in consumers when a parent re-renders. When the top level app component changes then everything else re-renders. May not be a big deal because nothing changes except if difference between DOM and Virtual DOM.

const MemoizedExtraFooter = React.memo(ExpensiveFooter)

// and then <MemoizedExtarFooter />

export default React.memo(ExpensiveFooter)

Everything doesn’t need to be in global context.

Back to Why Bother Splitting Context Files Into State and Dispatch?

See here on stack overflow this answer: “I think It does make a difference making a separate context provider for the state and for the dispatch. If you have child components that only require the dispatch function, and not the state, those components will be re-render as well if the state of the provider changed. however, if there are separate context provider, one for the state and one for the dispatch, child components only using the dispatch won’t need to re-render when the state changes.”

Your going to want to read this and this as well.

Why and how to move out of Main.js/”_app.js” to Separate Files?

In Max’s course (and others) I’ve seen we have a separate file with state (via useState/useReducer) and context. Sometimes this may just be for certain functionality (notification-context) and not truly global.

function CountProvider({children}) {
  const [state, dispatch] = React.useReducer(countReducer, {count: 0})
  // NOTE: you *might* need to memoize this value
  // Learn more in http://kcd.im/optimize-context
  const value = {state, dispatch}
  return <CountContext.Provider value={value}>{children}</CountContext.Provider>
}

Ideas

Follow a pattern similar to Max’s to get the state out of the “_apps.tsx” file. In one context file have a “AppContext” that is the default export and uses the “createContext” hook.

const AppContext = createContext({default here})

Then create one AppContext.Provider component that will wrap around you main component tree.

This should manage the state with useReducer and return two AppContext.providers, one with a value={state} and one with value={dispatch} and both wrapped around {props.children}.

Now it is the AppContext component that is wrapped around the components in “_app.js” file.

The question here is (and this is still something I don’t get from the RFTROU course) is how to split the “createContext” files and the reducer function and initial state? Does having one createContext but two “.Providers” in one app accomplish the same thing?

I will need to test out but I don’t think you can do this. Here is an article that shows a similar implementation but creates two contexts, one for state and one for dispatch, in the same file, (bonus: in typescript).

return (
    <UserStateContext.Provider value={state}>
      <UserDispatchContext.Provider value={dispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );

Better Autocompletion

Max uses this approach where he sets up a base context object to get better autocompletion. He says this is really just dummy data in the end. Passes this object into “useContext().”

Keep Your “Main.js” file or “_app.js” Files Clean

Keep the state in your separate context file under the basic context lines in the same file. Max says this new component , not only wraps “{props.children},” but also manages context-related state. James Q. Quick did something similar so re-watch that video here.

A Bigger Think About Context

Putting everything in Global Context or Redux Store is not necessarily the answer for everything. Kent C sent me to this video…

Important to know how many times different comps are re-rendered.

My take on the “don’t overuse context” argument is that “Can you blame me?” If the solution is to lift state why not lift it up to the highest point and it’s done. If you need to get more performant you can drop state down or refactor to use composition or break apart context to a specific part of component tree, etc. I have found myself refactoring over and over higher when I might as well have just sent to the top from the beginning.

Final Thoughts

Even global state should be managed in a separate file. Creating two contexts (one for state and one for dispatch) in one file seems like the way. Also have the wrapper component that 1.) manages the state via useReducer and 2.) uses two providers to split state and dispatch.

I’m sure there’s major room for improvement but:

import React, { createContext } from "react"
import { useImmerReducer } from "use-immer"

export const GlobalDispatchContext = createContext((() => {}) as React.Dispatch<GlobalActionTypes>)

export const GlobalStateContext = createContext({
  loggedIn: false,
  flashMessages: [] as any,
  language: "",
  theme: ""
})

type GlobalActionTypes = { type: "login" } | { type: "logout" } | { type: "flashMessage"; value: string } | { type: "setEnglish" } | { type: "setSpanish" } | { type: "setLatin" } | { type: "setLightTheme" } | { type: "setDarkTheme" }

export const GlobalContextProvider: React.FC = props => {
  const initialState = {
    loggedIn: false,
    flashMessages: [] as any,
    language: "english",
    theme: "dark"
  }

  function ourReducer(draft: typeof initialState, action: GlobalActionTypes): void {
    switch (action.type) {
      case "login":
        draft.loggedIn = true
        return
      case "logout":
        draft.loggedIn = false
        return
      case "flashMessage":
        draft.flashMessages.push(action.value)
        return
      case "setEnglish":
        draft.language = "english"
        return
      case "setSpanish":
        draft.language = "spanish"
        return
      case "setLatin":
        draft.language = "latin"
        return
      case "setLightTheme":
        draft.theme = "light"
        return
      case "setDarkTheme":
        draft.theme = "dark"
        return
      default:
        throw new Error("Bad action")
    }
  }

  const [state, dispatch] = useImmerReducer(ourReducer, initialState)
  return (
    <GlobalStateContext.Provider value={state}>
      <GlobalDispatchContext.Provider value={dispatch}>{props.children}</GlobalDispatchContext.Provider>
    </GlobalStateContext.Provider>
  )
}