React: useContext value is not updated in the nested function

假装没事ソ 提交于 2021-01-27 07:42:41

问题


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


回答1:


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);



回答2:


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.




回答3:


I think that the wrong part is in the provider value, you do this:

...
<FooContext.Provider value={value}> // <-- 'value' is simply a value
    {props.children}
</FooContext.Provider>
...

and in <SomeComponent /> you destructure the context value in this way:

const [value] = useContext(FooContext);

But this is wrong beacuse you set the provider value as simple value not as tuple.

SOLUTION To make your destructuring working, you shoud set your provider like this

...
<FooContext.Provider value={[value]}> // <---- USE TUPLE NOTATION
    {props.children}
</FooContext.Provider>
...



回答4:


The reason why your value state is giving back a value inside the function and giving a null inside a nested function is because of these reasons.

  1. First reason is you are passing an arrow function which means it will only give back a value on an onClick function.

    const myFunction = () => {
         console.log('2. value = ', value);
     }
    
  2. Second reason is that you have not called the nested function myFunction that you made.

Rather than do this and it will work:

  • Call your function inside the SomeView().

     myFunction();
    
  • Update your function.

      function myFunction() {
        console.log("2. value = ", value);
      }
    
  • Or if you want it run with your old code pass an onClick listener which will fire your myFunction() and will console.log the value.

     const myFunction = () => {
        console.log("2. value = ", value);
      };
    
     return <button onClick={myFunction}>Click me</button>;
    


来源:https://stackoverflow.com/questions/64259890/react-usecontext-value-is-not-updated-in-the-nested-function

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!