cf.completefrontend

Write the codeMedium

Stop a task list from re-rendering unchanged rows when you filter or tick a task

TaskList shows tasks with a filter box. It works, but every key in the filter renders every row again, and ticking one task renders all of them. Change it so a row renders only when something it shows has changed. Keep what it does.

TaskList takes tasks, a list of { id, title, done }, which it copies into state on the first render, and an optional onRowRender. A text box labelled Filter tasks shows only the tasks whose title contains the text, ignoring case. Each task is an <li> with a checkbox labelled with the task's title, checked when the task is done. Clicking it ticks or unticks the task, and a tick stays while the filter hides the task.

TaskRow calls onRender(task.id) every time it renders; TaskList passes onRowRender there. That's how the tests count renders, so keep the call in the row. The tests render without Strict Mode, so each update means one render.

What to fix: when the filter text changes, a row that was on screen and still is doesn't render again. Ticking a task renders that row once and no other. Rendering TaskList again with the same props renders no row. A row the filter hides and brings back renders once when it comes back. Export TaskList by name.

Write this (App.jsx)
import { useState } from "react";
 
const TASKS = [
  { id: "tests", title: "Write tests", done: false },
  { id: "review", title: "Review the pull request", done: true },
  { id: "deploy", title: "Deploy the app", done: false },
];
 
function TaskRow({ task, onToggle, onRender }) {
  onRender(task.id);
  return (
    <li>
      <label>
        <input type="checkbox" checked={task.done} onChange={() => onToggle(task.id)} />
        {task.title}
      </label>
    </li>
  );
}
 
export function TaskList({ tasks, onRowRender = () => {} }) {
  const [items, setItems] = useState(tasks);
  const [filter, setFilter] = useState("");
 
  function toggle(id) {
    setItems(items.map((task) => (task.id === id ? { ...task, done: !task.done } : task)));
  }
 
  const visible = items.filter((task) => task.title.toLowerCase().includes(filter.toLowerCase()));
 
  return (
    <div>
      <label>
        Filter tasks <input value={filter} onChange={(event) => setFilter(event.target.value)} />
      </label>
      <ul>
        {visible.map((task) => (
          <TaskRow key={task.id} task={task} onToggle={toggle} onRender={onRowRender} />
        ))}
      </ul>
    </div>
  );
}
 
export default function App() {
  return <TaskList tasks={TASKS} onRowRender={(id) => console.log("rendered", id)} />;
}
The checks your code must pass
  • shows every task, ticked when it's done
  • filters by title, ignoring case, and keeps ticks
  • typing in the filter doesn't render rows that stay on screen
  • a row the filter brings back renders once
  • ticking a task renders only that row, once
  • rendering TaskList again with the same props renders no row

Try it in the editor

The answer

Write it yourself first. Then compare it with a reference solution and the checks it passes.

Checking your sign-in…