React UseState is called twice inside eventListener

1k views Asked by At

I am starting to learn react in my free time. When starting Hooks,the setState method is called twice. The live code is at

https://codesandbox.io/s/elastic-saha-mdwwc?file=/src/App.js

The Add button works fine.But when I press enter, the setState function is called twice. Initial thought was that the content re rendered.But I have the useEffect dependency as an empty array. So no re-rendering is done and I am not sure how to debug this. Any help is appreciated :)


const App = () => {
  const [text, setText] = useState('');

  const [list, setList] = useState([]);

  const addToList = () => {
    if (text !== '') {
      setList([...list, text]);
      setText('');
    }
  }

  const deleteItem = index => {
    const deletedList = list.filter((_, i) => i !== index);
    setList(deletedList)
  }
  useEffect(() => {
    console.log("Reloaded");
    const listener = e => {
      if (e.code === 'Enter') {
        console.log("event triggered")
        setText(text => {
          if (text !== '') {
            console.log("updating")
            setList(a => ([...a, text]));
          }
          return ''
        });
      }
    }
    document.getElementById('textbox').addEventListener('keyup', listener);
    console.log('Event registered')
    return () => {
      document.getElementById('textbox').removeEventListener('keyup', listener);
      console.log('Event deregistered')

    }
  }, [])
  return (
    <div >
      <input type="text" id="textbox" onChange={(e) => setText(e.target.value)} value={text} />
      <button id="add" onClick={addToList}> Add</button>
      <ul>
        {
          list.map((a, i) => <li key={i}>{a} <button type="button" onClick={() => deleteItem(i)}>Delete</button></li>)
        }
      </ul>
    </div>
  );
}

export default App;
1

There are 1 answers

3
ultimoTG On BEST ANSWER

I think it's because of this in the useEffect in your code:

setText(text => {
    if (text !== '') {
        console.log("updating")
        setList(a => ([...a, text]));
    }
    return ''
});

Change it to the following:

useEffect(() => {
    console.log("Reloaded");
    const listener = (e) => {
      if (e.code === "Enter") {
        console.log("event triggered");
        if (text !== "") {
          console.log("updating");
          setList((a) => [...a, text]);
        }
        return "";
      }
    };
    document.getElementById("textbox").addEventListener("keydown", listener);
    console.log("Event registered");
    return () => {
      document
        .getElementById("textbox")
        .removeEventListener("keydown", listener);
      console.log("Event deregistered");
    };
  }, [text]);

Or,

useEffect(() => {
    console.log("Reloaded");
    const listener = (e) => {
      if (e.code === "Enter") {
        console.log("event triggered");
        if (e.target.value !== "") {
          console.log("updating");
          setList((a) => [...a, e.target.value]);
        }
        e.target.value = "";
        return "";
      }
    };
    document.getElementById("textbox").addEventListener("keydown", listener);
    console.log("Event registered");
    return () => {
      document
        .getElementById("textbox")
        .removeEventListener("keydown", listener);
      console.log("Event deregistered");
    };
  });

OR,

This one uses onKeyPress event and only runs useEffect once when the component is mounted

import React, { useState, useEffect } from "react";

const App = () => {
  const [text, setText] = useState("");
  const [list, setList] = useState([]);

  const addToList = () => {
    if (text !== "") {
      setList([...list, text]);
      setText("");
    }
  };

  const deleteItem = (index) => {
    const deletedList = list.filter((_, i) => i !== index);
    setList(deletedList);
  };

  const listener = (e) => {
    e.stopPropagation();
    if (e.key === "Enter") {
      console.log("event triggered");
      if (text !== "") {
        console.log("updating");
        setList((a) => [...a, text]);
      }
      setText("");
      return "";
    }
  };
  
  // not needed, only here to show it only runs once
  useEffect(() => {
    console.log("Reloaded");
  }, []);

  return (
    <div>
      <input
        type="text"
        id="textbox"
        onChange={(e) => setText(e.target.value)}
        onKeyPress={listener}
        value={text}
      />
      <button id="add" onClick={addToList}>
        {" "}
        Add
      </button>
      <ul>
        {list.map((a, i) => (
          <li key={i}>
            {a}{" "}
            <button type="button" onClick={() => deleteItem(i)}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

https://codesandbox.io/s/winter-dust-r3ylc?file=/src/App.js