skip to Main Content

I have an array of objects with the following format

const [type1Options, setType1Options] = useState([
  {
    name: "Name1",
    value: "Value1",
  },
  {
    name: "Name2",
    value: "Value2",
  },
]);

const [type2Options, setType2Options] = useState([
  {
    name: "Name1",
    value: "Value1",
  },
  {
    name: "Name2",
    value: "Value2",
  },
]);

I am rendering these objects category wise with copy and delete buttons per entry. Delete will delete the entry from the array, and copy will copy the clicked items content into a new entry and placed right below the clicked entry. Delete works just fine, but copy on the last entry doesn’t work as expected. Can someone help?

Sandbox: https://codesandbox.io/p/sandbox/i18-demo-7594gf?file=%2Fsrc%2FApp.js%3A5%2C2-24%2C6

Utils for copy and delete functions

export const deleteItems = (list, idx) => {
  const temp = [...list];
  temp.splice(idx, 1);
  return temp;
};

export const copyItems = (list, idx) => {
  const newItem = { ...list[idx] };
  const newItems = [...list.slice(0, idx + 1), newItem, ...list.slice(idx + 1)];
  return newItems;
};
import { useState } from "react";
import List from "./List";

export default function App() {
  const [type1Options, setType1Options] = useState([
    {
      name: "Name1",
      value: "Value1",
    },
    {
      name: "Name2",
      value: "Value2",
    },
  ]);

  const [type2Options, setType2Options] = useState([
    {
      name: "Name1",
      value: "Value1",
    },
    {
      name: "Name2",
      value: "Value2",
    },
  ]);

  return (
    <div>
      <List
        type1Options={type1Options}
        type2Options={type2Options}
        setType1Options={setType1Options}
        setType2Options={setType2Options}
      />
    </div>
  );
}
import Type1 from "./Type1";
import Type2 from "./Type2";

export default function List(props) {
  const { type1Options, type2Options, setType1Options, setType2Options } =
    props;

  return (
    <>
      <div>
        Category 1
        {type1Options.map((obj, index) => (
          <Type1
            index={index}
            obj={obj}
            type1Options={type1Options}
            setType1Options={setType1Options}
          />
        ))}
      </div>
      <br />
      <div>
        Category 2
        {type2Options.map((obj, index) => (
          <Type2
            index={index}
            obj={obj}
            type2Options={type2Options}
            setType1Options={setType2Options}
          />
        ))}
      </div>
    </>
  );
}
import "./styling.css";
import { deleteItems, copyItems } from "./utils";

export default function Type1(props) {
  const { index, obj, type1Options, setType1Options } = props;

  const copyHandler = () => setType1Options(copyItems(type1Options, index + 1));

  const deleteHandler = (index) =>
    setType1Options(deleteItems(type1Options, index + 1));

  return (
    <div className="box-container">
      <div className="box-header">
        <h3>Index {index + 1}</h3>
        <div className="box-buttons">
          <button onClick={copyHandler}>Copy</button>
          <button onClick={deleteHandler}>Delete</button>
        </div>
      </div>
      <div className="box-content">
        {obj.name}: {obj.value}
      </div>
    </div>
  );
}

2

