繁体   English   中英

在 useState 挂钩回调中使用副作用是否可以?

[英]Is it okey to use side effects in the useState hook callback?

想象一下情况:

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

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

useState回调中调用副作用的反应原则在编程上是否可行且很好? 它会以某种方式影响渲染过程吗?

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

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

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

1.使用副作用的后果

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

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

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

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

StrictMode - 检测意外的副作用

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

2. 遵循反应原则

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

关注点分离:

setCount除了设置count之外什么都不做。

编写声明性代码:

通常,您应该编写声明式代码,而不是命令式代码。

  • 即你的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
  • 即你应该写“B应该是价值X,取决于A”而不是“改变A,然后改变B”

在某些情况下,React 对您的副作用一无所知,因此您需要自己注意保持一致的状态。

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

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

副作用问题的示例

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

export function DoSideEffect(){
  const [ A, setA ] = useState(0);
  const [ B, setB ] = useState(0);

  return <div>
    <button onClick={ () => {
      setA( prevA => {                // <-- setA might be called multiple times, with the same value for prevA
        setB( prevB => prevB + 1 );   // <-- setB might be called multiple times, with a _different_ value for prevB
        return prevA + 1;
      } );
    } }>set count</button>
    { A } / { B }
  </div>;
}

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

export function DoSideEffect(){
  const someValueRef = useRef(0);
  const [ count, setCount ] = useState(0);

  return <div>
    <button onClick={ () => {
      setCount( prevCount => {
        someValueRef.current = someValueRef.current + 1; // <-- some side effect
        return prevCount; // <-- value doesn't change, so react doesn't re-render
      } );
    } }>do side effect</button>

    <button onClick={ () => {
      setCount(prevCount => prevCount + 1 );
    } }>set count</button>

    <span>{ count } / {
      someValueRef.current // <-- react doesn't necessarily display the current value
    }</span>
  </div>;
}

不,不能从状态更新函数发出副作用,它被视为纯函数

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

你可能会也可能不会使用React.StrictMode组件,但它是一种帮助检测意外副作用的方法。

检测意外的副作用

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

  • 渲染阶段确定需要对 DOM 等进行哪些更改。 在这个阶段,React 调用render ,然后将结果与之前的渲染进行比较。
  • 提交阶段是 React 应用任何更改的时候。 (在 React DOM 的情况下,这是 React 插入、更新和删除 DOM 节点的时候。)在这个阶段,React 还会调用诸如componentDidMountcomponentDidUpdate之类的生命周期。

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

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

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

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

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

  • 类组件constructorrendershouldComponentUpdate方法
  • 类组件静态getDerivedStateFromProps方法
  • 功能组件体
  • 状态更新函数( setState的第一个参数) <--
  • 传递给useStateuseMemouseReducer的函数

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

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

例子:

const setSomething = (val) => {
  setValue((prev) => {
    return prev + val;
  });
  fn();
  dispatch(action);
};

我不会

仅仅因为它有效并不意味着它是一个好主意。 您共享的代码示例将起作用,但我不会这样做。

将不相关的逻辑放在一起会使下一个必须使用此代码的人感到困惑; 很多时候,那个“下一个人”就是:六个月后的你,因为你完成了这个功能并继续前进,你已经忘记了所有关于这段代码的事情。 而现在你回来发现,一些银器已经存放在浴室的药柜里,一些床单在洗碗机里,所有的盘子都在一个标有“DVD”的盒子里。

我不知道您对发布的特定代码示例有多认真,但如果它是相关的:如果您正在使用dispatch ,这意味着您已经设置了某种减速器,或者使用useReducer钩子,或者可能使用还原。 如果这是真的,你可能应该考虑这个布尔值是否也属于你的 Redux 存储:

const [ value, setValue ] = useState(false)

function setSomething(val) {
  fn()
  dispatch({ ...action, val })
}

(但它可能不会,这很好!)

如果你使用的是真正的 Redux,你也会有 action-creators,这通常是放置触发副作用的代码的正确位置。

无论您使用哪种状态技术,我认为您应该更愿意避免将副作用代码放入您的各个组件中。 原因是组件通常应该是可重用的,但是如果您将副作用放入组件中,该副作用对于显示或与组件可视化的事物的交互不是必需的,那么您只会让其他人更难调用者使用此组件。

如果副作用对于该组件的工作方式至关重要,那么更好的处理方法是直接调用setValue和副作用函数,而不是将它们包装在一起。 毕竟,您实际上并不依赖 useState 回调来完成您的副作用。

const [ value, setValue ] = useState(false)

function setSomething(val) {
  setValue(value + val)
  fn()
  dispatch(action)
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM