Search code examples
reactjstypescriptreact-context

Passing multiple states in react context


I have a context that should pass down in the tree multiple states. Which is the correct approach to set Provider's value?

  1. Creating an object in place doesn't work since it is re-built at every re-render, causing the child to re-render as well also if the actual content of the context didn't change (because a new object is created, with a different memory address, causing the child to think the object changed).

How should I pass the context?

//Example:
interface Context {
 state1: number,
 state2: number,
}

const MyContext = React.createContext<Context|null>(null)

const MyFC: React.FC = (props)=>{
 const [state1,setState1] = useState<number>(0) //To pass down
 const [state2,setState2] = useState<number>(0) //To pass down
 const [state3,setState3] = useState<number>(0) //Not to pass down

 return(
   <MyContext.Provider value={????}>
      {/* components */}
   <MyContext>
 )
}

Solution

  • The options to pass down complex states in a context are multiple.

    Option 1: Split contexts that don't change together

    If elements composing a context are not tightly related consider splitting the context into different contexts. In this way you can pass single states without having to handle the problem of "unifying" states.

    Option 2: use useReducer to combine related states

    When using useState hook is advised to create a separate state for each element since, unlike the state in class components, updating single elements of a state can lead to unnecessary complexity.

    Use state is the most common tool to save state in functional components.

    However, when more complex states (and state management) are needed, useReducer can be used to wrap up states in a single object. Then you can pass reducer's state as value for the context

    function reducer(state,action) {
     switch(action.type){
       //Cases:
       //.... (works like redux reducer)
       default:
         return state
     }
    }
    
    const MyContext = React.createContext<Context|null>(null)
    
    const MyFC: React.FC = (props)=>{
     const [compressedState,dispatch] = useReducer(reducer,{state1:0,state2:0})
     const [state3,setState3] = useState<number>(0) //Not to pass down
    
     return(
       <MyContext.Provider value={compressedState}>
           {/* components */}
       <MyContext>
     )
    }
    
    

    Option 3: useMemo

    When neither of the previous options are feasible, useMemo is the way to go. Simply combine all the elements that needs to be passed to create a value that updates only when at least one of the dependencies changes.

    const MyContext = React.createContext<Context|null>(null)
    
    const MyFC: React.FC = (props)=>{
     const [state1,setState1] = useState<number>(0) //To pass down
     const [state2,setState2] = useState<number>(0) //To pass down
     const [state3,setState3] = useState<number>(0) //Not to pass down
    
     const contextValue= useMemo(()=>{
       return{
        state1: state1,
        state2: state2
       }
     },[state1,state2]);
    
     return(
       <MyContext.Provider value={contextValue}>
           {/* components */}
       <MyContext>
     )
    }
    

    Final note on useMemo Use memo becomes useful also when we need to pass both elements of useState (state and set state) or useReducer (state and dispatch).

    In context, we should always avoid to put inside the value prop an inline object. As said at the beginning of the post, this object is re-created at every render (new memory address) causing the hooks checking to it to re-render (since the observed memory value changed) as well down in the widget tree.

    Use memo can become handful in this scenario since it allows to avoid passing an inline object:

    //Example for useReducer. Works also with useState.
    
    const contextValue = useMemo(() => {
      return { state, dispatch };
    }, [state, dispatch]);
    

    Sources: