Like Dislike I
Prompt
A post card is already rendered in the starter code: title, content, and author are visible. Your task is to build the reaction buttons (thumbs up and thumbs down) and add them to the post footer.
The reaction buttons are a thumbs up (like) and a thumbs down (dislike), as shown in the example below. Each button displays its current count. Make the styling as close to the example as possible. Clicking one sends a request to the mock API using toggleReaction(type). The rules are:
- On a successful response, update the UI to reflect the new reaction state returned by the API
- Like and dislike are mutually exclusive: selecting one removes the other
- Clicking an already-active button removes the reaction (toggle off)
- Both buttons are disabled while a request is in progress
Example


Playground
Start by identifying what state you need. You need to know which reaction the current user has selected (null, 'like', or 'dislike'), and the current like and dislike counts. Initialize the counts from the postData that is already defined in the starter.
When a button is clicked, call toggleReaction(type) from ./server/api. This returns a Promise that resolves with { likes, dislikes, userReaction }. Update your three state values with this result. Use async/await inside an event handler function named handleReaction.
To prevent race conditions from rapid clicking, add a boolean state isLoading and set it to true before the API call and back to false in a finally block. Pass disabled={isLoading} to both buttons, not just the one clicked.
Solution
Explanation
I will walk you through this interview challenge step by step. This is the kind of question that looks simple but separates candidates who understand React state management from those who are just getting by. During this walkthrough I will show you what an ideal solution looks like, where candidates go wrong, and how interviewers evaluate your work.
Before writing a single line of code, read the prompt carefully. The key requirements are:
- Display a post with like and dislike buttons
- Each button shows its count
- Clicking sends an API request
- The UI reflects the response from the API (not our own guess)
- Like and dislike are mutually exclusive
- Buttons are disabled while a request is pending
Now let's solve it.
In the starter code, the Post component is already built and the post card is rendering on screen: title, content, and author are all visible. The reactions section is intentionally left out. Your job is to build that from scratch: create a ReactionButton component, add the reactions section to Post, wire up state and handleReaction in App.
Step 1: Identify the State
The first thing I do when approaching any UI challenge is ask: what needs to change over time? That tells me what state I need.
Looking at the requirements:
- Which reaction has the user selected? This drives which button is highlighted. It can be
null(no reaction),'like', or'dislike'. - The current like count, which changes when reactions are added or removed.
- The current dislike count, same.
- Whether a request is in progress, which controls the disabled state of both buttons.
Why null and not false?
A lot of candidates initialize userReaction with false or an empty string ''. Using null is the right choice here because it clearly communicates "no reaction selected". It is semantically absent, not an empty value. When you check userReaction === 'like', a null value will correctly evaluate to false, keeping your logic clean.
const [userReaction, setUserReaction] =
React.useState(null); // null | 'like' | 'dislike'
const [likes, setLikes] = React.useState(postData.likes);
const [dislikes, setDislikes] = React.useState(
postData.dislikes
);
const [isLoading, setIsLoading] = React.useState(false);Notice that likes and dislikes are initialized from postData.likes and postData.dislikes. The post already has initial counts, so we should show those right away and not start from 0.
Common Pitfalls
A very common mistake is initializing likes and dislikes with React.useState(0). What happens is the buttons display 0 and 0 on first render, then suddenly jump to 24 and 3 when the user first clicks. Always initialize state from the data you already have.
Step 2: Build the UI Skeleton
Next, I build the layout before wiring up any logic. I think of the UI in terms of components first.
Looking at the design, I can see three distinct pieces:
- A
ReactionButton, a reusable pill button that can represent either like or dislike. Since both buttons are nearly identical, extracting them into a shared component avoids duplication. I create this incomponents/ReactionButton.js. - A
Post, the card that displays the title, content, author, and the two reaction buttons. The starter gives us the card skeleton incomponents/Post.js. We add the reactions section to it. App, which owns all the state and the API logic.
I create ReactionButton.js in the components folder and add the reactions section to Post.js.
components/ReactionButton.js
import React from 'react';
export function ReactionButton({
icon,
count,
isActive,
variant,
onClick,
disabled,
label,
}) {
return (
<button
className={`reaction-btn ${isActive ? variant : ''}`}
onClick={onClick}
disabled={disabled}
aria-label={label}
aria-pressed={isActive}
>
{icon}
<span>{count}</span>
</button>
);
}components/Post.js
import React from 'react';
import { IconThumbsUp, IconThumbsDown } from '../icons';
import { ReactionButton } from './ReactionButton';
export function Post({
title,
content,
author,
userReaction,
likes,
dislikes,
isLoading,
onReaction,
}) {
return (
<article className="post">
<h2 className="post-title">{title}</h2>
<p className="post-content">{content}</p>
<footer className="post-footer">
<span className="post-author">By {author}</span>
<div className="reactions">
<ReactionButton
icon={<IconThumbsUp size={16} />}
count={likes}
isActive={userReaction === 'like'}
variant="like"
onClick={() => onReaction('like')}
disabled={isLoading}
label={`Like. ${likes} likes`}
/>
<ReactionButton
icon={<IconThumbsDown size={16} />}
count={dislikes}
isActive={userReaction === 'dislike'}
variant="dislike"
onClick={() => onReaction('dislike')}
disabled={isLoading}
label={`Dislike. ${dislikes} dislikes`}
/>
</div>
</footer>
</article>
);
}App.js
import React from 'react';
import './styles.css';
import { toggleReaction } from './server/api';
import { Post } from './components/Post';
const postData = {
id: 1,
title: 'The Future of Artificial Intelligence',
content:
'AI is rapidly transforming industries worldwide...',
author: 'Jane Doe',
likes: 24,
dislikes: 3,
};
export default function App() {
const [userReaction, setUserReaction] =
React.useState(null);
const [likes, setLikes] = React.useState(postData.likes);
const [dislikes, setDislikes] = React.useState(
postData.dislikes
);
const [isLoading, setIsLoading] = React.useState(false);
return (
<main className="page">
<Post
{...postData}
userReaction={userReaction}
likes={likes}
dislikes={dislikes}
isLoading={isLoading}
onReaction={() => {}}
/>
</main>
);
}At this point the UI renders but the buttons do nothing yet. I always build the skeleton first and make sure it looks right before adding logic.
Common Pitfalls
Two naming mistakes I see often:
-
Candidates name the variable
postand then try to create a component also calledPost. JavaScript is case-sensitive, but this still causes confusion and bugs. I usepostDatafor the data object andPostfor the component, which is completely unambiguous. -
Candidates name the event handler prop
handleReactioninstead ofonReaction. The convention in React isonprefix for props that accept callbacks (onClick,onChange,onReaction). UsinghandleReactionas a prop name reads like it is the implementation, not the interface. Keephandlefor the function defined inside the component, andonfor the prop name passed to child components.
Step 3: The Industry Standard Toggle API Pattern
Before writing handleReaction, there is an important design decision to discuss. This is the kind of thing an interviewer might ask you directly: what do you send to the API when a user clicks like while they are already liked?
There are two common approaches:
Option A: Two separate action types:
- Click like while not liked → send
{ type: 'like' } - Click like while already liked → send
{ type: 'unlike' }
Option B: Single toggle, server decides:
- Always send
{ type: 'like' }regardless of current state - The server knows the current state and toggles accordingly
- The response always returns the new state
Option B is the industry standard. This is how YouTube, Reddit, Twitter, and GitHub work. The server is the source of truth. Your client does not need to maintain a parallel copy of the toggle logic. The API response tells you exactly what the new state is. You just reflect it.
This also handles edge cases automatically. What if the user has the page open in two tabs? What if they liked a post a week ago and the page is stale? The server always returns the correct current state.
Step 4: Write handleReaction
async function handleReaction(type) {
if (isLoading) return;
setIsLoading(true);
try {
const result = await toggleReaction(type);
setUserReaction(result.userReaction);
setLikes(result.likes);
setDislikes(result.dislikes);
} catch {
// handle error
} finally {
setIsLoading(false);
}
}Let me walk through this:
- The early return
if (isLoading) returnis a guard, but the real protection comes fromdisabledon the buttons. I keep both for extra safety. setIsLoading(true)immediately before the call so both buttons disable.- We
await toggleReaction(type), passingtypeand trusting the server to return the new state. - We set all three state values from the server response.
- The
finallyblock ensuresisLoadingalways goes back tofalse, even if the request fails.
Always use finally for loading state
A common bug: candidates set isLoading to false inside the try block but not in the catch block. If the API call throws, isLoading stays true forever and the buttons remain disabled. Always reset loading state in finally, since it runs whether the request succeeds or fails.
Step 5: Disable Both Buttons During Loading
This is a small detail that matters a lot.
disabled = { isLoading };This goes on both ReactionButton components, not just the one the user clicked.
Common Pitfalls
A very common race condition: candidates only disable the button that was clicked. So the user clicks like (like button disables, request fires), then before the response arrives, they click dislike. Now two requests are in flight simultaneously. When they resolve, the final state depends on which resolves last, which is completely unpredictable. Disabling both buttons with a shared isLoading flag prevents this entirely.
Step 6: Styling the Active State
The CSS for active states is straightforward. Each button gets a single class based on its active state:
className={`reaction-btn ${isActive ? variant : ''}`}When isActive is true, the variant value ("like" or "dislike") is appended as a CSS class, giving us .reaction-btn.like and .reaction-btn.dislike. Notice that variant describes what the button is semantically, not which CSS class to apply. This is a cleaner component API than passing activeClass="liked" as a string, which would leak styling internals into the prop interface.
.reaction-btn.like {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.reaction-btn.like:hover:not(:disabled) {
background: #dbeafe;
border-color: #2563eb;
color: #2563eb;
}
.reaction-btn.dislike {
background: #fff1f2;
border-color: #f43f5e;
color: #f43f5e;
}
.reaction-btn.dislike:hover:not(:disabled) {
background: #ffe4e6;
border-color: #e11d48;
color: #e11d48;
}There is a specificity issue to be aware of here. The generic hover rule .reaction-btn:hover:not(:disabled) has three pseudo-classes and outranks .reaction-btn.like which only has two classes. Without the explicit active hover rules above, hovering over an already-liked button would revert it to gray, stripping the active color and confusing the user into thinking the reaction was removed. The explicit .like:hover and .dislike:hover rules override this correctly.
The industry standard is to keep the active color on hover, just slightly more intense (darker blue, darker rose). This signals "you can click again to undo" without losing the context that the reaction is currently active.
Step 7: Accessibility
If you have made it this far, you are already in strong shape. Accessibility is the cherry on top, not strictly required to pass, but it is a quick win and I personally enjoy discussing it. Two attributes, thirty seconds, and your solution goes from good to great.
These two attributes make the buttons work correctly for screen reader users and cost almost nothing to add:
aria-label={`Like. ${likes} likes`}
aria-pressed={isActive}aria-pressedtells screen readers this is a toggle button. Screen readers announce "Like, toggle button, pressed" or "unpressed" depending on state. Without this, a screen reader just reads "Like, button" with no indication of whether it's active.aria-labelincludes the count so users who cannot see the visual count still hear it.
Common Pitfalls
Never use a div as a button. Some candidates write <div onClick={...}> to avoid default button styles. This breaks keyboard navigation (the div is not focusable by default and does not respond to Enter/Space). It also breaks screen readers. Always use a semantic <button> element. Reset its default styles in CSS if needed.
Communicate While You Code
Throughout the interview, talk through your decisions. When you choose to trust the server response instead of computing the toggle result on the client, say it: "I am going to update state from the API response rather than computing the new reaction myself. That way the client and server always stay in sync and I do not have to duplicate the server's business logic." Interviewers love this kind of reasoning. It shows you understand the tradeoffs, not just the mechanics.
Interviewer Criteria
HTML/CSS
Does the layout match the example, with a post card and two pill-shaped buttons at the bottom?
Are the active states clearly visible? Blue for liked, rose/red for disliked.
Are CSS class names meaningful (
.reaction-btn,.like,.dislike) rather than generic (.btn,.active)?Is
box-sizing: border-boxapplied globally?
JavaScript
Is
async/awaitused correctly with atry/catch/finallyblock?Is the handler named
handleReaction(a named function, not an anonymous arrow)?Are ES6 features used cleanly:
const, arrow functions, template literals, destructuring?Does the candidate explain why the same
typeis sent regardless of current state (server-side toggle pattern)?
React
Is
userReactioninitialized tonulland notfalseor''?Are
likesanddislikesinitialized frompostDataand not0?Is state updated from the API response and not computed client-side?
Are both buttons disabled during loading, not just the clicked one?
Is
setIsLoading(false)in afinallyblock and not just in thetry?Is
aria-pressedused on the buttons for toggle accessibility?
Component Architecture
Are
ReactionButtonandPostin their own files under acomponents/folder?Is
ReactionButtonreusable and not duplicated JSX for like and dislike?Does
PostacceptonReactionas a prop and not defined inline inApp?Does
Appown all the state and pass it down, not scattered across components?Is the data variable named
postDatato avoid collision with thePostcomponent?
Performance and Edge Cases
Is rapid clicking handled? Do both buttons disable during a pending request?
Is the API error handled gracefully? Does the app not crash on a failed request?
Can the candidate discuss what would change if there were multiple posts on the page?
Time Checkpoints
- 10:00 AM
Interview starts 👥
- 10:03 AM
Prompt given by the interviewer
- 10:05 AM
Candidate reads the prompt, asks clarifying questions (what does the mock API return? what happens on error?), and starts coding
- 10:10 AM
State identified: userReaction (null | like | dislike), likes, dislikes, isLoading
- 10:15 AM
UI skeleton rendered: Post and ReactionButton components built
- 10:20 AM
handleReaction wired up with toggleReaction mock API
- 10:30 AM
Loading state added, both buttons disable during pending request
- 10:35 AM
Active styling working for liked (blue) and disliked (rose) states
- 10:40 AM
Accessibility: aria-pressed and aria-label added to buttons
- 10:50 AM
Discussed server-side toggle pattern and tradeoffs with interviewer
- 10:55 AM
Discussion with interviewer about what Part II would look like (optimistic updates)
- 11:00 AM
Interview ends ✅