0
votes

I am trying to delete an item (const removeItem) from a list using an onClick event in React. The state is managed with hooks. I know my way of deleting the item is not the right way yet (i'm putting the name to null), but this is not the issue.

After i set a user to null (i update the users object), i expect a render to happen (useEffect) but it does not. If i switch components and go back to this one, it works, but when clicking the X button, nothing happens in the view.

component Home:

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

const Home = props => {
  const [users, setUsers] = useState(props.users);

  console.log(users);

  useEffect(() => {}, [users]);

  const addItem = e => {
    users.push(e);
    console.log(e);
    e.preventDefault();
  };

  const removeItem = item => {
    users.forEach(user => {
      if (user.id === item) {
        user.name = null;
      }
    });
    console.log(users);
    setUsers(users);

  };

  return (
    <div className="Home">
      <form className="form" id="addUserForm">
        <input
          type="text"
          className="input"
          id="addUser"
          placeholder="Add user"
        />
        <button className="button" onClick={addItem}>
          Add Item
        </button>
      </form>

      <ul>
        {users.map(item => {
          return (
            <>
              <li key={item.id}>{item.name + " " + item.count}</li>
              <button className="button" onClick={() => removeItem(item.id)}>
                X
              </button>
            </>
          );
        })}
      </ul>
    </div>
  );
};

export default Home;

How i get my users object:

import React from "react";

const LotsOfUsers =[...Array(100).keys()].map((item, key) => item = {
    name : `User ${key}`,
    id: key,
    count: 0
})

export default LotsOfUsers;

main App:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useRouteMatch,
  useParams
} from "react-router-dom";
import Home from "./Home"
import CTX from './store'
import LotsOfUsers from "./LotsOfUsers";

export default function App() {
  return (
    <CTX.Provider value={{}}>
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/">
            <Home users={LotsOfUsers} text="hello world"/>
          </Route>
        </Switch>
      </div>
    </Router>
    </CTX.Provider>
  );
}

function About() {
  return <h2>About</h2>;
}

function Topics() {
  let match = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>

      <ul>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>
            Props v. State
          </Link>
        </li>
      </ul>

      {/* The Topics page has its own <Switch> with more routes
          that build on the /topics URL path. You can think of the
          2nd <Route> here as an "index" page for all topics, or
          the page that is shown when no topic is selected */}
      <Switch>
        <Route path={`${match.path}/:topicId`}>
          <Topic />
        </Route>
        <Route path={match.path}>
          <h3>Please select a topic.</h3>
        </Route>
      </Switch>
    </div>
  );
}

function Topic() {
  let { topicId } = useParams();
  return <h3>Requested topic ID: {topicId}</h3>;
}
2
try useEffect(() => {}, users);leverglowh
hello ! useEffect(() => {}, users); and useEffect(() => {}, [users]); do not work either of them.Link
yeah you have to pass a new reference to setUsers instead of the old users either by filter or simply creating a copy of users, modify the copy, and pass copy to setUsersleverglowh

2 Answers

3
votes

Looking into your code, I've noticed 2 times that you change users without the use of setUsers.

  const addItem = e => {
    users.push(e); // <--- here
    console.log(e);
    e.preventDefault();
  };

  const removeItem = item => {
    users.forEach(user => {
      if (user.id === item) {
        user.name = null; // <--- here
      }
    });
    console.log(users);
    setUsers(users);

  };

In that way, you are updating your users, without letting react know about it. On both cases you have to update users with setUsers and not mutating the array directly.

  const addItem = e => {
    setUsers(users.concat(e)); // sidenote: if e is your event, then you might be looking for e.target.value here 
    console.log(e);
    e.preventDefault();
  };

  const removeItem = item => {
    setUsers(users.filter(user => user.id !== item));
  };

Both .concat() and .filter() use your users array, and return a new array based on the changes you want to apply, and then is used by setUsers to update your users.

So, in extend what I'm actualy doing on removeItem is:

  const removeItem = item => {
    const newUsers = users.filter(user => user.id !== item); // users remains untouched
    setUsers(newUsers);
  };

Also, you don't need useEffect hook for this scenario to work properly.

I hope this solves your problem.

2
votes

You are committing THE Fundamental sin of the react universe. "Mutation" of state.

  const removeItem = item => {
    users.forEach(user => {
      if (user.id === item) {
        user.name = null; // <---  HERE
      }
    });
    console.log(users);
    setUsers(users);

  };

Check this codesandbox for a working demo of this issue. https://codesandbox.io/s/jovial-panini-unql4

The react reconciler checks to see whether the 2 objects are equal and since you have just mutated and set the value it registers as the same object and there will be no state change triggered. Hence the view will not be re-rendered and useEffect will not be triggered.