Перепишите 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.