A form initially holds a draft. Confirmation turns it into a new entry. The state owner manages this change. An effect synchronises with something outside React’s calculation, here the browser window title. These tasks should not be mixed. Input is processed in an action, while the window title is handled by an effect.
01Extend the interface
Use the project from tutorial 10 and replace src/App.tsx. value and onChange make the field controlled: React owns the displayed text. label and htmlFor connect the field to its label.
import React, { useEffect, useState } from 'react';
export function App() {
const [title, setTitle] = useState('');
const [entries, setEntries] = useState<string[]>([]);
const [error, setError] = useState('');
useEffect(() => {
const previous = document.title;
document.title = `Entries: ${entries.length}`;
return () => { document.title = previous; };
}, [entries.length]);
function submit(event: React.FormEvent) {
event.preventDefault();
const value = title.trim();
if (!value) { setError('Title required'); return; }
setEntries(old => [...old, value]);
setTitle(''); setError('');
}
return <main><h1>House notes</h1><form onSubmit={submit}>
<label htmlFor="title">Title</label>
<input id="title" value={title} onChange={event => setTitle(event.target.value)} />
<button>Add</button><p role="alert">{error}</p>
</form><ul>{entries.map((value, index) => <li key={index}>{value}</li>)}</ul></main>;
}
02Use submit rather than rendering
preventDefault stops normal HTML form navigation. The action normalises title and rejects empty text. The functional state update creates a new list and preserves existing entries.
03Clean up an effect
The entries.length dependency says when synchronisation is needed again. The returned function restores the previous window title. An effect needs appropriate cleanup when it changes external resources or subscriptions.
04Distinguish drafts from data
After successful submission, we clear only title and error. entries remains. This is still not a database commit. FlowForm Studio explicitly distinguishes UI drafts, server state and version conflicts.
Whitespace produces Title required. Roof creates a list item and sets the window title to Entries: 1.
Your exercise
Add permanently unique IDs to list entries. Why is the index insufficient for later deletion or reordering?
Show the solution and explanation
Use objects with id and title. Generate the ID once in submit using crypto.randomUUID and use key={entry.id}. An index is tolerable only in this append-only learning example. Reordering would confuse identity with position.
Project context and sources
The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.