Rewrite redux-orm reducer with redux-toolkit

时光毁灭记忆、已成空白 提交于 2020-05-15 04:21:07

问题


Issue (tl;dr)

How can we create a custom redux-orm reducer with redux-toolkit's createSlice?

Is there a simpler, recommended, more elegant or just other solution than the attempt provided in this question?

Details

The example of a custom redux-orm reducer looks as follows (simplified):

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;
}

It's possible to simplify reducers with the createSlice function of redux-toolkit (based on the redux-toolkit usage-guide):

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

However, at the beginning of redux-orm reducer we need to create a session

const session = orm.session(dbState);

then we do our redux-orm reducer magic, and at the end we need to return the state

return session.state;

So we miss something like beforeEachReducer and afterEachReducer methods in the createSlice to add this functionality.

Solution (attempt)

We created a withSession higher-order function that creates the session and returns the new state.

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

We need to wrap every reducer logic in this 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


回答1:


This is a fascinating question for me, because I created Redux Toolkit, and I wrote extensively about using Redux-ORM in my "Practical Redux" tutorial series.

Off the top of my head, I'd have to say your withSession() wrapper looks like the best approach for now.

At the same time, I'm not sure that using Redux-ORM and createSlice() together really gets you a lot of benefit. You're not making use of Immer's immutable update capabilities inside, since Redux-ORM is handling updates within the models. The only real benefit in this case is generating the action creators and action types.

You might be better off just calling createAction() separately, and using the original reducer form with the generated action types in the switch statement:

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;
}

I see what you're saying about adding some kind of "before/after" handlers, but that would add too much complexity. RTK is intended to handle the 80% use case, and the TS types for createSlice are already incredibly complicated. Adding any more complexity here would be bad.




回答2:


I came across this question looking to combine the benefits of redux-toolkit and redux-orm. I was able to come up with a solution I've been pretty happy with so far. Here is what my redux-orm model looks like:

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;

The redux-toolkit slice is created as a static property on the class, and then the model and its actions are exported in a manner similar to Ducks (ORMDucks??).

The only other modification to make is to define a custom updater for redux-orm's reducer:

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

See a more complete example here: https://gist.github.com/JoshuaCWebDeveloper/25a302ec891acb6c4992fe137736160f

Some Notes

  • @markerikson makes a good point about some of the features of redux-toolkit not being used since redux-orm is managing the state. For me, the two greatest benefits of using this method are not having to wrangle a whole bunch of action creators and not having to contend with awful switch statements :D.
  • I am using the stage 3 class fields and static class features proposals. (See https://babeljs.io/docs/en/babel-plugin-proposal-class-properties). To make this ES6 compatible, you can easily refactor the model class to define its static props using the current syntax (i.e. Book.modelName = 'Book';).
  • If you decide to mix models like the one above with models that don't define a slice, then you'll need to tweak the logic in the createReducer updater slightly.

For a real world example, see how I use the model in my project here: https://github.com/vallerance/react-orcus/blob/70a389000b6cb4a00793b723a25cac52f6da519b/src/redux/models/OrcusApp.js. This project is still in the early stages. The largest question in my mind is how well this method will scale; however, I am optimistic that it will continue to provide numerous benefits as my project matures.




回答3:


Try using normalized-reducer. It's a higher-order-reducer that takes a schema describing the relationships, and returns a reducer, action, and selectors that write/read according to the relationships.

It also integrates easily with Normalizr and Redux Toolkit.



来源:https://stackoverflow.com/questions/59619753/rewrite-redux-orm-reducer-with-redux-toolkit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!