How to use splice in React Hooks

Viewed 1225

When I selected a ID in select box, I want to put a new name in the selected ID of the names array. I used useState and useRef, but "splice" dosen't work. I don't know how to pass the selected ID to splice function. It's so hard to me. Help me, please.

import React, { useRef, useState } from 'react';
const App = ()=>{
  const [names, setNames] = useState([
    {id: 1, text: 'aaa'},
    {id: 2, text: '222'},
    {id: 3, text: 'bbb'},
  ]);
  const [inputText, setInputText] = useState('');
  const [nextId, setNextId] = useState(4);
  const onChange = e => setInputText(e.target.value);
  const inputEl = useRef(null);
  const optionId = useRef(null);
  const onClick = () => {
      const nextNames = names.concat({
        id: nextId,
        text: inputText,
        });
      setNextId(nextId + 1);
      setNames(nextNames);
      setInputText('');
      console.log(inputEl.current);
      console.log(optionId.current);
  };
  const onSelect = id => {
    const nextNames = names.splice(id, 0, {id:id, text: inputText});
  
    setNames(nextNames);
    setInputText('');
  }
  const onRemove = id => {
    const nextNames = names.filter(name => name.id !== id);
    setNames(nextNames);
  };
  const nameList = names.map(name => (
    <li key={name.id} onDoubleClick={()=> onRemove(name.id)}>{name.text}</li>
  ));
  const idOption = names.map((name, index)=>(
     <option key={name.id}>{index}</option>
     ));
  return(
    <div>
      <input value={inputText} onChange={onChange} ref={inputEl}/>
      <button onClick={onClick}>추가</button>
      <select ref={optionId} onSelect={onSelect}>
        <option>ID</option>
            {idOption}
        <option>last</option>
      </select>
      <ul>{nameList}</ul>
    </div>
  );
};
3 Answers

Array.prototype.splice mutates the array it's called on in-place, which is almost always what you don't want to use and is considered anti-pattern in React when the array is part of state. You need to shallow copy the array to correctly return a new array reference for React's reconciliation process.

The splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place.

Since you want to either add new elements to the array or update existing elements, you will first need to search the array to determine which operation you need to do. If adding a new element you can simply shallow copy the previous array into a new array reference and append the new element. If updating an existing element then mapping the previous state to a new array is what you want.

const onSelect = id => {
  setNames(names => {
    // find if match exists
    const match = names.find(name => name.id === id);

    if (match) {
      // match found, map array
      return names.map(name => name.id === id
        ? {
          ...name,
          text: inputText,
        }
        : name
      );
    }
    
    // no match found, concat new data
    return names.concat({ id, text: inputText });
  });
  setInputText('');
}

Splice accepts indexes, so that will not work check the reference.

But you should use slice instead because splice mutates the array you are working on and you should not mutate useState value directly. See slice reference for more information.

Nevertheless, you have better options for what you want to do, such as map and reduce.

Here is an example with reduce:

  const onSelect = id => {
    const nextNames = names.reduce((acc, next) => {
      const name = {...next};

      // only change the text prop if we find an id match
      if (name.id === id) {
        name.text = inputText;
      }

      // copy the name to the new array
      return [...acc, name];
    }, []);
  
    setNames(nextNames);
    setInputText('');
  }

Thanks for your answers. However, it dosen't work properly. The working of below code is exactly what I want. These code is class component style, but I need same working in React Hooks. Please, have a look below code and if you possible, please, change to code with react hooks. Thank you.

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      components: []
    };
  }

  addNewElement(element, selectedIndex) {
    if (selectedIndex == "last") {
      this.state.components.push(element);
    } else {
      this.state.components.splice(selectedIndex, 0, element);
    }
    this.setState({ components: this.state.components });
  }
  render() {
    let input, option;
    // log out state
    console.log(this.state.components);
    return (
      <div>
        <h3>{this.state.components.join(", ")}</h3>
        <input
          placeholder="enter element"
          ref={node => {
            input = node;
          }}
          />
        <select
          className={this.state.components.length ? "" : "hidden"}
          ref={node => {
            option = node;
          }}
          >
          <option value="" disabled selected>Insert at index...</option>
          {this.state.components.map((component, index) =>
                                     <option>{index}</option>
                                    )}
          <option>last</option>
        </select>
        <button
          onClick={() => {
            this.addNewElement(input.value, option.value);
          }}
          >
          Click
        </button>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
Related