Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rewrite redux-orm reducer with redux-toolkit

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

like image 764
mrkvon Avatar asked Jan 06 '20 22:01

mrkvon


People also ask

Can I mutate state in Redux toolkit?

One of the primary rules of Redux is that our reducers are never allowed to mutate the original / current state values!

What is difference between Redux and Redux toolkit?

Redux Toolkit makes it easier to write good Redux applications and speeds up development, by baking in our recommended best practices, providing good default behaviors, catching mistakes, and allowing you to write simpler code. Redux Toolkit is beneficial to all Redux users regardless of skill level or experience.


1 Answers

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.

like image 97
markerikson Avatar answered Sep 19 '22 11:09

markerikson