Перепишите redux-orm reducer с помощью redux-toolkit

Выпуск (tl;dr)

Как мы можем создать пользовательский перевождь-ОРМ редуктор с Redux-набора инструментов"сек createSlice?

Есть ли более простое, рекомендованное, более элегантное или просто другое решение, чем попытка, представленная в этом вопросе?

Детали

Пример кастомного редуктора redux-orm выглядит следующим образом (упрощенно):

function ormReducer(dbState, action) {
    const session = orm.session(dbState);
    const { Book } = session;

    switch (action.type) {
    case 'CREATE_BOOK':
        Book.create(action.payload);
        break;
    case 'REMOVE_AUTHOR_FROM_BOOK':
        Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
        break;
    case 'ASSIGN_PUBLISHER':
        Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
        break;
    }

    return session.state;
}

Можно упростить редукторы с помощью createSlice функция перевождь-инструментарий (на основе перевождь-инструментарий использования-гиде):

const ormSlice = createSlice({
  name: 'orm',
  initialState: [],
  reducers: {
    createBook(state, action) {},
    removeAuthorFromBook(state, action) {},
    assignPublisher(state, action) {}
  }
})
const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer

Однако в начале redux-orm reducer нам нужно создать сеанс

const session = orm.session(dbState);

затем мы выполняем магию редуктора redux-orm, и в конце нам нужно вернуть состояние

return session.state;

Так что мы упускаем что-то вроде beforeEachReducer а также afterEachReducer методы в createSlice чтобы добавить эту функциональность.

Решение (попытка)

Мы создали withSession функция высшего порядка, которая создает сеанс и возвращает новое состояние.

const withSession = reducer => (state, action) => {
  const session = orm.session(state);
  reducer(session, action);
  return session.state;
}

Нам нужно обернуть каждую логику редуктора в этот withSession.

import { createSlice } from '@reduxjs/toolkit';
import orm from './models/orm'; // defined elsewhere
// also define or import withSession here

const ormSlice = createSlice({
  name: 'orm',
  initialState: orm.session().state, // we need to provide the initial state
  reducers: {
    createBook: withSession((session, action) => {
      session.Book.create(action.payload);
    }),
    removeAuthorFromBook: withSession((session, action) => {
      session.Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
    }),
    assignPublisher: withSession((session, action) => {
      session.Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
    }),
  }
})

const { actions, reducer } = ormSlice
export const { createBook, removeAuthorsFromBook, assignPublisher } = actions
export default reducer

3 ответа

Для меня это интересный вопрос, потому что я создал Redux Toolkit и много писал об использовании Redux-ORM в моей серии руководств "Практический Redux".

Сверху в голове, я должен сказать, что ты withSession() wrapper на данный момент кажется лучшим подходом.

В то же время я не уверен, что использование Redux-ORM и createSlice()вместе действительно приносит вам много пользы. Вы не используете неизменяемые возможности обновления Immer внутри, поскольку Redux-ORM обрабатывает обновления в моделях. Единственная реальная выгода в этом случае - создание создателей действий и типов действий.

Вам может быть лучше просто позвонить createAction() отдельно и с использованием исходной формы редуктора со сгенерированными типами действий в операторе switch:

export const createBook = createAction("books/create");
export const removeAuthorFromBook = createAction("books/removeAuthor");
export const assignPublisher = createAction("books/assignPublisher");

export default function ormReducer(dbState, action) {
    const session = orm.session(dbState);
    const { Book } = session;

    switch (action.type) {
    case createBook.type:
        Book.create(action.payload);
        break;
    case removeAuthorFromBook.type:
        Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
        break;
    case assignPublisher.type:
        Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
        break;
    }

    return session.state;
}

Я понимаю, что вы говорите о добавлении каких-то обработчиков "до / после", но это добавит слишком много сложностей. RTK предназначен для использования в 80% случаев, а типы TS дляcreateSliceуже невероятно сложны. Добавлять здесь дополнительную сложность было бы плохо.

Я столкнулся с этим вопросом, пытаясь объединить преимущества https://redux-toolkit.js.org/ и redux-orm. Я смог придумать решение, которым до сих пор был очень доволен. Вот как выглядит моя модель redux-orm:

class Book extends Model {

    static modelName = 'Book';

    // Declare your related fields.
    static fields = {
        id: attr(), // non-relational field for any value; optional but highly recommended
        name: attr(),
        // foreign key field
        publisherId: fk({
            to: 'Publisher',
            as: 'publisher',
            relatedName: 'books',
        }),
        authors: many('Author', 'books'),
    };

    static slice = createSlice({
      name: 'BookSlice',
      // The "state" (Book) is coming from the redux-orm reducer, and so will
      // never be undefined; therefore, `initialState` is not needed.
      initialState: undefined,
      reducers: {
        createBook(Book, action) {
            Book.create(action.payload);
        },
        removeAuthorFromBook(Book, action) {
            Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
        },
        assignPublisher(Book, action) {
            Book.withId(action.payload.bookId).publisherId = action.payload.publisherId;
        }
      }
    });

    toString() {
        return `Book: ${this.name}`;
    }
    // Declare any static or instance methods you need.

}

export default Book;
export const { createBook, removeAuthorFromBook, assignPublisher } = Book.slice.actions;

Срез redux-toolkit создается как статическое свойство в классе, а затем модель и ее действия экспортируются аналогично Ducks (ORMDucks??).

Единственная другая модификация, которую необходимо сделать, - это определить настраиваемое средство обновления для редуктора redux-orm:

const ormReducer = createReducer(orm, function (session, action) {
    session.sessionBoundModels.forEach(modelClass => {
        if (typeof modelClass.slice.reducer === 'function') {
            modelClass.slice.reducer(modelClass, action, session);
        }
    });
});

См. Более полный пример здесь:https://gist.github.com/JoshuaCWebDeveloper/25a302ec891acb6c4992fe137736160f

Некоторые примечания

  • @markerikson хорошо отмечает, что некоторые функции redux-toolkit не используются, поскольку redux-orm управляет состоянием. Для меня два самых больших преимущества использования этого метода - это отсутствие необходимости спорить с целой кучей создателей действий и отсутствие необходимости бороться с ужаснымиswitchзаявления:D.
  • Я использую поля класса стадии 3 и предложения статических функций класса. (См. https://babeljs.io/docs/en/babel-plugin-proposal-class-properties). Чтобы сделать этот ES6 совместимым, вы можете легко реорганизовать класс модели, чтобы определить его статические свойства, используя текущий синтаксис (т.е.Book.modelName = 'Book';).
  • Если вы решите смешивать модели, подобные приведенной выше, с моделями, которые не определяют slice, то вам нужно будет настроить логику в createReducer Updater слегка.

В качестве реального примера посмотрите, как я использую модель в своем проекте здесь: https://github.com/vallerance/react-orcus/blob/70a389000b6cb4a00793b723a25cac52f6da519b/src/redux/models/OrcusApp.js. Этот проект все еще находится на начальной стадии. Самый большой вопрос в моей голове - насколько хорошо этот метод будет масштабироваться; тем не менее, я оптимистично настроен, что по мере развития моего проекта он будет и дальше приносить многочисленные выгоды.

Попробуйте использовать normalized-reducer. Это редуктор более высокого порядка, который принимает схему, описывающую отношения, и возвращает редуктор, действие и селекторы, которые записывают / читают в соответствии с отношениями.

Он также легко интегрируется с Normalizr и Redux Toolkit.

Другие вопросы по тегам