Counter Component
Requirements
Create two counters: counter state and counter ref, each with increment and decrement buttons.
The counter state should update and reflect its value in the UI when incremented or decremented.
The counter ref should update its value when incremented or decremented without triggering a UI update.
After triggering a rerender with the counter state, the counter ref should show its updated value.
Notes
Understand the difference between useState and useRef in React.
Learn how useRef can store mutable values without causing rerenders.
Practice triggering and observing rerenders with useState.
Gain clarity on when to use useState vs useRef depending on UI updates.
https://www.reactchallenges.com/challenges/counter-component
App.tsx
import { BsDashLg, BsPlusLg } from "react-icons/bs";
import { useState, useRef } from "react";
export default function App() {
const [count, setCount] = useState(0);
let countRef = useRef(0);
return (
<main className="py-24 min-h-dvh bg-gray-100 p-4">
<div className="flex flex-col gap-8 w-full max-w-xl mx-auto shadow-2xl p-8 bg-white rounded-2xl">
<h1 className="text-4xl font-bold text-center">
useState vs useRef counter
</h1>
<div className="flex items-center justify-center gap-8 py-1">
<div className="flex flex-col gap-6 p-4 rounded-lg">
<h2 className="text-2xl font-semibold text-center">
Counter state
</h2>
<div className="flex flex-row items-center justify-center gap-4">
<button
className="bg-teal-500 size-12 rounded-xl hover:bg-teal-700 text-white flex items-center justify-center cursor-pointer"
aria-label="decrement-state"
onClick={() => {
setCount((prev) => prev - 1);
}}
>
<BsDashLg className="size-8" />
</button>
<span
data-testid="state-value"
className="text-2xl font-semibold"
>
{count}
</span>
<button
className="bg-teal-500 size-12 rounded-xl hover:bg-teal-700 text-white flex items-center justify-center cursor-pointer"
aria-label="increment-state"
onClick={() => {
setCount((prev) => prev + 1);
}}
>
<BsPlusLg className="size-8" />
</button>
</div>
</div>
<div className="flex flex-col gap-6 p-4 rounded-lg">
<h2 className="text-2xl font-semibold text-center">Counter ref</h2>
<div className="flex flex-row items-center justify-center gap-4">
<button
className="bg-teal-500 size-12 rounded-xl hover:bg-teal-700 text-white flex items-center justify-center cursor-pointer"
aria-label="decrement-ref"
onClick={() => {
countRef.current -= 1;
}}
>
<BsDashLg className="size-8" />
</button>
<span data-testid="ref-value" className="text-2xl font-semibold">
{countRef.current}
</span>
<button
className="bg-teal-500 size-12 rounded-xl hover:bg-teal-700 text-white flex items-center justify-center cursor-pointer"
aria-label="increment-ref"
onClick={() => {
countRef.current += 1;
}}
>
<BsPlusLg className="size-8" />
</button>
</div>
</div>
</div>
</div>
</main>
);
}ToDo List
Overview
Create a simple ToDo list app with add, delete, complete, and clear completed tasks.
Requirements
Implement a task list where the user can:
Add new tasks.
A task is added when submitting the form (pressing Enter in the input or clicking the Add button).
Delete existing tasks.
Mark tasks as complete and toggle their completion status.
Clear all completed tasks.
The Clear button should only appear if there are completed tasks.
Notes
Consider how to manage the list of tasks in state efficiently.
Think about how to conditionally render the Clear button only when needed.
Remember to update the UI immediately when tasks are added, deleted, or toggled.
Use keys properly when rendering lists in React to avoid unnecessary re-renders.
Tests
adds a task when submitting the form
toggles task completion and apply line-through when checkbox is clicked
deletes a task when delete button is clicked
shows only ‘Clear complete’ button when there is at least one completed task
clears completed tasks when ‘Clear complete’ is clicked
Write me a react challenge for the above scenario.
function App() {
return (
<main>
<h1>ToDo App</h1>
<form>
<input placeholder="Add a task..." />
<button>Add</button>
</form>
<ul>
{/* Render tasks here */}
</ul>
{/* Conditionally render */}
<button>Clear completed</button>
</main>
);
}https://www.reactchallenges.com/challenges/to-do-list
import { useState } from "react";
type Task = {
id: string;
text: string;
completed: boolean;
};
export default function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [input, setInput] = useState("");
// ➕ Add task
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!input.trim()) return;
const newTask: Task = {
id: crypto.randomUUID(),
text: input,
completed: false,
};
setTasks((prev) => [...prev, newTask]);
setInput("");
}
// ✅ Toggle task
function toggleTask(id: string) {
setTasks((prev) =>
prev.map((task) =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}
// ❌ Delete task
function deleteTask(id: string) {
setTasks((prev) => prev.filter((task) => task.id !== id));
}
// 🧹 Clear completed
function clearCompleted() {
setTasks((prev) => prev.filter((task) => !task.completed));
}
const hasCompleted = tasks.some((task) => task.completed);
return (
<main className="p-6 max-w-xl mx-auto">
<h1 className="text-3xl font-bold mb-4">ToDo App</h1>
{/* ➕ Form */}
<form onSubmit={handleSubmit} className="flex gap-2 mb-4">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a task..."
className="flex-1 border px-3 py-2 rounded"
/>
<button className="bg-blue-500 text-white px-4 rounded">
Add
</button>
</form>
{/* 📋 Task List */}
<ul className="flex flex-col gap-2">
{tasks.map((task) => (
<li
key={task.id}
className="flex items-center justify-between border p-2 rounded"
>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
/>
<span
className={
task.completed ? "line-through text-gray-400" : ""
}
>
{task.text}
</span>
</div>
<button
onClick={() => deleteTask(task.id)}
className="text-red-500"
>
Delete
</button>
</li>
))}
</ul>
{/* 🧹 Clear Completed */}
{hasCompleted && (
<button
onClick={clearCompleted}
className="mt-4 bg-gray-800 text-white px-4 py-2 rounded"
>
Clear completed
</button>
)}
</main>
);
}Uncontrolled Form
Requirements
Display the default values in the form inputs.
Manage form values without using any React hooks.
Handle form submission using a submit handler.
Collect the values from all inputs when the form is submitted.
Display the collected data inside an alert to verify the submission.
Refactor each input into a reusable Input component.
https://www.reactchallenges.com/challenges/uncontrolled-form
App.tsx
const defaultData = {
email: "john@example.com",
name: "John Doe",
country: "USA",
};
export default function App() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);
alert(JSON.stringify(data));
}
return (
<main className="py-24 min-h-dvh bg-gray-100 p-4">
<div className="flex flex-col items-center gap-8 w-full max-w-xl mx-auto shadow-2xl p-8 bg-white rounded-2xl">
<h1 className="text-4xl font-bold text-center">Uncontrolled form</h1>
<form
onSubmit={handleSubmit}
className="flex flex-col gap-8 min-w-sm"
aria-label="user form"
>
<label className="block text-sm font-medium text-gray-600">
<span className="font-semibold mb-2 block">Name</span>
<input
name="name"
type="text"
className="w-full font-light rounded-xl border-gray-300 border shadow-sm px-4 py-2"
defaultValue={defaultData.name}
/>
</label>
<label className="block text-sm font-medium text-gray-600">
<span className="font-semibold mb-2 block">Email</span>
<input
name="email"
type="email"
className="w-full font-light rounded-xl border-gray-300 border shadow-sm px-4 py-2"
defaultValue={defaultData.email}
/>
</label>
<label className="block text-sm font-medium text-gray-600">
<span className="font-semibold mb-2 block">Country</span>
<input
name="country"
type="text"
className="w-full font-light rounded-xl border-gray-300 border shadow-sm px-4 py-2"
defaultValue={defaultData.country}
/>
</label>
<button
type="submit"
className="bg-teal-500 text-white px-6 py-2 rounded-xl hover:bg-teal-600 transition-colors self-end cursor-pointer"
>
Submit
</button>
</form>
</div>
</main>
);
}
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string;
};
export function Input({ label, ...props }: InputProps) {
return (
<label className="block text-sm font-medium text-gray-600">
<span className="font-semibold mb-2 block">{label}</span>
<input
{...props}
className="w-full font-light rounded-xl border-gray-300 border shadow-sm px-4 py-2"
/>
</label>
);
}Wikipedia Search
Requirements
Implement a search form that fetches results from the Wikipedia OpenSearch API:
https://en.wikipedia.org/w/api.php?action=opensearch&search=SEARCH_TERM&format=json&origin=*
Display the search results as clickable links that open in a new tab.
Track the last 5 search terms with timestamps.
Do not duplicate terms; if a term is searched again, move it to the most recent position.
Only keep the most recent 5 terms.
Clear the search input after submitting a search.
App.tsx
import { useState } from "react";
type WikiApiResponse = [string, string[], string[], string[]];
type Term = { term: string; time: Date };
type Result = { title: string; url: string };
export default function App() {
const [results, setResults] = useState<Result[]>([]);
const [history, setHistory] = useState<Term[]>([]);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const searchTerm = formData.get("search") as string;
if (!searchTerm?.trim()) return;
const res = await fetch(
`https://en.wikipedia.org/w/api.php?action=opensearch&search=${encodeURIComponent(searchTerm)}&format=json&origin=*`,
);
const data = await res.json();
const titles: string[] = data[1];
const links: string[] = data[3];
const formatted = titles.map((title, i) => ({
title,
url: links[i],
}));
setResults(formatted);
const now = new Date();
setHistory((prev) => {
const filtered = prev.filter(
(item) => item.term.toLowerCase() !== searchTerm.toLowerCase(),
);
const updated = [...filtered, { term: searchTerm, time: now }];
return updated.slice(-5);
});
form.reset();
}
return (
<div className="flex flex-col justify-center items-center min-h-dvh bg-gray-100 p-4">
<div className="flex flex-col gap-8 w-full max-w-2xl">
<h1 className="text-3xl font-semibold text-gray-700">Wiki fetching</h1>
<form
onSubmit={handleSubmit}
className="p-8 rounded-2xl shadow-md flex flex-col border border-neutral-200"
>
<label
htmlFor="search"
className="text-xl font-medium text-gray-600 mb-2"
>
Search:
</label>
<div className="flex">
<input
id="search"
name="search"
className="grow font-light rounded-l-lg border-gray-300 border px-4 py-2"
/>
<button
type="submit"
className="bg-teal-500 text-white px-4 py-2 rounded-r-lg hover:bg-teal-600 transition-colors cursor-pointer"
>
Submit
</button>
</div>
</form>
<div className="grid grid-cols-2 gap-8 border p-8 border-neutral-200 rounded-2xl shadow-md min-h-96">
<div>
<h2 className="text-2xl mb-4">Results</h2>
<ul>
{results.map((item) => (
<li key={item.url}>
<a
href={item.url}
className="underline text-blue-500 hover:text-blue-700"
target="_blank"
rel="noopener noreferrer"
>
{item.title}
</a>
</li>
))}
</ul>
</div>
<div>
<h2 className="text-2xl mb-4">Last 5 terms</h2>
<ul>
{history.map((item) => (
<li className="gap-2 flex" key={item.term}>
<span className="font-medium">{item.term}</span>
<span className="font-light">
{item.time.toLocaleString()}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
);
}Memory Card Game
https://www.reactchallenges.com/challenges/memory-card-game
Rick & Morty: Double Fetching
https://www.reactchallenges.com/challenges/rick-and-morty-double-fetching
React Chrono: Start/Pause & Stop
Requirements
Display the timer starting at 00:00.0.
Toggle the timer with a Start/Pause button:
Start counting when pressed.
Pause counting when pressed again.
Reset the timer to 00:00.0 with a Stop button.
Update the timer display in tenths of a second while running.
Ensure that multiple presses of Start/Pause behave correctly (do not create multiple intervals).
Make the buttons accessible with proper aria-labels.
https://www.reactchallenges.com/challenges/react-chrono-start-pause-and-stop
with useEffect
import { useEffect, useRef, useState } from "react";
import { FaPlay, FaPause, FaStop } from "react-icons/fa";
export default function App() {
const [timeInms, setTimeInms] = useState<number>(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const handlePausePlay: () => void = () => {
setIsRunning((prev) => !prev);
};
const handleStopAndReset: () => void = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setIsRunning(false);
setTimeInms(0);
};
useEffect(() => {
if (!isRunning) return;
intervalRef.current = setInterval(() => {
setTimeInms((prev) => prev + 100);
}, 100);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [isRunning]);
const formatTime = (ms: number): string => {
const totalSeconds = Math.floor(ms / 1000);
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
const seconds = String(totalSeconds % 60).padStart(2, "0");
const tenths = Math.floor((ms % 1000) / 100);
return `${minutes}:${seconds}.${tenths}`;
};
return (
<main className="flex flex-col justify-center items-center min-h-dvh bg-gray-100 p-4">
<div className="flex flex-col gap-8 w-full max-w-md">
<h1 className="text-4xl font-bold text-center">Chrono</h1>
<div className="rounded-2xl shadow-2xl bg-gray-200 p-12 flex flex-col gap-12">
<div className="text-center text-4xl font-mono">
{formatTime(timeInms)}
</div>
<div className="flex gap-8 justify-center">
<button
className="cursor-pointer hover:scale-110 transition-transform"
aria-label={isRunning ? "Pause" : "Play"}
onClick={handlePausePlay}
>
{isRunning ? (
<FaPause className="size-8" />
) : (
<FaPlay className="size-8" />
)}
</button>
<button
className="cursor-pointer hover:scale-110 transition-transform"
aria-label="Stop"
onClick={handleStopAndReset}
>
<FaStop className="size-8" />
</button>
</div>
</div>
</div>
</main>
);
}Without useEffect
import { useRef, useState } from "react";
import { FaPlay, FaPause, FaStop } from "react-icons/fa";
const formatTime = (ms: number): string => {
const totalSeconds = Math.floor(ms / 1000);
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
const seconds = String(totalSeconds % 60).padStart(2, "0");
const tenths = Math.floor((ms % 1000) / 100);
return `${minutes}:${seconds}.${tenths}`;
};
export default function App() {
const [timeInms, setTimeInms] = useState<number>(0);
const [isRunning, setIsRunning] = useState<boolean>(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const handlePausePlay: () => void = () => {
if (isRunning) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setIsRunning(false);
} else {
intervalRef.current = setInterval(() => {
setTimeInms((prev) => prev + 100);
}, 100);
setIsRunning(true);
}
};
const handleStopAndReset: () => void = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = null;
setIsRunning(false);
setTimeInms(0);
};
return (
<main className="flex flex-col justify-center items-center min-h-dvh bg-gray-100 p-4">
<div className="flex flex-col gap-8 w-full max-w-md">
<h1 className="text-4xl font-bold text-center">Chrono</h1>
<div className="rounded-2xl shadow-2xl bg-gray-200 p-12 flex flex-col gap-12">
<div className="text-center text-4xl font-mono">
{formatTime(timeInms)}
</div>
<div className="flex gap-8 justify-center">
<button
className="cursor-pointer hover:scale-110 transition-transform"
aria-label={isRunning ? "Pause" : "Play"}
onClick={handlePausePlay}
>
{isRunning ? (
<FaPause className="size-8" />
) : (
<FaPlay className="size-8" />
)}
</button>
<button
className="cursor-pointer hover:scale-110 transition-transform"
aria-label="Stop"
onClick={handleStopAndReset}
>
<FaStop className="size-8" />
</button>
</div>
</div>
</div>
</main>
);
}Accessible Accordion
Requirements
Render all accordion items closed initially.
Open an item when its header button is clicked.
Close an item when its header button is clicked again.
If multiple=false, ensure only one item can be open at a time.
If multiple=true, allow multiple items to be open simultaneously.
Manage proper aria-expanded, aria-controls, and aria-hidden attributes for accessibility.
Support smooth height transitions when opening and closing items.
App.tsx does not need to be modified, although you may edit it to test the multiple={true} behavior.
You can use data to populate the accordion items.
https://www.reactchallenges.com/challenges/accessible-accordion