简体   繁体   中英

React: Confusing behavior with useEffect and/or useState hooks

React noob here. I'm trying to make a simple practice app to help with my React learning and I'm getting a lot of odd behavior. It's a Todo app with multiple todo lists to choose from. The behavior I'm going for is a list of Todo lists, where you select one and the todo items for that (like wunderlist/msft todo). Select a different list and it's todo items show, etc. At this point it's using static json where each item has a child array.

useEffect - I'm trying to use this to load the data. It keeps complaining about missing dependencies. When I add them it complains about that too. It complains if I use and empty array for the second parameter and seems to fire multiple times.

useState - I'm using this to store the data. It's initialized to an empty array. But render is firing before useEffect and it's saying my data is undefined so my second list never renders.

I have several console.logs in the code and they're all firing multiple times.

I'm sure it's just noob mistakes but I'm pretty stumped at this point. Here's my code:

data/Todo.js

 const TodoData = [ { Id: 1, Title: "Groceries", TodoList: [ { Id: 1, Title: "Apples" }, { Id: 2, Title: "Oranges" }, { Id: 3, Title: "Bananas" } ] }, { Id: 2, Title: "Daily Tasks", TodoList: [ { Id: 11, Title: "Clean Kitchen" }, { Id: 12, Title: "Feed Pets" }, { Id: 13, Title: "Do Stuff" } ] }, { Id: 3, Title: "Hardware Store", TodoList: [] }, { Id: 4, Title: "Costco", TodoList: [ { Id: 21, Title: "Diapers" }, { Id: 22, Title: "Cat Food" }, { Id: 23, Title: "Apples" }, { Id: 24, Title: "Bananas" } ] }, { Id: 5, Title: "Work", TodoList: [ { Id: 34, Title: "TPS Reports" } ] } ]; export default TodoData;

App.Js

 import React, { useEffect, useState } from "react"; import TodoData from "./data/Todo.js"; function App() { const [todoData, setTodoData] = useState([]); const [todoDetails, setTodoDetails] = useState([]); useEffect(() => { if (todoData.length === 0) { getTodoData(); } }, [todoData]); const getTodoData = () => { setTodoData(TodoData); console.log("getting todo data"); getTodoDetails(1); }; const getTodoDetails = id => { const result = TodoData.filter(x => x.Id === id); console.log(result[0]); setTodoDetails(result[0]); }; const handleClick = e => { const selectedId = Number(e.target.getAttribute("data-id")); getTodoDetails(selectedId); }; return ( <div className="App"> <div className="container-fluid"> <div className="row"> <div className="list-group col-md-4 offset-md-1"> {todoData.map(todos => ( <button key={todos.Id} data-id={todos.Id} className="btn list-group-item d-flex justify-content-between align-items-center" onClick={handleClick} > {todos.Title} <span className="badge badge-primary badge-pill"> {todos.TodoList.length} </span> </button> ))} </div> <div className="col-md-6"> <ul className="list-group"> <h2>{todoDetails.Title} List</h2> {console.log(todoDetails.TodoList)} {/* this fails miserably */} {/* {todoDetails.TodoList.map(details => ( <li className="list-group-item" key={details.Id}> {details.Title} </li> ))} */} <li className="list-group-item">Cras justo odio</li> <li className="list-group-item">Dapibus ac facilisis in</li> <li className="list-group-item">Morbi leo risus</li> </ul> </div> </div> </div> </div> ); } export default App;
Demo: https://codesandbox.io/s/interesting-dan-rdoyh?fontsize=14&hidenavigation=1&theme=dark

Q: useEffect - I'm trying to use this to load the data. It keeps complaining about missing dependencies

A: you can ignore it via eslint (react-hooks/exhaustive-deps) , there is nothing to worry about


Q: useState - I'm using this to store the data. It's initialized to an empty array. But render is firing before useEffect and it's saying my data is undefined so my second list never renders.

A: Do read the comments in code, hope that will clear your doubts

// you are intializing `todoDetails` with array, when there will be object
// hence the error while mapping inside the html/jsx
// const [todoDetails, setTodoDetails] = useState([]);

// init with `null`, why?
const [todoDetails, setTodoDetails] = useState(null);

// so that you can do something like this
{
  todoDetails && (   // <---------- HERE
    <ul className="list-group">
      <h2>{todoDetails.Title} List</h2>

      {console.log(todoDetails.TodoList)}

      {/* this fails miserably */}
      {todoDetails.TodoList.map(details => (
        <li className="list-group-item" key={details.Id}>
          {details.Title}
        </li>
      ))}
    </ul>
  )
}

Q: Now you are getting multiple console.log ,

A: the reason behind this is <React.StrictMode> ,

React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

I've removed it from index.js in demo, so you can see the diff


WORKING DEMO:

