У меня есть пользовательский хук, который возвращает действие. Родительский компонент «Контейнер» использовал пользовательский хук и передал действие в качестве реквизита дочернему компоненту.
Когда действие выполняется из дочернего компонента, фактическая отправка происходит дважды. Теперь, если дочерние элементы используют хук напрямую и вызывают действие, отправка происходит только один раз.
Как это воспроизвести:
Откройте приведенную ниже песочницу и откройте devtools в chrome, чтобы вы могли видеть журналы консоли, которые я добавил.
https://codesandbox.io/s/j299ww3lo5?fontsize=14
Main.js (дочерний компонент), вы увидите, что мы вызываем props.actions.getData()
В DevTools очистите журналы. В предварительном просмотре введите любое значение в форме и нажмите кнопку. В журнале консоли вы увидите такие действия, как redux-logger, и заметите, что действие STATUS_FETCHING выполняется дважды без изменения состояния.
Теперь перейдите в Main.js и закомментируйте строку 9 и раскомментируйте строку 10. Теперь мы используем пользовательский хук напрямую.
В DevTools очистите журналы. В предварительном просмотре введите любое значение в форме и нажмите кнопку. В журнале консоли теперь вы увидите, что STATUS_FETCHING был выполнен только один раз, и состояние изменяется соответствующим образом.
Хотя очевидного штрафа за производительность как такового нет, я не понимаю, ПОЧЕМУ это происходит. Возможно, я слишком сосредоточен на крючках и упускаю что-то настолько глупое... пожалуйста, освободите меня от этой головоломки. Спасибо!
@ford04 Да, я тоже это заметил.





