skip to Main Content

I’m building a Redux store using Redux Toolkit and I want to store a map of users in my state, like this:

export type UserType = {
  _id: string
  firstName: string; 
  lastName: string;
  email: string;
};

interface UserStoreType {
  users: Map<string, UserType> 
}

const initialState: UserStoreType = {
  users: new Map();
};

export const userSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
      append: (state, action) => {
        const user = action.payload;
        state.users.set(user._id, user);
      }
  },
});

export const userAction = userSlice.actions;
export const userReducer = userSlice.reducer;

However, I’ve seen some mentions that using Map or Set in Redux state is an anti-pattern and should be avoided.
Is it considered bad practice to use Map/Set in my Redux state like this? If so, what is the recommended approach? Should I just use a plain JavaScript object instead of a Map?
I’m using Redux Toolkit, so I know I can safely mutate the state in reducers. But I’m not sure if using Map/Set specifically is problematic or discouraged for any reasons.
Any guidance on best practices here would be appreciated!

I tried implementing my Redux state using a Map like in the code example above. I was expecting that this would allow me to efficiently look up users by ID.
The main reason I opted for a Map over a plain JavaScript object is because I thought it would provide better performance for lookups compared to searching through an object by key.
I also liked the API that Map provides, such as being able to iterate through keys/values easily.
My expectation was that using Map would be a reasonable approach for storing key/value pairs in my Redux state.
However, after reading some discussions online, it seems using Map/Set might not be recommended. So I wanted to get some clarity around if I should avoid using Map/Set or if it’s actually ok to use them in this case.
Let me know if any other details would be helpful! I’m open to changing my implementation if there are good reasons not to use Map/Set here.

2

