Я пробую перехватчики 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 };
};
В обычном компоненте класса вы должны сделать один вызов для обновления состояния, которое может быть сложным объектом, но «способ перехвата», по-видимому, состоит в разделении состояния на более мелкие единицы, побочным эффектом которого, по-видимому, является многократное повторение. рендерится, когда они обновляются отдельно. Есть идеи, как это смягчить?
Вау! Я только что обнаружил это, и это полностью разрушило мое понимание того, как работает реагирующий рендеринг. Я не могу понять каких-либо преимуществ для работы таким образом - кажется довольно произвольным, что поведение в асинхронном обратном вызове отличается от поведения в обычном обработчике событий. Кстати, в моих тестах кажется, что согласование (то есть обновление реальной DOM) не происходит до тех пор, пока не будут обработаны все вызовы setState, поэтому промежуточные вызовы рендеринга в любом случае тратятся впустую.
«кажется довольно произвольным, что поведение в асинхронном обратном вызове отличается от поведения в обычном обработчике событий» - это не произвольно, а скорее по реализации [1]. React пакетирует все вызовы setState, выполняемые во время обработчика событий React, и применяет их непосредственно перед выходом из собственного обработчика событий браузера. Однако несколько setStates вне обработчиков событий (например, в сетевых ответах) не будут объединены в пакет. Так что в этом случае вы получите два повторных рендера. [1] github.com/facebook/react/issues/10231#issuecomment-31664495 0
«но« способ перехвата », кажется, состоит в том, чтобы разделить состояние на более мелкие единицы» - это немного вводит в заблуждение, потому что множественные повторные отрисовки происходят только тогда, когда функции setX вызываются в рамках асинхронного обратного вызова. Источники: github.com/facebook/react/issues/14259#issuecomment-43963262 2, blog.logrocket.com/…



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Вы можете объединить состояние loading и состояние data в один объект состояния, а затем выполнить один вызов setState, и будет только один рендеринг.
Примечание: В отличие от setState в компонентах класса, setState, возвращенный из useState, не объединяет объекты с существующим состоянием, он полностью заменяет объект. Если вы хотите выполнить слияние, вам нужно будет прочитать предыдущее состояние и самостоятельно слить его с новыми значениями. Обратитесь к документы.
Я бы не стал слишком беспокоиться о чрезмерном вызове рендеров, пока вы не определите, что у вас проблемы с производительностью. Рендеринг (в контексте 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/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/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/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id = "app"></div>Спасибо, Яншун, я думаю, что слияние состояния - это ответ, но не идеальный. Я просто не уверен насчет твоего последнего предложения. Если у меня есть datatable, и он дважды отображается с одними и теми же данными, разве все строки и их дочерние элементы не отображаются и обновляются в DOM дважды? Не уверен, что вы имели в виду под пакетной обработкой, но я исхожу из фона Angular 1.x, где все постоянно перерисовывается :)
Я согласен, что это не идеально, но все же это разумный способ сделать это. Вы можете написать свой собственный хук, который выполняет слияние, если вы не хотите выполнять слияние самостоятельно. Я обновил последнее предложение, чтобы было понятнее. Вы знакомы с тем, как работает React? Если нет, вот несколько ссылок, которые могут помочь вам лучше понять - responsejs.org/docs/reconciliation.html, blog.vjeux.com/2013/javascript/react-performance.html.
@jonhobbs Я добавил альтернативный ответ, который создает ловушку useMergeState, чтобы вы могли автоматически слить состояние.
Превосходно, еще раз спасибо. Я все больше знаком с React, но могу многое узнать о его внутреннем устройстве. Я их прочту.
Хорошее решение, я использую его немного модифицированным способом, см. stackoverflow.com/a/55102681/121143
Любопытно, при каких обстоятельствах это может быть правдой ?: "React может выполнять пакетные вызовы setState и обновлять DOM браузера с последним новым состоянием."
Хорошо ... я подумал об объединении состояния или использовании отдельного состояния полезной нагрузки, а также о том, чтобы другие состояния считывались из объекта полезной нагрузки. Тем не менее, очень хороший пост. 10/10 прочитаю еще раз
Пакетное обновление в react-hookshttps://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.
Это очень полезный ответ, поскольку он решает проблему, вероятно, в большинстве случаев, без необходимости что-либо делать. Спасибо!
Обновление: начиная с React 18 (функция подписки) все обновления будут автоматически пакетироваться, независимо от того, откуда они исходят. github.com/reactwg/react-18/discussions/21
Небольшое дополнение к ответу https://stackoverflow.com/a/53575023/121143
Прохладный! Для тех, кто планирует использовать этот хук, он может быть написан немного надежным способом для работы с функцией в качестве аргумента, например так:
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];
};
Прохладный! Я бы обернул setMergedState только useMemo(() => setMergedState, []), потому что, как говорится в документации, setState не меняется между повторными рендерингами: React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list., таким образом функция setState не воссоздается при повторных рендерингах.
Это также имеет другое решение, использующее 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
}
Поддержка машинописного текста. Спасибо П. Гэлбрейт, который ответил на это решение, Те, кто использует машинописный текст, могут использовать это:
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 попросил предоставить доступ к prevState внутри нашего setState, так что вот способ достижения этой цели:
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) или простым объектом. Вы можете найти эта реализация isFunction Алекса Гранде здесь.
Уведомление. Для тех, кто хочет много использовать этот ответ, я решил превратить его в библиотеку. Вы можете найти это здесь:
Github: https://github.com/thevahidal/react-use-setstate
NPM: https://www.npmjs.com/package/react-use-setstate
Expected 0 arguments, but got 1.ts(2554) - это ошибка, которую я получаю, пытаясь использовать useReducer таким образом. Точнее setState. Как это исправить? Файл - js, но мы по-прежнему используем проверки ts.
Я думаю, что идея слияния двух государств осталась прежней. Но в некоторых случаях вы не можете объединить состояния.
Я думаю, что это отличный ответ в сочетании с пакетным обновлением хуков React, опубликованным ниже (github.com/facebook/react/issues/14259). К счастью, мой вариант использования был прост, и я смог просто переключиться на useReducer с useState, но в других случаях (например, в некоторых, перечисленных в выпуске GitHub) это не так просто.
Для тех, кто использует машинописный код useReducer<Reducer<MyState, Partial<MyState>>>(...), где MyState - это тип вашего объекта состояния.
Спасибо, приятель, это действительно помогло мне. Также спасибо @ P.Galbraith за решение TypeScript :)
Я искал реализовать это решение, но как мы можем получить доступ к предыдущему состоянию точно так же, как функция this.setState компонента на основе класса? вот так: this.setState (prevState => {})
@ user2420374 добавил новое обновление по вашему вопросу, надеюсь, еще не поздно!
@VahidAl, в итоге я реализовал setState с оператором распространения, но с этого момента я выберу эту реализацию, спасибо за обновление!
Это будет делать:
const [state, setState] = useState({ username: '', password: ''});
// later
setState({
...state,
username: 'John'
});
Если вы используете сторонние хуки и не можете объединить состояние в один объект или использовать useReducer, то решение заключается в использовании:
ReactDOM.unstable_batchedUpdates(() => { ... })
Рекомендовано Даном Абрамовым здесь
Смотрите это пример
Почти год спустя эта функция все еще кажется полностью недокументированной и все еще отмечена как нестабильная :-(
Чтобы воспроизвести поведение слияния this.setState из компонентов класса,
React docs рекомендовать для использования функциональной формы 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.
Проголосовал за это! Это одна из самых полезных функций, которые я нашел в React API. Обнаружил его, когда попытался сохранить функцию как состояние (я знаю, что хранить функции странно, но мне пришлось по какой-то причине, которую я не помню), и это не сработало должным образом из-за этой зарезервированной сигнатуры вызова
Вы также можете использовать useEffect для обнаружения изменения состояния и соответствующего обновления других значений состояния.
Вы можете уточнить? Он уже пользуется useEffect.
В дополнение к отвечатьЯншун Тай вам лучше запоминать функцию setMergedState, чтобы она возвращала одну и ту же ссылку при каждом рендеринге вместо новой функции. Это может иметь решающее значение, если линтер TypeScript заставляет вас передавать 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];
};
Если у вас есть зависимые состояния, вам, вероятно, следует использовать
useReducer