CodingNeed.

2 · Data and boundaries · 30 MIN

Stable keys and immutable list changes

Keys identify which list item persists between renders.

Each task has an ID that stays stable when its label or position changes. React uses that identity when reconciling lists. An array index is a poor key for a reorderable list because the item at that position can change. The filter operation creates a new array for removal, while the spread operation creates a new array for insertion. Never generate a fresh key during every render: that makes a persistent row appear to be a new component and can discard its local state or focus.

Remove the first of several tasks and verify the others retain their values.

Read the example

import {useState} from 'react';
type Task = {id:string;title:string};
export default function App() {
  const [tasks,setTasks] = useState<Task[]>([{id:'first',title:'Read the lesson'}]);
  return <main><h1>Practice tasks</h1><button onClick={()=>setTasks(rows=>[...rows,{id:crypto.randomUUID(),title:'New exercise'}])}>Add task</button>
    <ul>{tasks.map(task=><li key={task.id}>{task.title} <button aria-label={'Remove '+task.title} onClick={()=>setTasks(rows=>rows.filter(row=>row.id!==task.id))}>Remove</button></li>)}</ul></main>;
}
Check the expected output
One initial task; Add task appends a row and Remove deletes only its selected row.

Your challenge

Add editing and reorder controls while preserving each task ID and avoiding mutation.

Solution cost: O(n) filtering or rendering for n tasks. time · O(n) state and new arrays. space

Common trap

Index keys can attach a row’s state to the wrong data after insertion or deletion.

Study the project implementation
import {useState} from 'react';
type Task = {id:string;title:string};
export default function App() {
  const [tasks,setTasks] = useState<Task[]>([{id:'first',title:'Read the lesson'}]);
  return <main><h1>Practice tasks</h1><button onClick={()=>setTasks(rows=>[...rows,{id:crypto.randomUUID(),title:'New exercise'}])}>Add task</button>
    <ul>{tasks.map(task=><li key={task.id}>{task.title} <button aria-label={'Remove '+task.title} onClick={()=>setTasks(rows=>rows.filter(row=>row.id!==task.id))}>Remove</button></li>)}</ul></main>;
}

Further reading: Official documentation

Next lesson: Controlled forms and useful validation

Essential cookies keep your account signed in. Optional analytics is not configured on this site. Your choice does not affect access to lessons.

Read the Privacy Policy. You can change this choice in the footer.