如何处理 useEffect 闭包内的陈旧状态值? [英] How to deal with stale state values inside of a useEffect closure?

查看:230
本文介绍了如何处理 useEffect 闭包内的陈旧状态值?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下示例是一个 Timer 组件,它有一个按钮(用于启动计时器)和两个显示经过秒数和经过秒数乘以 2 的标签.

然而,它不起作用(CodeSandbox Demo)

代码

import React, { useState, useEffect } from "react";const 定时器 = () =>{const [doubleSeconds, setDoubleSeconds] = useState(0);const [秒,setSeconds] = useState(0);const [isActive, setIsActive] = useState(false);useEffect(() => {让间隔=空;如果(是活动的){间隔 = setInterval(() => {console.log(创建间隔");setSeconds((prev) => prev + 1);setDoubleSeconds(秒 * 2);}, 1000);} 别的 {清除间隔(间隔);}返回 () =>{console.log(销毁间隔");清除间隔(间隔);};}, [活跃]);返回 (<div className="app"><button onClick={() =>setIsActive((prev) => !prev)} type=button">{活跃 ?暂停计时器":播放计时器"}<h3>秒:{秒}</h3><h3>秒 x2:{doubleSeconds}</h3>

);};导出 { 定时器为默认值 };

问题

在 useEffect 调用中,seconds"value 将始终等于上次渲染 useEffect 块时(上次更改 isActive 时)的值.这将导致 setDoubleSeconds(seconds * 2) 语句失败.React Hooks ESLint 插件给了我一个关于这个问题的警告,内容如下:

<块引用>

React Hook useEffect 缺少依赖项:'seconds'.包括它或删除依赖项数组.你也可以更换如果setDoubleSeconds",则使用 useReducer 的多个 useState 变量需要秒"的当前值.(react-hooks/exhaustive-deps)eslint

正确的是,添加秒"到依赖数组(并将 setDoubleSeconds(seconds * 2) 更改为 setDoubleSeconds((seconds + 1) * ) 将呈现正确的结果.然而,这有一个令人讨厌的一面导致间隔在每次渲染时创建和销毁的效果(console.log(Destroying Interval") 在每次渲染时触发).

所以现在我正在查看 ESLint 警告中的其他建议如果‘setDoubleSeconds’需要‘seconds’的当前值,您也可以用 useReducer 替换多个 useState 变量".

我不明白这个建议.如果我创建一个减速器并像这样使用它:

import React, { useState, useEffect, useReducer } from "react";const reducer = (state, action) =>{开关(动作.类型){案例设置":{返回 action.seconds;}默认: {返回状态;}}};const 定时器 = () =>{const [doubleSeconds, dispatch] = useReducer(reducer, 0);const [秒,setSeconds] = useState(0);const [isActive, setIsActive] = useState(false);useEffect(() => {让间隔=空;如果(是活动的){间隔 = setInterval(() => {console.log(创建间隔");setSeconds((prev) => prev + 1);dispatch({ type: "SET", seconds });}, 1000);} 别的 {清除间隔(间隔);}返回 () =>{console.log(销毁间隔");清除间隔(间隔);};}, [活跃]);返回 (<div className="app"><button onClick={() =>setIsActive((prev) => !prev)} type=button">{活跃 ?暂停计时器":播放计时器"}<h3>秒:{秒}</h3><h3>秒 x2:{doubleSeconds}</h3>

);};导出 { 定时器为默认值 };

过时值的问题依然存在(CodeSandbox演示(使用 Reducers)).

问题

那么对于这个场景有什么建议呢?我是否会降低性能并简单地添加秒"?到依赖数组?我是否创建了另一个依赖于秒"的 useEffect 块?并调用setDoubleSeconds()";在那里?我是否合并秒"?和双秒"变成单个状态对象?我使用引用吗?

此外,您可能会想为什么不简单地更改 <h3>Seconds x2: {doubleSeconds}</h3>";到

Seconds x2: {seconds * 2}