Answers


  1. Issue was the mismatch of 1 and 2 in type1Options and type2Options in 2 files. here is the updated code.

    List.jsx

    import Type1 from "./Type1";
    import Type2 from "./Type2";
    
    export default function List(props) {
      const { type1Options, type2Options, setType1Options, setType2Options } =
        props;
    
      return (
        <>
          <div>
            Category 1
            {type1Options.map((obj, index) => (
              <Type1
                index={index}
                obj={obj}
                type1Options={type1Options}
                setType1Options={setType1Options}
              />
            ))}
          </div>
          <br />
          <div>
            Category 2
            {type2Options.map((obj, index) => (
              <Type2
                index={index}
                obj={obj}
                type2Options={type2Options}
                setType2Options={setType2Options}
              />
            ))}
          </div>
        </>
      );
    }
    

    and type2.jsx

    import React from "react";
    import "./styling.css";
    import { deleteItems, copyItems } from "./utils";
    
    export default function Type2(props) {
      const { index, obj, type2Options, setType2Options } = props;
    
      const copyHandler = () => setType2Options(copyItems(type2Options, index));
    
      const deleteHandler = () => setType2Options(deleteItems(type2Options, index));
    
      return (
        <div className="box-container">
          <div className="box-header">
            <h3>Index {index + 1}</h3>
            <div className="box-buttons">
              <button onClick={copyHandler}>Copy</button>
              <button onClick={deleteHandler}>Delete</button>
            </div>
          </div>
          <div className="box-content">
            {obj.name}: {obj.value}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. Issues

    • The Type2 component is passing index + 1 to the utility functions and copying/deleting the wrong array element.

      const copyHandler = () =>
        setType1Options(copyItems(type2Options, index + 1));
      
      const deleteHandler = (index) =>
        setType1Options(deleteItems(type2Options, index + 1));
      
    • The Type1 and Type2 components fail to pass the array index to the delete utility function, they instead are passing the onClick event object and the wrong element is removed from the source array.

      const deleteHandler = (index) => // <-- onClick event object
        setType1Options(deleteItems(type1Options, index));
      
      ...
      
      <button onClick={deleteHandler}>Delete</button>
      

    Solution/Suggestions

    • Pass the current index value to correctly copy or delete the array element at that index.
    • Don’t pass the onClick event object through to utility function.
    const copyHandler = () =>
      setType1Options(copyItems(type2Options, index));
    
    const deleteHandler = () =>
      setType1Options(deleteItems(type2Options, index)); // <-- index from props
    

    Additional tips/fixes/etc

    • You should also use functional state updates to correctly update from the previous state instead of whatever is closed over in callback scope.
    • The mapped options are missing a React key. I suggest adding an id property to the options to uniquely identify them, especially since you are mutating the array by adding and removing elements. This should help avoid any React rendering issues.
    • There is also a prop naming "mismatch" in Type2 where the setter is named like it’s the Type1 component setter, i.e. setType1Options instead of setType2Options. There is no bug here, but the naming convention could be confusing to future readers/maintainers of your code.

    App.jsx

    import { useState } from "react";
    import List from "./List";
    import { nanoid } from "nanoid"; // <-- generate GUIDs
    
    export default function App() {
      const [type1Options, setType1Options] = useState([
        {
          id: nanoid(), // <-- generate GUID
          name: "Name1",
          value: "Value1",
        },
        {
          id: nanoid(), // <-- generate GUID
          name: "Name2",
          value: "Value2",
        },
      ]);
    
      const [type2Options, setType2Options] = useState([
        {
          id: nanoid(), // <-- generate GUID
          name: "Name1",
          value: "Value1",
        },
        {
          id: nanoid(), // <-- generate GUID
          name: "Name2",
          value: "Value2",
        },
      ]);
    
      return (
        <div>
          <List
            type1Options={type1Options}
            type2Options={type2Options}
            setType1Options={setType1Options}
            setType2Options={setType2Options}
          />
        </div>
      );
    }
    

    utils.ts

    import { nanoid } from "nanoid";
    
    export const deleteItems = (list, idx) => {
      const temp = [...list];
      temp.splice(idx, 1);
      return temp;
    };
    
    export const copyItems = (list, idx) => {
      const newItem = {
        ...list[idx],
        id: nanoid(), // <-- generate new id for copied element
      };
    
      return [
        ...list.slice(0, idx + 1),
        newItem,
        ...list.slice(idx + 1)
      ];
    };
    

    List.jsx

    export default function List({
      type1Options,
      type2Options,
      setType1Options,
      setType2Options,
    }) {
      return (
        <>
          <div>
            Category 1
            {type1Options.map((obj, index) => (
              <Type1
                key={obj.id} // <-- use id as React key
                index={index}
                obj={obj}
                setType1Options={setType1Options}
              />
            ))}
          </div>
          <br />
          <div>
            Category 2
            {type2Options.map((obj, index) => (
              <Type2
                key={obj.id} // <-- use id as React key
                index={index}
                obj={obj}
                setType2Options={setType2Options}
              />
            ))}
          </div>
        </>
      );
    }
    

    Type1.jsx

    export default function Type1({ index, obj, setType1Options }) {
      const copyHandler = () =>
        setType1Options((type1Options) => copyItems(type1Options, index));
    
      const deleteHandler = () =>
        setType1Options((type1Options) => deleteItems(type1Options, index));
    
      return (
        <div className="box-container">
          <div className="box-header">
            <h3>Index {index + 1}</h3>
            <div className="box-buttons">
              <button onClick={copyHandler}>Copy</button>
              <button onClick={deleteHandler}>Delete</button>
            </div>
          </div>
          <div className="box-content">
            {obj.name}: {obj.value}
          </div>
        </div>
      );
    }
    

    Type2.jsx

    export default function Type2({ index, obj, setType2Options }) {
      const copyHandler = () =>
        setType2Options((type2Options) => copyItems(type2Options, index));
    
      const deleteHandler = () =>
        setType2Options((type2Options) => deleteItems(type2Options, index));
    
      return (
        <div className="box-container">
          <div className="box-header">
            <h3>Index {index + 1}</h3>
            <div className="box-buttons">
              <button onClick={copyHandler}>Copy</button>
              <button onClick={deleteHandler}>Delete</button>
            </div>
          </div>
          <div className="box-content">
            {obj.name}: {obj.value}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search