Jose
Jose

Reputation: 5200

React: Setting state to an es6 Map

This is more of a general, best-practices question.

I've been playing around here and there with JavaScript Maps and have been trying to find more info on whether or not it's considered an anti-pattern/code smell to set state properties to a Map. The link below is an issue thread in the Redux repo with some comments such as:

"You can use Maps and Sets as state values, but it's not recommended due to serializability concerns."

However this thread is about Redux. What about vanilla React? Anyone have any strong opinions or insight? Sorry if this question is in the wrong place.

https://github.com/reduxjs/redux/issues/1499

Upvotes: 37

Views: 48872

Answers (4)

nanobar
nanobar

Reputation: 66355

If you don't want to use an immutable library you can create a new Map on change (this is a shallow copy of the map, like spreading an object):

const [someMap, setSomeMap] = useState(new Map())

And when you need to update it:

setSomeMap(new Map(someMap).set('someKey', 'a new value'))

The same concept applies to Redux:

case 'SomeAction':
  return {
    ...state,
    yourMap: new Map(state.yourMap).set('someKey', 'a new value')
  }

With regards to serializability it's not a concern for local state. It's good practice to have a Redux store that is serializable though.

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.

You can see that JSON.stringify unfortunately doesn't work on maps:

console.log(JSON.stringify(
  new Map([['key1', 'value1'], ['key2', 'value2']])
))

If you can get in between the serialization process you can use Array.from:

console.log(JSON.stringify(
  Array.from(new Map([['key1', 'value1'], ['key2', 'value2']]))
))

Upvotes: 37

Rok Strniša
Rok Strniša

Reputation: 7202

While I love immutable.js, I don't recommend using it, because it has many circular dependencies between its types, which means that any tree shaking is ineffective, leading to a substantial chunk in your bundle (131 KB parsed or 18 KB zipped for v5), even if you only use one of its types (e.g. immutable Map).

Instead, I recommend using the built-in Map with object indirection (as already suggested by Leon Adler), since assignment creates a new container object, causing React to re-render.

JavaScript:

const [someMap, setSomeMap] = useState({ map: new Map() });

function getSomeMapValue(key) {
    return someMap.map.get(key);
}

function updateSomeMap(key, value) {
    setSomeMap(({ map }) => ({ map: map.set(key, value) }));
}

TypeScript:

const [someMap, setSomeMap] = useState<{ map: Map<string, string> }>({ map: new Map() });

function getSomeMapValue(key: string): string | undefined {
    return someMap.map.get(key);
}

function updateSomeMap(key: string, value: string): void {
    setSomeMap(({ map }) => ({ map: map.set(key, value) }));
}

This way, you can do O(1) updates, have a map as part of React state that preserves insertion order, and have a small(er) bundle.

Upvotes: 1

Leon Adler
Leon Adler

Reputation: 3331

For the rare use cases where a Map/Set/WeakMap/WeakSet are the right tool, you can absolutely use them as state, albeit a bit "weird". (Caution: Here be dragons)

The problem, as other posters have mentioned, is that React compares state and props by referential equality - Object.is(oldState, newState) -, at which point React is like "oh, this is the same Map, I don't need to do anything."

So per the other answers, you would either have to use a dependency (immutable), or copy the whole Map to a new Map, which can be very slow, depending on the size of your data.

As a valid workaround, you can set an object as state that contains a Map/Set, which still allows you to cause a rerender when the map was updated:

// Lets assume we receive items which can be reordered or filtered,
// but remain the same object reference unless they change -
// and we do not know if the item objects have a primary key (.id).
// 
// We still want to persist if an item is selected or not.
//
function ListOfThousandObjects({ items }) {
  const [selected, setSelected] = useState({ items: new Map() });

  const toggleSelected = useCallback(item => {
    setSelected((selected) => {
      selected.items.set(item, !selected.items.get(item));
      return { items: selected.items };
      // ^ We return a new item, causing a rerender -
      //   without having to copy all the items in the Map.
    });
  }, []);

  useEffect(() => {
    saveSelectionToSessionStorage(selected.items);
  }, [selected]);
  // ^ This feels weird but works, as the object
  //   reference changes, even if the map is the same

  return <ul>
    {items.map(item => (
      <ObjectListRow
        key={generateStableUniqueId(item)}
        item={item}
        onToggle={() => toggleSelected(item)
      />
    ))}
  </ul>
}

Upvotes: 5

Abdul Rauf
Abdul Rauf

Reputation: 6221

React state should be immutable because React uses shallow compare to check for equality. When comparing scalar values (numbers, strings) it compares their values. When comparing objects, it does not compare their properties - only their references are compared (i.e. "do they point to same object ?").

ES6 Maps are not immutable and are optimized for mutability, that's why it's not recommended to use these in React as it is. React will not know whether map is updated or not.

var map1 = new Map();
var map2 = map1.set('b', 2); // mutate map
map1 === map2; // true because reference remains unchanged after mutation

You can use Maps if you want but you need to use some immutability helper e.g. Immutable.js. Following example is using immutable map

const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 2); // Set to same value
map1 === map2; // true
const map3 = map1.set('b', 4); // Set to different value
map1 === map3; // false

References:

https://github.com/reduxjs/redux/issues/1499#issuecomment-194002599

https://stackoverflow.com/a/36084891/2073920

Upvotes: 31

Related Questions