并删除doubleSeconds"状态?".在我的实际应用程序中,doubleSeconds 被传递给 Child 组件,我不希望 Child 组件知道 seconds 是如何映射到 doubleSeconds 的,因为这会降低 Child 的可重用性.

谢谢!

解决方案

您可以通过多种方式访问​​效果回调中的值,而无需将其添加为 dep.

  1. setState.您可以通过其设置器点击状态变量的最新值.

setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));

  1. 参考您可以将 ref 作为依赖项传递,它永远不会改变.不过,您需要手动使其保持最新状态.

const secondsRef = useRef(0);const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);

然后您可以使用 secondsRef.current 访问代码块中的 seconds 而不会触发 deps 更改.

setDoubleSeconds(secondsRef.current * 2);

在我看来,你永远不应该从 deps 数组中省略一个依赖项.如果您需要不更改 deps,请使用上述 hack 来确保您的值是最新的.

总是首先考虑是否有比将值写入回调更优雅的编写代码的方法.在您的示例中,doubleSeconds 可以表示为 seconds 的导数.

const [seconds, setSeconds] = useState(0);const doubleSeconds = 秒 * 2;

有时应用程序并不那么简单,因此您可能需要使用上述技巧.

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.

However, it does not work (CodeSandbox Demo)

The Code

import React, { useState, useEffect } from "react";

const Timer = () => {
  const [doubleSeconds, setDoubleSeconds] = useState(0);
  const [seconds, setSeconds] = useState(0);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isActive) {
      interval = setInterval(() => {
        console.log("Creating Interval");
        setSeconds((prev) => prev + 1);
        setDoubleSeconds(seconds * 2);
      }, 1000);
    } else {
      clearInterval(interval);
    }
    return () => {
      console.log("Destroying Interval");
      clearInterval(interval);
    };
  }, [isActive]);

  return (
    <div className="app">
      <button onClick={() => setIsActive((prev) => !prev)} type="button">
        {isActive ? "Pause Timer" : "Play Timer"}
      </button>
      <h3>Seconds: {seconds}</h3>
      <h3>Seconds x2: {doubleSeconds}</h3>
    </div>
  );
};

export { Timer as default };

The Problem

Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:

React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'. (react-hooks/exhaustive-deps)eslint

And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).

So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".

I do not understand this recommendation. If I create a reducer and use it like so:

import React, { useState, useEffect, useReducer } from "react";

const reducer = (state, action) => {
    switch (action.type) {
        case "SET": {
            return action.seconds;
        }
        default: {
            return state;
        }
    }
};

const Timer = () => {
    const [doubleSeconds, dispatch] = useReducer(reducer, 0);
    const [seconds, setSeconds] = useState(0);
    const [isActive, setIsActive] = useState(false);

    useEffect(() => {
        let interval = null;
        if (isActive) {
            interval = setInterval(() => {
                console.log("Creating Interval");
                setSeconds((prev) => prev + 1);
                dispatch({ type: "SET", seconds });
            }, 1000);
        } else {
            clearInterval(interval);
        }
        return () => {
            console.log("Destroying Interval");
            clearInterval(interval);
        };
    }, [isActive]);

    return (
        <div className="app">
            <button onClick={() => setIsActive((prev) => !prev)} type="button">
                {isActive ? "Pause Timer" : "Play Timer"}
            </button>
            <h3>Seconds: {seconds}</h3>
            <h3>Seconds x2: {doubleSeconds}</h3>
        </div>
    );
};

export { Timer as default };

The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).

The Question(s)

So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?

Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.

Thanks!

解决方案

You can access a value inside an effect callback without adding it as a dep in a few ways.

  1. setState. You can tap the up-to-date value of a state variable through its setter.

setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));

  1. Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.

const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);

You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.

setDoubleSeconds(secondsRef.current * 2);

In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.

Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.

const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;

Sometimes applications aren't that simple so you may need to use the hacks described above.

这篇关于如何处理 useEffect 闭包内的陈旧状态值?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