Как вложить редукторы Redux Toolkit для одного свойства

Я переношу кодовую базу с ванильного Redux на Redux Toolkit. Я пытаюсь найти хороший способ вложить редукторы, созданные с помощью createReducer, только для одного свойства.

Допустим, у меня есть установка, подобная следующему надуманному примеру, с вложенным user редуктором и вложенным friends редуктором. Пользователь может изменить свое имя, которое влияет только на себя, а также добавлять и удалять своих друзей, что влияет на него самого и его свойство массива friends, которым управляет редуктор friends.

const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";

const initialState = {
  username: "",
  email: "",
  lastActivity: 0,
  friends: [],
};

const user = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_NAME: {
      const { newName, time } = action.payload;

      return {
        ...state,
        name: newName,
        lastActivity: time,
      };
    }
    case ADD_FRIEND:
    case REMOVE_ALL_FRIENDS: {
      const { time } = action.payload;

      return {
        ...state,
        friends: friends(state.friends, action),
        lastActivity: time,
      };
    }
    default: {
      return {
        ...state,
        friends: friends(state.friends, action),
      };
    }
  }
};

const friends = (state = initialState.friends, action) => {
  switch (action.type) {
    case ADD_FRIEND: {
      const { newFriend } = action.payload;

      return [...state, newFriend];
    }
    case REMOVE_ALL_FRIENDS: {
      return [];
    }
    default: {
      return state;
    }
  }
};

Отметить:

  • friends обязательно коррелирует с user, поэтому я решил вложить его в срез состояния.
  • user вручную вызывает friends reducer для вычисления friends среза состояния с возможно перекрывающимися типами действий и только для этого одного friends свойства.

Сейчас я пытаюсь реорганизовать это с помощью Redux Toolkit createReducers. Моя первая попытка была следующей:

import { createReducer } from "@reduxjs/toolkit";

const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";

const initialState = {
  username: "",
  email: "",
  lastActivity: 0,
  friends: [],
};

const user = createReducer(initialState, (builder) => {
  builder
    .addCase(CHANGE_NAME, (state, action) => {
      const { newName, time } = action.payload;

      state.name = newName;
      state.lastActivity = time;
    })
    .addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
    (state, action) => {
      const { time } = action.payload;
      state.lastActivity = time;
      state.friends = friends(state.friends, action);
    };
});

const friends = createReducer(initialState, (builder) => {
  builder
    .addCase(ADD_FRIEND, (state, action) => {
      const { newFriend } = action.payload;

      state.push(newFriend);
    })
    .addCase(REMOVE_ALL_FRIENDS, () => []);
});

Отметить:

  • Здесь основное внимание уделяется последнему сопоставителю редуктора user.
  • Редуктор friends теперь имеет два способа изменения состояния: изменение состояния путем нажатия на него с помощью .push и возврата нового пустого состояния путем прямого возврата [].

По моей интуиции, это сработает, поскольку это похоже на ту же логику. Однако это работает только для действия ADD_FRIEND и ничего не делает или выдает ошибку об одновременном изменении состояния и возврате нового состояния для типа действия REMOVE_ALL_FRIENDS.

Похоже, это связано с тем, что модифицируемое состояние превращает его в объект ImmerJS Proxy в редукторе user, но когда он передается редуктору friends, он возвращает объект состояния напрямую вместо того, чтобы изменять его, заставляя RTK выдавать ошибку, как он говорит вы должны изменить только состояние возврата или, но не то и другое одновременно. В обработчике для ADD_FRIEND это не проблема, поскольку он всегда изменяет состояние, как и все обработчики в user.

В качестве обходного пути я вручную проверил, возвращает ли friends редуктор Proxy или новое состояние напрямую, и если он возвращает новое состояние, он устанавливает его в редукторе user, но я уверен, что есть способ лучше:

import { createReducer, current } from "@reduxjs/toolkit";

