29

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
  • 16,148
  • 4
  • 74
  • 121
a person
  • 1,266
  • 2
  • 15
  • 24
  • 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 Patten Ferreira 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
  • 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

5 Answers5

34

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)
    })
}
Dupocas
  • 18,570
  • 6
  • 30
  • 49
  • 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
  • @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
  • 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
7

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,263
  • 1
  • 16
  • 29
  • 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
6

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,658
  • 4
  • 14
0

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
  • 13,809
  • 12
  • 96
  • 141
-7
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState();

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

};