10

I'm using redux-toolkit and I'm trying to save my state to local storage after each update of the store without using any third-parties libraries. The reason for this is redux-persist is no longer updated and I don't know any good alternative. After lots of time looking for solution, I came up with using createListenerMiddleware.

import { configureStore, createListenerMiddleware } from "@reduxjs/toolkit";
import counterSlice, { decrement, increment } from "../Slices/counterSlice";

const listenerMiddleware = createListenerMiddleware()
listenerMiddleware.startListening({
    actionCreator: increment,
    effect: () => (
        localStorage.setItem('count', JSON.stringify(store.getState().counter))
    )
})
const listenerMiddleware2 = createListenerMiddleware()
listenerMiddleware.startListening({
    actionCreator: decrement,
    effect: () => (
        localStorage.setItem('count', JSON.stringify(store.getState().counter))
    )
})
const counterState = JSON.parse(localStorage.getItem('count') || "null")
export const store = configureStore({
    preloadedState: {
        counter: counterState === null ? { value: 0 } : counterState
    },
    reducer: {
        counter: counterSlice
    },
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(listenerMiddleware2.middleware, listenerMiddleware.middleware)
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Could someone tell me if this is a good idea, and if not, is there any other way of doing it properly.

Andrew T.
  • 4,701
  • 8
  • 43
  • 62
Mr.Nobody
  • 117
  • 1
  • 4

1 Answers1

11

You are on the right track but there are a few changes:

Accessing State

Rather than using the store variable directly to call getState(), you should access the getState() function from the arguments of the effect callback.

effect: (action: Action, listenerApi: ListenerApi) => void | Promise<void>

Your effect function gets called with two arguments: the action which triggered the effect and the listenerApi object, which gives you access to getState() and many other utilities.

The listener middleware is not aware of the TypeScript type for the store so you need to make it aware somehow. One way to do this is with an inline as assertion on the listenerApi.getState() call.

listenerMiddleware.startListening({
  actionCreator: increment,
  effect: (action, listenerApi) =>
    localStorage.setItem("count", JSON.stringify((listenerApi.getState() as RootState).counter))
});

You can also follow the TypeScript Usage section of the docs and use the TypedStartListening utility type.

Handling Multiple actions

You don't need multiple listener middleware instances because you can attach many listeners to the same middleware. Simple call startListening multiple times on the same listenerMiddleware variable.

However in this instance one startListening call is all that you need! Both of your cases already have the same effect callback. But we need to be able to match both increment and decrement actions. There are a few different ways that a listener can detect matching actions. Instead of using actionCreator, which can only match a single action, we will use the matcher property. We create a matcher function with a little help from the isAnyOf utility.

Our final middleware looks like this:

import { createListenerMiddleware, isAnyOf } from "@reduxjs/toolkit";
import { decrement, increment } from "./slice";
import type { RootState } from "./index";

export const listenerMiddleware = createListenerMiddleware();
listenerMiddleware.startListening({
  matcher: isAnyOf(increment, decrement),
  effect: (action, listenerApi) =>
    localStorage.setItem(
      "count",
      JSON.stringify((listenerApi.getState() as RootState).counter)
    )
});

Complete CodeSandbox Demo

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102