46

I have an array that I have to add/remove elements from, and I figured I would use Set to accomplish this because of its add has and delete.

const [tags, setTags] = React.useState(new Set())

If I want to add something to tags, how can I do this with setTags? Or do I just call tags.add() ?

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
a person
  • 1,518
  • 3
  • 17
  • 26
  • 1
    You can't since a Set is not an immutable data structure and React's state needs to be immutable. – Emile Bergeron Nov 11 '19 at 18:38
  • 1
    Does this answer your question? [Using a Set data structure in React's state](https://stackoverflow.com/questions/44482788/using-a-set-data-structure-in-reacts-state) – Elder Nov 11 '19 at 18:38
  • Thanks! So is it a bad idea to use Set if I have to re-create it each time? It seems I'd have to clone a regular array as well, so I'm wondering which is preferred. – a person Nov 11 '19 at 18:50
  • 1
    A `Set` stores unique values, so unless you want to code the part where you filter out duplicated values, creating a new `Set` each time would be ok. – Emile Bergeron Nov 11 '19 at 18:57

9 Answers9

53

A Set is by definition mutable, React won't trigger a new render if you merely call const newSet = set.add(0) cause the shallow comparison between previous and new will always assert to true

You can use the spread operator to change references between each update yet still maintaining all of Set's behaviors

Adding an element

const [state, setState] = useState(new Set())

const addFoo = foo =>{
    setState(previousState => new Set([...previousState, foo]))
}

You could still use the add method since it returns the updated set

const addFoo = foo =>{
    setState(prev => new Set(prev.add(foo)))
}

Removing an element

Removing is a little trickier. You first need to turn it into an array, filter and spread the result

const removeFoo = foo =>{
    setState(prev => new Set([...prev].filter(x => x !== foo)))
}

For clarity

const removeFoo = foo =>{ 
    setState(prev =>{
        return prev.filter(x => x !== foo)
    })
}

Alternatively you can also make use of the delete methods as follows:

const removeFoo = foo =>{ 
  setState(prev => {
    prev.delete(foo);
    return new Set(prev);
  })
}
Dupocas
  • 20,285
  • 6
  • 38
  • 56
  • Thanks! What about removing since that returns a boolean value? Sorry I'm not too familiar ... – a person Nov 11 '19 at 18:53
  • Updated the answer! – Dupocas Nov 11 '19 at 19:05
  • 2
    @Dupocas , why spread/filter inside of `Set` in `removeFoo`? Why you don't use `delete` method? `const removeFoo = foo => setState(prev => new Set([...prev.delete(foo)]));` – WebBrother Oct 18 '21 at 13:52
  • 3
    I'd recommend preserving integrity of the prev state like so: ```const addFoo = foo => setState(prev => (new Set(prev)).add(foo))``` and ```const removeFoo = foo => setState(prev => (new Set(prev)).delete(foo))``` – Dustin Gaudet Nov 03 '21 at 12:03
  • Can also easily do full set unions: ```const unionFoo = (fooArr) => setState(prev => { const s = new Set(prev); [...fooArr].forEach(s.add, s); return s; })``` and differences: ```const differenceFoo = (fooArr) => setState(prev => { const s = new Set(prev); [...fooArr].forEach(s.delete, s); return s; })``` – Dustin Gaudet Nov 03 '21 at 12:19
  • 2
    Not sure about using `new Set(prev).delete(foo)` in the comment above - the return value of `Set.add()` is a Set but the return value of `Set.delete()` is a Boolean. – nickdos Jul 05 '22 at 00:33
  • `setState(prev => (prev.delete(item), new Set(prev)))` solves the return type and is not cnsistent with the `Set.add` variant. or `setState(prev => new Set((prev.delete(item), prev)))` – Qwerty May 17 '23 at 04:25
10

You have to create a new set, otherwise react won't know that it needs to rerender. Something like the below will work.

setTags(tags => new Set(tags).add(tag))
Jake Luby
  • 1,718
  • 5
  • 16
10

I followed the following approach and is working pretty well with my React Native component.


const DummyComponent = () => {
const [stateUserIds, setStateUseIds] = useState(new Set());

....
....

const handleUserSelected = user => {
    // Since we cannot mutate the state value directly better to instantiate new state with the values of the state
    const userIds = new Set(stateUserIds);

    if (userIds.has(user.userId)) {
      userIds.delete(user.userId);
    } else {
      userIds.add(user.userId);
    }
    setStateUseIds(userIds);
  };

....
....


return (
   <View> 
       <FlatList
          data={dummyUsers}
          renderItem={({item, index}) => {
            const selected = stateUserIds.has(item.userId);
            return (
              
                <View style={{flex: 2}}>
                  <Switch
                    isSelected={selected}
                    toggleSwitch={() => handleUserSelected(item)}
                  />
                </View>
            );
          }}
          keyExtractor={(item, index) => item.userId.toString()}
        />
  </View>)

}

Hope this helps someone with same use Case. Please refer to the following Code Sandbox example

Pratap Sharma
  • 2,633
  • 2
  • 18
  • 32
  • 1
    That looks good. A small improvement would be to remove "setStateUseIds(userIds)" from the if/else blocks and simply add it after the if/else, since it runs in both cases. – Yamo93 Jul 30 '21 at 13:57
3

If you want to use a set as your saved state (rerendering on each change to the set's membership) you can wrap up the techniques described above into a nice little hook that returns an object that behaves (almost...see below) like a set.

Specifically, I use the following custom hook:

const useSetState = (initial) => {
    const [set, setSet] = useState(new Set(...initial))
    return {
        add: el =>
            setSet((set) => {
                if (set.has(el)) return set
                set.add(el)
                return new Set(set)
            }),
        delete: el => {
            setSet((set) => {
                if (!set.has(el)) return set
                set.delete(el)
                return new Set(set)
            })
        },
        has: el => set.has(el),
        clear: () => setSet(new Set()),
        [Symbol.iterator]: () => set.values(),
        forEach: (fn) => set.forEach(fn),
        keys: () => set.keys(),
        values: () => set.values(),
        get size() {
            return set.size
        }

    }
}

You can use the hook like const set = useSetState([]) and then call set.add(el), set.delete(el) etc..

However, because of the way this hook is implemented the returned object isn't quite a set in that instanceof, Object.prototype.toString etc won't recognize it as a set (also, I didn't implement the thisArg of forEach). If you really cared about this you could implement the same logic using proxies but that seems excessive for most applications imo

Peter Gerdes
  • 2,288
  • 1
  • 20
  • 28
2

I personally found the fact that all these solutions re-create the set on each modification, which is often counter productive. I wrote the below hook to get around this issue, and IMO having a piece of arbitrary state to trigger a re-render is less hacky than re-creating the set on each change:

function useSet<T>(initialValue?: T[]) {
    // a peice of state to trigger a re-render whenever the set changes
    // imo this is less hacky than re-creating the set on every state change
    const [_, setInc] = useState(false)

    //use a ref for the set instead
    const set = useRef(new Set<T>(initialValue))
    
    const add = useCallback(
        (item: T) => {
            if (set.current.has(item)) return
            setInc((prev) => !prev)
            set.current.add(item)
        },
        [setInc],
    )

    const remove = useCallback(
        (item: T) => {
            if (!set.current.has(item)) return
            setInc((prev) => !prev)
            set.current.delete(item)
        },
        [setInc],
    )

    return [set.current, add, remove] as const
}

The only downside here is if a consumer chooses to bypass the supplied add and remove methods and mutate the ref directly.

AaronHS
  • 1,334
  • 12
  • 29
1

I decided to try using an object/array in place of a set. It should be noted that with objects insertion order is not always preserved. But if you have a number of dynamically generated checkboxes and you receive a change event:

// set
function handleChange(e, name) {
  setSet(prv => e.target.checked
    ? new Set([...prv, name])
    : new Set([...prv].filter(v => v != name)));
}
// object
function handleChange(e, name) {
  setSet(prv => ({...prv, [name]: e.target.checked}));
}
// array
function handleChange(e, name) {
  setSet(prv => e.target.checked
    ? (prv.includes(text) ? prv : [...prv, text])
    : prv.filter(v2 => v2 != v));
}

Initialize:

const [set, setSet] = useState(new Set);
const [set, setSet] = useState({});
const [set, setSet] = useState([]);

Add:

setSet(prv => new Set([...prv, text]));
setSet(prv => ({...prv, [text]: true}));
setSet(prv => prv.includes(text) ? prv : [...prv, text]);

Remove:

setSet(prv => new Set([...prv].filter(v2 => v2 != v)));
setSet(prv => ({...prv, [v]: false}));
setSet(prv => prv.filter(v2 => v2 != v));

Iterate:

[...set].map(v => ...);
Object.keys(set).filter(v => set[v]).map(v => ...);
set.map(...);

The code sandbox.

x-yuri
  • 16,722
  • 15
  • 114
  • 161
0

Adding onto @Peter Gerdes's answer, here is a TypeScript version of the hook:

import { useState } from 'react';

const useSetState = <T>(initial: T[]) => {
    const [set, setSet] = useState<Set<T>>(new Set(initial));
    return {
        add: (el: T) =>
            setSet((set) => {
                if (set.has(el)) return set;
                set.add(el);
                return new Set(set);
            }),
        delete: (el: T) => {
            setSet((set) => {
                if (!set.has(el)) return set;
                set.delete(el);
                return new Set(set);
            });
        },
        has: (el: T) => set.has(el),
        clear: () => setSet(new Set()),
        [Symbol.iterator]: () => set.values(),
        forEach: (callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any) => set.forEach(callbackfn, thisArg),
        keys: () => set.keys(),
        values: () => set.values(),
        get size() {
            return set.size;
        },
    };
};
export default useSetState;
Ali Bdeir
  • 4,151
  • 10
  • 57
  • 117
0

Updated for 2023

  • Works just like const set = new Set([...])
  • Maintains the same Set object throughout mutations and renders
  • Provides access to all set methods and properties
  • Typescript

Usage

const Component = ({ ...props }: Props) 
  const set = useSet([1, 2, 3]);

  // ...
}
// @/lib/use-state.ts

import { useMemo, useRef, useState } from "react";

export function useSet<T>(initialValue: T[]) {
  const triggerRender = useState(0)[1];
  const set = useRef(new Set<T>(initialValue))
  return useMemo(() => ({
    add(item) {
      if (set.current.has(item)) return
      set.current.add(item)
      triggerRender(i => ++i)
    },
    delete(item) {
      if (!set.current.has(item)) return
      set.current.delete(item)
      triggerRender(i => ++i)
    },
    clear() {
      if (set.current.size === 0) return
      set.current.clear()
      triggerRender(i => ++i)
    },
    has: (item) => set.current.has(item),
    keys: () => set.current.keys(),
    values: () => set.current.values(),
    forEach: (...args) => set.current.forEach(...args),
    [Symbol.iterator]: () => set.current.values(),
    get size() { return set.current.size },
  }) as Set<T>, [])
}
Spencer
  • 878
  • 8
  • 5
-9
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState();

const setCount = () => {
    setCounter(count + 1);
    setMoreStuff(...);

};