为什么 useReducer 的调度会导致重新渲染?
Why is useReducer's dispatch causing re-renders?
假设我像这样实现一个简单的全局加载状态:
// hooks/useLoading.js
import React, { createContext, useContext, useReducer } from 'react';
const Context = createContext();
const { Provider } = Context;
const initialState = {
isLoading: false,
};
function reducer(state, action) {
switch (action.type) {
case 'SET_LOADING_ON': {
return {
...state,
isLoading: true,
};
}
case 'SET_LOADING_OFF': {
return {
...state,
isLoading: false,
};
}
}
}
export const actionCreators = {
setLoadingOn: () => ({
type: 'SET_LOADING_ON',
}),
setLoadingOff: () => ({
type: 'SET_LOADING_OFF',
}),
};
export const LoadingProvider = ({ children }) => {
const [{ isLoading }, dispatch] = useReducer(reducer, initialState);
return <Provider value={{ isLoading, dispatch }}>{children}</Provider>;
};
export default () => useContext(Context);
然后假设我有一个组件会改变加载状态,但从不使用它,如下所示:
import React from 'react';
import useLoading, { actionCreators } from 'hooks/useLoading';
export default () => {
const { dispatch } = useLoading();
dispatch(actionCreators.setLoadingOn();
doSomethingAsync().then(() => dispatch(actionCreators.setLoadingOff()))
return <React.Fragment />;
};
根据 useReducer 文档,dispatch 具有稳定的身份。我将其解释为当一个组件从 useReducer 中提取调度时,当连接到该调度的状态发生变化时它不会重新渲染,因为对调度的引用将始终相同。基本上dispatch可以"treated like a static value".
然而,当此代码运行时,行 dispatch(actionCreators.setLoadingOn())
触发了对全局状态的更新,并且 useLoading
挂钩再次 运行,dispatch(actionCreators.setLoadingOn())
[=25] =](无限重新渲染 -_-)
我是不是没有正确理解useReducer?或者我正在做的其他事情可能会导致无限重新渲染?
第一个问题是您永远不应该在渲染时触发任何 React 状态更新,包括 useReducers
的 dispatch()
和 useState
的设置器。
第二个问题是,是的,分派 while 总是导致 React 排队状态更新并尝试调用 reducer,如果 reducer returns 一个新值,React 将继续重新渲染。不管你从哪个组件发送 - 引起状态更新和重新渲染是 useReducer
首先的重点。
"stable identity" 意味着 dispatch
变量将在渲染中指向相同的函数引用。
除了已经指出的在渲染时设置状态之外,我想我可以阐明如何利用 dispatch 的稳定标识来避免不必要的 re-renders,就像您期望的那样。
您的 Provider 值是一个对象 (value={{ isLoading, dispatch}})。这意味着值本身的身份将在上下文状态更改时更改(例如,当 isLoading 更改时)。因此,即使您有一个组件,您 仅 像这样使用分派:
const { dispatch } = useLoading()
当 isLoading 发生变化时,该组件将 re-render。
如果您觉得 re-rendering 已经失控,利用 dispatch stable identity 的方法是创建两个 Provider,一个用于状态(isLoading in this案例)和一个用于调度,如果你这样做,一个组件只需要像这样调度:
const dispatch = useLoadingDispatch()
当 isLoading 发生变化时,不会 re-render。
请注意,这可能是过度优化,在简单的情况下可能不值得。
这是一组很好的文章,可以进一步阅读该主题:
https://kentcdodds.com/blog/how-to-optimize-your-context-value
https://kentcdodds.com/blog/how-to-use-react-context-effectively
假设我像这样实现一个简单的全局加载状态:
// hooks/useLoading.js
import React, { createContext, useContext, useReducer } from 'react';
const Context = createContext();
const { Provider } = Context;
const initialState = {
isLoading: false,
};
function reducer(state, action) {
switch (action.type) {
case 'SET_LOADING_ON': {
return {
...state,
isLoading: true,
};
}
case 'SET_LOADING_OFF': {
return {
...state,
isLoading: false,
};
}
}
}
export const actionCreators = {
setLoadingOn: () => ({
type: 'SET_LOADING_ON',
}),
setLoadingOff: () => ({
type: 'SET_LOADING_OFF',
}),
};
export const LoadingProvider = ({ children }) => {
const [{ isLoading }, dispatch] = useReducer(reducer, initialState);
return <Provider value={{ isLoading, dispatch }}>{children}</Provider>;
};
export default () => useContext(Context);
然后假设我有一个组件会改变加载状态,但从不使用它,如下所示:
import React from 'react';
import useLoading, { actionCreators } from 'hooks/useLoading';
export default () => {
const { dispatch } = useLoading();
dispatch(actionCreators.setLoadingOn();
doSomethingAsync().then(() => dispatch(actionCreators.setLoadingOff()))
return <React.Fragment />;
};
根据 useReducer 文档,dispatch 具有稳定的身份。我将其解释为当一个组件从 useReducer 中提取调度时,当连接到该调度的状态发生变化时它不会重新渲染,因为对调度的引用将始终相同。基本上dispatch可以"treated like a static value".
然而,当此代码运行时,行 dispatch(actionCreators.setLoadingOn())
触发了对全局状态的更新,并且 useLoading
挂钩再次 运行,dispatch(actionCreators.setLoadingOn())
[=25] =](无限重新渲染 -_-)
我是不是没有正确理解useReducer?或者我正在做的其他事情可能会导致无限重新渲染?
第一个问题是您永远不应该在渲染时触发任何 React 状态更新,包括 useReducers
的 dispatch()
和 useState
的设置器。
第二个问题是,是的,分派 while 总是导致 React 排队状态更新并尝试调用 reducer,如果 reducer returns 一个新值,React 将继续重新渲染。不管你从哪个组件发送 - 引起状态更新和重新渲染是 useReducer
首先的重点。
"stable identity" 意味着 dispatch
变量将在渲染中指向相同的函数引用。
除了已经指出的在渲染时设置状态之外,我想我可以阐明如何利用 dispatch 的稳定标识来避免不必要的 re-renders,就像您期望的那样。
您的 Provider 值是一个对象 (value={{ isLoading, dispatch}})。这意味着值本身的身份将在上下文状态更改时更改(例如,当 isLoading 更改时)。因此,即使您有一个组件,您 仅 像这样使用分派:
const { dispatch } = useLoading()
当 isLoading 发生变化时,该组件将 re-render。
如果您觉得 re-rendering 已经失控,利用 dispatch stable identity 的方法是创建两个 Provider,一个用于状态(isLoading in this案例)和一个用于调度,如果你这样做,一个组件只需要像这样调度:
const dispatch = useLoadingDispatch()
当 isLoading 发生变化时,不会 re-render。
请注意,这可能是过度优化,在简单的情况下可能不值得。
这是一组很好的文章,可以进一步阅读该主题: https://kentcdodds.com/blog/how-to-optimize-your-context-value https://kentcdodds.com/blog/how-to-use-react-context-effectively