[英]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.) (我记得见过一些特殊的用例,其中将一些代码放在更新程序函数中据说是唯一的解决方案,但我不记得它是什么。我希望在评论中提供一个示例。)
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.
...因为上述方法可能会被多次调用,所以它们不包含副作用很重要。 ...
...
You should not put side effects inside the updater function, because it validates some principles, like separation of concerns and writing declarative code.您不应该将副作用放在更新程序函数中,因为它验证了一些原则,例如关注点分离和编写声明性代码。
setCount
should do nothing but setting the count
. setCount
除了设置count
之外什么都不做。
Generally, you should write your code declarative, not imperative .通常,您应该编写声明式代码,而不是命令式代码。
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,并且您的代码变得不那么可靠。
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 ):例如,在这段代码中,您会期望
A
和B
始终相同,但它可能会给您带来意想不到的结果,例如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 .不,不能从状态更新函数发出副作用,它被视为纯函数。
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
andcomponentDidUpdate
during this phase.(在 React DOM 的情况下,这是 React 插入、更新和删除 DOM 节点的时候。)在这个阶段,React 还会调用诸如
componentDidMount
和componentDidUpdate
之类的生命周期。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
(orUNSAFE_componentWillMount
)componentWillMount
(或UNSAFE_componentWillMount
)componentWillReceiveProps
(orUNSAFE_componentWillReceiveProps
)componentWillReceiveProps
(或UNSAFE_componentWillReceiveProps
)componentWillUpdate
(orUNSAFE_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
, andshouldComponentUpdate
methods类组件
constructor
、render
和shouldComponentUpdate
方法- Class component static
getDerivedStateFromProps
method类组件静态
getDerivedStateFromProps
方法- Function component bodies
功能组件体
- State updater functions (the first argument to
setState
) <--状态更新函数(
setState
的第一个参数) <--- Functions passed to
useState
,useMemo
, oruseReducer
传递给
useState
、useMemo
或useReducer
的函数
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);
};
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.