useReducer Действие отправлено дважды - PullRequest
0 голосов
/ 26 февраля 2019

Сценарий

У меня есть пользовательский хук, который возвращает действие.Родительский компонент «Контейнер» использовал пользовательский хук и передает действие как реквизит дочернему компоненту.

Проблема

Когда действие выполняется из дочернего компонента, фактическая отправка происходит дважды.Теперь, если дети использовали крючок напрямую и вызвали действие, отправка происходит только один раз.

Как воспроизвести его:

Откройте приведенную ниже песочницу и откройте 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 выполнено только один раз, и состояние изменяется соответствующим образом.

Несмотря на то, что нет явных штрафных санкций, как таковых, я не понимаю, ПОЧЕМУ это происходит.Возможно, я слишком сосредоточен на крючках, и мне не хватает чего-то такого глупого ... пожалуйста, освободите меня от этой головоломки.Спасибо!

1 Ответ

0 голосов
/ 27 февраля 2019

Чтобы сначала выяснить существующее поведение, действие STATUS_FETCHING фактически только «отправлялось» (т. Е. Если вы выполняете console.log прямо перед 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.log("prevState: ", state);
    console.log("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.log("prevState: ", state);
  console.log("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);
}

Edit useAirportsData

Вот связанныйОтветьте за вариант этой проблемы, когда у редуктора есть зависимости (например, от реквизита или другого состояния), которые требуют, чтобы он был определен в другой функции: React useReducer Hook срабатывает дважды / как передать реквизиты редуктору?

Ниже приведен очень надуманный пример, демонстрирующий сценарий, в котором изменение редуктора во время рендеринга требует его повторного выполнения.В консоли видно, что при первом запуске редуктора с помощью одной из кнопок он выполняется дважды - один раз с исходным редуктором (addSubtractReducer), а затем снова с другим редуктором (multiplyDivideReducer).Последующие отправки, похоже, запускают повторную визуализацию безоговорочно, без предварительного выполнения редуктора, поэтому выполняется только правильный редуктор.Вы можете увидеть особенно интересное поведение в журналах, если сначала отправите действие «nochange».

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.log("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.log("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);

Edit different reducers

...