skip to Main Content

I have an array of objects as a state variable. I am trying to write a function that changes a field in one of the objects.

interface IItem{
 id: string,
 selected: boolean
}

const [array, setArray] = useState<IItem[]>([])

const toggleItem = (id : string) => {
  const item = array.find(i => i.id === id)
  if (item) {
     const copy = {...item, selected: !!item.selected}
     setArray(array.map(i => i.id === id ? copy : i))
  }
} 

The code works, but it looks too complex for this. Is there a way to simplify? (Note, that the order of the items in the array matters).

3

Answers


  1. You can simplify the function as follows: (same thing, just reduced lines!)

    const toggleItem = (id: string) => {
       setArray(array.map(i => (i.id === id) ? {...i, selected: !i.selected} : i));
    };
    
    • If the id matches, we create a new object with the same properties as
      the current item (…i), but we toggle the selected field using
      !i.selected.
    Login or Signup to reply.
  2. If you want to simplify state changes, I would recommend checking out the package Immer. With Immer you get a writeable draft of the state that you can mutate. The code might be as long, be if you’re working with even more deeply nested object it is very helpful. I also think in you case it helps a lot with readability.

    import { useState } from "react";
    import { produce } from "immer";
    
    interface IItem {
      id: string;
      selected: boolean;
    }
    
    const [array, setArray] = useState<IItem[]>([]);
    
    const toggleItem = (id: string) => {
      setArray(
        produce((draft) => {
          const item = draft.find((el) => el.id === id);
          item.selected = !item.selected;
        }),
      );
    };
    

    EDIT

    If you don’t want to introduce a package for local state, I don’t think there’s an obvious way to change your code to make it less complex. Here’s an alternative at least which I would say is a bit more easy to read.

    interface IItem {
      id: string;
      selected: boolean;
    }
    
    const [array, setArray] = useState<IItem[]>([]);
    
    const toggleItem = (id: string) => {
      setArray((previous) =>
        previous.map((item) => {
          if (item.id === id) {
            return {
              ...item,
              selected: true,
            };
          }
          return item;
        }),
      );
    };
    
    
    Login or Signup to reply.
  3. Hide it away in a custom hook:

    interface IItem {
      id: string,
      selected: boolean
    }
    
    function useSelect<T>({ initialItems = [] }) {
      const [items, setItems] = useState<IItem[]>(initialItems);
    
      const toggleItem = (id: string) => {
        const item = items.find(i => i.id === id)
        if (item) {
          const copy = { ...item, selected: !!item.selected }
          setItems(items.map(i => i.id === id ? copy : i))
        }
    
        return { items, toggleItem };
      }
    }
    

    and use it like:

    function MyView() {
      const { items, toggleItem } = useSelect({ initialItems: [...] });
    
      return (
        <>
          {items.map(({ id, selected }) => (
            <div
              key={id}
              onClick={() => toggleItem(id)}>
              {id} {selected ? '(selected)' : ''}
            </div>
          ))}
        </>
      )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search