Answers


    1. Using Maps/Sets is already not recommended by the Redux maintainers. See the Redux style-guide/best-practices, specifically Do Not Put Non-Serializable Values in State or Actions listed as "essential", meaning:

      These rules help prevent errors, so learn and abide by them at all costs. Exceptions may exist, but should be very rare and only be
      made by those with expert knowledge of both JavaScript and Redux.

    2. I was expecting that this would allow me to efficiently look up users
      by ID. The main reason I opted for a Map over a plain JavaScript
      object is because I thought it would provide better performance for
      lookups compared to searching through an object by key.

      Using plain Javascript objects also provides O(1), e.g. constant time, lookup using property accessors, e.g. someObject[someKey]. It’s quite common to create a "Map lookup" object for just this purpose.

      const users = {
        "cH-ihuFgEUoPlW3Hr3AMz": {
          email: "nUwAwCD2IOTEcV3IMRqHR",
          firstName: "Qcmt9extT4u86RyW3kUOL",
          lastName: "y0kG4nMwjB9HVgk59qP4i",
          _id: "cH-ihuFgEUoPlW3Hr3AMz",
        },
        "Bt7Beuq_fvKStxKSau6jd": {
          email: "X-rWOsEdvIbmRhEgrc54F",
          firstName: "Cdiohj-_sdPoy2NiIgpHl",
          lastName: "Go6N-mWZP0yPhSZOHJcka",
          _id: "Bt7Beuq_fvKStxKSau6jd",
        },
        "dy82a_8uCPGFoQoJzlNWc": {
          email: "iakeMqaF5lhfbFi9nSt9F",
          firstName: "LskDqhRWDxSfM_n4RRm13",
          lastName: "Xt4-pVYqnyAs6fE5-DUi1",
          _id: "dy82a_8uCPGFoQoJzlNWc",
        },
      };
      
      console.log(users["dy82a_8uCPGFoQoJzlNWc"]);
    3. Redux-Toolkit uses Immer.js under the hood, so in order to use Map/Set you’ll need to do extra step to enable their usage.

      // In your application's entrypoint
      import { enableMapSet } from "immer";
      
      enableMapSet();
      

      Additionally, React works on shallow reference equality checks, so any state updates necessarily need to create new references. RTK (*and immer) abstracts this away which allows you to write mutable state updates and it handles creating the new references under the hood. I don’t have performance specs but shallow copying is at least an O(n) operation in both cases (Map/Set vs JS Object) to copy each key/value.

    4. Even after you’ve enabled Map/Set to be used in RTK the Redux-Devtools are not as helpful. The state updates correctly but you don’t have direct insight into the value.

      enter image description here

    5. After all the above setup you’ll still see an error in the console regarding serializability.

      proxyConsole.js:64 A non-serializable value was detected in the state, in the path: `users.users`. Value: 
      Map(3) {'ZuE_o3JpGrCN7QvJ_dhv5' => {…}, '2fty9kKttftgQqsWOJ8hp' => {…}, 'v5vmQUXrD3WPwWsp9-LrH' => {…}}
        [[Entries]]
          0: {"ZuE_o3JpGrCN7QvJ_dhv5" => Object}
          1: {"2fty9kKttftgQqsWOJ8hp" => Object}
          2: {"v5vmQUXrD3WPwWsp9-LrH" => Object}
        add: ƒ dontMutateFrozenCollections()
        clear: ƒ dontMutateFrozenCollections()
        delete: ƒ dontMutateFrozenCollections()
        set: ƒ dontMutateFrozenCollections()
        size: 3
        [[Prototype]]: Map
      
       
      Take a look at the reducer(s) handling this action type: counter/append.
      (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)
      console.<computed>    @   proxyConsole.js:64
      dispatch  @   VM3453:6
      addNewUser    @   App.tsx:13
      Show 23 more frames
      

      You can modify the store to ignore this specific part of state so the error is not produced. See Serializability Middleware.

      const store = configureStore({
        reducer: {
          users: userReducer
        },
        middleware: (getDefaultMiddleware) =>
          getDefaultMiddleware({
            serializableCheck: {
              ignoredPaths: ["users.users"]
            }
          })
      });
      
    6. As your state becomes more complex or you later decide you would like to persist your Redux store, you’ll again likely run into the issue/problem of serializability. Most solutions involve serializing the state to JSON for longer-term storage like localStorage. Map objects just don’t serialize well out-of-the-box. Plain Javascript objects are just easier to work with.

       
      const fooMap = new Map([["foo", "foo"], ["bar", "bar"]]);
      const fooObj = { foo: "foo", bar: "bar" };
      
      console.log("fooMap:", fooMap, fooMap.toString(), JSON.stringify(fooMap));
      console.log("fooObj:", fooObj, fooObj.toString(), JSON.stringify(fooObj));

    Conclusion

    For all the reasons above I’d say yeah, using Map/Set in Redux state is a poor decision and should not be used. Just about any performance gain you get from using a Map/Set is IMHO easily wiped out by all the extra setup/configuration/logic necessary to make working with them easier.

    Login or Signup to reply.
  1. The Redux docs advises against using Map, Set, or any non-plain Objects in Redux state management. They recommend using plain JS objects and arrays as they are easier to handle with existing tools and practices in the Redux ecosystem. This makes the state easier to manage and serialize. It also adheres to Immer’s immutability practices.

    Here are the primary reasons:

    1. Serialization and debugging
    2. Immutability concerns

    Serialization and debugging

    Can I put functions, promises, or other non-serializable items in my store state?

    It is highly recommended that you only put plain serializable objects, arrays, and primitives into your store. It’s technically possible to insert non-serializable items into the store, but doing so can break the ability to persist and rehydrate the contents of a store, as well as interfere with time-travel debugging.

    If you are okay with things like persistence and time-travel debugging potentially not working as intended, then you are totally welcome to put non-serializable items into your Redux store. Ultimately, it’s your application, and how you implement it is up to you. As with many other things about Redux, just be sure you understand what tradeoffs are involved.

    Redux state is typically serialized (converted to a string format) when using tools like the Redux DevTools for debugging. JavaScript’s Map and Set types are not easily serializable to JSON without custom logic, as JSON.stringify() does not correctly handle Map and Set. This can lead to difficulties when trying to inspect the state in developer tools or persisting the state in storage that only supports string data, like localStorage.

    Immutability concerns

    What are the benefits of immutability?

    Immutability can bring increased performance to your app, and leads to simpler programming and debugging, as data that never changes is easier to reason about than data that is free to be changed arbitrarily throughout your app.

    In particular, immutability in the context of a Web app enables sophisticated change detection techniques to be implemented simply and cheaply, ensuring the computationally expensive process of updating the DOM occurs only when it absolutely has to (a cornerstone of React’s performance improvements over other libraries).

    Redux relies heavily on immutability for efficient updates, especially when connected with React or similar libraries. While it’s possible to treat Map and Set objects immutably (by always creating a new Map or Set with changes rather than modifying the original), common methods for these types like .set() and .delete() mutate the object in place. This mutation can cause subtle bugs in Redux, particularly when components do not re-render in response to state changes because Redux did not detect any changes (due to direct mutation).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search