Update October 2022: You can also use redux-toolkit's createListenerMiddleware
in versions 1.8 and up, as explained in this answer.
Changing localStorage
is a side-effect so you don't want to do it in your reducer. A reducer should always be free of side-effects. One way to handle this is with a custom middleware.
Writing Middleware
Our middleware gets called after every action is dispatched. If the action is login
or logout
then we will change the localStorage
value. Otherwise we do nothing. Either way we pass the action off to the next middleware in the chain with return next(action)
.
The only difference in the middleware between redux-toolkit and vanilla redux is how we detect the login
and logout
actions. With redux-toolkit the action creator functions include a helpful match()
function that we can use rather than having to look at the type
. We know that an action
is a login action if login.match(action)
is true. So our middleware might look like this:
const authMiddleware = (store) => (next) => (action) => {
if (authActions.login.match(action)) {
// Note: localStorage expects a string
localStorage.setItem('isAuthenticated', 'true');
} else if (authActions.logout.match(action)) {
localStorage.setItem('isAuthenticated', 'false');
}
return next(action);
};
Applying Middleware
You will add the middleware to your store in the configureStore
function. Redux-toolkit includes some middleware by default with enables thunk, immutability checks, and serializability checks. Right now you are not setting the middleware
property on your store at all, so you are getting all of the defaults included. We want to make sure that we keep the defaults when we add our custom middleware.
The middleware
property can be defined as a function which gets called with the redux-toolkit getDefaultMiddleware
function. This allows you to set options for the default middleware, if you want to, while also adding our own. We will follow the docs example and write this:
const store = configureStore({
reducer: {
movements: movementsSlice.reducer,
auth: authSlice.reducer,
},
// Note: you can include options in the argument of the getDefaultMiddleware function call.
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authMiddleware)
});
Don't do this, as it will remove all default middleware
const store = configureStore({
reducer: {
movements: movementsSlice.reducer,
auth: authSlice.reducer,
},
middleware: [authMiddleware]
});
Syncing State via Middleware
We could potentially streamline our middleware by matching all auth
actions. We do that by using the String.prototype.startsWith()
method on the action.type
(similar to the examples in the addMatcher
docs section which use .endswith()
).
Here we find the next state by executing next(action)
before we change localStorage
. We set the localStorage
value to the new state returned by the auth
slice.
const authMiddleware = (store) => (next) => (action) => {
const result = next(action);
if ( action.type?.startsWith('auth/') ) {
const authState = store.getState().auth;
localStorage.setItem('auth', JSON.stringify(authState))
}
return result;
};
Or you can use the redux-persist package, which does that for you.