简体   繁体   中英

Strange behaviour when removing element from array of divs (React Hooks)

I am struggling to understand a strange behaviour while deleting an element from an array of divs. What I want to do is create an array of divs representing a list of purchases. Each purchase has a delete button that must delete only the clicked one. What is happening is that when the delete button is clicked on the purchase x all the elements with indexes greather than x are deleted.

Any help will be appreciated, including syntax advices :)

import React, { useState } from "react";


const InvestmentSimulator = () => {

  const [counter, increment] = useState(0);
  const [purchases, setPurchases] = useState([
    <div key={`purchase${counter}`}>Item 0</div>
  ]);

  function addNewPurchase() {
    increment(counter + 1);
    const uniqueId = `purchase${counter}`;

    const newPurchases = [
      ...purchases,
      <div key={uniqueId}>
        <button onClick={() => removePurchase(uniqueId)}>delete</button>
        Item number {uniqueId}
      </div>
    ];

    setPurchases(newPurchases);
  }

  const removePurchase = id => {
    setPurchases(
      purchases.filter(function(purchase) {
        return purchase.key !== `purchase${id}`;
      })
    );
  };

  const purchasesList = (
    <div>
      {purchases.map(purchase => {
        if (purchases.indexOf(purchase) === purchases.length - 1) {
          return (
            <div key={purchases.indexOf(purchase)}>
              {purchase}
              <button onClick={() => addNewPurchase()}>add</button>
            </div>
          );
        }
        return purchase;
      })}
    </div>
  );

  return <div>{purchasesList}</div>;
};
export default InvestmentSimulator;

Never use indices of an array as key. Check out this article for more information about that. If you want to use index doing something as I did below.

const purchasesList = (
    <div>
      {purchases.map((purchase, i) => {
        const idx = i;
        if (purchases.indexOf(purchase) === purchases.length - 1) {
          return (
            <div key={idx}>
              {purchase}
              <button onClick={() => addNewPurchase()}>add</button>
            </div>
          );
        }
        return purchase;
      })}
    </div>
  );

There are several issues with your code, so I'll go through them one at a time:


Don't store JSX in state

State is for storing serializable data, not UI. You can store numbers, booleans, strings, arrays, objects, etc... but don't store components.


Keep your JSX simple

The JSX you are returning is a bit convoluted. You are mapping through purchases , but then also returning an add button if it is the last purchase. The add button is not related to mapping the purchases, so define it separately:

return (
    <div>
        // Map purchases
        {purchases.map(purchase => (
            // The JSX for purchases is defined here, not in state
            <div key={purchase.id}>
                <button onClick={() => removePurchase(purchase.id)}>
                    delete
                </button>
                Item number {purchase.id}
            </div>
        ))}
        // An add button at the end of the list of purchases
        <button onClick={() => addNewPurchase()}>add</button>
    </div>
)

Since we should not be storing JSX in state, the return statement is where we turn our state values into JSX.


Don't give confusing names to setter functions.

You have created a state variable counter , and named the setter function increment . This is misleading - the function increment does not increment the counter, it sets the counter . If I call increment(0) , the count is not incremented, it is set to 0.

Be consistent with naming setter functions. It is the accepted best practice in the React community that the setter function has the same name as the variable it sets, prefixed with the word "set" . In other words, your state value is counter , so your setter function should be called setCounter . That is accurate and descriptive of what the function does:

const [counter, setCounter] = useState(0)

State is updated asynchronously - don't treat it synchronously

In the addNewPurchase function, you have:

increment(counter + 1)
const uniqueId = `purchase${counter}`

This will not work the way you expect it to. For example:

const [myVal, setMyVal] = useState(0)

const updateMyVal = () => {
  console.log(myVal)
  setMyVal(1)     
  console.log(myVal)
}

Consider the above example. The first console.log(myVal) would log 0 to the console. What do you expect the second console.log(myVal) to log? You might expect 1 , but it actually logs 0 also.

State does not update until the function finishes execution and the component re-renders, so the value of myVal will never change part way through a function. It remains the same for the whole function.

In your case, you're creating an ID with the old value of counter .


The component

Here is an updated version of your component:

const InvestmentSimulator = () => {
    // Use sensible setter function naming
    const [counter, setCounter] = useState(0)

    // Don't store JSX in state
    const [purchases, setPurchases] = useState([])

    const addNewPurchase = () => {
        setCounter(prev => prev + 1)
        setPurchases(prev => [...prev, { id: `purchase${counter + 1}` }])
    }

    const removePurchase = id => {
        setPurchases(prev => prev.filter(p => p.id !== id))
    }

    // Keep your JSX simple
    return (
        <div>
            {purchases.map(purchase => (
                <div key={purchase.id}>
                    <button onClick={() => removePurchase(purchase.id)}>
                        delete
                    </button>
                    Item number {purchase.id}
                </div>
            ))}
            <button onClick={() => addNewPurchase()}>add</button>
        </div>
    )
}

Final thoughts

Even with these changes, the component still requires a bit of a re-design.

For example, it is not good practice to use a counter to create unique IDs. If the counter is reset, items will share the same ID. I expect that each of these items will eventually store more data than just an ID, so give them each a unique ID that is related to the item, not related to its place in a list.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM