在 useState 钩子回调中使用副作用可以吗?

und*_*ood 9 javascript reactjs react-hooks

想象一下情况:

const [value, setValue] = useState(false);

const setSomething = (val) => {
  setValue((prev) => {
    fn(); dispatch(action); // or any other side effect
    
    return prev + val;
  });
};
Run Code Online (Sandbox Code Playgroud)

在回调中调用副作用在编程上是否符合反应原则useState?它会以某种方式影响渲染过程吗?

kca*_*kca 13

更新函数中使用副作用是不行的。它可能会影响渲染过程,具体取决于具体的副作用。

\n

反应原则(关注点分离、声明性代码)并不好。

\n

(我记得见过一些特殊的用例,据说将一些代码放入更新程序函数中是唯一的解决方案,但我不记得它是什么。我希望评论中提供一个示例。)

\n

1. 使用副作用的后果

\n

使用副作用是不行的,基本上与为什么你不应该在其他任何地方使用 useEffect 之外的副作用的原因相同。

\n

一些副作用可能会影响渲染过程,其他副作用可能工作正常(技术上),但您不应该依赖setter函数内部发生的情况。

\n

React保证,例如,如果您调用setState( prev => prev + 1 ),那么state现在将比以前多 1。

\n

React 不保证幕后会发生什么来实现该目标。React 可能会多次调用这些 setter 函数,或者根本不调用这些函数,并且可以按任何顺序:

\n
\n

StrictMode - 检测意外的副作用

\n

...因为上述方法可能会被多次调用,所以\xe2\x80\x99重要的是它们不包含副作用。...

\n
\n

2.遵循react原则

\n

您不应该在更新程序函数中添加副作用,因为它违反了一些原则,例如关注点分离和编写声明性代码。

\n

关注点分离:

\n

setCount除了设置count.

\n

编写声明性代码:

\n

一般来说,您应该以声明性方式编写代码,而不是命令性代码

\n
    \n
  • 也就是说,您的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
  • \n
  • 即你应该写“B 的值应该是 X,依赖于 A”而不是“更改 A,然后更改 B”
  • \n
\n

在某些情况下,React 并不“知道”任何有关副作用的信息,因此您需要自己注意一致的状态。

\n

有时你无法避免编写一些命令式代码。

\n

useEffect是否可以帮助您保持状态一致,例如允许您将某些命令性代码与某些状态相关联。“指定依赖项”。\n如果您不使用useEffect,您仍然可以编写工作代码,但您只是没有使用 React 为此目的提供的工具。您没有按照应有的方式使用 React,并且您的代码变得不太可靠。

\n

副作用问题的示例

\n

例如,在这段代码中,您会期望AB始终相同,但它可能会给您带来意想不到的结果,例如B增加 2 而不是 1(例如,当处于 DEV 模式和严格模式时):

\n
export function DoSideEffect(){\n  const [ A, setA ] = useState(0);\n  const [ B, setB ] = useState(0);\n\n  return <div>\n    <button onClick={ () => {\n      setA( prevA => {                // <-- setA might be called multiple times, with the same value for prevA\n        setB( prevB => prevB + 1 );   // <-- setB might be called multiple times, with a _different_ value for prevB\n        return prevA + 1;\n      } );\n    } }>set count</button>\n    { A } / { B }\n  </div>;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

例如,这不会显示副作用后的当前值count,直到由于其他原因重新渲染组件,例如增加:

\n
export function DoSideEffect(){\n  const someValueRef = useRef(0);\n  const [ count, setCount ] = useState(0);\n\n  return <div>\n    <button onClick={ () => {\n      setCount( prevCount => {\n        someValueRef.current = someValueRef.current + 1; // <-- some side effect\n        return prevCount; // <-- value doesn\'t change, so react doesn\'t re-render\n      } );\n    } }>do side effect</button>\n\n    <button onClick={ () => {\n      setCount(prevCount => prevCount + 1 );\n    } }>set count</button>\n\n    <span>{ count } / {\n      someValueRef.current // <-- react doesn\'t necessarily display the current value\n    }</span>\n  </div>;\n}\n
Run Code Online (Sandbox Code Playgroud)\n


Dre*_*ese 7

不,状态更新函数产生副作用是不行的,它应该被视为纯函数

\n
    \n
  1. 对于相同的参数,函数返回值是相同的(局部静态变量、非局部变量、可变引用参数或输入流没有变化),并且
  2. \n
  3. 函数应用程序没有副作用(局部静态变量、非局部变量、可变引用参数或输入/输出流不会发生变化)。
  4. \n
\n

您可能正在使用该组件,也可能没有使用该React.StrictMode组件,但它是一种帮助检测意外副作用的方法。

\n
\n

检测意外的副作用

\n

从概念上讲,React 确实分两个阶段工作:

\n
    \n
  • 渲染阶段确定需要进行哪些更改\ne . DOM。在此阶段,React 调用render,然后\n将结果与之前的渲染进行比较。
  • \n
  • 提交阶段是 React 应用任何更改的时候。(对于 React DOM,这是 React 插入、更新和删除 DOM 节点的时间。)在此阶段,React 还会调用像componentDidMount和一样的生命周期。componentDidUpdate
  • \n
\n

提交阶段通常非常快,但渲染可能很慢。因此,即将推出的并发模式(默认情况下尚未启用)将渲染工作分解为多个部分,暂停和恢复工作以避免阻塞浏览器。这意味着 React\n可能在提交之前多次调用渲染阶段生命周期,\n也可能在根本不提交的情况下调用它们(因为错误\n或更高优先级的中断)。

\n

渲染阶段生命周期包括以下类组件方法:

\n
    \n
  • constructor
  • \n
  • componentWillMount(或者UNSAFE_componentWillMount
  • \n
  • componentWillReceiveProps(或者UNSAFE_componentWillReceiveProps
  • \n
  • componentWillUpdate(或者UNSAFE_componentWillUpdate
  • \n
  • getDerivedStateFromProps
  • \n
  • shouldComponentUpdate
  • \n
  • render
  • \n
  • setState更新函数(第一个参数) <--
  • \n
\n

由于上述方法可能会被多次调用,因此\xe2\x80\x99s\n重要的是它们不包含副作用。忽略此规则可能会导致各种问题,包括内存泄漏和无效的应用程序状态。不幸的是,检测这些问题可能很困难,因为它们通常是不确定的。

\n

严格模式可以\xe2\x80\x99t 自动为您检测副作用,但它\n可以通过使副作用更具确定性来帮助您发现它们。\n这是通过有意两次调用以下函数来完成的:

\n
    \n
  • 类组件 constructor rendershouldComponentUpdate方法
  • \n
  • 类组件静态getDerivedStateFromProps方法
  • \n
  • 功能组件体
  • \n
  • 状态更新函数(第一个参数setState <--
  • \n
  • 传递给useStateuseMemo、 或 的函数useReducer
  • \n
\n
\n

从关于故意双重调用状态更新器函数的两个突出显示的要点中获取线索,并将状态更新器函数视为纯函数。

\n

对于您共享的代码片段,我认为根本没有理由从更新程序回调中调用这些函数。它们可以/应该在回调之外调用。

\n

例子:

\n
const setSomething = (val) => {\n  setValue((prev) => {\n    return prev + val;\n  });\n  fn();\n  dispatch(action);\n};\n
Run Code Online (Sandbox Code Playgroud)\n