React 19 hooks patterns and component best practices for this project
This project uses React 19.2.3 with TypeScript and functional components. React 19 brings major improvements including the React Compiler for automatic optimization, new hooks for better data handling, and enhanced form capabilities.
Detected Version: React 19.2.3 React DOM: 19.2.3 Component Style: Functional components with hooks TypeScript: Strict mode enabled Compiler: React Compiler (built-in with Next.js 16)
The React Compiler automatically optimizes components:
useMemo - compiler handles ituseCallback - compiler handles it// Before React 19 (manual optimization)
const filteredTasks = useMemo(() => {
return tasks.filter(task => task.completed);
}, [tasks]);
// React 19 (automatic optimization)
const filteredTasks = tasks.filter(task => task.completed);
// Compiler automatically memoizes this
When to still use manual memoization:
use() HookSimplifies async data and context consumption:
import { use } from 'react';
// Reading context
const theme = use(ThemeContext);
// Reading promises (Server Components)
const data = use(fetchDataPromise);
Note: For this client-side task manager, we won't use use() for data fetching since we're managing state locally with useState.
useFormStatus()Provides form submission status:
'use client';
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending, data, method } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Adding...' : 'Add Task'}
</button>
);
}
useOptimistic()Handles optimistic UI updates:
'use client';
import { useOptimistic } from 'react';
function TaskList({ tasks }) {
const [optimisticTasks, addOptimisticTask] = useOptimistic(
tasks,
(state, newTask) => [...state, { ...newTask, pending: true }]
);
async function addTask(task) {
addOptimisticTask(task); // Show immediately
await saveTask(task); // Persist to server
}
return optimisticTasks.map(task => (
<TaskItem key={task.id} task={task} />
));
}
Note: For this client-side app, optimistic updates aren't needed since state changes are synchronous.
Always use explicit prop interfaces:
interface TaskInputProps {
onAddTask: (description: string) => void;
}
export default function TaskInput({ onAddTask }: TaskInputProps) {
const [description, setDescription] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onAddTask(description);
setDescription('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
}
useStateFor local component state:
'use client';
import { useState } from 'react';
export default function TaskList() {
const [tasks, setTasks] = useState<Task[]>([]);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
const addTask = (description: string) => {
setTasks([...tasks, {
id: crypto.randomUUID(),
description,
completed: false,
createdAt: new Date(),
}]);
};
const toggleTask = (id: string) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
));
};
const deleteTask = (id: string) => {
setTasks(tasks.filter(task => task.id !== id));
};
return (/* JSX */);
}
useEffect is for real side effects only:
// ✅ Good: Real side effects
useEffect(() => {
// Browser API interaction
document.title = `${tasks.length} tasks remaining`;
}, [tasks]);
useEffect(() => {
// Subscription
const subscription = eventSource.subscribe(handleEvent);
return () => subscription.unsubscribe();
}, []);
// ❌ Bad: Routine data fetching (use Server Components instead)
useEffect(() => {
fetch('/api/tasks').then(/* ... */);
}, []);
For this project: Use useEffect for:
Don't use for:
import { useRef, useEffect } from 'react';
export default function TaskInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
// Clear and focus input after adding task
inputRef.current?.focus();
};
// Auto-focus on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
Extract domain logic into custom hooks:
// hooks/useTasks.ts
'use client';
import { useState } from 'react';
export function useTasks() {
const [tasks, setTasks] = useState<Task[]>([]);
const addTask = (description: string) => {
setTasks([...tasks, {
id: crypto.randomUUID(),
description,
completed: false,
createdAt: new Date(),
}]);
};
const toggleTask = (id: string) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
));
};
const deleteTask = (id: string) => {
setTasks(tasks.filter(task => task.id !== id));
};
return { tasks, addTask, toggleTask, deleteTask };
}
// Usage in component
export default function TaskList() {
const { tasks, addTask, toggleTask, deleteTask } = useTasks();
return (/* JSX */);
}
Benefits:
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle form submission
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); // If needed
// Handle click
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
if (e.key === 'Escape') {
handleCancel();
}
};
// Conditional rendering with &&
{tasks.length === 0 && (
<p>No tasks yet. Add one above!</p>
)}
// Conditional rendering with ternary
{loading ? (
<Spinner />
) : (
<TaskList tasks={tasks} />
)}
// Multiple conditions
{tasks.length === 0 ? (
<EmptyState />
) : filter === 'active' ? (
<ActiveTasks tasks={activeTasks} />
) : (
<CompletedTasks tasks={completedTasks} />
)}
// Always use stable, unique keys
{tasks.map(task => (
<TaskItem
key={task.id} // Use stable ID, not index
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))}
Key Rules:
Math.random() for keysconst [value, setValue] = useState('');
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
// Parent component manages shared state
function TaskManager() {
const [tasks, setTasks] = useState<Task[]>([]);
return (
<>
<TaskInput onAddTask={(desc) => {/* add to tasks */}} />
<TaskList tasks={tasks} />
</>
);
}
// Compose small components
function TaskList({ tasks, children }) {
return (
<div>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
{children}
</div>
);
}
// Usage
<TaskList tasks={tasks}>
<TaskStats count={tasks.length} />
</TaskList>
// ✅ Do: Write clean code, let compiler optimize
const filteredTasks = tasks.filter(task => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
// ❌ Don't: Premature manual optimization
const filteredTasks = useMemo(() => {
return tasks.filter(/* ... */);
}, [tasks, filter]);
import { Profiler } from 'react';
<Profiler id="TaskList" onRender={onRenderCallback}>
<TaskList tasks={tasks} />
</Profiler>
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
<button
onClick={handleDelete}
aria-label={`Delete task "${task.description}"`}
>
🗑️
</button>
<input
type="checkbox"
checked={task.completed}
onChange={handleToggle}
aria-label={`Mark "${task.description}" as ${task.completed ? 'incomplete' : 'complete'}`}
/>
<button
onClick={handleAction}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
}}
>
Action
</button>
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
// Return focus to input after action
inputRef.current?.focus();
};
// ❌ Bad
tasks.push(newTask);
setTasks(tasks);
// ✅ Good
setTasks([...tasks, newTask]);
// ❌ Bad
{tasks.map((task, index) => (
<TaskItem key={index} task={task} />
))}
// ✅ Good
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
// ❌ Bad
if (condition) {
const [state, setState] = useState(0);
}
// ✅ Good
const [state, setState] = useState(0);
if (condition) {
// Use state conditionally
}
// ❌ Bad (runs on every render)
useEffect(() => {
console.log(tasks);
});
// ✅ Good (runs when tasks change)
useEffect(() => {
console.log(tasks);
}, [tasks]);
interface TaskItemProps {
task: Task;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}
export default function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (/* JSX */);
}
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
// e is properly typed
};
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
// e.target.value is typed as string
};
interface ContainerProps {
children: React.ReactNode;
}
function Container({ children }: ContainerProps) {
return <div>{children}</div>;
}