Чтобы сначала прояснить существующее поведение, действие STATUS_FETCHING на самом деле было «отправлено» только один раз (т. Е. Если вы делаете console.info прямо перед вызовом dispatch в getData внутри useApiCall.js) один раз, но код редуктора выполнялся дважды.
Я, вероятно, не знал бы, что искать, чтобы объяснить, почему, если бы не мои исследования при написании этого несколько связанного ответа: React hook рендерит дополнительное время.
В этом ответе вы найдете следующий блок кода из React:
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
В частности, обратите внимание на комментарии, указывающие на то, что React, возможно, придется переделать часть работы если редуктор менялся. Проблема в том, что в вашем useApiCallReducer.js вы определяли свой редьюсер внутри своего пользовательского хука useApiCallReducer. Это означает, что при повторном рендеринге вы каждый раз предоставляете новую функцию редуктора, даже если код редуктора идентичен. Если вашему редюсеру не нужно использовать аргументы, переданные пользовательскому хуку (вместо того, чтобы просто использовать аргументы state и action, переданные редюсеру), вы должны определить редьюсер на внешнем уровне (т.е. не вложенным в другую функцию). В общем, я бы рекомендовал избегать определения функции, вложенной в другую, если она фактически не использует переменные из области видимости, в которую она вложена.
Когда React видит новый редюсер после повторного рендеринга, ему приходится отбрасывать часть работы, которую он проделал ранее, пытаясь определить, потребуется ли повторный рендеринг, потому что ваш новый редюсер может дать другой результат. Это всего лишь часть деталей оптимизации производительности в коде React, о которых вам в основном не нужно беспокоиться, но стоит знать, что если вы переопределяете функции без необходимости, вы можете в конечном итоге победить некоторые оптимизации производительности.
Чтобы решить эту проблему, я изменил следующее:
import { useReducer } from "react";
import types from "./types";
const initialState = {
data: [],
error: [],
status: types.STATUS_IDLE
};
export function useApiCallReducer() {
function reducer(state, action) {
console.info("prevState: ", state);
console.info("action: ", action);
switch (action.type) {
case types.STATUS_FETCHING:
return {
...state,
status: types.STATUS_FETCHING
};
case types.STATUS_FETCH_SUCCESS:
return {
...state,
error: [],
data: action.data,
status: types.STATUS_FETCH_SUCCESS
};
case types.STATUS_FETCH_FAILURE:
return {
...state,
error: action.error,
status: types.STATUS_FETCH_FAILURE
};
default:
return state;
}
}
return useReducer(reducer, initialState);
}
вместо этого быть:
import { useReducer } from "react";
import types from "./types";
const initialState = {
data: [],
error: [],
status: types.STATUS_IDLE
};
function reducer(state, action) {
console.info("prevState: ", state);
console.info("action: ", action);
switch (action.type) {
case types.STATUS_FETCHING:
return {
...state,
status: types.STATUS_FETCHING
};
case types.STATUS_FETCH_SUCCESS:
return {
...state,
error: [],
data: action.data,
status: types.STATUS_FETCH_SUCCESS
};
case types.STATUS_FETCH_FAILURE:
return {
...state,
error: action.error,
status: types.STATUS_FETCH_FAILURE
};
default:
return state;
}
}
export function useApiCallReducer() {
return useReducer(reducer, initialState);
}
Вот связанный ответ для варианта этой проблемы, когда у редуктора есть зависимости (например, от реквизита или другого состояния), которые требуют, чтобы он был определен в другой функции: React useReducer Hook срабатывает дважды / как передать реквизит редуктору?
Ниже приведен очень надуманный пример, демонстрирующий сценарий, в котором изменение редьюсера во время рендеринга требует его повторного выполнения. Вы можете видеть в консоли, что при первом запуске редьюсера с помощью одной из кнопок он выполняется дважды — один раз с начальным редьюсером (addSubtractReducer), а затем снова с другим редьюсером (multiplyDivideReducer). Последующие отправки, по-видимому, безоговорочно запускают повторный рендеринг без предварительного выполнения редьюсера, поэтому выполняется только правильный редуктор. Вы можете увидеть особенно интересное поведение в журналах, если вы сначала отправляете действие «без изменений».
import React from "react";
import ReactDOM from "react-dom";
const addSubtractReducer = (state, { type }) => {
let newState = state;
switch (type) {
case "increase":
newState = state + 10;
break;
case "decrease":
newState = state - 10;
break;
default:
newState = state;
}
console.info("add/subtract", type, newState);
return newState;
};
const multiplyDivideReducer = (state, { type }) => {
let newState = state;
switch (type) {
case "increase":
newState = state * 10;
break;
case "decrease":
newState = state / 10;
break;
default:
newState = state;
}
console.info("multiply/divide", type, newState);
return newState;
};
function App() {
const reducerIndexRef = React.useRef(0);
React.useEffect(() => {
reducerIndexRef.current += 1;
});
const reducer =
reducerIndexRef.current % 2 === 0
? addSubtractReducer
: multiplyDivideReducer;
const [reducerValue, dispatch] = React.useReducer(reducer, 10);
return (
<div>
Reducer Value: {reducerValue}
<div>
<button onClick = {() => dispatch({ type: "increase" })}>Increase</button>
<button onClick = {() => dispatch({ type: "decrease" })}>Decrease</button>
<button onClick = {() => dispatch({ type: "nochange" })}>
Dispatch With No Change
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
это был довольно подробный ответ и решение. Спасибо, Райан!
Вы сказали, что «ваш новый редюсер может дать другой результат»… Пожалуйста, назовите один сценарий, в котором результат редуктора изменится при следующем рендеринге… Я имею в виду, почему бы не повторно использовать «последний измененный результат»?
@kstratis Я добавил пример в конец своего ответа.
@RyanCogswell Отлично. Это имеет смысл. И последнее: фрагмент исходного кода React, который вы выделили ранее в своем ответе, кажется, сравнивает состояния редуктора... однако в этом сценарии React также необходимо сравнить сам редуктор... любые подсказки, где это происходит в исходном коде. ?
@kstratis Редуктор сравнивается здесь: github.com/facebook/react/blob/v16.9.0/packages/…
@RyanCogswell Это потрясающе! Ваша помощь очень ценится! Лучший.
Удалите <React.StrictMode>, и проблемы будут устранены.
Спасибо большое, вы спасли мне жизнь!
Вау, это так невероятно... это было именно так, StrictMode приводил к двойному вызову редуктора... Но я подумал, что это "невозможно" или какая-то другая проблема, поскольку вызовы console.info в редюсере не удваивались. ... пока я не прочитал это http://reactjs.org/docs/… «Начиная с React 17, React автоматически изменяет методы консоли, такие как console.info(), чтобы отключить журналы во втором вызове функций жизненного цикла» (!!!!) см. также это
Если вы используете React.StrictMode, React будет вызывать ваш редюсер несколько раз с одними и теми же аргументами, чтобы проверить чистоту вашего редуктора. Вы можете отключить StrictMode, чтобы проверить правильность запоминания вашего редуктора.
Из https://github.com/facebook/react/issues/16295#issuecomment-610098654:
There is no "problem". React intentionally calls your reducer twice to make any unexpected side effects more apparent. Since your reducer is pure, calling it twice doesn't affect the logic of your application. So you shouldn't worry about this.
In production, it will only be called once.
удалив <React.StrictMode>, диспетчеризация не будет вызываться несколько раз, я тоже столкнулся с этой проблемой, и это решение сработало
Я удалил <React.StrictMode>, но у меня возникла та же проблема.
Как говорится в документации React:
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: [...] Functions passed to useState, useMemo, or useReducer
Это связано с тем, что редукторы должны быть чистыми, они должны каждый раз выдавать один и тот же результат с одними и теми же аргументами, и React Strict Mode проверяет (иногда) это автоматически, дважды вызывая редюсер.
Это не должно быть проблемой, потому что это поведение ограничено разработкой, оно не появится в производстве, поэтому я бы не рекомендовал выходить из игры <React.StrictMode>, потому что это может быть очень полезно для выявления многих проблем, связанных с ключами, побочными эффектами и многим другим. .
Кажется, связанные коды вопросов и сама коробка были исправлены OP. Ответ Райана Когсвелла имеет исходный код.