Search code examples
reactjsreact-hooksreact-context

ReactJS with nested context providers: useContext always returns undefined


I have a wrapper component (Main.js) which wraps around all sites in my ReactJS project. I use multiple providers/contexts (MyProvider.js/YourProvider.js) which asynchronously load a value which is used in various child components. They are nested in my Main.js.

My problem is that the direct child components of Main.js (Child1.js) only receive undefined when accessing the contexts. Even useEffects do not detect the changes. The child components of the child components (Child2.js) also receive undefined, but are changed after.

Why is this the case?
Why does it only occur of the direct children and why does it work for the sub children?
Is this connected to the nesting of the providers?

Here a small example:

Main.js

import { MyProvider } from "../../contexts/MyProvider.js";
import { YourProvider } from "../../contexts/YourProvider.js";
import Header from "./Header.js";
import Footer from "./Footer.js";

export default function Main({children}) {
    return (
        <MyProvider>
            <YourProvider>
                <Header/>
    
                <main>
                    <div>
                        {children}
                    </div>
                </main>

                <Footer/>
            </YourProvider>
        </MyProvider>
    );
}

MyProvider.js/YourProvider.js

import { createContext, useState, useEffect } from 'react';
import { getMyValue } from "../../lib.js";

export const MyContext = createContext();

export function MyProvider({children}) {
    const [myValue, setMyValue] = useState(undefined);

    useEffect(() => {
        getMyValue().then(result => {
            setMyValue(result);
        });
    }, []);

    return (
        <MyContext.Provider value={myValue}>
            {children}
        </MyContext.Provider>
    );
}

Child1.js

import { useContext, useEffect } from "react";
import { MyContext } from "../../contexts/MyProvider.js";
import { YourContext } from "../../contexts/YourProvider.js";
import Main from "./Main.js";
import Child2 from "./Child2.js";

export default function Child1({children}) {
    const myValue = useContext(MyContext);
    const yourValue = useContext(YourContext);

    useEffect(() => {
        console.log(myValue); // always undefined
        console.log(yourValue); // always undefined
    }, [myValue, yourValue]);

    return (
        <Main>
            <p>{myValue}</p>
            <p>{yourValue}</p>
            <Child2/>
        </Main>
    );
}

Child2.js

import { useContext, useEffect } from "react";
import { MyContext } from "../../contexts/MyProvider.js";
import { YourContext } from "../../contexts/YourProvider.js";

export default function Child2({children}) {
    const myValue = useContext(MyContext);
    const yourValue = useContext(YourContext);

    useEffect(() => {
        console.log(myValue); // initially undefined, then the real value
        console.log(yourValue); // initially undefined, then the real value
    }, [myValue, yourValue]);

    return (
        <div>
            <p>{myValue}</p>
            <p>{yourValue}</p>
        </div>
    );
}

Solution

  • You are accessing MyContext and YourContext above Main in Child1. Context consumers must be children of their providers docs.

    It works in Child2 because you are passing it as a child to Main and Main's children are defined as children of MyProvider and YourProvider.

    What you need to do is move your providers above your consumers for example:

    function App() {
        return (
            <Main>
                <Child1/>    
            </Main>
        );
    }
    
    export default function Child1({children}) {
        const myValue = useContext(MyContext);
        const yourValue = useContext(YourContext);
    
        useEffect(() => {
            console.log(myValue); // always undefined
            console.log(yourValue); // always undefined
        }, [myValue, yourValue]);
    
        return (
            <>
                <p>{myValue}</p>
                <p>{yourValue}</p>
                <Child2/>
            </>
        );
    }