lifelonglearner
lifelonglearner

Reputation: 326

How do I add a class to a specific li element with React?

I have this working todo list React app.

The state variable tasks contains objects, with a string for the task, and done, a boolean. I know how to add a class based on that boolean, but what I'm confused about is how to change that specific object's done variable using a click event.

Here's App.js:

import React, { useState } from 'react';
import './App.css';
import v4 from 'uuid';

function App() {
  const [ tasks, setTasks ] = useState([{task: 'Test task',
                                         done: false},
                                         {task: 'other task',
                                         done: false},
                                         {task: 'blah task',
                                         done: false}])
  const [ formVal, setFormVal ] = useState('');

  const handleSubmit = e => {
    e.preventDefault();
    setTasks([...tasks, {task: formVal, done: false}]);
    // Todo: clear form after submit
  };

  const handleChange = e => {
    setFormVal(e.target.value);
  }

  const handleClick = e => {
    // This is probably a good place to use useReduce
  }

  return (
    <>
      <h1>To Do List</h1>
      <br />
      <ol>
        {
          tasks.map(item => (
            <li key={v4()} className={item.done ? 'done' : ''} onClick={handleClick}>{item.task}</li>
          ))
        }
      </ol>
      <form onSubmit={handleSubmit}>
        <label>Add a task   </label>
        <input type="text" placeholder='What to do?' onChange={handleChange}></input>
        <input type="submit" />
      </form>
    </>
  );
}

export default App;

Upvotes: 0

Views: 1232

Answers (3)

Drew Reese
Drew Reese

Reputation: 202618

A good place to start is to create a curried handleClick handler that takes an id and returns the function to be used as the callback. Use a functional state update to map previous state to a new array and toggle the done value when the id matches.

const handleClick = id => e => {
  setTasks(tasks => tasks.map(task => task.id === id ? {
    ...task,
    done: !task.done,
  } : task));
};

This requires your data to also have stable ids. As is, your code will generate new id/keys each time it is rendered, thus negating the purpose of react keys to begin with (i.e. each render cycle each array element has a new key and the entire array will be rerendered). Instead of generating the keys in the render array mapping, generate them when data is added to the tasks array.

Code

import React, { useState } from "react";
import "./App.css";
import v4 from "uuid";

function App() {
  const [tasks, setTasks] = useState([
    { task: "Test task", id: 'xxxxxx:xxxx:xx', done: false }, // <-- data has id field
    { task: "other task", id: 'yyyyyyy:yyyy:yy', done: false },
    { task: "blah task", id: 'zzzzzzz:zzzz:zz', done: false }
  ]);
  const [formVal, setFormVal] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    setTasks([...tasks, { task: formVal, id: v4(), done: false }]); // <-- generate id when adding task
    // Todo: clear form after submit
  };

  const handleChange = e => {
    setFormVal(e.target.value);
  }

  const handleClick = id => e => {
    setTasks(tasks =>
      tasks.map(task =>
        task.id === id
          ? {
              ...task,
              done: !task.done
            }
          : task
      )
    );
  };

  return (
    <>
      <h1>To Do List</h1>
      <br />
      <ol>
        {tasks.map(item => (
          <li
            key={item.id} // <-- use item.id as key
            className={item.done ? "done" : ""}
            onClick={handleClick(item.id)} // <-- pass item.id to handler
          >
            {item.task}
          </li>
        ))}
      </ol>
      <form onSubmit={handleSubmit}>
        <label>Add a task </label>
        <input type="text" placeholder="What to do?" onChange={handleChange} />
        <input type="submit" />
      </form>
    </>
  );
}

export default App;

Edit - Clearing Form on Submit

If you with to clear the input upon submission then make the input a controlled input, i.e. give it a value, and in the submit handler simply reset the value

...

function App() {
  ...
  const [formVal, setFormVal] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    ...
    setFormVal(''); // <-- clear form value
  };

  ...

  return (
    <>
      ...
      <form onSubmit={handleSubmit}>
        ...
        <input
          type="text"
          placeholder="What to do?"
          onChange={handleChange}
          value={formVal} // <-- set input value from state
        />
        ...
      </form>
    </>
  );
}

export default App;

Upvotes: 2

ash1102
ash1102

Reputation: 457

map can take 2 arguments where the second one is the index of the element you can send the index and then use that to retrieve the element and update it.

here is the changes i have made

const handleClick = index => {
    // This is probably a good place to use useReduce
    console.log(index);
    console.log(tasks[index]);
    let newTasks = [...tasks];

    newTasks[index] = { ...tasks[index], done: true };
    console.log(newTasks);
    setTasks(newTasks );
  };





<ol>
    {tasks.map((item, index) => (
      <li
        className={item.done ? "done" : ""}
        `enter code here`onClick={() => handleClick(index)}
      >
        {item.task}
        {/* to check the change */}
        {String(item.done)} 
      </li>
    ))}


  </ol>

Upvotes: 1

leverglowh
leverglowh

Reputation: 869

Give your task object an id or something to identify it, for the moment we can take the task name as a non repetable value. Add a dataset prop to your li such as

<li data-task={item.task} onClick={handleClick} key={v4()}>

Then in your handleClick you can access that with e.target.dataset.task, now you have the task name, filter through the state and change the done value!

Upvotes: 0

Related Questions