javascriptreactjstypescriptreact-hooksreact-context

Context Provider not passing value to children


I am trying to create a parent/overarching context. The purpose of the context is to store UserInfo which should then be available to all children.

UserInfo is set by getting config from an API.

Context.ts:

import type { User } from 'my-lib';
import { Dispatch, SetStateAction } from 'react';

export const UserContextProps = {
    setUser?: Dispatch<SetStateAction<User>>,
    userConfig?: User
}

export const UserContext = createContext<UserContextProps>({});

UserProvider.tsx

import { UserContext } from './Context'

const UserProvider = ({children}) =>{
    const [data, setUser] = useState<User>({});
  
    useEffect(() =>{
        fetchData().then(res => setUser(res.data))
    },[]);       
   
   return(
       <UserContext.Provider value={{setUser: setUser, userConfig: data}}>
           {Object.keys(user).length > 0 ? (
               <>{children}</>
           ) : (
               <div>Loading</div>
           )}
       </UserContext.Provider>
   )
}

Our app is made up of many small sites like cart.html, description.html, profile.html each of these has its own component. I am trying to wrap each of these with UserProvider so the children:

1. Render after userConfig is set
2. Have access to userConfig

Cart.tsx

import { UserProvider } from 'UserProvider';
import React, { useContext } from 'react';
import { UserContext } from './Context'

export const UserCartApp = () => {

    const { userConfig } = useContext(UserContext);

    return (
        <UserProvider>
            <Header userConfig={userConfig} />
            <SelectedProducts userConfig={userConfig} />
            <SubTotal userConfig={userConfig} />
            <Footer />
        </UserProvider>
    )
}

Inside i have:

Header.tsx

export const Header = ({userConfig}) => {
    
    console.log('header config', userConfig);
....
....

The above console.log of userConfig is always {}


Solution

  • Issue

    Components not wrapped in the Context provider will receive the default value.

    export const UserContext = createContext<UserContextProps>({});
    

    The UserContext has a default value of {}.

    Since UserCartApp is the component rendering the UserProvider component providing the UserContext it can't itself access the provided value.

    export const UserCartApp = () => {
      const { userConfig } = useContext(UserContext); // <-- receives default {} value
    
      return (
        <UserProvider> // <-- Provides the context
          <Header userConfig={userConfig} />
          <SelectedProducts userConfig={userConfig} />
          <SubTotal userConfig={userConfig} />
          <Footer />
        </UserProvider>
      );
    };
    

    Solution

    Lift the UserProvider component higher in the ReactTree such that it can provide the UserContext value to UserCartApp.

    Example:

    <UserProvider>
      <UserCartApp />
    </UserProvider>
    
    export const UserCartApp = () => {
      const { userConfig } = useContext(UserContext); // <-- receives { setUser: setUser, userConfig: data } value
    
      return (
        <>
          <Header userConfig={userConfig} />
          <SelectedProducts userConfig={userConfig} />
          <SubTotal userConfig={userConfig} />
          <Footer />
        </>
      );
    };
    

    Improvement/Suggestion

    One of the problems the React Context API solves for though is props drilling. It doesn't make sense to access the context value in the parent component and then pass it down as props to each child component. An improvement here would be to simply have each child use the const { userConfig } = useContext(UserContext);.

    Example:

    const Header = () => {
      const { userConfig } = useContext(UserContext);
    
      ...
    };
    
    const SelectedProducts = () => {
      const { userConfig } = useContext(UserContext);
    
      ...
    };
    
    const SubTotal = () => {
      const { userConfig } = useContext(UserContext);
    
      ...
    };
    
    export const UserCartApp = () => {
      return (
        <>
          <Header />
          <SelectedProducts />
          <SubTotal />
          <Footer />
        </>
      );
    };