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

React: useContext value is not updated in the nested function

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

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'

,但基本上由于某种原因,该值在嵌套函数内仍保持为 null

我认为错误的部分在 provider 值中,你这样做:

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

并且在 <SomeComponent /> 中,您以这种方式解构上下文值:

const [value] = useContext(FooContext);

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

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

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

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

export const FooContext = createContext(null);

大多数情况下,有两种方法可以在提供程序组件中传递值。您可以将 objecttuple 传递给 Provider 组件中的 value 道具。

我将通过在提供程序组件中传递 object 来为您提供示例。 @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'

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

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

之所以你的值状态在函数内部返回一个值,而在嵌套函数内部返回一个空值,是因为这些原因。

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

说明

这太经典了stale closure problem。我不知道闭包在哪里过时了,因为你没有向我们展示你如何使用 myFunction,但我确定这就是原因。

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

但是调用 myFunction 可能会在稍后发生,因为您可以传递 myFunction。比方说,你将它传递给某个地方的 setTimeout(myFunction, 1000)

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

此时创建了myFunction的新版本,在其闭包中捕获了新值value = 'x'。但是 setTimeout 被传递 myFunction 的旧版本 ,它捕获 value = null。 1000 毫秒后,调用 myFunction,并打印 2. value = null。事情就是这样。


解决方案

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

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

我不知道你的具体案例的细节,所以我不能告诉你什么是最好的方式来回答你的问题。但是一个非常幼稚的策略是使用对象 属性 而不是 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>)
}

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

ref = useRef({}).current 创建同一对象 ref 的稳定引用,该引用不会在 re-render 中发生变化。你把它放在 myFunction 的闭包中。它就像一个门户,可以“传送”跨闭包边界的状态更新。

现在即使过时的关闭问题仍然存在,有时您仍然可以调用过时的版本myFunction,这是无害的!因为旧的 ref 和新的 ref 一样,它的 属性 ref.value 保证是 up-to-date 因为你总是 re-assign 它 ref.value = value 当 re-rendered.