如何在React的useState中使用Set?

47

我有一个数组,需要添加/删除元素,我想使用Set来完成这个操作,因为它具有addhasdelete这些方法。

const [tags, setTags] = React.useState(new Set())
如果我想向tags添加一些内容,我该如何使用setTags来实现呢?还是说我只需调用tags.add()即可?

如果我想对tags进行添加操作,应该使用tags.add()而不是setTags


1
你不能这样做,因为Set不是一个不可变的数据结构,而React的状态需要是不可变的。 - Emile Bergeron
1
这个回答解决了你的问题吗?在React的state中使用Set数据结构 - Elder
谢谢!如果每次都需要重新创建Set,使用它是不是一个不好的主意?看起来我也需要克隆一个常规数组,所以我想知道哪个更受欢迎。 - a person
1
一个 Set 存储唯一的值,因此,除非你想编写过滤重复值的代码部分,每次创建一个新的 Set 都是可以的。 - Emile Bergeron
9个回答

55

Set 是可变的,如果你仅仅调用 const newSet = set.add(0) ,React 不会触发新的渲染,因为先前和现在进行浅比较时总是会断言为 true

你可以使用 spread 运算符,在每次更新之间改变引用,同时仍然保留 Set 的所有行为

添加元素

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

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

您仍然可以使用add方法,因为它返回更新后的集合

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

移除元素

移除操作要稍微复杂一些。首先你需要将其转换成数组,然后使用filter函数并展开结果。

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

为了清晰明了

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

或者,您也可以按照以下方式使用删除方法:
const removeFoo = foo =>{ 
  setState(prev => {
    prev.delete(foo);
    return new Set(prev);
  })
}

谢谢!那删除操作怎么办,因为它会返回一个布尔值?抱歉我不太熟悉... - a person
更新了答案! - Dupocas
2
@Dupocas,为什么在removeFoo函数中使用Set内部的spread/filter?为什么不使用delete方法?const removeFoo = foo => setState(prev => new Set([...prev.delete(foo)])); - WebBrother
3
我建议保留先前状态的完整性,代码如下: const addFoo = foo => setState(prev => (new Set(prev)).add(foo))const removeFoo = foo => setState(prev => (new Set(prev)).delete(foo)) - Dustin Gaudet
2
上面的评论中不确定使用 new Set(prev).delete(foo) - Set.add() 的返回值是一个 Set,但 Set.delete() 的返回值是一个布尔值。 - nickdos
显示剩余2条评论

11

你需要创建一个新的集合,否则React将不知道它需要重新渲染。类似以下内容将起作用。

setTags(tags => new Set(tags).add(tag))

10

我按照以下方法进行,我的React Native组件运行得非常好。


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>)

}

希望这篇文章能够帮助到有相同使用场景的人。请参考下面的Code Sandbox示例

1
看起来不错。一个小的改进是从if/else块中删除“setStateUseIds(userIds)”,并在if/else之后简单地添加它,因为它在两种情况下都运行。 - Yamo93

3
如果你想使用集合作为你的保存状态(在每次集合成员变化时重新渲染),你可以将上述技术封装成一个漂亮的小钩子,返回一个几乎像集合一样行为的对象(下面详见)。
具体来说,我使用以下自定义钩子:
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
        }

    }
}

你可以使用钩子 const set = useSetState([]),并调用 set.add(el)set.delete(el) 等方法。

然而,由于这个钩子的实现方式,返回的对象并不完全是一个 Set 对象,因为 instanceof、Object.prototype.toString 等都无法将其识别为 Set 对象(此外,我没有实现 forEach 方法中的 thisArg)。如果你真的关心这个问题,你可以使用代理来实现相同的逻辑,但在大多数情况下,我认为这似乎有些过度了。

2

我个人发现所有这些解决方案在每次修改时都重新创建集合,这常常是低效的。我编写了下面的钩子来解决这个问题,我认为使用一个任意状态触发重新渲染要比在每次更改时重新创建集合更加优雅:

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
}

这里唯一的缺点是,如果消费者选择绕过提供的addremove方法并直接改变引用,那么就会出现问题。

1
我决定尝试使用对象/数组来代替集合。需要注意的是,使用对象时插入顺序并不总是被保留。但如果你有一些动态生成的复选框,并且收到一个change事件:
// 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));
}

初始化:

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

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

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

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

这是 code sandbox


0

@Peter Gerdes的答案的基础上,这里提供一个 TypeScript 版本的钩子:

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;

0

2023年更新

  • const set = new Set([...])一样工作
  • 在变化和渲染过程中保持相同的Set对象
  • 提供对所有set方法和属性的访问
  • Typescript

用法

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>, [])
}

-9
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState();

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

};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接