ReactEasy30 min

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

Like button active state
Dislike button active state

Playground

Hint 1

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.

Hint 2

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.

Hint 3

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:

  1. Which reaction has the user selected? This drives which button is highlighted. It can be null (no reaction), 'like', or 'dislike'.
  2. The current like count, which changes when reactions are added or removed.
  3. The current dislike count, same.
  4. 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 in components/ReactionButton.js.
  • A Post, the card that displays the title, content, author, and the two reaction buttons. The starter gives us the card skeleton in components/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:

  1. Candidates name the variable post and then try to create a component also called Post. JavaScript is case-sensitive, but this still causes confusion and bugs. I use postData for the data object and Post for the component, which is completely unambiguous.

  2. Candidates name the event handler prop handleReaction instead of onReaction. The convention in React is on prefix for props that accept callbacks (onClick, onChange, onReaction). Using handleReaction as a prop name reads like it is the implementation, not the interface. Keep handle for the function defined inside the component, and on for 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:

  1. The early return if (isLoading) return is a guard, but the real protection comes from disabled on the buttons. I keep both for extra safety.
  2. setIsLoading(true) immediately before the call so both buttons disable.
  3. We await toggleReaction(type), passing type and trusting the server to return the new state.
  4. We set all three state values from the server response.
  5. The finally block ensures isLoading always goes back to false, 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-pressed tells 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-label includes 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-box applied globally?

JavaScript

  • Is async/await used correctly with a try/catch/finally block?

  • 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 type is sent regardless of current state (server-side toggle pattern)?

React

  • Is userReaction initialized to null and not false or ''?

  • Are likes and dislikes initialized from postData and not 0?

  • 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 a finally block and not just in the try?

  • Is aria-pressed used on the buttons for toggle accessibility?

Component Architecture

  • Are ReactionButton and Post in their own files under a components/ folder?

  • Is ReactionButton reusable and not duplicated JSX for like and dislike?

  • Does Post accept onReaction as a prop and not defined inline in App?

  • Does App own all the state and pass it down, not scattered across components?

  • Is the data variable named postData to avoid collision with the Post component?

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 ✅