Search code examples
reactjsnested-functionuse-context

React: useContext value is not updated in the nested function


I have a simple context that sets some value that it get from backend, pseudo code:

export const FooContext = createContext();

export function Foo(props) {
    const [value, setValue] = useState(null);

    useEffect(() => {
        axios.get('/api/get-value').then((res) => {
            const data = res.data;
            setValue(data);
        });
    }, []);

    return (
        <FooContext.Provider value={[value]}>
            {props.children}
        </FooContext.Provider>
    );
}

function App() {
    return (
        <div className="App">
            <Foo>
                <SomeView />
            </Foo>
        </div>
    );
}

function SomeView() {
    const [value] = useContext(FooContext);
    
    console.log('1. value =', value);
    
    const myFunction = () => {
        console.log('2. value = ', value);
    }
    
    return (<div>SomeView</div>)

Sometimes I get:

1. value = 'x'
2. value = null

So basically for some reason the value stays as null inside the nested function despite being updated to 'x'.


Solution

  • Explanation

    This is such a classic stale closure problem. I cannot tell where the closure goes outdated because you didn't show us how you use the myFunction, but I'm sure that's the cause.

    You see, in JS whenever you create a function it will capture inside its closure the surrounding scope, consider it a "snapshot" of states at the point of its creation. value in the question is one of these states.

    But calling myFunction could happen sometime later, cus you can pass myFunction around. Let's say, you pass it to setTimeout(myFunction, 1000) somewhere.

    Now before the 1000ms timeout, say the <SomeView /> component has already been re-rendered cus the axios.get completed, and the value is updated to 'x'.

    At this point a new version of myFunction is created, in the closure of which the new value value = 'x' is captured. But setTimeout is passed an older version of myFunction which captures value = null. After 1000ms, myFunction is called, and print 2. value = null. That's what happened.


    Solution

    The best way to properly handle stale closure problem is, like all other programming problems, to have a good understanding of the root cause. Once you're aware of it, code with caution, change the design pattern or whatever. Just avoid the problem in the first place, don't let it happen!

    The issue is discussed here, see #16956 on github. In the thread multiple patterns and good practices are suggested.

    I don't know the detail of your specific case, so I cannot tell what's the best way to your question. But a very naive strategy is to use object property instead of variable.

    function SomeView() {
        const [value] = useContext(FooContext);
    
        const ref = useRef({}).current;
        ref.value = value;
        
        console.log('1. value =', value);
        
        const myFunction = () => {
            console.log('2. value = ', ref.value);
        }
    
        return (<div>SomeView</div>)
    }
    

    Idea is to depend on a stable reference of object.

    ref = useRef({}).current create a stable reference of same object ref that don't change across re-render. And you carry it within the closure of myFunction. It acts like a portal that "teleports" the state update across the boundary of closures.

    Now even though stale closure problem still happens, sometimes you might still call outdated version of myFunction, it's harmless! Cus the old ref is the same as new ref, and it's property ref.value is guaranteed to be up-to-date since you always re-assign it ref.value = value when re-rendered.