在 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
回调中调用副作用以编程方式是否符合反应原则?它会以某种方式影响渲染过程吗?
我不会
仅仅因为它有效并不意味着它是个好主意。您分享的代码示例可以运行,但我不会这样做。
将不相关的逻辑放在一起会使下一个必须使用此代码的人感到困惑;很多时候,那个“下一个人”是 你:你,从现在起六个月后,在你因为完成这个功能而忘记了所有关于这个代码之后继续前进。而现在你回来发现一些银器已经存放在浴室的药柜里,一些床单在洗碗机里,所有的盘子都在一个标有“DVD”的盒子里。
我不知道你对你发布的特定代码示例有多认真,但如果它是相关的:如果你使用 dispatch
这意味着你已经设置了某种减速器,使用 useReducer
挂钩,或者可能使用 Redux。如果 那是 true,您可能应该考虑这个布尔值是否也属于您的 Redux 存储:
const [ value, setValue ] = useState(false)
function setSomething(val) {
fn()
dispatch({ ...action, val })
}
(但也可能不是,没关系!)
如果您使用的是真正的 Redux,您还会有 action-creators,这通常是放置触发副作用的代码的正确位置。
无论您使用何种状态技术,我认为您应该避免将 side-effect 代码放入您的各个组件中。原因是组件通常应该是可重用的,但是如果您将 side-effect 放入组件中,而该组件对于组件可视化的显示或交互不是必需的,那么您只会让它变得更难供其他呼叫者使用此组件。
如果 side-effect 是 这个组件如何工作所必需的,那么更好的处理方法是调用 setValue
和 side-effect 直接运行而不是将它们包装在一起。毕竟,您实际上并不依赖 useState 回调来完成您的 side-effect.
const [ value, setValue ] = useState(false)
function setSomething(val) {
setValue(value + val)
fn()
dispatch(action)
}
不能在 updater function 中使用 任何 副作用。它 可能 影响渲染过程,具体取决于具体的副作用。
不符合 React 原则(关注点分离,声明式代码)。
(我记得曾经见过一些特殊的用例,其中将一些代码放入更新程序函数中是唯一的解决方案,但我不记得它是什么了。
通过重构代码也可能有更好的解决方案。)
1。使用副作用的后果
不能使用 任何 副作用,原因与您不应该在 useEffect 之外的其他任何地方使用副作用的原因相同。
一些副作用可能会影响渲染过程,其他副作用可能工作正常(技术上),但你不应该依赖发生的事情 在 setter 函数中。
React 保证如果您调用 setState( prev => prev + 1 )
,那么 state
现在会比以前多一个。
React 不保证幕后会发生什么来实现这个目标。 React 可能会以任何顺序多次调用这些 setter 函数,或者根本不调用。
例子
例如在此代码中,您会期望 A
和 B
始终相同,但它可能会给您带来 意外结果 ,例如 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>;
}
例如这不会在副作用后显示 当前值 ,直到组件由于某些其他原因 re-rendered,例如增加 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
.
正在编写声明性代码:
通常,您应该编写代码 。
IE。你的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
IE。你应该写 "B 应该是值 X,依赖于 A" 而不是 "改变 A,然后改变 B"
在某些情况下,React 对您的副作用“一无所知”,因此您需要自己注意一致的状态。
有时候你无法避免写一些命令式代码。
useEffect
可以帮助您保持状态一致,例如,将一些命令式代码与某些状态相关联,又名。 “指定依赖关系”。
如果您不使用 useEffect
,您仍然可以编写工作代码,但您只是没有使用 React 为此目的提供的工具。您没有按照应有的方式使用 React,您的代码变得不那么可靠了。
不,从状态更新函数发出 side-effects 是不行的,它被认为是 pure function。
- 函数 return 值对于相同的参数是相同的(局部静态变量、non-local 变量、可变引用参数或输入流没有变化),并且
- 函数应用没有副作用(局部静态变量、non-local 变量、可变引用参数或 input/output 流没有变化)。
您可能会也可能不会使用 React.StrictMode
component, but it's a method to help detect unexpected side effects。
Detecting unexpected side effects
Conceptually, React does work in two phases:
- The render phase determines what changes need to be made to
e.g. the DOM. During this phase, React calls
render
and then
compares the result to the previous render.
- The commit phase is
when React applies any changes. (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.
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).
Render phase lifecycles include the following class component methods:
constructor
componentWillMount
(or UNSAFE_componentWillMount
)
componentWillReceiveProps
(or UNSAFE_componentWillReceiveProps
)
componentWillUpdate
(or UNSAFE_componentWillUpdate
)
getDerivedStateFromProps
shouldComponentUpdate
render
setState
updater functions (the first argument) <--
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
- Class component static
getDerivedStateFromProps
method
- Function component bodies
- State updater functions (the first argument to
setState
) <--
- Functions passed to
useState
, useMemo
, or useReducer
从关于有意 double-invoking 状态更新函数的两个突出显示的要点中获取线索,并将状态更新函数视为纯函数。
对于您分享的代码片段,我认为没有理由从更新程序回调中调用函数。他们 could/should 在 外部 回调中被调用。
示例:
const setSomething = (val) => {
setValue((prev) => {
return prev + val;
});
fn();
dispatch(action);
};
设想情况:
const [value, setValue] = useState(false);
const setSomething = (val) => {
setValue((prev) => {
fn(); dispatch(action); // or any other side effect
return prev + val;
});
};
在 useState
回调中调用副作用以编程方式是否符合反应原则?它会以某种方式影响渲染过程吗?
我不会
仅仅因为它有效并不意味着它是个好主意。您分享的代码示例可以运行,但我不会这样做。
将不相关的逻辑放在一起会使下一个必须使用此代码的人感到困惑;很多时候,那个“下一个人”是 你:你,从现在起六个月后,在你因为完成这个功能而忘记了所有关于这个代码之后继续前进。而现在你回来发现一些银器已经存放在浴室的药柜里,一些床单在洗碗机里,所有的盘子都在一个标有“DVD”的盒子里。
我不知道你对你发布的特定代码示例有多认真,但如果它是相关的:如果你使用 dispatch
这意味着你已经设置了某种减速器,使用 useReducer
挂钩,或者可能使用 Redux。如果 那是 true,您可能应该考虑这个布尔值是否也属于您的 Redux 存储:
const [ value, setValue ] = useState(false)
function setSomething(val) {
fn()
dispatch({ ...action, val })
}
(但也可能不是,没关系!)
如果您使用的是真正的 Redux,您还会有 action-creators,这通常是放置触发副作用的代码的正确位置。
无论您使用何种状态技术,我认为您应该避免将 side-effect 代码放入您的各个组件中。原因是组件通常应该是可重用的,但是如果您将 side-effect 放入组件中,而该组件对于组件可视化的显示或交互不是必需的,那么您只会让它变得更难供其他呼叫者使用此组件。
如果 side-effect 是 这个组件如何工作所必需的,那么更好的处理方法是调用 setValue
和 side-effect 直接运行而不是将它们包装在一起。毕竟,您实际上并不依赖 useState 回调来完成您的 side-effect.
const [ value, setValue ] = useState(false)
function setSomething(val) {
setValue(value + val)
fn()
dispatch(action)
}
不能在 updater function 中使用 任何 副作用。它 可能 影响渲染过程,具体取决于具体的副作用。
不符合 React 原则(关注点分离,声明式代码)。
(我记得曾经见过一些特殊的用例,其中将一些代码放入更新程序函数中是唯一的解决方案,但我不记得它是什么了。 通过重构代码也可能有更好的解决方案。)
1。使用副作用的后果
不能使用 任何 副作用,原因与您不应该在 useEffect 之外的其他任何地方使用副作用的原因相同。
一些副作用可能会影响渲染过程,其他副作用可能工作正常(技术上),但你不应该依赖发生的事情 在 setter 函数中。
React 保证如果您调用 setState( prev => prev + 1 )
,那么 state
现在会比以前多一个。
React 不保证幕后会发生什么来实现这个目标。 React 可能会以任何顺序多次调用这些 setter 函数,或者根本不调用。
例子
例如在此代码中,您会期望 A
和 B
始终相同,但它可能会给您带来 意外结果 ,例如 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>;
}
例如这不会在副作用后显示 当前值 ,直到组件由于某些其他原因 re-rendered,例如增加 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
.
正在编写声明性代码:
通常,您应该编写代码
IE。你的代码应该“描述”状态应该是什么,而不是一个接一个地调用函数。
IE。你应该写 "B 应该是值 X,依赖于 A" 而不是 "改变 A,然后改变 B"
在某些情况下,React 对您的副作用“一无所知”,因此您需要自己注意一致的状态。
有时候你无法避免写一些命令式代码。
useEffect
可以帮助您保持状态一致,例如,将一些命令式代码与某些状态相关联,又名。 “指定依赖关系”。
如果您不使用 useEffect
,您仍然可以编写工作代码,但您只是没有使用 React 为此目的提供的工具。您没有按照应有的方式使用 React,您的代码变得不那么可靠了。
不,从状态更新函数发出 side-effects 是不行的,它被认为是 pure function。
- 函数 return 值对于相同的参数是相同的(局部静态变量、non-local 变量、可变引用参数或输入流没有变化),并且
- 函数应用没有副作用(局部静态变量、non-local 变量、可变引用参数或 input/output 流没有变化)。
您可能会也可能不会使用 React.StrictMode
component, but it's a method to help detect unexpected side effects。
Detecting unexpected side effects
Conceptually, React does work in two phases:
- The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls
render
and then compares the result to the previous render.- The commit phase is when React applies any changes. (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.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).
Render phase lifecycles include the following class component methods:
constructor
componentWillMount
(orUNSAFE_componentWillMount
)componentWillReceiveProps
(orUNSAFE_componentWillReceiveProps
)componentWillUpdate
(orUNSAFE_componentWillUpdate
)getDerivedStateFromProps
shouldComponentUpdate
render
setState
updater functions (the first argument) <--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- Class component static
getDerivedStateFromProps
method- Function component bodies
- State updater functions (the first argument to
setState
) <--- Functions passed to
useState
,useMemo
, oruseReducer
从关于有意 double-invoking 状态更新函数的两个突出显示的要点中获取线索,并将状态更新函数视为纯函数。
对于您分享的代码片段,我认为没有理由从更新程序回调中调用函数。他们 could/should 在 外部 回调中被调用。
示例:
const setSomething = (val) => {
setValue((prev) => {
return prev + val;
});
fn();
dispatch(action);
};