const user = createReducer(initialState, (builder) => {
  builder
    .addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
    (state, action) => {
      const { time } = action.payload;

      state.lastActivity = time;

      const result = friends(state.friends, action);

      let output;
      
      // If state has been returned directly this will error and we can set the state manually,
      // Else this will not error because a Proxy has been returned, and thus the state has been
      // set already by the sub-reducer.
      try {
        output = current(result);
      } catch (error) {
        output = result;
      }

      if (output) {
        state.progress = output;
      }
    };
});

Тогда у меня вопрос, как я могу это исправить, чтобы мне не приходилось вручную проверять тип возвращаемого значения и можно было легко вкладывать редукторы RTK друг в друга, будь то реструктуризация моих редукторов или исправление логики кода?

В идеале я бы по-прежнему хотел, чтобы редуктор friends был вложен в редуктор user, так как именно так много ванильного кода Redux структурируют свою логику состояния с множеством разных редукторов, обрабатывающих много разных частей состояния, вместо того, чтобы все они были вложены в корень. уровень с одним вызовом combineReducers, но если есть лучшее и более чистое решение, я тоже с этим согласен.

Спасибо за любую помощь и извините за длинный пост - просто хотел быть как можно более подробным, поскольку другие онлайн-решения, похоже, не решают эту точную проблему.


person Matthew W.    schedule 26.02.2021    source источник
comment
Если у вас все еще возникают проблемы после исправления того, что я предложил, опубликуйте CodeSandbox, где у вас возникла ошибка. Вот моя, где это работает: codeandbox.io/s/ старый-ручей-sqmz4? file = / src / App.js   -  person Linda Paiste    schedule 27.02.2021
comment
@LindaPaiste Спасибо за ответ! Вы заставили меня узнать, что проблема заключалась в том, что я возвращал новое состояние из пользовательского редуктора, распространяя его и задавая свойство friends в развернутом объекте, а затем, когда friends изменил состояние, ImmerJS выдал ошибку, поскольку я возвращал состояние из user reducer, а также одновременно модифицируя его из friends reducer.   -  person Matthew W.    schedule 27.02.2021


Ответы (3)


В этом конкретном случае легко обработать свойство friends в редукторе user: state.friends.push(newFriend) или state.friends = []. Но не должно быть проблем с тем, чтобы держать его отдельно.

Я заметил несколько проблем при попытке запустить ваш код:

  • Использование initialState для всего пользователя в качестве начального состояния friends вместо initialState.friends или []
  • Несовпадающие круглые скобки в addMatcher вокруг action.type флажка
  • Назначение state.name вместо state.username

После их исправления мне не удалось воспроизвести вашу проблему. Я могу успешно добавлять и удалять друзей.

person Linda Paiste    schedule 27.02.2021

Проблема заключалась в том, что мой исходный код редуктора user был в том, что редуктор возвращал новый объект состояния, распределяя состояние и устанавливая свойство friends в этом расширении объекта. Это привело к ошибке ImmerJS, поскольку он возвращал новое значение из редуктора user и одновременно изменял его в редукторе friends.

Мой опубликованный код работал (с некоторыми изменениями благодаря Линде), но для исправления моего исходного кода (а я не публиковал версию, которая вызвала ошибки - извинения), мне пришлось изменить следующее:

.addMatcher(
  (action) =>
    action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
  (state, action) => ({
    ...state,
    lastActivity: action.payload.time,
    friends: friends(state.friends, action)
  })
)

to:

.addMatcher(
  (action) =>
    action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
  (state, action) => {
    const { time } = action.payload;
    state.lastActivity = time;
    state.friends = friends(state.friends, action);
  }
)

Спасибо всем за помощь.

person Matthew W.    schedule 27.02.2021

На самом деле это могло быть ошибкой в ​​Redux Toolkit. Не могли бы вы сообщить о проблеме с воспроизведением CodeSandbox на нашем трекере проблем на github?

person phry    schedule 26.02.2021