ReduxJS/Toolkit: createSlice and configureStore
posted in javascript on • by Wouter Van Schandevijl •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 sliceconfigureStore
: abstracts away the horror that wascreateStore
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
- ImmutableStateInvariantMiddleware: detects any mutations to state
- SerializableStateInvariantMiddleware: detects any non-serializable values in state or actions
- ActionCreatorInvariantMiddleware: detects if an action creator has been dispatched (an action creator should’ve been called before being dispatched)
// 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 |