Многократные вызовы для обновления состояния из useState в компоненте вызывают многократные повторные рендеринг
Я впервые пробовал перехватывать React, и все казалось хорошим, пока я не понял, что когда я получаю данные и обновляю две разные переменные состояния (данные и флаг загрузки), мой компонент (таблица данных) отображается дважды, хотя оба вызова до состояния обновления происходят те же функции. Вот моя функция 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 };
};
В обычном компоненте класса вы бы сделали один вызов, чтобы обновить состояние, которое может быть сложным объектом, но "способ зацепки", по-видимому, состоит в том, чтобы разделить состояние на более мелкие единицы, побочным эффектом которых является многократный отображает, когда они обновляются отдельно. Любые идеи, как смягчить это?
9 ответов
Вы могли бы объединить loading
состояние и data
состояние в один объект состояния, а затем вы можете сделать один setState
позвоните, и будет только один рендер.
Примечание: в отличие от setState
в компонентах класса setState
вернулся из useState
не объединяет объекты с существующим состоянием, оно полностью заменяет объект. Если вы хотите выполнить слияние, вам нужно прочитать предыдущее состояние и объединить его с новыми значениями самостоятельно. Обратитесь к документации.
Я бы не стал слишком сильно беспокоиться о том, как коллировать рендеры, пока вы не определите, что у вас проблемы с производительностью Рендеринг (в контексте React) и фиксация обновлений виртуального DOM в реальном DOM - разные вопросы. Рендеринг здесь относится к созданию виртуальных DOM, а не к обновлению DOM браузера. Реагировать может партия 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(() => {
// 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>
У этого также есть другое решение, использующее 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
}
Пакетное обновление в реакционных хуках https://github.com/facebook/react/issues/14259
В настоящее время React будет обновлять пакетные состояния, если они запускаются из события на основе React, например, нажатия кнопки или изменения ввода. Обновления не будут пакетироваться, если они запускаются вне обработчика событий React, например, асинхронный вызов.
Это будет делать:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
Чтобы воспроизвести this.setState
поведение слияния компонентов класса, в документации React рекомендуется использовать функциональную формуuseState
с распространением объекта - нет необходимости вuseReducer
:
setState(prevState => {
return {...prevState, loading, data};
});
Теперь два состояния объединены в одно, что сэкономит вам цикл рендеринга.
У одного объекта состояния есть еще одно преимущество: loading
а также data
являются зависимыми государствами. Некорректные изменения состояния становятся более очевидными, когда состояние объединяется:
setState({ loading: true, data }); // ups... loading, but we already set data
Вы можете еще лучше обеспечить согласованные состояния, 1.) сделав статус -loading
, success
, error
и т. д. - явно в вашем состоянии и 2.) с использованиемuseReducer
чтобы инкапсулировать логику состояния в редукторе:
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: Убедитесь, что к пользовательским хукам добавлен префиксuse
(useData
вместо того getData
). Также передан обратный вызовuseEffect
не может быть async
.
Небольшое дополнение к ответу /questions/1724272/mnogokratnyie-vyizovyi-dlya-obnovleniya-sostoyaniya-iz-usestate-v-komponente-vyizyivayut-mnogokratnyie-povtornyie-rendering/1724286#1724286
Круто! Для тех, кто планирует использовать этот хук, он может быть написан немного надежным способом для работы с функцией в качестве аргумента, например так:
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];
};
В дополнение к ответу user1751946 вам лучше запомнить функцию, чтобы она возвращала одну и ту же ссылку при каждом рендеринге вместо новой функции. Это может иметь решающее значение, если линтер TypeScript заставляет вас
YangshunsetMergedState
как зависимость в
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];
};
Вы также можете использовать
useEffect
для обнаружения изменения состояния и соответствующего обновления других значений состояния