NadfriJS
NadfriJS

Reputation: 265

Update one element of big list without re render others elements in react hooks?

i want to optimize my react App by testing with a large list of li Its a simple todo List.

By exemple, when click on a li, task will be line-through, and check icon will be green. This simple action is very slow with a large list because, the whole list is re render.

How to do this with React Hooks?

function App() {
  const [list, setList] = useState([]);
  const [input, setInput] = useState("");

  const inputRef = useRef(null);
  useEffect(() => inputRef.current.focus(), []);

//Pseudo Big List
  useEffect(() => {
    const test = [];
    let done = false;
    for (let i = 0; i < 5000; i++) {
      test.push({ task: i, done });
      done = !done;
    }
    setList(test);
  }, []);

  const handlerSubmit = (e) => {
    e.preventDefault();

    const newTask = { task: input, done: false };
    const copy = [...list, newTask];

    setList(copy);
    setInput("");
  };

  const checkHandler = (e, index) => {
    e.stopPropagation();
    const copy = [...list];
    copy[index].done = !copy[index].done;
    setList(copy);
  };

  const suppression = (e, index) => {
    e.stopPropagation();
    const copy = [...list];
    copy.splice(index, 1);
    setList(copy);
  };

  const DisplayList = () => {
    return (
      <ul>
        {list.map((task, index) => (
          <Li
            key={index}
            task={task}
            index={index}
            suppression={suppression}
            checkHandler={checkHandler}
          />
        ))}
      </ul>
    );
  };

  //JSX
  return (
    <div className='App'>
      <h1>TODO JS-REACT</h1>

      <form id='form' onSubmit={handlerSubmit}>
        <input
          type='text'
          placeholder='Add task'
          required
          onChange={(e) => setInput(e.target.value)}
          value={input}
          ref={inputRef}
        />
        <button type='submit'>
          <i className='fas fa-plus'></i>
        </button>
      </form>

      {list.length === 0 && <div id='noTask'>No tasks...</div>}

      <DisplayList />
    </div>
  );
}

export default App;

Li component

import React from "react";

export default function Li(props) {
  return (
    <li
      onClick={(e) => props.checkHandler(e, props.index)}
      className={props.task.done ? "line-through" : undefined}
    >
      {props.task.task}
      <span className='actions'>
        <i className={`fas fa-check-circle ${props.task.done && "green"}`}></i>
        <i
          className='fas fa-times'
          onClick={(e) => props.suppression(e, props.index)}
        ></i>
      </span>
    </li>
  );
}

CodeSandbox here: https://codesandbox.io/s/sad-babbage-kp3md?file=/src/App.js

Upvotes: 0

Views: 2813

Answers (2)

Reza
Reza

Reputation: 93

I had the same question, as @Dvir Hazout answered, I followed this article and made your code the changes you need:

function App() {
  const [list, setList] = useState([]);
  const { register, handleSubmit, reset } = useForm();

  //Pseudo Big List
  useEffect(() => {
    const arr = [];
    let done = false;
    for (let i = 0; i < 20; i++) {
      arr.push({ id: uuidv4(), task: randomWords(), done });
      done = !done;
    }
    setList(arr);
  }, []);

  const submit = ({ inputTask }) => {
    const newTask = { task: inputTask, done: false, id: uuidv4() };
    setList([newTask, ...list]);
    reset(); //clear input
  };

  const checkHandler = useCallback((id) => {
    setList((list) =>
      list.map((li) => (li.id !== id ? li : { ...li, done: !li.done }))
    );
  }, []);

  const suppression = useCallback((id) => {
    setList((list) => list.filter((li) => li.id !== id));
  }, []);

  //JSX
  return (
    <div className="App">
      <h1>TODO JS-REACT</h1>

      <form onSubmit={handleSubmit(submit)}>
        <input type="text" {...register("inputTask", { required: true })} />

        <button type="submit">
          <i className="fas fa-plus"></i>
        </button>
      </form>

      {list.length === 0 && <div id="noTask">No tasks...</div>}

      <ul>
        {list.map((task, index) => (
          <Li
            key={task.id}
            task={task}
            suppression={suppression}
            checkHandler={checkHandler}
          />
        ))}
      </ul>
    </div>
  );
}

Li component

import React, { memo } from "react";

const Li = memo(({ task, suppression, checkHandler }) => {
  // console.log each time a Li component re-rendered
  console.log(`li ${task.id} rendered.`);
  return (
    <li
      onClick={(e) => checkHandler(task.id)}
      className={task.done ? "line-through" : undefined}
    >
      {task.task}
      <span className="actions">
        <i className={`fas fa-check-circle ${task.done && "green"}`}></i>
        <i className="fas fa-times" onClick={(e) => suppression(task.id)}></i>
      </span>
    </li>
  );
});

export default Li;

You can check it live here I know it's probably late for your question, but may help others ;)

Upvotes: 1

Dvir Hazout
Dvir Hazout

Reputation: 261

You can use React.memo and wrap the Li component. This will cache the instances of the Li component based on shallow comparison. Read more in the docs

Otherwise, if you don't need the state in the container, you can keep it locally in the Li component and then it won't cause a whole list rerender.

Upvotes: 0

Related Questions