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 →