Why This Question Exists
Like/Dislike tests whether you instinctively reach for the simplest correct state model. Most candidates use two boolean flags — isLiked and isDisliked. That works, but it introduces a class of bugs: you must always remember to clear one when setting the other. Miss that in one place and you get a state where both appear active simultaneously.
The Wrong Approach: Two Booleans
// ❌ WRONG — requires manual synchronisation
const [isLiked, setIsLiked] = useState(false);
const [isDisliked, setIsDisliked] = useState(false);
const handleLike = () => {
setIsLiked(l => !l);
setIsDisliked(false); // easy to forget this line
};
const handleDislike = () => {
setIsDisliked(d => !d);
setIsLiked(false); // and this one
};
This works when you remember to clear the other flag. But it's fragile — two independent state updates means React could in theory render a frame between them with both true. More importantly, it violates the single-source-of-truth principle.
The Right Approach: Tri-State Enum
// ✅ CORRECT — single source of truth, mutual exclusivity is free
type Vote = 'liked' | 'disliked' | null;
const [vote, setVote] = useState<Vote>(null);
const toggle = (val: Vote) => setVote(v => v === val ? null : val);
// Clicking Like when already liked → null (deactivate)
// Clicking Like when disliked → 'liked' (automatically clears disliked)
// Clicking Like when neutral → 'liked'
The mutual exclusivity is now structural — there is literally no state where both are active. The toggle logic fits in one line. This pattern is called a discriminated union or enum-style state.
Deriving Counts — never store what you can compute
// ❌ WRONG — counts can drift out of sync with vote state
const [likes, setLikes] = useState(142);
const [dislikes, setDislikes] = useState(8);
// ✅ CORRECT — counts derived from vote state, always in sync
const likes = initialLikes + (vote === 'liked' ? 1 : 0);
const dislikes = initialDislikes + (vote === 'disliked' ? 1 : 0);
If you store counts in state, you have to update three pieces of state on every click (vote, likes, dislikes). They can drift out of sync if an update is missed. Derived values are always correct because they're computed fresh on every render.
ℹ Interview TipSay: "I use a tri-state enum ('liked' | 'disliked' | null) rather than two booleans. This makes mutual exclusivity structurally guaranteed — there's no code path where both can be true. I derive the counts from this state rather than storing them separately, so they're always consistent."
Accessibility — aria-pressed
<button
aria-pressed={vote === 'liked'}
aria-label="Like this post"
onClick={() => toggle('liked')}
>
👍 {likes}
</button>
aria-pressed is the semantic attribute for toggle buttons. Screen readers announce "Like button, pressed" when active and "Like button, not pressed" when inactive. Without it, the visual active state is completely invisible to assistive technology users.
⚠ Common Pitfall: Using checkbox semantics for toggle buttonsDevelopers sometimes reach for <input type="checkbox"> for toggle buttons to get built-in aria semantics. But a checkbox announces "checked/unchecked" — a Like button should announce "pressed/not pressed". Use <button aria-pressed> for toggle actions.