Sep 14, 2026
/
By Ariffud M.
/
This React tutorial teaches you how to set up a React app, write JSX, build components, pass props, manage state, use hooks, handle forms, add routes, fetch API data, and build a small working project.
It works like a beginner React course, with practical lessons, examples, and exercises built around a task tracker app you’ll complete by the end.
Here’s the learning path in this React tutorial:
- Prepare a React project with Vite, Node.js, and npm.
- Learn JSX, build React components, and pass data with props.
- Add interactive state, events, and controlled forms.
- Use hooks like useState and useEffect to manage logic.
- Add React Router and fetch API data.
- Build a simple React task tracker project.
What will you learn in this React tutorial?
You’ll learn React from project setup to an app that’s ready to publish online, with examples and short exercises that help you apply each concept as you go.
The lessons use the same task tracker as a running project, so you can see how individual React features work together in a complete app.
The table below shows what you’ll practice in each lesson and how it contributes to the final project:
| Lesson | Concept | Practice task | Project outcome |
| 1 | Node.js, npm, and Vite | Start the development server | React project running locally |
| 2 | JSX and rendering | Update the page markup | First custom interface |
| 3 | Components and props | Build reusable task items | Structured task list |
| 4 | State and events | Toggle and delete tasks | Interactive task tracker |
| 5 | Forms | Validate and add tasks | User-created tasks |
| 6 | Hooks | Run an effect and create a custom hook | Reusable component logic |
| 7 | React Router | Add multiple page views | Navigation between views |
| 8 | API data | Adapt fetched data and handle request states | External task data with loading and error feedback |
| 9 | Final project | Test and build the app for production | Complete task tracker ready to deploy |
React tutorial prerequisites
The prerequisites for following this React tutorial are basic knowledge of HTML, CSS, JavaScript, and running commands in a terminal. We’ll show you how to install Node.js and npm, so you don’t need them beforehand.
You can follow along even if you’ve never used React before.
JavaScript is the most important prerequisite because you’ll use it when working with React to create components, handle events, update state, and work with data.
You should be familiar with JavaScript variables, functions, arrays, objects, destructuring, modules, and array methods such as map() and filter().
What app will you build in this React tutorial?
You’ll build a React task tracker that lets you add, complete, reopen, and delete tasks, then open individual task details and navigate between pages.
The finished app will also load starter tasks from an API, show loading and error feedback, and include a responsive layout that works on smaller screens. By the end, you’ll have a complete production build that’s ready to deploy.
How do you set up a React project?
You set up the React project by installing Node.js and npm, then using Vite to create and run the app.
The following sections walk you through installing the required tools, creating the React project, and understanding its main files.
Check our Node.js tutorial if you want to learn more about Node.js or need a refresher on the basics.
Install Node.js and npm
To install Node.js and npm, download the latest Long-Term Support (LTS) version of Node.js from its official website. Then, double-click the installer to install it on your computer. npm comes with Node.js, so you don’t need to install it separately.
You need Node.js to run tools like Vite, while npm installs and manages the packages your React project uses.
After installation, open your terminal and check that both tools are available:
node --version npm --version
Each command should return a version number. Reinstall Node.js if either command doesn’t return one.
Create a React app with Vite
Create the task tracker app with Vite by running the project setup command, installing its packages, and starting the local development server.
This tutorial uses Vite because Create React App, a tool previously used to start React projects, is deprecated. Vite gives you a ready-to-use React project, so you can start coding without configuring the setup yourself.
Run the following commands in your terminal:
npm create vite@latest react-task-tracker -- --template react cd react-task-tracker npm install npm run dev
The –template react option creates a JavaScript React project, while npm install installs the packages listed in package.json. Then, npm run dev starts the development server.
Vite will show a local address in your terminal, such as http://localhost:5173. Open it in your browser to see the starter React app.
Understand the React project files
The main React project files you’ll work with are inside the src folder, including main.jsx, App.jsx, and the project’s CSS files. As you build the task tracker, you’ll also create folders for components and pages.
Here’s what you’ll use each important file or folder for:
| File or folder | What you use it for |
| package.json | Lists the project packages and commands, such as npm run dev |
| index.html | Provides the HTML page where your React app appears |
| src/main.jsx | Starts the React app and displays the main App component |
| src/App.jsx | Contains the main app component and, later, its routes |
| src/App.css | Stores styles for the main app |
| src/index.css | Stores global styles for the app |
| src/components | Stores reusable components you’ll create, such as Header.jsx and TaskItem.jsx |
| src/pages | Stores the page components you’ll create for React Router |
Match filename capitalization exactly, such as App.jsx and TaskItem.jsx, because some operating systems treat uppercase and lowercase filenames differently.
How does React render a user interface?
React renders a user interface by running your components, using the JSX they return to determine what should appear, and updating the page in your browser with the result.
This process has three main steps:
- Trigger. The initial render starts when React runs createRoot(…).render() in src/main.jsx. Later, changes to state can trigger another render.
- Render. React runs your components and reads their JSX to determine what the interface should look like.
- Commit. React applies the necessary changes to the page so you see the latest interface.
For example, changing a task’s status triggers another render. React calculates the latest interface, then commits the necessary changes to the page.
What is JSX in React?
JSX is a JavaScript syntax extension that lets you write HTML-like markup inside React components. You use it to describe what should appear on the page while keeping that markup close to the JavaScript that controls it.
JSX looks similar to HTML, but it follows a few different rules:
- Wrap multiple elements in one parent element or a fragment such as <>…>.
- Close every tag, including self-closing tags such as .
- Use className instead of the HTML class attribute.
- Write most attributes in camelCase, such as onClick.
- Put JavaScript values or expressions inside curly braces, such as {taskName}.
For example, replace the contents of src/App.jsx with:
const taskName="Learn JSX";
function App() {
return (
{taskName}
);
}
export default App;
Here, the JSX defines the heading and paragraph that appear in your browser. The {taskName} expression inserts the value assigned to the taskName variable into the paragraph.
For a quick exercise, change ‘Learn JSX’ to another task. Then, inside the element, add another paragraph below
{taskName}
, such as
Not started
.
Save App.jsx, then check that the updated task and the new status appear in your browser.
How do you render your first React component?
You render your first React component by importing App.jsx into src/main.jsx and passing
Open src/main.jsx to see how it works:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
,
);
The document.getElementById(‘root’) part finds the element with the root ID in index.html. React then displays the app inside that element, which is why the JSX you added to App.jsx appears in your browser.
You’ll add more components inside App as you build the task tracker. StrictMode also helps you catch common problems while developing, and its extra checks don’t run in production.
How does React update the DOM?
React updates the Document Object Model (DOM) during the commit step by applying the changes needed to make the page match the latest render.
The DOM represents the elements currently displayed in your browser. React can update a changed element without recreating unrelated parts of the page.
For example, when a task changes from Open to Done, React can update the task’s displayed status while leaving unchanged elements, such as the page heading, alone.
How do React components work?
React components work as reusable JavaScript functions that return JSX for part of the interface. Each one handles one part, such as a header, task list, or individual task, then combines with other components to build the full page.
Keeping these parts separate makes your React project easier to update.
Create your first React component
Create your first React component by defining a JavaScript function with a capitalized name and returning JSX from it. You’ll start with the task tracker header.
First, create the src/components folder. Inside it, create Header.jsx:
function Header() {
return (
);
}
export default Header;
The export default Header line lets you import the component into another file.
Next, replace the contents of src/App.jsx with:
import Header from './components/Header.jsx';
function App() {
return (
);
}
export default App;
Add
Pass data with props
To pass data with props, add values to a child component when you render it, then read those values inside the child.
Create src/components/TaskItem.jsx:
function TaskItem({ title, done }) {
return (
{title}
);
}
export default TaskItem;
The { title, done } syntax reads the title and done props passed to the component.
Now import TaskItem into src/App.jsx and render it twice:
import Header from './components/Header.jsx';
import TaskItem from './components/TaskItem.jsx';
function App() {
return (
);
}
export default App;
The first TaskItem appears complete, while the second appears incomplete because they receive different done values.
Note that props are read-only. A child component can use the values it receives, but it should never change them. Only the parent passing the prop can change it.
Compose components into a page
Compose components into a page by creating a TaskList component, nesting it inside App, and rendering a TaskItem for each task in the list.
Create src/components/TaskList.jsx:
import TaskItem from './TaskItem.jsx';
function TaskList({ tasks }) {
if (tasks.length === 0) {
return No tasks yet.
;
}
return (
-
{tasks.map((task) => (
))}
);
}
export default TaskList;
The map() method creates one TaskItem for each task in the array. The key gives React a stable way to identify each task when the list changes.
Next, update src/App.jsx:
import Header from './components/Header.jsx';
import TaskList from './components/TaskList.jsx';
const tasks = [
{ id: 1, title: 'Learn JSX', done: true },
{ id: 2, title: 'Build a component', done: false },
];
function App() {
return (
<>
>
);
}
export default App;
This structure keeps the task-list markup out of App.jsx and gives each part of the interface a clear responsibility.
How do state and events make a React app interactive?
State and events make a React app interactive by storing data that can change (state) and responding to actions such as clicks (events). For the task tracker app, you’ll use state to store the tasks and click events to mark them complete or delete them.
React updates the page to show the new data when you interact with a task and its state changes.
Add state with useState
Add state with useState by calling it inside your component and passing the starting value. useState gives you the current value and a function for updating it. Use it when your component needs to remember data that can change.
In src/App.jsx, import useState and move the task data into state:
import { useState } from 'react';
import Header from './components/Header.jsx';
import TaskList from './components/TaskList.jsx';
const initialTasks = [
{ id: 1, title: 'Learn JSX', done: true },
{ id: 2, title: 'Build a component', done: false },
];
function App() {
const [tasks, setTasks] = useState(initialTasks);
return (
<>
>
);
}
export default App;
In const [tasks, setTasks] = useState(initialTasks), tasks contains the current task list, while setTasks updates it. initialTasks provides the starting value when the component first renders.
For practice, temporarily change useState(initialTasks) to useState([]). The task list should show No tasks yet. Change it back to useState(initialTasks) before continuing.
Handle click events
To handle click events, pass functions to the onClick props in TaskItem and connect them to the task state in App.
First, add these functions below the useState line in App.jsx:
function toggleTask(id) {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === id
? { ...task, done: !task.done }
: task
)
);
}
function deleteTask(id) {
setTasks((currentTasks) =>
currentTasks.filter((task) => task.id !== id)
);
}
Then, pass both functions to TaskList:
Next, update src/components/TaskList.jsx, so it passes the functions and each task ID to TaskItem:
import TaskItem from './TaskItem.jsx';
function TaskList({ tasks, onToggle, onDelete }) {
if (tasks.length === 0) {
return No tasks yet.
;
}
return (
-
{tasks.map((task) => (
))}
);
}
export default TaskList;
Finally, update src/components/TaskItem.jsx:
function TaskItem({
id,
title,
done,
onToggle,
onDelete,
}) {
return (
{done ? ‘✓ ‘ : ‘○ ‘}
{title}
);
}
export default TaskItem;
Click Mark done or Mark open to change a task’s status. Click Delete to remove it from the list.
The arrow function in onClick={() => onToggle(id)} waits until you click the button before calling onToggle. Writing onClick={onToggle(id)} instead would call the function immediately while React renders the component.
For a quick test, temporarily add a Log task button inside the
element in TaskItem.jsx and give it onClick={() => console.log(title)}.Click the button and confirm that the task title appears in your browser’s developer console. Then, remove the button before continuing.
Update arrays and objects without mutation
Update arrays and objects in React state by creating new versions instead of changing the existing state directly. The toggleTask and deleteTask functions you just added both follow this pattern.
In toggleTask, map() creates a new array. For the task with the matching ID, the spread syntax copies the existing task into a new object and changes its done value:
currentTasks.map((task) =>
task.id === id
? { ...task, done: !task.done }
: task
)
The other task objects stay unchanged.
In deleteTask, filter() creates a new array without the task whose ID matches the one you want to remove:
currentTasks.filter((task) => task.id !== id)
Avoid changing state directly, for example:
task.done = true;
Instead, create a new object with the updated value:
{ ...task, done: true }
The same rule applies to arrays. Instead of methods such as push(), create a new array and pass it to setTasks.
How do React forms collect user input?
React forms collect user input by storing field values in state and updating them as you type. In the task tracker, TaskForm will control the title field and pass submitted values to App.
Build a controlled form
You can build a controlled form by setting the input value from React state and updating that state whenever you type.
Create src/components/TaskForm.jsx:
import { useState } from 'react';
function TaskForm({ onAdd }) {
const [title, setTitle] = useState('');
function handleSubmit(event) {
event.preventDefault();
onAdd(title);
setTitle('');
}
return (
);
}
export default TaskForm;
The value={title} prop keeps the field value connected to title state, while onChange updates that state as you type.
When you submit the form, handleSubmit prevents the browser’s default page reload, passes the current title to onAdd, and clears the field.
Validate input before updating state
Validate the task title before calling onAdd so empty or whitespace-only values don’t become tasks.
In TaskForm.jsx, replace handleSubmit with:
function handleSubmit(event) {
event.preventDefault();
const trimmedTitle = title.trim();
if (trimmedTitle === '') {
return;
}
onAdd(trimmedTitle);
setTitle('');
}
The trim() method removes spaces from the beginning and end of the title. A value containing only spaces becomes an empty string, so the function stops before passing it to onAdd.
This validation is enough for the task tracker. You can add other rules, such as a maximum title length, when your project needs them.
Add new tasks to the project
To add new tasks to your project, connect TaskForm to the tasks state in App.jsx.
First, import TaskForm at the top of src/App.jsx:
import TaskForm from './components/TaskForm.jsx';
Then, add this function inside App, below your existing useState line:
function addTask(title) {
const newTask = {
id: Date.now(),
title,
done: false,
};
setTasks((currentTasks) => [
...currentTasks,
newTask,
]);
}
Finally, render TaskForm above TaskList and pass addTask through the onAdd prop:
When you submit a valid title, addTask creates a new task with an ID and an initial done value of false, then adds it to the task list.
How do React hooks manage logic and side effects?
React hooks manage component logic by letting you store changing data, connect to external systems, and reuse logic across components.
You’ve already used useState for task data and form values. Next, you’ll use useEffect to run code that connects your component to something outside React, such as an API.
React also provides other hooks, including useRef, useMemo, and useCallback, but you don’t need them for this task tracker yet.
Use useEffect to fetch data
To use useEffect to fetch data, start the request inside the effect and store the response in state.
Create src/components/ApiTasks.jsx:
import { useEffect, useState } from 'react';
function ApiTasks() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
let ignore = false;
async function loadTasks() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/todos?_limit=5'
);
if (!response.ok) {
throw new Error(
`Request failed with status ${response.status}`
);
}
const data = await response.json();
if (!ignore) {
setTasks(data);
}
} catch (error) {
if (!ignore) {
console.error(error);
}
}
}
loadTasks();
return () => {
ignore = true;
};
}, []);
return (
-
{tasks.map((task) => (
-
{task.completed ? ‘✓ ‘ : ‘○ ‘}
{task.title}
))}
);
}
export default ApiTasks;
This example requests five sample tasks from JSONPlaceholder after the component renders. When the request succeeds, setTasks stores the response and triggers another render with the task data.
The empty dependency array in useEffect(…, []) means the effect runs once after the component first appears, instead of after every render. With StrictMode enabled, React runs the effect twice during development to help you spot missing cleanup.
This doesn’t happen in production, so two requests in your network tab are expected.
The ignore variable prevents the request from updating state after React cleans up the effect.
To check the result, temporarily import ApiTasks into src/App.jsx and render
Remove the temporary
Avoid common useEffect mistakes
Avoid common useEffect mistakes by reserving effects for code that needs to synchronize with something outside React, not values you can calculate during rendering or actions you can handle directly.
For example, calculate the number of completed tasks directly from tasks:
const completedTasks = tasks.filter( (task) => task.done );
React recalculates completedTasks when the component renders, so you don’t need an effect to store the result separately.
Keep user-triggered logic in event handlers as well. For example, add a task in the form submission handler and delete a task in its click handler, rather than using an effect.
Also check an effect’s dependencies when it updates state. Updating a value that causes the same effect to run again can create an update loop.
Create a custom hook
You can create a custom hook by moving reusable logic that uses state or other hooks into a JavaScript function whose name starts with use. The function can call other React hooks and return the values or functions your components need.
For example, create src/hooks/useTaskFilter.js to keep the task-filtering logic in one place:
import { useState } from 'react';
function useTaskFilter(tasks) {
const [filter, setFilter] = useState('all');
const filteredTasks = tasks.filter((task) => {
if (filter === 'done') {
return task.done;
}
if (filter === 'active') {
return !task.done;
}
return true;
});
return {
filter,
setFilter,
filteredTasks,
};
}
export default useTaskFilter;
A component can then call useTaskFilter(tasks) to get the current filter, change it with setFilter, and display filteredTasks. Each component that calls the hook gets its own filter state.
You’ll use this hook in the state and events exercise later, so keep the file even though the rest of the project doesn’t import it.
You don’t need to create a custom hook for every piece of logic, though. Keep simple logic inside the component when that makes the code easier to follow.
How do React Router and API data turn components into an app?
React Router and API data turn your components into an app by connecting components to URLs and filling them with data from external sources.
In the task tracker, React Router will add dashboard, task details, and about views, while API data will provide tasks that aren’t hardcoded in the project.
Add pages with React Router
To add pages with React Router, install it first, then wrap App with BrowserRouter and map URL paths to components.
Install the latest version of React Router:
npm install react-router
Important
Important! React Router v7 and later use react-router as the main package. Older tutorials use react-router-dom, so import the routing APIs used in this project from react-router.
Next, update src/main.jsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
,
);
BrowserRouter lets React Router use the browser URL to determine which view to display.
Create src/pages/About.jsx:
function About() {
return (
About
A task tracker built while learning React.
);
}
export default About;
Then, create src/pages/TaskDetails.jsx:
import { useParams } from 'react-router';
function TaskDetails({ tasks }) {
const { taskId } = useParams();
const task = tasks.find(
(task) => task.id === Number(taskId)
);
if (!task) {
return Task not found.
;
}
return (
{task.title}
Status: {task.done ? 'Done' : 'Open'}
);
}
export default TaskDetails;
The :taskId part of the URL identifies which task to display.
In src/App.jsx, keep your existing state and task functions. Import Routes, Route, and the new page components:
import { Route, Routes } from 'react-router';
import About from './pages/About.jsx';
import TaskDetails from './pages/TaskDetails.jsx';
Then, replace the current return statement in App with:
return (
<>
}
/>
}
/>
}
/>
>
);
The / route displays your dashboard, /tasks/:taskId displays an individual task, and /about displays the about page.
To navigate between the main views, import Link into Header.jsx:
import { Link } from 'react-router';
Then, add these links inside the
To open the task details page, import Link into TaskItem.jsx and add this link inside the task item:
View details
You can now move between views without reloading the entire page.
Fetch and display API data
You can fetch and display API data in the task tracker by converting the response to the same data structure your existing task components use.
JSONPlaceholder stores a task’s completion status in completed, while your task tracker uses done. In src/components/ApiTasks.jsx, replace:
setTasks(data);
with:
const apiTasks = data.map((task) => ({
id: task.id,
title: task.title,
done: task.completed,
}));
setTasks(apiTasks);
Then, update the task status in the returned JSX:
{task.done ? '✓ ' : '○ '}
{task.title}
Each API task now has the same id, title, and done properties as the task objects used elsewhere in the project. Keeping one consistent structure means your components don’t need separate logic for local and API data.
Show loading and error states
To show loading and error states, track the request status in ApiTasks.jsx and display feedback before the task list.
Add two state values below the existing tasks state:
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
In loadTasks, replace the current catch block with:
} catch {
if (!ignore) {
setError('Could not load tasks.');
}
}
Then, add a finally block after it:
finally {
if (!ignore) {
setLoading(false);
}
}
Before the component returns the task list, add:
if (loading) {
return Loading tasks...
;
}
if (error) {
return {error}
;
}
Now Loading tasks… appears while the request is running, Could not load tasks. appears when it fails, and the task list appears after a successful request.
For practice, temporarily replace the API endpoint with https://jsonplaceholder.typicode.com/not-a-real-endpoint and confirm that the error message appears.
Restore the original endpoint after testing it. Then, remove
How to build a small React project from start to finish
To build a small React project from start to finish, combine the components, state, forms, hooks, routing, and API techniques into one app.
You’ll continue with the task tracker project you built earlier, so you don’t need to create another one.
Complete each step and check the result before continuing. This makes it easier to catch problems before you add another feature.
1. Create the project structure
Create the project structure by separating reusable components and page-level views inside src. The final task tracker only needs a few files:
react-task-tracker/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── components/
│ ├── Header.jsx
│ ├── TaskForm.jsx
│ ├── TaskItem.jsx
│ └── TaskList.jsx
├── pages/
│ ├── About.jsx
│ ├── Dashboard.jsx
│ └── TaskDetails.jsx
├── App.css
├── App.jsx
├── index.css
└── main.jsx
This structure covers the core task tracker. The exercises later add optional files such as EmptyState.jsx, TaskStats.jsx, useTaskFilter.js, and ApiTaskDetails.jsx.
Use components for reusable interface elements such as the header, form, and task items. Use pages for complete views that React Router connects to URLs.
Create any missing folders in your editor. On macOS or Linux, you can also run:
mkdir -p src/components src/pages
Checkpoint: Confirm that components and pages appear inside src and that the files are organized as shown above.
2. Build the layout components
You can build the task tracker layout by adding a shared header and a centered content area for the app’s main features.
Create src/components/Header.jsx:
function Header() {
return (
);
}
export default Header;
Then, replace src/App.jsx with:
import Header from './components/Header.jsx';
import './App.css';
function App() {
return (
<>
Your task tracker is ready for tasks.
>
);
}
export default App;
Next, replace the starter styles in src/index.css with the global styles:
:root {
font-family: Arial, sans-serif;
color: #1f2937;
background: #f3f4f6;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
button,
input {
font: inherit;
}
button {
cursor: pointer;
}
Then, replace src/App.css with the layout styles:
.header {
padding: 1rem 1.5rem;
background: #ffffff;
border-bottom: 1px solid #d1d5db;
}
.header h1 {
margin: 0;
}
.container {
width: min(720px, calc(100% - 2rem));
margin: 2rem auto;
}
Keeping the global rules in index.css prevents Vite’s starter styles from affecting the page layout. App.css can then contain styles specific to the task tracker interface.
Checkpoint: You should see the Task tracker heading above the placeholder text, with the content centered on a light gray background.
3. Build the task list
To build the task list, use TaskItem to display each task and TaskList to render the full collection.
Create src/components/TaskItem.jsx:
function TaskItem({
id,
title,
done,
onToggle,
onDelete,
}) {
return (
{done ? ‘✓ ‘ : ‘○ ‘}
{title}
);
}
export default TaskItem;
Then, create src/components/TaskList.jsx:
import TaskItem from './TaskItem.jsx';
function TaskList({
tasks,
onToggle,
onDelete,
}) {
if (tasks.length === 0) {
return No tasks yet.
;
}
return (
-
{tasks.map((task) => (
))}
);
}
export default TaskList;
Add these styles to src/App.css:
.task-list {
display: grid;
gap: 0.75rem;
padding: 0;
margin: 1rem 0 0;
list-style: none;
}
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
}
.task-item.done .task-title {
color: #6b7280;
text-decoration: line-through;
}
.task-actions {
display: flex;
gap: 0.5rem;
}
TaskList will appear once App passes task data and action handlers to it.
Checkpoint: Save TaskItem.jsx and TaskList.jsx, then check that the project still runs without errors in the browser or developer console.
4. Add task state
Add the shared task state and action functions in App.jsx so TaskList can display, complete, reopen, and delete tasks. Start with two local tasks so you can verify these actions before connecting the app to API data.
Replace src/App.jsx with:
import { useState } from 'react';
import Header from './components/Header.jsx';
import TaskList from './components/TaskList.jsx';
import './App.css';
const initialTasks = [
{
id: 1,
title: 'Build the task list',
done: true,
},
{
id: 2,
title: 'Add React state',
done: false,
},
];
function App() {
const [tasks, setTasks] = useState(initialTasks);
function toggleTask(id) {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === id
? { ...task, done: !task.done }
: task
)
);
}
function deleteTask(id) {
setTasks((currentTasks) =>
currentTasks.filter((task) => task.id !== id)
);
}
return (
<>
>
);
}
export default App;
Keep tasks in App.jsx so the dashboard and task-details page can use the same task data when you add routes later.
Checkpoint: Select Mark done or Mark open and confirm that the task status changes immediately. Delete a task and confirm that it disappears from the list. Delete both tasks to confirm that No tasks yet. appears.
5. Add the task form
Add the task form by connecting TaskForm to App so you can create validated tasks and add them to the shared task list.
Create src/components/TaskForm.jsx:
import { useState } from 'react';
function TaskForm({ onAdd }) {
const [title, setTitle] = useState('');
function handleSubmit(event) {
event.preventDefault();
const trimmedTitle = title.trim();
if (trimmedTitle === '') {
return;
}
onAdd(trimmedTitle);
setTitle('');
}
return (
);
}
export default TaskForm;
Next, import TaskForm into src/App.jsx:
import TaskForm from './components/TaskForm.jsx';
Add addTask inside App:
function addTask(title) {
const newTask = {
id: Date.now(),
title,
done: false,
};
setTasks((currentTasks) => [
...currentTasks,
newTask,
]);
}
Then, render TaskForm above TaskList:
Add the form styles to src/App.css:
.task-form {
display: grid;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.task-form-row {
display: flex;
gap: 0.5rem;
}
.task-form input {
flex: 1;
min-width: 0;
padding: 0.75rem;
}
addTask creates each new task in the same id, title, and done format used by the rest of the app, so newly added tasks work with the existing toggle and delete actions.
Checkpoint: Add a valid task and confirm that it appears in the list. Mark it done, reopen it, and delete it to confirm that new tasks work with the existing actions. Then, submit a title containing only spaces and confirm that no task is added.
6. Add routes
To add routes, create dashboard, task-details, and about pages, then connect them to URLs with React Router.
First, update src/components/Header.jsx so users can navigate between the main pages:
import { Link } from 'react-router';
function Header() {
return (
);
}
export default Header;
Next, create src/pages/Dashboard.jsx to group the task form, task list, and completion count on the main page:
import TaskForm from '../components/TaskForm.jsx';
import TaskList from '../components/TaskList.jsx';
function Dashboard({
tasks,
onAdd,
onToggle,
onDelete,
}) {
const total = tasks.length;
const completed = tasks.filter(
(task) => task.done
).length;
return (
{completed} of {total} tasks completed
);
}
export default Dashboard;
Create src/pages/About.jsx for the second main view:
function About() {
return (
About this app
This task tracker is a beginner React project
for practicing components, state, forms,
hooks, routing, and API data.
);
}
export default About;
Then, create src/pages/TaskDetails.jsx so each task can have its own URL:
import { useParams } from 'react-router';
function TaskDetails({ tasks }) {
const { taskId } = useParams();
const task = tasks.find(
(task) => task.id === Number(taskId)
);
if (!task) {
return Task not found.
;
}
return (
{task.title}
Status: {task.done ? 'Done' : 'Open'}
);
}
export default TaskDetails;
The :taskId part of /tasks/:taskId is a dynamic URL segment. For example, opening /tasks/2 gives useParams() a taskId value of “2”. Number(taskId) converts that string to a number so it can match the numeric task IDs.
Next, add a link from each task to its details page. Import Link at the top of src/components/TaskItem.jsx:
import { Link } from 'react-router';
Then, add the View details link inside
View details
Enable browser routing in src/main.jsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
,
);
Next, import the routing components and page components into src/App.jsx:
import { Route, Routes } from 'react-router';
import Dashboard from './pages/Dashboard.jsx';
import About from './pages/About.jsx';
import TaskDetails from './pages/TaskDetails.jsx';
Replace the current return statement in App with:
return (
<>
}
/>
}
/>
}
/>
>
);
The / route shows the dashboard, /tasks/:taskId shows the selected task, and /about shows information about the project.
Finally, update the header styles in src/App.css and add styling for its navigation:
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.5rem;
background: #ffffff;
border-bottom: 1px solid #d1d5db;
}
.header nav {
display: flex;
gap: 1rem;
}
Checkpoint: Open Dashboard and About, then select View details on a task. Each action should update the URL and display the corresponding page without a full page reload.
7. Fetch starter data
Fetch the starter tasks into App.jsx so the dashboard and task-details page can use the same API-loaded task data.
Remove the initialTasks array, then update the React import at the top of src/App.jsx:
import { useEffect, useState } from 'react';
Replace useState(initialTasks) with state for the tasks and request status:
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
Then, add the API request inside App:
useEffect(() => {
let ignore = false;
async function loadTasks() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/todos'
);
if (!response.ok) {
throw new Error(
`Request failed with status ${response.status}`
);
}
const data = await response.json();
const apiTasks = data
.slice(0, 4)
.map((task) => ({
id: task.id,
title: task.title,
done: task.completed,
}));
if (!ignore) {
setTasks(apiTasks);
}
} catch {
if (!ignore) {
setError('Could not load tasks.');
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
loadTasks();
return () => {
ignore = true;
};
}, []);
JSONPlaceholder uses completed for each task’s status, while the task tracker uses done. The map() call converts the API response to the same id, title, and done structure used throughout the app.
Next, pass loading and error to Dashboard in the / route:
} />
Update the Dashboard parameters:
function Dashboard({
tasks,
loading,
error,
onAdd,
onToggle,
onDelete,
}) {
Then, add these checks before calculating total and completed:
if (loading) {
return Loading tasks...
;
}
if (error) {
return {error}
;
}
The dashboard now shows Loading tasks… while the request runs and Could not load tasks. if the request fails.
Pass the same request state to TaskDetails so a direct visit to a task URL doesn’t show Task not found. before the tasks finish loading:
} />
Then, update src/pages/TaskDetails.jsx:
import { useParams } from 'react-router';
function TaskDetails({
tasks,
loading,
error,
}) {
const { taskId } = useParams();
if (loading) {
return Loading task...
;
}
if (error) {
return {error}
;
}
const task = tasks.find(
(task) => task.id === Number(taskId)
);
if (!task) {
return Task not found.
;
}
return (
{task.title}
Status: {task.done ? 'Done' : 'Open'}
);
}
export default TaskDetails;
Checking loading before looking for the task prevents the details page from treating an empty task array as a missing task while the API request is still running.
Tasks you add, complete, reopen, or delete after loading remain in React state for the current session. Refreshing the page shows the four starter tasks again because the app doesn’t save those changes to permanent storage.
Checkpoint: Refresh the dashboard and confirm that Loading tasks… appears before four starter tasks load. Then, open a task details page and refresh it directly. You should see Loading task… before the selected task appears instead of briefly seeing Task not found.
8. Test and polish the app
Test and polish the app by adding responsive styles, checking its main features and routes, and creating a production build.
First, add this responsive styling to src/App.css:
@media (max-width: 600px) {
.header {
align-items: flex-start;
flex-direction: column;
}
.task-form-row {
flex-direction: column;
}
.task-item {
align-items: stretch;
flex-direction: column;
}
.task-actions {
flex-wrap: wrap;
}
}
At widths of 600px or less, the navigation, form, and task controls now have more room by stacking or wrapping instead of staying in a single row.
Next, test the complete app:
- Refresh the dashboard and confirm that the loading message is followed by four starter tasks.
- Add a valid task and confirm that it appears once.
- Submit an empty or whitespace-only title and confirm that no task is added.
- Mark an open task as done and confirm that its status and completed count update.
- Reopen a completed task and confirm that the count updates again.
- Delete a task and confirm that it disappears and the total count changes.
- Select View details and confirm that the correct task title and status appear.
- Refresh a task-details page directly and confirm that Loading task… appears before the task loads.
- Navigate between Dashboard and About, then test the browser’s back and forward buttons.
- Temporarily use an invalid API endpoint and confirm that Could not load tasks. appears. Restore the working endpoint afterward.
- Resize the browser to a phone-width screen and confirm that the header, form, task items, and action buttons remain usable.
- Check the developer console for React warnings or errors.
After the app passes these checks, create the production build:
npm run build
A successful build confirms that Vite can compile the app for production.
Task changes still reset after a refresh because the app stores them only in React state rather than permanent storage.
Checkpoint: Confirm that the app passes the checklist without React warnings or errors and that npm run build completes successfully.
What React exercises should beginners complete?
Beginners should complete React exercises on JSX and components, props, state and events, forms, routing, and API data.
The exercises below extend your finished task tracker with optional features that help you practice each concept one at a time.
JSX and components exercise
For the JSX and components exercise, create a reusable EmptyState.jsx component that appears when the task list is empty.
Your component should:
- Return valid JSX with one parent element.
- Include the heading No tasks yet.
- Include a short message that tells you to add your first task.
- Replace the existing No tasks yet. paragraph in TaskList.jsx.
Expected output: Delete all tasks, and the new empty-state message should appear instead of the task list.
Props exercise
Practice props by replacing the existing dashboard stats with a reusable TaskStats.jsx component that receives the total, completed, and remaining task counts from Dashboard.jsx.
Create src/components/TaskStats.jsx and add props for total, completed, and remaining.
In Dashboard.jsx, calculate the remaining tasks:
const remaining = total - completed;
Then, import TaskStats:
import TaskStats from '../components/TaskStats.jsx';
Replace the existing stats paragraph:
{completed} of {total} tasks completed
with:
Inside TaskStats.jsx, display all three values with JSX.
Expected output: The dashboard shows one summary, such as 4 total, 1 done, 3 remaining. Adding, completing, reopening, or deleting a task should update the values automatically.
State and events exercise
In the state and events exercise, use the useTaskFilter hook to add All, Active, and Done filter buttons to the dashboard.
In Dashboard.jsx, import the hook:
import useTaskFilter from '../hooks/useTaskFilter.js';
Then, call it inside Dashboard:
const {
filter,
setFilter,
filteredTasks,
} = useTaskFilter(tasks);
Add the filter buttons above TaskList:
Finally, pass filteredTasks to TaskList instead of tasks:
Expected output: All shows every task, Active shows tasks that aren’t complete, and Done shows completed tasks. Adding, completing, reopening, or deleting a task should update the filtered list automatically.
Forms exercise
For the forms exercise, add a due-date field to TaskForm, save the selected date with each new task, and display it in the task list.
Your changes should:
- Store the due date with another useState call in TaskForm.jsx.
- Add an input with type=”date” to the form.
- Require both the task title and due date before calling onAdd.
- Pass the due date to onAdd with the title.
- Update addTask so each new task stores a dueDate property.
- Pass dueDate={task.dueDate} from TaskList.jsx to TaskItem.
- Add dueDate to the TaskItem function parameters and display it when the task has a due date.
- Clear both form fields after a valid submission.
For step 6, update the TaskItem call in TaskList.jsx:
Then, update the TaskItem parameters:
function TaskItem({
id,
title,
done,
dueDate,
onToggle,
onDelete,
}) {
Display the date wherever it fits naturally in the task markup, for example, below the title.
Expected output: A newly added task shows its selected due date. The form shouldn’t add a task when the title or due date is missing.
Routing and API exercise
For the routing and API exercise, create a separate ApiTaskDetails.jsx page that loads one task from JSONPlaceholder using the taskId route parameter.
Create src/pages/ApiTaskDetails.jsx:
import { Link, useParams } from 'react-router';
import { useEffect, useState } from 'react';
function ApiTaskDetails() {
const { taskId } = useParams();
const [task, setTask] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
let ignore = false;
async function loadTask() {
setLoading(true);
setError('');
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${taskId}`
);
if (!response.ok) {
throw new Error(
`Request failed with status ${response.status}`
);
}
const data = await response.json();
if (!ignore) {
setTask(data);
}
} catch {
if (!ignore) {
setError('Could not load task.');
}
} finally {
if (!ignore) {
setLoading(false);
}
}
}
loadTask();
return () => {
ignore = true;
};
}, [taskId]);
if (loading) {
return Loading task...
;
}
if (error) {
return {error}
;
}
return (
{task.title}
Status: {task.completed ? 'Done' : 'Open'}
Back to dashboard
);
}
export default ApiTaskDetails;
Next, import ApiTaskDetails into src/App.jsx:
import ApiTaskDetails from './pages/ApiTaskDetails.jsx';
Then, add a separate route for the exercise inside Routes:
} />
The taskId value in the URL determines which JSONPlaceholder task the page requests. For example, /api-tasks/1 requests task 1, while /api-tasks/2 requests task 2.
Expected output: Opening /api-tasks/1 shows Loading task… before displaying task 1 and its status. Changing the URL to another valid task ID loads that task, while an unsuccessful request shows Could not load task. The Back to dashboard link returns you to the main task list.
What common React mistakes should beginners avoid?
Common React mistakes beginners should avoid include changing state directly, using unstable list keys, misusing useEffect, and overcomplicating components or data flow.
| Mistake | Why it causes problems | Better approach |
| Changing state directly | React may not re-render when you change an existing array or object and reuse the same reference. You can also accidentally change data that other code still relies on | Create a new array or object with methods such as map(), filter(), or spread syntax, then pass it to the state setter |
| Using missing or unstable key values | React can associate a rendered item with the wrong data when you add, delete, or reorder list items. For example, an input value or component state can appear on the wrong task | Use a stable ID from the item data, such as task.id |
| Overusing useEffect | Using an effect to calculate values from existing state can trigger an extra render. An effect that updates one of its own dependencies can also run repeatedly and create an update loop | Calculate values during rendering and handle user actions in event handlers. Use useEffect when you need to synchronize with something outside React |
| Building oversized components | A change to one feature can require editing a file that also contains unrelated form, list, routing, or data-loading logic. This makes it harder to find where a problem starts and change one feature without affecting another | Split the component when a part of the interface has its own clear responsibility, such as a form, task list, or navigation |
| Passing props through many unused components | Every component between the data source and its destination must accept and forward the prop, even when it doesn’t use the value. Renaming or changing that prop can require edits across several files | Keep state close to the components that use it. Move shared state to their closest common parent, and consider context when deeply nested components need the same data |
| Adding advanced libraries too early | You have more APIs, configuration, and data-flow patterns to learn at the same time. This can make it unclear whether a value or behavior comes from React or the added library | Learn components, props, state, events, forms, and hooks first. Add a library when your project has a specific problem it can solve |
What should you do after building your React app?
After building your project locally, deploy your React app so other people can access it online. Once it’s live, keep improving the project as you learn new React skills.
When deploying your app, choose a reliable hosting provider such as Hostinger. The React hosting plans include a free domain for one year on annual plans.
They also include free managed SSL certificates that remain active as long as your app is hosted with Hostinger, so you can serve it securely over HTTPS.
After deployment, test the live app to make sure it works as it did on your local computer. Check the navigation, forms, task actions, API requests, and loading and error states.
Because React Router handles routes in the browser, some hosts return a 404 error when you refresh a URL such as /about. Set your host to serve index.html for all routes to fix this.
Publishing your app isn’t the end of the project. You can add persistent task storage so changes survive a refresh or improve React performance to speed up your app.
Focus on one useful improvement at a time so you can understand and test each change before adding another.
All of the tutorial content on this website is subject to
Hostinger’s rigorous editorial standards and values.
Apply for Premium Hosting
Source Credit: https://www.hostinger.com/in/tutorials/react-tutorial/
