从组件中的 useState 多次调用状态更新程序会导致多次重新渲染
Multiple calls to state updater from useState in component causes multiple re-renders
我第一次尝试 React hooks 一切都很好,直到我意识到当我获取数据并更新两个不同的状态变量(数据和加载标志)时,我的组件(一个数据 table ) 被渲染两次,即使对状态更新器的两次调用都发生在同一个函数中。这是我的 api 函数,它将两个变量返回到我的组件。
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
在普通的 class 组件中,您只需调用一次即可更新状态,这可能是一个复杂的对象,但 "hooks way" 似乎是将状态拆分为更小的单元,一个当它们分别更新时,其副作用似乎是多次重新渲染。有什么想法可以缓解这种情况吗?
您可以将 loading
状态和 data
状态合并到一个状态对象中,然后您可以执行一个 setState
调用,并且只会有一个渲染。
注意:与class组件中的setState
不同,useState
返回的setState
不合并对象在现有状态下,它完全替换了对象。如果你想进行合并,你需要自己读取以前的状态并将其与新值合并。参考docs.
在您确定存在性能问题之前,我不会太担心过度调用渲染。渲染(在 React 上下文中)和将虚拟 DOM 更新提交给真实的 DOM 是不同的事情。这里的渲染是指生成虚拟DOM,而不是更新浏览器DOM。 React 可能会批处理 setState
调用并使用最终的新状态更新浏览器 DOM。
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
备选方案 - 编写您自己的状态合并钩子
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
在 react-hooks 中批量更新 https://github.com/facebook/react/issues/14259
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.
补充一点回答
酷!对于那些计划使用这个钩子的人来说,可以用一种更健壮的方式来编写它来使用函数作为参数,比如这样:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
更新: 优化版本,当传入的部分状态没有改变时,状态不会被修改。
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
这还有另一个使用useReducer
的解决方案!首先我们定义新的 setState
.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
之后我们可以简单地像旧的 类 this.setState
一样使用它,只是没有 this
!
setState({loading: false, data: test.data.results})
正如您在我们的新 setState
中注意到的那样(就像我们之前对 this.setState
所做的那样),我们不需要一起更新所有状态!例如,我可以像这样改变我们的状态之一(它不会改变其他状态!):
setState({loading: false})
太棒了,哈?!
所以让我们把所有的部分放在一起:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript 支持。
感谢 P. Galbraith 回复此解决方案,
使用打字稿的可以使用这个:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
其中 MyState
是您的状态对象的类型。
例如在我们的例子中,它将是这样的:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
以前的状态支持。
在评论中 user2420374 要求一种方法来访问我们 setState
中的 prevState
,所以这里有一种方法可以实现这个目标:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction
检查传递的参数是函数(这意味着您正在尝试访问 prevState)还是普通对象。你可以找到 this implementation of isFunction by Alex Grande here.
通知。对于那些想大量使用这个答案的人,我决定把它变成一个图书馆。你可以在这里找到它:
这样做:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
如果你使用的是第三方钩子,无法将状态合并到一个对象中或使用useReducer
,那么解决方案是使用:
ReactDOM.unstable_batchedUpdates(() => { ... })
丹·阿布拉莫夫推荐here
看到这个example
要从 class 组件复制 this.setState
合并行为,
useReducer
:
的 React 文档 recommend to use the functional form of useState
with object spread -
setState(prevState => {
return {...prevState, loading, data};
});
这两个状态现在合并为一个,这将为您节省一个渲染周期。
一个状态对象还有另一个优点:loading
和 data
是 dependent 状态。当状态放在一起时,无效的状态更改会变得更加明显:
setState({ loading: true, data }); // ups... loading, but we already set data
您甚至可以通过 1.) 使状态更好 ensure consistent states - loading
、success
、error
等 - explicit 在你的状态和 2.) 使用 useReducer
将状态逻辑封装在 reducer 中:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
PS:确保 prefix 使用 use
自定义 Hooks(useData
而不是 getData
)。也传递给 useEffect
的回调不能是 async
.
您还可以使用 useEffect
来检测状态变化,并相应地更新其他状态值
除了 Yangshun Tay's 之外,您最好记住 setMergedState
函数,这样它将 return 每次渲染时引用相同的引用而不是新函数。如果 TypeScript linter 强制您将 setMergedState
作为父组件中的 useCallback
或 useEffect
的依赖项传递,这可能是至关重要的。
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};
我第一次尝试 React hooks 一切都很好,直到我意识到当我获取数据并更新两个不同的状态变量(数据和加载标志)时,我的组件(一个数据 table ) 被渲染两次,即使对状态更新器的两次调用都发生在同一个函数中。这是我的 api 函数,它将两个变量返回到我的组件。
const getData = url => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setLoading(false);
setData(test.data.results);
}
}, []);
return { data, loading };
};
在普通的 class 组件中,您只需调用一次即可更新状态,这可能是一个复杂的对象,但 "hooks way" 似乎是将状态拆分为更小的单元,一个当它们分别更新时,其副作用似乎是多次重新渲染。有什么想法可以缓解这种情况吗?
您可以将 loading
状态和 data
状态合并到一个状态对象中,然后您可以执行一个 setState
调用,并且只会有一个渲染。
注意:与class组件中的setState
不同,useState
返回的setState
不合并对象在现有状态下,它完全替换了对象。如果你想进行合并,你需要自己读取以前的状态并将其与新值合并。参考docs.
在您确定存在性能问题之前,我不会太担心过度调用渲染。渲染(在 React 上下文中)和将虚拟 DOM 更新提交给真实的 DOM 是不同的事情。这里的渲染是指生成虚拟DOM,而不是更新浏览器DOM。 React 可能会批处理 setState
调用并使用最终的新状态更新浏览器 DOM。
const {useState, useEffect} = React;
function App() {
const [userRequest, setUserRequest] = useState({
loading: false,
user: null,
});
useEffect(() => {
// Note that this replaces the entire object and deletes user key!
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
备选方案 - 编写您自己的状态合并钩子
const {useState, useEffect} = React;
function useMergeState(initialState) {
const [state, setState] = useState(initialState);
const setMergedState = newState =>
setState(prevState => Object.assign({}, prevState, newState)
);
return [state, setMergedState];
}
function App() {
const [userRequest, setUserRequest] = useMergeState({
loading: false,
user: null,
});
useEffect(() => {
setUserRequest({ loading: true });
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUserRequest({
loading: false,
user: data.results[0],
});
});
}, []);
const { loading, user } = userRequest;
return (
<div>
{loading && 'Loading...'}
{user && user.name.first}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
在 react-hooks 中批量更新 https://github.com/facebook/react/issues/14259
React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.
补充一点回答
酷!对于那些计划使用这个钩子的人来说,可以用一种更健壮的方式来编写它来使用函数作为参数,比如这样:
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newState =>
typeof newState == "function"
? setState(prevState => ({ ...prevState, ...newState(prevState) }))
: setState(prevState => ({ ...prevState, ...newState }));
return [state, setMergedState];
};
更新: 优化版本,当传入的部分状态没有改变时,状态不会被修改。
const shallowPartialCompare = (obj, partialObj) =>
Object.keys(partialObj).every(
key =>
obj.hasOwnProperty(key) &&
obj[key] === partialObj[key]
);
const useMergedState = initial => {
const [state, setState] = React.useState(initial);
const setMergedState = newIncomingState =>
setState(prevState => {
const newState =
typeof newIncomingState == "function"
? newIncomingState(prevState)
: newIncomingState;
return shallowPartialCompare(prevState, newState)
? prevState
: { ...prevState, ...newState };
});
return [state, setMergedState];
};
这还有另一个使用useReducer
的解决方案!首先我们定义新的 setState
.
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
之后我们可以简单地像旧的 类 this.setState
一样使用它,只是没有 this
!
setState({loading: false, data: test.data.results})
正如您在我们的新 setState
中注意到的那样(就像我们之前对 this.setState
所做的那样),我们不需要一起更新所有状态!例如,我可以像这样改变我们的状态之一(它不会改变其他状态!):
setState({loading: false})
太棒了,哈?!
所以让我们把所有的部分放在一起:
import {useReducer} from 'react'
const getData = url => {
const [state, setState] = useReducer(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null}
)
useEffect(async () => {
const test = await api.get('/people')
if(test.ok){
setState({loading: false, data: test.data.results})
}
}, [])
return state
}
Typescript 支持。 感谢 P. Galbraith 回复此解决方案, 使用打字稿的可以使用这个:
useReducer<Reducer<MyState, Partial<MyState>>>(...)
其中 MyState
是您的状态对象的类型。
例如在我们的例子中,它将是这样的:
interface MyState {
loading: boolean;
data: any;
something: string;
}
const [state, setState] = useReducer<Reducer<MyState, Partial<MyState>>>(
(state, newState) => ({...state, ...newState}),
{loading: true, data: null, something: ''}
)
以前的状态支持。
在评论中 user2420374 要求一种方法来访问我们 setState
中的 prevState
,所以这里有一种方法可以实现这个目标:
const [state, setState] = useReducer(
(state, newState) => {
newWithPrevState = isFunction(newState) ? newState(state) : newState
return (
{...state, ...newWithPrevState}
)
},
initialState
)
// And then use it like this...
setState(prevState => {...})
isFunction
检查传递的参数是函数(这意味着您正在尝试访问 prevState)还是普通对象。你可以找到 this implementation of isFunction by Alex Grande here.
通知。对于那些想大量使用这个答案的人,我决定把它变成一个图书馆。你可以在这里找到它:
这样做:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
如果你使用的是第三方钩子,无法将状态合并到一个对象中或使用useReducer
,那么解决方案是使用:
ReactDOM.unstable_batchedUpdates(() => { ... })
丹·阿布拉莫夫推荐here
看到这个example
要从 class 组件复制 this.setState
合并行为,
useReducer
:
useState
with object spread - setState(prevState => {
return {...prevState, loading, data};
});
这两个状态现在合并为一个,这将为您节省一个渲染周期。
一个状态对象还有另一个优点:loading
和 data
是 dependent 状态。当状态放在一起时,无效的状态更改会变得更加明显:
setState({ loading: true, data }); // ups... loading, but we already set data
您甚至可以通过 1.) 使状态更好 ensure consistent states - loading
、success
、error
等 - explicit 在你的状态和 2.) 使用 useReducer
将状态逻辑封装在 reducer 中:
const useData = () => {
const [state, dispatch] = useReducer(reducer, /*...*/);
useEffect(() => {
api.get('/people').then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// keep state consistent, e.g. reset data, if loading
else if (status === "loading") return { ...state, data: undefined, status };
return state;
};
const App = () => {
const { data, status } = useData();
return status === "loading" ? <div> Loading... </div> : (
// success, display data
)
}
const useData = () => {
const [state, dispatch] = useReducer(reducer, {
data: undefined,
status: "loading"
});
useEffect(() => {
fetchData_fakeApi().then(test => {
if (test.ok) dispatch(["success", test.data.results]);
});
}, []);
return state;
};
const reducer = (state, [status, payload]) => {
if (status === "success") return { ...state, data: payload, status };
// e.g. make sure to reset data, when loading.
else if (status === "loading") return { ...state, data: undefined, status };
else return state;
};
const App = () => {
const { data, status } = useData();
const count = useRenderCount();
const countStr = `Re-rendered ${count.current} times`;
return status === "loading" ? (
<div> Loading (3 sec)... {countStr} </div>
) : (
<div>
Finished. Data: {JSON.stringify(data)}, {countStr}
</div>
);
}
//
// helpers
//
const useRenderCount = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return renderCount;
};
const fetchData_fakeApi = () =>
new Promise(resolve =>
setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
);
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
PS:确保 prefix 使用 use
自定义 Hooks(useData
而不是 getData
)。也传递给 useEffect
的回调不能是 async
.
您还可以使用 useEffect
来检测状态变化,并相应地更新其他状态值
除了 Yangshun Tay's setMergedState
函数,这样它将 return 每次渲染时引用相同的引用而不是新函数。如果 TypeScript linter 强制您将 setMergedState
作为父组件中的 useCallback
或 useEffect
的依赖项传递,这可能是至关重要的。
import {useCallback, useState} from "react";
export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
const [state, setState] = useState(initialState);
const setMergedState = useCallback((newState: Partial<T>) =>
setState(prevState => ({
...prevState,
...newState
})), [setState]);
return [state, setMergedState];
};