Skip to main content

step-by-step

Step 01​

Create a React functional component called Counter that displays a number starting at zero and two buttons labeled "Increment" and "Decrement". The component should manage its own state, update the displayed value when the buttons are clicked, and prevent the value from going below zero. When the value is zero, the "Decrement" button should be disabled.

import { useState } from "react";

export default function () {
const [number, setNumber] = useState<number>(0);

function decrement() : void{
setNumber(prev => prev-1);
}

function increment() : void {
setNumber(prev => prev+1);
}

const isDecrementButtonDisabled = number === 0;

return (
<section>

<button onClick={decrement} disabled={isDecrementButtonDisabled}>
Decrement
</button>

<span> {number} </span>

<button onClick={increment}>
Increment
</button>

</section>
)
}

Step 02​

Create a React functional component that manages a list of todo items stored in state. The component should initially render three predefined todos, display them in a list, and include a button that adds a new todo with an incremental id and a default label when clicked. Each todo item must be rendered with a stable key, and the UI should update immediately when a new item is added.

import { useState } from "react";
import { v4 as uuidv4 } from "uuid";

interface Todo {
id: string;
label: string;
}

const DEFAULT_TODOS: Todo[] = [
{ id: uuidv4(), label: "New Task 1" },
{ id: uuidv4(), label: "New Task 2" },
{ id: uuidv4(), label: "New Task 3" },
];

function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.label}</li>
))}
</ul>
);
}

function createTodo(count: number): Todo {
return {
id: uuidv4(),
label: `New Task ${count + 1}`,
};
}

export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>(DEFAULT_TODOS);

function handleAddTodo() {
setTodos(prev => [...prev, createTodo(prev.length)]);
}

return (
<section>
<button onClick={handleAddTodo}>
Add Task
</button>
<TodoList todos={todos} />
</section>
);
}

Step 03​

Extend the existing todo application so that each rendered item can be removed individually by the user. The interaction should remove only the selected item while keeping the rest of the list intact and correctly rendered. After multiple additions and removals, the UI must remain consistent, with no unexpected reordering, visual glitches, or React key warnings. The solution should work reliably even when items are removed in rapid succession or from the middle of the list.

import { useState } from "react";
import { v4 as uuidv4 } from "uuid";

interface Todo {
id: string;
label: string;
}

const DEFAULT_TODOS: Todo[] = [
{ id: uuidv4(), label: "New Task 1" },
{ id: uuidv4(), label: "New Task 2" },
{ id: uuidv4(), label: "New Task 3" },
];

function TodoItem({ todo, onDelete }: { todo: Todo; onDelete: (id: string) => void }) {
return (
<li>
<span>{todo.label}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}

function TodoList({ todos, onDelete }: { todos: Todo[]; onDelete: (id: string) => void }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={onDelete}
/>
))}
</ul>
);
}

export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>(DEFAULT_TODOS);

function handleAddTodo() {
setTodos(prev => [
...prev,
{ id: uuidv4(), label: `New Task ${prev.length + 1}` },
]);
}

function handleDeleteTodo(id: string) {
setTodos(prev => prev.filter(todo => todo.id !== id));
}

return (
<section>
<TodoList todos={todos} onDelete={handleDeleteTodo} />
<button onClick={handleAddTodo}>Add Task</button>
</section>
);
}

Step 04​

Update the todo application to include a search functionality. Create a controlled input field that filters the list of todos in real-time as the user types. The filtering should be case-insensitive, and if no items match the search term, a "No results found" message should be displayed

import { useState, useMemo } from "react";
import { v4 as uuidv4 } from "uuid";

interface Todo {
id: string;
label: string;
}

const DEFAULT_TODOS: Todo[] = [
{ id: uuidv4(), label: "Finish React project" },
{ id: uuidv4(), label: "Study for technical interview" },
{ id: uuidv4(), label: "Review JavaScript performance" },
];

export default function FilterableTodoApp() {
const [todos, setTodos] = useState<Todo[]>(DEFAULT_TODOS);
const [searchTerm, setSearchTerm] = useState<string>("");

// Performance Optimization: Derived state calculated during render
const filteredTodos = useMemo(() => {
return todos.filter((todo) =>
todo.label.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [todos, searchTerm]);

function handleAddTodo() {
const newTodo: Todo = {
id: uuidv4(),
label: `Task ${todos.length + 1}`,
};
setTodos((prev) => [...prev, newTodo]);
}

function handleDelete(id: string) {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}

return (
<section style={{ padding: "20px" }}>
<h2>Step 04 - Real-time Filtering</h2>

<input
type="text"
placeholder="Search tasks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: "8px",
marginBottom: "20px",
width: "100%",
maxWidth: "300px"
}}
/>

<div style={{ marginBottom: "10px" }}>
<button onClick={handleAddTodo}>Add Task</button>
</div>

{filteredTodos.length > 0 ? (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id} style={{ marginBottom: "8px" }}>
<span style={{ marginRight: "10px" }}>{todo.label}</span>
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
) : (
<p style={{ color: "#666", fontStyle: "italic" }}>
No results found for "{searchTerm}"
</p>
)}
</section>
);
}

Step 05​

Update the todo application to include a search functionality. Create a controlled input field that filters the list of todos in real-time as the user types. The filtering should be case-insensitive, and if no items match the search term, a "No results found" message should be displayed

import { useState, useEffect, useMemo } from "react";

interface Todo {
id: number;
title: string;
completed: boolean;
}

export default function AsyncFilterableTodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState<string>("");

useEffect(() => {
async function fetchTodos() {
try {
setIsLoading(true);
setError(null);

const response = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=10");

if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}

const data = await response.json();
setTodos(data);
} catch (err: any) {
setError(err.message || "An unexpected error occurred.");
} finally {
setIsLoading(false);
}
}

fetchTodos();
}, []);

// Derived state: Filtering the API results based on user input
const filteredTodos = useMemo(() => {
return todos.filter((todo) =>
todo.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [todos, searchTerm]);

if (isLoading) return <p>Loading tasks from API...</p>;

if (error) return (
<section>
<p><strong>Error:</strong> {error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</section>
);

return (
<section>
<h2>Step 06 - API Integration & Filtering</h2>

<input
type="text"
placeholder="Filter API results..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>

{filteredTodos.length > 0 ? (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>
{todo.completed ? "βœ… " : "β­• "}
{todo.title}
</li>
))}
</ul>
) : (
<p> No tasks match your search. </p>
)}
</section>
);
}