Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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'.

like image 230
Bob Sacamano Avatar asked Oct 08 '20 09:10

Bob Sacamano


2 Answers

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.

like image 97
hackape Avatar answered Sep 19 '22 01:09

hackape


Firstly, you need to provide some default value to your context if no value then set the default value as null.

export const FooContext = createContext(null);

Mostly there is two way for passing the value in the Provider Component. You can pass by an object or tuple to value props in the Provider component.

I'll give you an example by passing an object in the Provider Component. @dna has given an example of a tuple.

<FooContext.Provider value={{value,setValue}}>
     {props.children}
</FooContext.Provider>

Now if you want to use that value in another component you need to destructure the object like this

const {value, setValue} = useContext(FooContext);

If you have called the nested myFunction() correctly like shown below then the value would also be x instead of null.

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

Output :

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

Now, the question is why it is returning a single character value instead of the state value.

In Javascript, a string is an array of characters. Eg.

const string = ['s','t','r','i','n','g'];
//This is equivalent to
const string = "string";

In your case, your state value may be a string. So, when you destructuring the string you will get the first character of the string.

You will understand more if I give you an example.

const string = "subrato";

const [str] = string;
console.log(str);
like image 31
Subrato Pattanaik Avatar answered Sep 22 '22 01:09

Subrato Pattanaik