Typed component props
type UserCardProps = {
user: User;
selected?: boolean;
onSelect: (id: number) => void;
};
function UserCard({
user,
selected = false,
onSelect,
}: UserCardProps) {
return (
<button
aria-pressed={selected}
onClick={() => onSelect(user.id)}
>
{user.name}
</button>
);
}
useState
const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] =
useState<Status>("idle");
setCount(current => current + 1);
Use the updater form when the next value depends on the previous value.
useEffect
useEffect(() => {
let cancelled = false;
async function load() {
try {
setStatus("loading");
const data = await getJson<User[]>("/api/users");
if (!cancelled) {
setUsers(data);
setStatus("success");
}
} catch (error) {
if (!cancelled) {
setError(getMessage(error));
setStatus("error");
}
}
}
load();
return () => {
cancelled = true;
};
}, []);
Dependencies include values used inside the effect that come from component scope.
useMemo and useCallback
const visibleUsers = useMemo(() => {
const normalized = query.trim().toLowerCase();
return users.filter(user =>
user.name.toLowerCase().includes(normalized)
);
}, [users, query]);
const handleSelect = useCallback((id: number) => {
setSelectedId(id);
}, []);
Do not add these automatically. Use them when referential stability or expensive recalculation actually matters.
Events and forms
function handleChange(
event: React.ChangeEvent<HTMLInputElement>
) {
setQuery(event.target.value);
}
function handleSubmit(
event: React.FormEvent<HTMLFormElement>
) {
event.preventDefault();
saveUser();
}
Derived state
Prefer calculating values from existing state rather than storing duplicates.
// Better
const completed = tasks.filter(task => task.done);
const completedCount = completed.length;
// Usually avoid
const [completedCount, setCompletedCount] =
useState(0);
Conditional rendering
if (status === "loading") return <p>Loading…</p>;
if (status === "error") return <p role="alert">{error}</p>;
return (
<ul>
{users.length === 0 ? (
<li>No users found.</li>
) : (
users.map(user => (
<li key={user.id}>{user.name}</li>
))
)}
</ul>
);
Immutable updates
setUsers(current =>
current.map(user =>
user.id === updated.id ? updated : user
)
);
setUsers(current =>
current.filter(user => user.id !== id)
);
setUsers(current => [...current, newUser]);