【问题标题】:Is it okey to use side effects in the useState hook callback?在 useState 钩子回调中使用副作用可以吗?
【发布时间】:2022-07-14 14:03:16
【问题描述】:

想象一下情况:

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

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

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

【问题讨论】:

  • 我认为这不是一个好主意。最好在依赖数组中用value创建一个useEffect。
  • 调度一些动作可能没问题,但我真的想不出这样做的理由。我宁愿将它全部包装在某个事件处理程序中
  • 很难。这就是为什么useEffect 是一个东西。只有当您明确 100% 准确地知道自己在做什么时,上述内容才是可以接受的。在这种情况下,你就不会问了。
  • 不能将 any 副作用放在那里,原因与您不应在 useEffect 之外的任何其他地方使用副作用的原因相同。 一些副作用可能会起作用,只是这违反了编写声明性代码和清洁依赖管理的原则。可能有特殊的用例,但它应该被视为一种解决方法。您能否提供一个“正常”反应模式(如 useEffect)不起作用(或较差)的示例?

标签: javascript reactjs react-hooks


【解决方案1】:

updater function不能使用“任何”副作用。它可能会影响渲染过程,具体取决于具体的副作用。

不适合 React 原则(关注点分离,声明性代码)。

(我记得看到过一些特殊的用例,其中将一些代码放入更新程序函数是唯一的解决方案,但我不记得它是什么了。 通过重构代码可能还有更好的解决方案。)

1。使用副作用的后果

不能使用 ANY 副作用,原因与您不应在 useEffect 之外的任何其他地方使用副作用的原因相同。

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

反应保证,例如如果您致电setState( prev => prev + 1 ),那么state 现在将比以前多一个。
React 不保证为了实现该目标会在幕后发生什么。 React 可能会以任意顺序多次调用这些 setter 函数,或者根本不调用。

示例

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

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

2。遵循反应原则

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

关注点分离:

setCount 应该只设置count

编写声明性代码:

通常,您应该编写代码declarative, not imperative
IE。您的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
IE。你应该写 "B 的值应该是 X,依赖于 A" 而不是 "改变 A,然后改变 B"

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

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

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

【讨论】:

    【解决方案2】:

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

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

    您可能会或可能不会使用React.StrictMode 组件,但这是一种帮助detect unexpected side effects 的方法。

    检测意外的副作用

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

    • render 阶段确定需要进行哪些更改 例如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

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

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

    例子:

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

    【讨论】:

      【解决方案3】:

      我不会

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

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

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

      const [ value, setValue ] = useState(false)
      
      function setSomething(val) {
        fn()
        dispatch({ ...action, val })
      }
      

      (但它可能不会,没关系!)

      如果您使用的是真正的 Redux,您还将拥有动作创建者,这通常是放置触发副作用的代码的正确位置。

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

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

      const [ value, setValue ] = useState(false)
      
      function setSomething(val) {
        setValue(value + val)
        fn()
        dispatch(action)
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-10-16
        • 2020-08-15
        • 2021-04-10
        • 2019-07-24
        • 2022-01-24
        • 1970-01-01
        • 1970-01-01
        • 2021-10-20
        相关资源
        最近更新 更多