简体   繁体   English

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

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

Imagine situation:想象一下情况:

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

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

Is it programmatically okey and fine with react principles to call side effects inside useState callback?useState回调中调用副作用的反应原则在编程上是否可行且很好? May it affect the render process somehow?它会以某种方式影响渲染过程吗?

It is not ok to use side effects inside the updater function .updater 函数中使用副作用是不行的。 It might affect the render process, depending on the specific side effect.可能会影响渲染过程,具体取决于特定的副作用。

It is not fine with react principles (separation of concerns, declarative code).反应原则(关注点分离,声明性代码)并不好

(I remember to have seen some exceptional use cases where putting some code inside the updater function was said to be the only solution, but I can't remember what it was. I'd appreciate an example in the comments.) (我记得见过一些特殊的用例,其中将一些代码放在更新程序函数中据说是唯一的解决方案,但我不记得它是什么。我希望在评论中提供一个示例。)

1. Consequences of using side effects 1.使用副作用的后果

It is not ok to use side effects, basically for the same reasons why you shouldn't use side effects outside useEffect anywhere else.使用副作用是不行的,基本上与你不应该在 useEffect 之外的任何其他地方使用副作用的原因相同。

Some side effects might affect the render process, other side effects might work fine (technically), but you are not supposed to rely on what happens inside the setter functions.一些副作用可能会影响渲染过程,其他副作用可能会正常工作(技术上),但您应该依赖 setter 函数内部发生的事情

React guarantees that eg if you call setState( prev => prev + 1 ) , then state would now be one more than before. React保证,例如,如果你调用setState( prev => prev + 1 ) ,那么state现在会比以前多一个。

React does not guarantee what will happen behind the scenes to achieve that goal. React 不保证为了实现这个目标会在幕后发生什么。 React might call these setter functions multiple times , or not at all, and in any order: React 可能会多次调用这些 setter 函数,或者根本不调用,并且以任意顺序:

StrictMode - Detecting unexpected side effects StrictMode - 检测意外的副作用

... Because the above methods might be called more than once, it's important that they do not contain side-effects. ...因为上述方法可能会被多次调用,所以它们不包含副作用很重要。 ... ...

2. following react principles 2. 遵循反应原则

You should not put side effects inside the updater function, because it validates some principles, like separation of concerns and writing declarative code.您不应该将副作用放在更新程序函数中,因为它验证了一些原则,例如关注点分离和编写声明性代码。

Separation of concerns:关注点分离:

setCount should do nothing but setting the count . setCount除了设置count之外什么都不做。

Writing declarative code:编写声明性代码:

Generally, you should write your code declarative, not imperative .通常,您应该编写声明式代码,而不是命令式代码。

  • Ie your code should "describe" what the state should be, instead of calling functions one after another.即你的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
  • Ie you should write "B should be of value X, dependent on A" instead of "Change A, then change B"即你应该写“B应该是价值X,取决于A”而不是“改变A,然后改变B”

In some cases React doesn't "know" anything about your side effects, so you need to take care about a consistent state yourself.在某些情况下,React 对您的副作用一无所知,因此您需要自己注意保持一致的状态。

Sometimes you can not avoid writing some imperative code.有时你无法避免编写一些命令式代码。

useEffect is there to help you with keeping the state consistent, by allowing you to eg relate some imperative code to some state, aka. useEffect可以帮助您保持状态一致,例如允许您将某些命令式代码与某些状态相关联,也就是。 "specifying dependencies". “指定依赖关系”。 If you don't use useEffect , you can still write working code, but you are just not using the tools react is providing for this purpose .如果您不使用useEffect ,您仍然可以编写工作代码,但您只是没有使用 react 为此目的提供的工具 You are not using React the way it is supposed to be used, and your code becomes less reliable.您没有按照应有的方式使用 React,并且您的代码变得不那么可靠。

Examples for problems with side effects副作用问题的示例

Eg in this code you would expect that A and B are always identical, but it might give you unexpected results , like B being increased by 2 instead of 1 (eg when in DEV mode and strict mode ):例如,在这段代码中,您会期望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>;
}

Eg this would not display the current value after the side effect, until the component is re-rendered for some other reason, like increasing the count :例如,这不会在副作用之后显示当前值,直到组件由于某些其他原因重新渲染,例如增加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>;
}

No, it is not ok to issue side-effects from a state updater function, it is to be considered a pure function .不,不能从状态更新函数发出副作用,它被视为纯函数

  1. The function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and对于相同的参数,函数返回值是相同的(局部静态变量、非局部变量、可变引用参数或输入流没有变化),并且
  2. The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).函数应用程序没有副作用(局部静态变量、非局部变量、可变引用参数或输入/输出流没有突变)。

You may, or may not, be using the React.StrictMode component, but it's a method to help detect unexpected side effects .你可能会也可能不会使用React.StrictMode组件,但它是一种帮助检测意外副作用的方法。

Detecting unexpected side effects检测意外的副作用

