react:useContext 值未在嵌套函数中更新

IT技术 reactjs nested-function use-context
2021-05-08 13:35:47

我有一个简单的上下文,它设置了一些从后端获取的值,伪代码:

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

有时我得到:

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

所以基本上由于某种原因,尽管被更新为'x'.

4个回答

解释

这是一个如此经典的陈旧闭包问题我不知道闭包在哪里过时了,因为您没有向我们展示您如何使用myFunction,但我确定这就是原因。

您会看到,在 JS 中,每当您创建一个函数时,它都会在其闭包内捕获周围的范围,将其视为创建时状态“快照”value问题是这些状态之一。

但是呼叫myFunction可能会在稍后的某个时间发生,因为您可以myFunction绕过。比方说,你把它传递给setTimeout(myFunction, 1000)某个地方。

现在在 1000 毫秒超时之前,假设<SomeView />组件已经完成重新渲染axios.get,并且value更新为'x'.

此时,一个新版本myFunction创建,其中的新值封value = 'x'被捕获。但是setTimeout通过了一个旧版本myFunction捕获value = null. 1000 毫秒后,myFunction被调用并打印2. value = null这就是发生的事情。


解决方案

像所有其他编程问题一样,正确处理陈旧闭包问题的最佳方法是充分了解根本原因。一旦你意识到这一点,小心编码,改变设计模式或其他什么。首先要避免问题,不要让它发生!

此处讨论了该问题,请参阅github 上的#16956在线程中建议了多种模式和良好实践。

我不知道你的具体情况的细节,所以我不能告诉你的问题的最佳方式是什么。但是一个非常幼稚的策略是使用对象属性而不是变量。

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

想法是依赖于对象的稳定引用。

ref = useRef({}).current创建相同对象的稳定引用,ref在重新渲染时不会改变。并且您在myFunction. 它就像一个门户,可以跨越闭包的边界“传送”状态更新。

现在即使过时的关闭问题仍然发生,有时你可能仍然会调用过时的版本myFunction,它是无害的!因为 oldref与 new 相同ref,并且它的属性ref.value保证是最新的,因为您总是ref.value = value在重新渲染时重新分配它

首先,如果没有值,您需要为上下文提供一些默认值,然后将默认值设置为 null。

export const FooContext = createContext(null);

大多数情况下,有两种方法可以在 Provider 组件中传递值。您可以通过object传递tuplevalueProvider 组件中的props。

我将通过object在 Provider 组件中传递 给你一个例子@dna 给出了一个tuple.

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

现在,如果您想在另一个组件中使用该值,您需要像这样解构对象

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

如果您myFunction()正确调用了嵌套,如下所示,那么该值也将 x代替 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>

输出 :

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

现在,问题是为什么它返回单个字符值而不是状态值。

在 Javascript 中,字符串是一个字符数组。例如。

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

在您的情况下,您的状态值可能是一个字符串。因此,当您解构字符串时,您将获得字符串的第一个字符。

举个例子你就明白了。

const string = "subrato";

const [str] = string;
console.log(str);

我认为错误的部分在于提供者的value,你这样做:

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

并以<SomeComponent />这种方式解构上下文值:

const [value] = useContext(FooContext);

但这是错误的,因为您将 provider 值设置为简单值而不是元组。

解决方案为了让你的解构工作,你应该像这样设置你的提供者

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

您的值状态在函数内返回一个值并在嵌套函数内返回 null 的原因是由于这些原因。

  1. 第一个原因是您正在传递一个箭头函数,这意味着它只会在 onClick 函数上返回一个值。

    const myFunction = () => {
         console.log('2. value = ', value);
     }
    
  2. 第二个原因是您没有调用myFunction您创建的嵌套函数

而不是这样做,它会起作用:

  • SomeView().

     myFunction();
    
  • 更新您的功能。

      function myFunction() {
        console.log("2. value = ", value);
      }
    
  • 或者,如果您希望它与旧代码一起运行,则传递一个 onClick 侦听器,该侦听器将触发您的 myFunction() 并将 console.log 值。

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