Search code examples
javascriptreactjsreact-contextuse-context

Deleting items in ReactJS, using useState and useContext


As a fairly new addict to programming, I'm stuck with a simple app that lists items. Adding items, is OK. But as for deleting one item, I just can't figure out how to implement this function. Now on clicking an item the whole array of items is deleted. I have tried to put the deleteItem function in other files, but I seem to have mixed up the references.

ItemContext.js:

import React, { useState, createContext } from 'react';
import { v1 as uuidv1 } from 'uuid';
export const ItemContext = createContext();

export const ItemProvider = (props) => {
  const [items, setItems] = useState([
    {
      name: "Red Beans",
      amount: 23,
      id: uuidv1()
    },
    {
      name: "Nuts for Bunnies",
      amount: 33,
      id: uuidv1()
    },
    {
      name: "Chopped Tomatoes",
      amount: 2,
      id: uuidv1() 
    }
]);
  return ( 
      <ItemContext.Provider value={[items, setItems]} >
        {props.children}
      </ItemContext.Provider>
   );
}

AddItem.js:

    import React, { useState, useContext } from 'react';
import { ItemContext } from './ItemContext';

const AddItem = () => {
  const [name, setName] = useState('');
  const [amount, setAmount] = useState('');
  const [items, setItems] = useContext(ItemContext);

  const updateName = (e) => {
    setName(e.target.value)
  }

  const updateAmount = (e) => {
    setAmount(e.target.value)
  }

  const addItem = (e) => {
    e.preventDefault();
    setItems(prevItems => [...prevItems, {name: name, amount: amount, key: items.id}])
  }

  return ( 
      <form className="new-item" onSubmit={addItem}>
        <input type="text" name="name" value={name} onChange={updateName} placeholder="Add a new item"/>
        <input type="text" name="amount" value={amount} onChange={updateAmount} placeholder="Amount"/>
        <button>Submit</button>
      </form>
   );
}
 
export default AddItem;

Item.js:

import React, { useState, useContext } from 'react';
import { ItemContext } from './ItemContext';

const Item = ({name, amount, /*deleteItem*/ }) => {
  const [id, setId] = useState('');
  const [items, setItems] = useContext(ItemContext);

  const deleteItem = (id) => {
    // const newItems = items.filter(item => items.id !== id);
    // setItems(newItems);
    const newItems = Object.assign([], ...items)
    items.splice(id, 1);
    setItems(newItems);
  }

  return (
    <div>
      <h3>{name}</h3>
      <p>{amount}</p>
      <button onClick={() => deleteItem(items.id)} className="delete-btn">Delete</button>
    </div>
  );
}
 
export default Item;

ItemsList.js:

import React, { useState, useContext } from 'react';
import Item from './Item';
import { ItemContext } from './ItemContext';
import { v1 as uuidv1 } from 'uuid';

const ItemsList = () => {
  const [items, setItems] = useContext(ItemContext);
  const [id, setId] = useState('');

  const deleteItem = (id) => {
    // const newItems = items.filter(item => items.id !== id);
    // setItems(newItems);
    const newItems = Object.assign([], ...items)
    items.splice(id, 1);
    setItems(newItems);
  }

  return (
    <div className="items-list">
      {items.map(item => (
        <Item name={item.name} amount={item.amount} key={uuidv1()} deleteItem={deleteItem}/>
      ))}
    </div> 
   );
}
 
export default ItemsList;

Solution

  • Initially you need to provide a valid initial value to the ItemContext context you are creating. Its signature should match what the context value will be. I suggest using an object instead of an array.

    export const ItemContext = createContext({
      items: [],
      addItem: () => {},
      deleteItem: () => {}
    });
    

    Then define addItem and deleteItem handlers to include in the context value. You want to avoid exposing out the setItems state function directly so ItemProvider maintains control over the state invariant.

    const initialState = [
      {
        name: "Red Beans",
        amount: 23,
        id: uuidv1()
      },
      {
        name: "Nuts for Bunnies",
        amount: 33,
        id: uuidv1()
      },
      {
        name: "Chopped Tomatoes",
        amount: 2,
        id: uuidv1()
      }
    ];
    
    const ItemProvider = (props) => {
      const [items, setItems] = useState(initialState);
    
      const addItem = (item) => setItems((items) => [...items, item]);
      const deleteItem = (id) =>
        setItems((items) => items.filter((item) => item.id !== id));
    
      const value = {
        items,
        addItem,
        deleteItem
      };
    
      return (
        <ItemContext.Provider value={value}>
          {props.children}
        </ItemContext.Provider>
      );
    };
    

    Then destructure the ItemContext value where you are consuming it.

    AddItem.js - Make sure to assign a new uuidV1 GUID for new items. I think this was your biggest issue, you were assigning undefined to a key property (key: items.id) instead of creating a new GUID for the id property.

    import React, { useState, useContext } from 'react';
    import { ItemContext } from './ItemContext';
    
    const AddItem = () => {
      const [name, setName] = useState("");
      const [amount, setAmount] = useState("");
      const { addItem } = useContext(ItemContext);
    
      const updateName = (e) => {
        setName(e.target.value);
      };
    
      const updateAmount = (e) => {
        setAmount(e.target.value);
      };
    
      const submitHandler = (e) => {
        e.preventDefault();
        addItem({
          name: name,
          amount: amount,
          id: uuidv1() // <-- new GUID here!!
        });
      };
    
      return (
        <form className="new-item" onSubmit={submitHandler}>
          <input
            type="text"
            name="name"
            value={name}
            onChange={updateName}
            placeholder="Add a new item"
          />
          <input
            type="text"
            name="amount"
            value={amount}
            onChange={updateAmount}
            placeholder="Amount"
          />
          <button>Submit</button>
        </form>
      );
    };
    

    Item.js - Get deleteItem from the context and ensure to pass item id to component.

    import React, { useState, useContext } from 'react';
    import { ItemContext } from './ItemContext';
    
    const Item = ({ name, amount, id }) => {
      const { deleteItem } = useContext(ItemContext);
    
      return (
        <div>
          <h3>{name}</h3>
          <p>{amount}</p>
          <button onClick={() => deleteItem(id)} className="delete-btn">
            Delete
          </button>
        </div>
      );
    };
    

    ItemsList.js - Set the React key to the current item id and also pass the id as a prop to `Item.

    import React, { useState, useContext } from 'react';
    import Item from './Item';
    import { ItemContext } from './ItemContext';
    
    const ItemsList = () => {
      const { items } = useContext(ItemContext);
    
      return (
        <div className="items-list">
          {items.map((item) => (
            <Item
              key={item.id} // <-- Static React key to item
              id={item.id} // <-- Pass id as prop for delete handler to use
              name={item.name}
              amount={item.amount}
            />
          ))}
        </div>
      );
    };
    

    Demo

    Edit deleting-items-in-reactjs-using-usestate-and-usecontext