Conceptually, React does work in two phases:从概念上讲,React 确实分两个阶段工作:

  • The render phase determines what changes need to be made to eg the DOM.渲染阶段确定需要对 DOM 等进行哪些更改。 During this phase, React calls render and then compares the result to the previous render.在这个阶段,React 调用render ,然后将结果与之前的渲染进行比较。
  • The commit phase is when React applies any changes.提交阶段是 React 应用任何更改的时候。 (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase. (在 React DOM 的情况下,这是 React 插入、更新和删除 DOM 节点的时候。)在这个阶段,React 还会调用诸如componentDidMountcomponentDidUpdate之类的生命周期。

The commit phase is usually very fast, but rendering can be slow.提交阶段通常非常快,但渲染可能很慢。 For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser.出于这个原因,即将到来的并发模式(默认情况下尚未启用)将渲染工作分成几部分,暂停和恢复工作以避免阻塞浏览器。 This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).这意味着 React 可能在提交之前多次调用渲染阶段生命周期,或者它可能在根本不提交的情况下调用它们(因为错误或更高优先级的中断)。

Render phase lifecycles include the following class component methods:渲染阶段生命周期包括以下类组件方法:

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

Because the above methods might be called more than once, it's important that they do not contain side-effects.因为上述方法可能会被多次调用,所以它们不包含副作用很重要。 Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state.忽略此规则会导致各种问题,包括内存泄漏和无效的应用程序状态。 Unfortunately, it can be difficult to detect these problems as they can often be non-deterministic.不幸的是,很难检测到这些问题,因为它们通常是不确定的。

Strict mode can't automatically detect side effects for you, but it can help you spot them by making them a little more deterministic.严格模式不能自动为您检测副作用,但可以通过使它们更具确定性来帮助您发现它们。 This is done by intentionally double-invoking the following functions:这是通过有意双重调用以下函数来完成的:

  • Class component constructor , render , and shouldComponentUpdate methods类组件constructorrendershouldComponentUpdate方法
  • Class component static getDerivedStateFromProps method类组件静态getDerivedStateFromProps方法
  • Function component bodies功能组件体
  • State updater functions (the first argument to setState ) <--状态更新函数( setState的第一个参数) <--
  • Functions passed to useState , useMemo , or useReducer传递给useStateuseMemouseReducer的函数

Take a cue from the two highlighted bullet points regarding the intentional double-invoking of state updater functions and treat the state updater functions as pure functions.从两个突出显示的要点中获取关于有意双重调用状态更新器函数的提示,并将状态更新器函数视为纯函数。

For the code snippet you shared, I see no reason at all for the functions to be called from within the updater callback.对于您共享的代码片段,我认为没有理由从更新程序回调中调用函数。 They could/should be called outside the callback.他们可以/应该在回调之外被调用。

Example:例子:

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

I would not我不会

Just because it works doesn't mean it's a good idea.仅仅因为它有效并不意味着它是一个好主意。 The code sample you shared will function, but I wouldn't do it.您共享的代码示例将起作用,但我不会这样做。

Putting unrelated logic together will confuse the next person who has to work with this code;将不相关的逻辑放在一起会使下一个必须使用此代码的人感到困惑; very often, that "next person" is you : you, six months from now, after you've forgotten all about this code because you finished this feature and moved on.很多时候,那个“下一个人”就是:六个月后的你,因为你完成了这个功能并继续前进,你已经忘记了所有关于这段代码的事情。 And now you come back and discover that some of the silverware has been stored in the bathroom medicine cabinet, and some of the linens are in the dishwasher, and all the plates are in a box labeled "DVDs".而现在你回来发现,一些银器已经存放在浴室的药柜里,一些床单在洗碗机里,所有的盘子都在一个标有“DVD”的盒子里。

I don't know how serious you are about the specific code sample you posted, but in case it's relevant: if you're using dispatch that means you've set up some kind of reducer, either with the useReducer hook, or possibly with Redux.我不知道您对发布的特定代码示例有多认真,但如果它是相关的:如果您正在使用dispatch ,这意味着您已经设置了某种减速器,或者使用useReducer钩子,或者可能使用还原。 If that's true, you should probably consider whether this boolean belongs in your Redux store, too:如果这是真的,你可能应该考虑这个布尔值是否也属于你的 Redux 存储:

const [ value, setValue ] = useState(false)

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

(But it might not, and that's fine!) (但它可能不会,这很好!)

If you're using actual Redux, you'll also have action-creators, and that's generally the correct place to put code that triggers side effects.如果你使用的是真正的 Redux,你也会有 action-creators,这通常是放置触发副作用的代码的正确位置。

Regardless of whatever state tech you're using, I think you should prefer to avoid putting side-effect code into your individual components.无论您使用哪种状态技术,我认为您应该更愿意避免将副作用代码放入您的各个组件中。 The reason is that components are generally supposed to be reusable, but if you put a side-effect into the component that is not essential to display or interaction of the thing being visualized by the component, then you've just made it harder for other callers to use this component.原因是组件通常应该是可重用的,但是如果您将副作用放入组件中,该副作用对于显示或与组件可视化的事物的交互不是必需的,那么您只会让其他人更难调用者使用此组件。

If the side-effect is essential to how this component works, then a better way to handle this would be to call setValue and the side-effect function directly instead of wrapping them up together.如果副作用对于该组件的工作方式至关重要,那么更好的处理方法是直接调用setValue和副作用函数,而不是将它们包装在一起。 After all, you don't actually depend on the useState callback to accomplish your side-effect.毕竟,您实际上并不依赖 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