ReduxJS/Toolkit: createSlice and configureStore

The very minimum you need to know

ReduxJS/Toolkit: createSlice and configureStore

posted in javascript on  •   • 

The legacy Redux createStore could make a grown man cry, and probably has. createSlice and configureStore GREATLY simplifies this process while at the same time also significantly reducing the boilerplate – you can basically delete all your actions as they are now defined by the reducers!

  • createSlice: each top level object of your state is typically a slice
  • configureStore: abstracts away the horror that was createStore

createSlice

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export const todoSlice = createSlice({
  // Action prefix: todos/addTodo, todos/removeTodo etc
  name: "todos",
  initialState: [] as Todo[],
  reducers: {
    // A default reducer:
    // The actions are exported from the reducers (below)
    removeTodo: (state, action: PayloadAction<number>) => {
      return state.filter(todo => todo.id !== action.payload);
    },

    // A prepared reducer
    // When you need to modify the payload before handling
    addTodo: {
      reducer: (state, action: PayloadAction<Todo>) => {
        state.push(action.payload);
      },
      prepare: (todo: Partial<Todo>) => {
        // The addTodo action takes a Partial<Todo>
        // But the reducer handles a complete Todo.
        const payload = {
          text: 'Default Text',
          ...todo,
        };
        return { payload };
      }
    },
  },


  // We'll get into more detail in Part 3: createAsyncThunk
  extraReducers: builder => {
    // This is where we would handle the pending, fulfilled and rejected actions
    // from a createAsyncThunk

    // Handling an action from another slice is also a use case for extraReducers
    builder.addCase(addCustomer, (state, action: PayloadAction<{name: string}>) => {
      state.push({text: `Onboard new customer ${action.payload.name}`});
    });
  },


  // We'll get into more detail in Part 5: createSelector
  selectors: {
    selectTodo: (state, id: number): Todo | undefined => {
      return state.find(x => x.id === id);
    },
  }
});

export const { addTodo, removeTodo } = todoSlice.actions;
export const { selectTodo } = todoSlice.selectors;

Dispatching an action

See Part 4: TypeScript for how to create the useAppDispatch hook.

import { addTodo } from "./todoSlice";

const TodoAdd = () => {
  const dispatch = useAppDispatch();
  const [todo, setTodo] = useState<Todo>({});
  return <button onClick={() => dispatch(addTodo(todo))}>Add</button>
}

configureStore

Once you’ve defined some slices, time to combine them into your store.

// Create the store
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: {
    todos: todoSlice.reducer,
    user: userSlice.reducer,
  },

  // DevTools extension is added by default
  // Here we disable it for production:
  devTools: process.env.NODE_ENV !== 'production',

  // Optionally hydrate the store from somewhere:
  preloadedState: localStorage.getItem('redux'),
});


// Bind it to our main <App /> component
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";

const root = createRoot(
  document.getElementById('root')
);
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

This will create a store with “RootState”:

type RootState = {
  todos: ReturnType<typeof todoSlice.getInitialState>;
  user: ReturnType<typeof userSlice.getInitialState>;
}

It really doesn’t get any easier than that. Without overriding anything, this will also setup the must have Redux DevTools Extension (⭐ 13k).

This can be overriden/configured by passing devTools: false | DevToolsOptions to configureStore.

Middleware

The redux-thunk middleware is added by default, since you are going to be doing some http calls 😊

During development builds, the following middleware is also automatically added

// Extend the default middleware
configureStore({
  middleware: getDefaultMiddleware => getDefaultMiddleware()
    .concat(loggingMiddleware, persistMiddleware)
    .prepend(listenerMiddleware.middleware)
});

// Explicitly configure your own middleware
configureStore({
  middleware: () => new Tuple(myMiddleware, logger),
})

// Enable/Disable or configure the default middleware
configureStore({
  middleware: getDefaultMiddleware => getDefaultMiddleware({
    // Always added (unless explicitly passing false!)
    thunk: boolean | {
      extraArgument: any
    },

    // Only added during development:
    immutableCheck: boolean | {
      isImmutable: not object, null or undefined,
      ignoredPaths: undefined,
      warnAfter: 32ms,
    },
    serializableCheck: boolean | {
      isSerializable: (value: any) => boolean,
      ignoredActions: string[],
      ignoreState: boolean,
      ignoreActions: boolean,
      ...
    },
    actionCreatorCheck: boolean | {
      isActionCreator: (action: unknown) => action is Function & { type?: unknown }
    }
  })
});

Custom Middleware

Some ready made redux middlewares:

reduxjs/redux-thunk : Thunk middleware for Redux (included)

rt2zz/redux-persist : Persist and rehydrate a redux store

redux-observable/redux-observable : RxJS middleware for action side effects using ‘Epics’

LogRocket/redux-logger : Logger for Redux

omnidan/redux-undo : ♻️ higher order reducer to add undo/redo

redux-utilities/redux-promise : Dispatch promises

agraboso/redux-api-middleware : Redux middleware for calling an API.

Or roll your own:

export const loggingMiddleware = ({dispatch, getState}) => next => action => {
  console.log('Dispatching', action);
  const result = next(action);
  console.log('State after dispatch', getState());
  return result;
}

// redux-promise is basically a simplified version of redux-thunk
export const resolvePromiseMiddleware = ({dispatch, getState}) => next => action => {
  if (action.payload instanceof Promise) {
    return action.payload.then(result => dispatch({...action, payload: result}));
  }
  return next(action);
}

When writing middleware that is only going to do something for specific actions, check out the matching utilities provided by the toolkit.

If you need to modify middleware after store creation, you can do so with createDynamicMiddleware.

Listener middleware is what ReduxJS/Toolkit currently recommends using instead of using something like redux-saga or redux-observable.

Enhancers

Middleware is added to the store by the applyMiddleware enhancer. Enhancers are the most powerful extension method available in Redux and it’s unlikely you’re going to be writing any yourself.

const store = configureStore({
  enhancers: getDefaultEnhancers => getDefaultEnhancers().concat(...),
})

The by default added enhancers are DevTools and autoBatchEnhancer.

Middleware vs Enhancers

Feature Middleware Store Enhancer
Intercepts actions? ✅ Yes ❌ No
Modifies store behavior? ❌ No ✅ Yes
Used for side effects? ✅ Yes ❌ No
Applied via? middleware enhancers
Example Action logger DevTools

Stuff that came into being during the making of this post
Tags: tutorial react