Home > Back-end >  loading components twice, probably because of useEffect wrong set-up
loading components twice, probably because of useEffect wrong set-up

Time:01-17

I have built a ToDo React App (https://codesandbox.io/s/distracted-easley-zjdrkv) that does the following:

  • User write down an item in the input bar
  • User hit "enter"
  • Item is saved into the list below (local storage, will update later)
  • There is some logic to parse the text and identify tags (basically if the text goes "@tom:buy milk" --> tag=tom, text=buy milk)

The problem I am facing are:

  • useEffect runs twice at load, and I don't understand why
  • After the first item gets saved, if I try saving a second item, the app crashes. Not sure why, but I feel it has to do with the point above...and maybe the event listener "onKeyDown"

App

import { useState, useEffect } from 'react'
import './assets/style.css';
import data from '../data/data.json'


import InputBar from "./components/InputBar/InputBar"
import NavBar from "./components/NavBar/NavBar"
import TabItem from "./components/Tab/TabItem"

function App() {
  const [dataLoaded, setDataLoaded] = useState(
    () => JSON.parse(localStorage.getItem("toDos")) || data
  )

  useEffect(() => {
    localStorage.setItem("toDos", JSON.stringify(dataLoaded))
    console.log('update')
  }, [dataLoaded])



  function deleteItem(id){
    console.log(id)
    setDataLoaded(oldData=>{
      return {
        ...oldData,
        "items":oldData.items.filter(el => el.id !== id)
      }
    })

  }

  
  return (
    <div className='container'>

      <NavBar/>
      <InputBar
        setNewList = {setDataLoaded}
      />
      {
        //Items
        dataLoaded.items.map(el=>{
          console.log(el)
          return  <TabItem item={el} key={el.id} delete={deleteItem}/>
        })
      }   

    </div>
  )
}

export default App


InputBar

import { useState, useEffect } from 'react'
import { nanoid } from 'nanoid'
import '../../assets/style.css';


export default function InputBar(props){

    const timeElapsed = Date.now();
    const today = new Date(timeElapsed);

    function processInput(s) {
       
        let m = s.match(/^(@. ?:)?(. )/)

        if (m) {
            return {
                tags: m[1] ? m[1].slice(1, -1).split('@') : ['default'],
                text: m[2],
                created: today.toDateString(),
                id:nanoid()
            }
        }
    }


    function handleKeyDown(e) {

        console.log(e.target.value)
        console.log(document.querySelector(".main-input-div input").value)

        if(e.keyCode==13){
            props.setNewList(oldData =>{
                return {
                        ...oldData,
                        "items" : [processInput(e.target.value), ...oldData.items]
                    }
                }                
            )
            e.target.value=""
        }
    }

   

   
    return(
        <div className="main-input-div">
            <input type="text" onKeyDown={(e) => handleKeyDown(e)}/>
        </div>
    )
}

Tab

import { useState } from 'react'
import "./tab-item.css"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTrash } from "@fortawesome/free-solid-svg-icons";



export default function TabItem(props) {


    return (
        <div className="tab-item">
            <div className="tab-item-text">{props.item.text}</div>
            <div className="tab-item-actions">
                <FontAwesomeIcon icon={faTrash} onClick={()=>props.delete(props.item.id)}/>
            </div>
            <div className="tab-item-details">
                <div className="tab-item-details-tags">
                    { 
                        props.item.tags.map(el=><div className="tab-item-details-tags-tag">{el}</div>)
                    }                    
                </div>
            </div>
            <div className="tab-item-date">{props.item.created}</div>
        </div>
    )
}

CodePudding user response:

Your app is using strict mode, which in a development mode renders components twice to help detect bugs (https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects).

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

As for the crash, I think it's happening due to props.setNewList being an asynchronous call and the resetting of e.target.value - something like this seemed to fix it for me:

function handleKeyDown(e) {

    console.log(e.target.value)
    console.log(document.querySelector(".main-input-div input").value)

    if(e.keyCode==13){
        const inputVal = e.target.value;
        props.setNewList(oldData =>{
            return {
                    ...oldData,
                    "items" : [processInput(inputVal), ...oldData.items]
                }
            }                
        )
        e.target.value=""
    }
}

I will add, that using document.querySelector to get values isn't typical usage of react, and you might want to look into linking the input's value to a react useState hook. https://reactjs.org/docs/forms.html#controlled-components

CodePudding user response:

The above answer is almoost correct. I am adding more info to the same concepts.

useEffect running twice: This is most common ask in recent times. It's because the effect runs twice only in development mode & this behavior is introduced in React 18.0 & above. The objective is to let the developer see & warn of any bugs that may appear due to a lack of cleanup code when a component unmounts. React is basically trying to show you the complete component mounting-unmounting cycle. Note that this behavior is not applicable in the production environment. Please check https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed for a detailed explanation.

App crashes on second time: It's probably because you are trying to update the input value from event.target.value if you want to have control over the input value, your input should be a controlled component meaning, your react code should handle the onChange of input and store it in a state and pass that state as value to the input element & in your onKeyDown handler, reset the value state. That should fix the crash.

export default function InputBar(props){
    const [inputVal, setInputVal] = useState("");

    function handleKeyDown(e) {
        console.log(e.target.value)
        console.log(document.querySelector(".main-input-div input").value)
        if(e.keyCode==13){
            props.setNewList(oldData =>{
                return {
                        ...oldData,
                        "items" : [processInput(e.target.value), ...oldData.items]
                    }
                }                
            )
            setInputVal("")
        }
    }

    return(
        <div className="main-input-div">
            <input 
            type="text"
            value={inputVal}
            onChange={(e) => {setInputVal(e.target.value)}}
            onKeyDown={(e) => handleKeyDown(e)}
            />
        </div>
    )
}

Hope this helps. Cheers!

  • Related