编辑angerous-oskar-37rq9

The code that you commented and said that is failing is due to todoDetails.TodoList be undefined on the first time you are rendering the component. To avoid that, either initialize the todoDetails with one item or check if todoDetails.TodoList is defined before calling the .map on it.

{todoDetails.TodoList ? todoDetails.TodoList.map(details => (
  <li className="list-group-item" key={details.Id}>
    {details.Title}
  </li>
)) : null}

or

const [todoDetails, setTodoDetails] = useState(TodoData[0]);

Don't worry too much about the eslint (react-hooks/exhaustive-deps) error. You need to understand what this second parameter of the useEffect means. In your code, you passed the todoData to the useEffect :

  useEffect(() => {
    if (todoData.length === 0) {
      getTodoData();
    }
  }, [todoData]);

This means that, whenever the todoData changes, the useEffect will run, behaving like a componentDidUpdate . If you pass only an empty array, it means that your useEffect has no dependencies, and it will be executed only once, behaving like a componentDidMount . So, what you did is fine here.

Besides that, here are some tips that I can give you about your code:

About your handleClick function, you can remove it and simple call the getTodoDetails(id) passing the id instead of adding it to the data-id and getting it later, this way:

<button
  key={todos.Id}
  className="btn list-group-item d-flex justify-content-between align-items-center"
  onClick={() => getTodoDetails(todos.Id)} // <---- change is in here
>
  {todos.Title}
  <span className="badge badge-primary badge-pill">
    {todos.TodoList.length}
  </span>
</button>

On your getTodoDetails , you can change the filter to find , since you don't need to go through your entire array to find one item, making it more expensive. Plus, with that, you don't need to access the first item of the array.

const getTodoDetails = id => {
  const result = TodoData.find(x => x.Id === id);
  console.log(result);
  setTodoDetails(result);
};

You are using your todoDetails as an object and initializing it as an array . If your todoDetails will always have only one todo, initialize it as an object, this way you can prevent accessing todoDetails.Title from an array. The useEffect will always be executed after rendering.

const [todoDetails, setTodoDetails] = useState({});

You have to put the logic of getTodoData into the useEffect hook

useEffect(() => {
    const getTodoData = () => {
      setTodoData(TodoData);
      console.log("getting todo data");
      getTodoDetails(1);
    };

    if (todoData.length === 0) {
      getTodoData();
    }
  }, [todoData.length]);

And in rendering

{
  // check todoDetails has TodoList
  todoDetails.TodoList &&
    todoDetails.TodoList.map((details) => (
      <li className="list-group-item" key={details.Id}>
        {details.Title}
      </li>
    ));
}

here is the updated demo

The useEffect hook can be weird sometimes. To fix it, you simply pass in your getTodoData function directly like so:

 const getTodoData = () => {
  setTodoData(TodoData);
  console.log("getting todo data");
  getTodoDetails(1);
};

useEffect(getTodoData, []);

It's important to define the getTodoData function before passing it into useEffect otherwise you will get an error.

I uncommented your code that "fails miserably" and was able to get everything working flawlessly.

Here is the entirety of your fixed App.js file:

import React, { useEffect, useState } from "react";
import TodoData from "./data/Todo.js";

function App() {
  const [todoData, setTodoData] = useState([]);
  const [todoDetails, setTodoDetails] = useState([]);

  const getTodoData = () => {
    setTodoData(TodoData);
    console.log("getting todo data");
    getTodoDetails(1);
  };

  useEffect(getTodoData, []);

  const getTodoDetails = id => {
    const result = TodoData.filter(x => x.Id === id);
    console.log(result[0]);
    setTodoDetails(result[0]);
  };

  const handleClick = e => {
    const selectedId = Number(e.target.getAttribute("data-id"));
    getTodoDetails(selectedId);
  };

  return (
    <div className="App">
      <div className="container-fluid">
        <div className="row">
          <div className="list-group col-md-4 offset-md-1">
            {todoData.map(todos => (
              <button
                key={todos.Id}
                data-id={todos.Id}
                className="btn list-group-item d-flex justify-content-between align-items-center"
                onClick={handleClick}
              >
                {todos.Title}
                <span className="badge badge-primary badge-pill">
                  {todos.TodoList.length}
                </span>
              </button>
            ))}
          </div>
          <div className="col-md-6">
            <ul className="list-group">
              <h2>{todoDetails.Title} List</h2>

              {console.log(todoDetails.TodoList)}

              {todoDetails.TodoList.map(details => (
                <li className="list-group-item" key={details.Id}>
                  {details.Title}
                </li>
              ))}
            </ul>
          </div>
        </div>
       </div>
    </div>
   );
}
export default App;

This Stackoverflow post is similar to yours and may help you understand how to avoid errors when using the useEffect hook in the future. I hope this helps!

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