React becomes much easier to learn once you stop trying to memorize every hook, API and library at the same time.
I prefer learning React in layers. First understand components. Then understand how data moves through those components. After that, state, events, forms, Effects and routing start making much more sense.
That is the approach I’m going to use in this guide.
We’ll learn React from scratch by building small pieces of a task application while covering the concepts I think matter most for someone learning modern React today.
I’m also intentionally avoiding several patterns that appear in older React tutorials. We won’t start with Create React App, we won’t make class components the default, and we won’t use useEffect every time one value depends on another.
What Is React?
React is a JavaScript library for building user interfaces from reusable components.
Instead of building one large page containing all of our UI logic, React encourages us to break the interface into pieces.
Application
│
├── Header
│
├── TaskForm
│
├── TaskList
│ ├── TaskItem
│ ├── TaskItem
│ └── TaskItem
│
└── Footer
Each component can receive data, render UI and sometimes manage its own state.
This component model is the idea I would understand before worrying about Redux, Next.js, React Server Components or advanced performance optimization.
The current official React documentation is the reference I recommend keeping open while learning.
Which React Version Are We Learning?
React has changed significantly since the original version of this article was published.
The current React documentation covers React 19.2, but I don’t think a beginner should start by trying to learn every feature introduced in React 19.
The fundamentals are still more important:
- components
- JSX
- props
- state
- events
- rendering lists
- forms
- sharing state
- Effects
- custom Hooks
Once those concepts feel natural, newer APIs become much easier to understand.
What You Should Know Before React
You don’t need to be a JavaScript expert before starting React, but React will feel unnecessarily difficult if basic JavaScript syntax is still completely unfamiliar.
I would be comfortable with these JavaScript concepts first:
constandlet- functions and arrow functions
- objects and arrays
- destructuring
- spread syntax
map(),filter()andfind()- template literals
- modules using
importandexport - Promises and
async/await
You don’t need to spend weeks studying old ES5 versus ES6 terminology before starting React. Learn the JavaScript syntax you will actually use and improve your JavaScript knowledge while building things.
Create React App Is No Longer the Starting Point
Many older React tutorials begin with:
npx create-react-app my-app
I wouldn’t use that command for a new React project today.
Create React App has officially been deprecated.
React now recommends using an appropriate framework for many production applications. But if the goal is to understand React itself or build a straightforward client-side application, using a build tool such as Vite is still a practical way to learn.
That is exactly what we need here.
Set Up React with Vite
Make sure a supported Node.js version is installed, then create the project:
npm create vite@latest react-task-app -- --template react
cd react-task-app
npm install
npm run dev
Vite will display the local development URL in your terminal.
The exact Node.js requirements for Vite can change, so instead of hardcoding an old Node version into this tutorial, check the current Vite documentation if the installer reports a compatibility issue.
Understanding the Basic Project Structure
A fresh Vite React project is intentionally small.
react-task-app/
│
├── public/
│
├── src/
│ ├── assets/
│ ├── App.jsx
│ ├── App.css
│ ├── index.css
│ └── main.jsx
│
├── index.html
├── package.json
└── vite.config.js
The two files I care about first are main.jsx and App.jsx.
main.jsx connects React to the page:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
createRoot(
document.getElementById('root')
).render(
<StrictMode>
<App />
</StrictMode>
);
App.jsx is simply a React component that becomes the starting point for our application UI.
Your First React Component
A React component can be a JavaScript function that returns UI.
function Welcome() {
return <h2>Welcome to my task app</h2>;
}
export default Welcome;
We can use that component from another component:
import Welcome from './components/Welcome.jsx';
function App() {
return (
<main>
<Welcome />
</main>
);
}
export default App;
That is the basic React model:
Component
|
v
Returns UI
|
v
Can contain other components
Component Names Start with a Capital Letter
This small rule causes plenty of confusion when someone is new to React.
React treats lowercase JSX names as HTML elements:
<button>
<div>
<section>
Capitalized names represent React components:
<Welcome />
<TaskList />
<UserProfile />
So write:
function TaskList() {
// ...
}
rather than:
function taskList() {
// ...
}
What Is JSX?
JSX lets us describe UI using syntax that looks similar to HTML inside JavaScript.
function TaskTitle() {
const taskCount = 3;
return (
<section>
<h2>My Tasks</h2>
<p>You have {taskCount} tasks.</p>
</section>
);
}
The braces let us insert JavaScript expressions into JSX:
{taskCount}
{user.name}
{price * quantity}
{isAdmin ? 'Admin' : 'User'}
JSX is not a template language completely separate from JavaScript. JavaScript and UI logic are deliberately allowed to work closely together.
A Few JSX Rules Worth Knowing
JSX looks like HTML, but there are some differences.
Use className instead of class:
<div className="task-card">
Task
</div>
Self-close elements when appropriate:
<img src="/task.png" alt="Task" />
<input type="text" />
A component also needs to return one parent structure. A Fragment is useful when you don’t want an additional DOM element:
function Header() {
return (
<>
<h1>Task Manager</h1>
<p>Stay organized.</p>
</>
);
}
Props: Passing Data into Components
Components become more useful when the same component can render different data.
Props are how a parent component passes information to a child.
function Task({ title, completed }) {
return (
<article>
<h3>{title}</h3>
<p>
{completed ? 'Completed' : 'Pending'}
</p>
</article>
);
}
Now the parent can reuse it:
function App() {
return (
<main>
<Task
title="Learn React components"
completed={true}
/>
<Task
title="Learn React state"
completed={false}
/>
</main>
);
}
I think of props as function arguments for components.
Component receives props
|
v
Uses those values
|
v
Returns UI
Props Should Not Be Mutated
A component should treat its props as read-only input.
I would not do this:
function User({ user }) {
user.name = 'Changed';
return <p>{user.name}</p>;
}
If information needs to change, that normally belongs in state owned by the appropriate component.
State: Data That Changes the UI
Props come from a parent. State is information that a component remembers between renders.
The simplest way to understand state is with a counter.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>
Increase
</button>
</div>
);
}
useState(0) gives us two things:
count
Current value
setCount()
Function that requests an update
Calling the setter tells React that the component needs to render again with the new state.
State Is a Snapshot
This mental model is important because React state does not behave like changing a normal variable in the middle of a function.
Consider this:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
You might expect that to add three.
But each call sees the count value from the current render.
When the next state depends on the previous state, use the updater form:
function handleClick() {
setCount(current => current + 1);
setCount(current => current + 1);
setCount(current => current + 1);
}
Understanding this early prevents many confusing state bugs later.
Handling User Events
React uses event handlers for things users do: clicking, typing, submitting a form and so on.
function SaveButton() {
function handleSave() {
console.log('Saved');
}
return (
<button onClick={handleSave}>
Save
</button>
);
}
Notice this:
onClick={handleSave}
not:
onClick={handleSave()}
The first passes the function to React. The second executes it during rendering.
Rendering Lists with map()
Real applications rarely hardcode every component individually.
Suppose our tasks look like this:
const tasks = [
{
id: 1,
title: 'Learn components',
completed: true
},
{
id: 2,
title: 'Learn state',
completed: false
},
{
id: 3,
title: 'Build a project',
completed: false
}
];
We can render them using map():
function TaskList({ tasks }) {
return (
<div>
{tasks.map(task => (
<Task
key={task.id}
title={task.title}
completed={task.completed}
/>
))}
</div>
);
}
Why React Needs Keys
The key helps React identify which item corresponds to which rendered component as lists change.
If your data already has a stable database ID, that is normally an excellent key:
key={task.id}
I avoid creating a random key during rendering:
key={Math.random()}
That gives React a different identity on every render and defeats the purpose of the key.
Updating Arrays in State
React state should normally be treated as immutable.
Instead of modifying an existing array:
tasks.push(newTask);
create a new array:
setTasks(currentTasks => [
...currentTasks,
newTask
]);
To remove a task:
setTasks(currentTasks =>
currentTasks.filter(
task => task.id !== taskId
)
);
To update one task:
setTasks(currentTasks =>
currentTasks.map(task =>
task.id === taskId
? {
...task,
completed: !task.completed
}
: task
)
);
map(), filter() and spread syntax appear constantly in React because they make it easy to create updated values without mutating the previous state.
Controlled Forms
A common React form pattern is to let state control the input value.
import { useState } from 'react';
function TaskForm({ onAddTask }) {
const [title, setTitle] = useState('');
function handleSubmit(event) {
event.preventDefault();
const trimmedTitle = title.trim();
if (!trimmedTitle) {
return;
}
onAddTask(trimmedTitle);
setTitle('');
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={event =>
setTitle(event.target.value)
}
placeholder="Enter a task"
/>
<button type="submit">
Add task
</button>
</form>
);
}
The flow is easy to follow:
User types
|
v
onChange
|
v
setTitle()
|
v
React renders
|
v
input displays title
Build the Task App
Now we can combine what we’ve learned into one small application.
import { useState } from 'react';
import TaskForm from './components/TaskForm.jsx';
import TaskList from './components/TaskList.jsx';
function App() {
const [tasks, setTasks] = useState([
{
id: 1,
title: 'Learn React state',
completed: false
}
]);
function addTask(title) {
const newTask = {
id: crypto.randomUUID(),
title,
completed: false
};
setTasks(current => [
...current,
newTask
]);
}
function toggleTask(taskId) {
setTasks(current =>
current.map(task =>
task.id === taskId
? {
...task,
completed: !task.completed
}
: task
)
);
}
function deleteTask(taskId) {
setTasks(current =>
current.filter(
task => task.id !== taskId
)
);
}
return (
<main>
<h1>Task Manager</h1>
<TaskForm
onAddTask={addTask}
/>
<TaskList
tasks={tasks}
onToggleTask={toggleTask}
onDeleteTask={deleteTask}
/>
</main>
);
}
export default App;
There is an important architectural idea hiding inside this example.
The App component owns the tasks because both the form and list need to work with them.
Lifting State Up
When two components need the same state, I usually look for their closest common parent.
App
|
owns tasks
/ \
v v
TaskForm TaskList
|
v
TaskItem
The data moves down through props.
Actions can move back up through callback props such as:
onAddTask
onToggleTask
onDeleteTask
This predictable one-way data flow is one of the most useful React concepts to understand.
Don’t Store Derived Values in State
Suppose we want to display how many tasks remain incomplete.
I would not create another state variable and keep it synchronized manually:
const [remaining, setRemaining] = useState(0);
We already have the information needed to calculate it.
const remaining = tasks.filter(
task => !task.completed
).length;
If a value can be calculated from existing props or state during rendering, I normally calculate it rather than creating a second piece of state that can fall out of sync.
Understanding useEffect Correctly
useEffect is one of the most misunderstood React Hooks.
I wouldn’t teach it as:
“Run some code whenever something changes.”
A better mental model is:
An Effect synchronizes a React component with something outside React.
Examples include:
- browser APIs
- network connections
- timers
- third-party widgets
- subscriptions
The React documentation explicitly describes Effects as an escape hatch for synchronizing with external systems.
You can read the current explanation in You Might Not Need an Effect.
An Example Where useEffect Makes Sense
Suppose we want the browser tab title to reflect the number of unfinished tasks.
import { useEffect } from 'react';
function App() {
// ...
const remaining = tasks.filter(
task => !task.completed
).length;
useEffect(() => {
document.title =
`${remaining} tasks remaining`;
}, [remaining]);
// ...
}
Here React is synchronizing with the browser’s document API, which is external to React.
An Example Where You Probably Don’t Need useEffect
This is unnecessary:
const [remaining, setRemaining] = useState(0);
useEffect(() => {
setRemaining(
tasks.filter(
task => !task.completed
).length
);
}, [tasks]);
Nothing external is being synchronized.
We can simply write:
const remaining = tasks.filter(
task => !task.completed
).length;
Less code, fewer renders and fewer opportunities for state to become inconsistent.
Fetching API Data
For a simple client-side application, you may still fetch data inside an Effect.
For example:
import {
useEffect,
useState
} from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const controller =
new AbortController();
async function loadUsers() {
try {
const response = await fetch(
'/api/users',
{
signal: controller.signal
}
);
if (!response.ok) {
throw new Error(
`Request failed: ${response.status}`
);
}
const data = await response.json();
setUsers(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(
'Unable to load users'
);
}
} finally {
setLoading(false);
}
}
loadUsers();
return () => {
controller.abort();
};
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>{error}</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
The cleanup matters because a request may still be running when the component is removed or when another request replaces it.
For larger applications, I wouldn’t automatically build an entire data layer from manual fetch() calls inside Effects. Modern frameworks and data libraries can provide routing-aware loading, caching, pending states and request coordination.
Reusable Logic with Custom Hooks
Custom Hooks let us extract reusable React logic without copying it between components.
Suppose several components need online status.
import {
useSyncExternalStore
} from 'react';
function subscribe(callback) {
window.addEventListener(
'online',
callback
);
window.addEventListener(
'offline',
callback
);
return () => {
window.removeEventListener(
'online',
callback
);
window.removeEventListener(
'offline',
callback
);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
}
Now a component can simply use:
function ConnectionStatus() {
const isOnline = useOnlineStatus();
return (
<p>
{isOnline
? 'Online'
: 'Offline'}
</p>
);
}
A custom Hook doesn’t create shared state by itself. It shares the logic used to work with that state or external system.
If you want a deeper reference after learning the basics, see my React Hooks Cheat Sheet.
When State Needs to Be Shared More Widely
Passing props through one or two component levels is normal.
When information genuinely belongs to a large part of the component tree, Context may become useful.
App
|
+--- Header
| |
| +--- UserMenu
|
+--- Dashboard
|
+--- Account
|
+--- Settings
All need current user
Examples of values commonly considered for Context include:
- authenticated user information
- theme
- locale
- application-level configuration
I wouldn’t move every piece of state into Context just because passing props feels repetitive. Local state is easier to understand when the information really is local.
When useReducer Becomes Useful
useState is enough for a lot of components.
If state transitions become complicated, useReducer can make those transitions more explicit.
function taskReducer(
tasks,
action
) {
switch (action.type) {
case 'added':
return [
...tasks,
action.task
];
case 'deleted':
return tasks.filter(
task =>
task.id !== action.id
);
case 'toggled':
return tasks.map(task =>
task.id === action.id
? {
...task,
completed:
!task.completed
}
: task
);
default:
throw new Error(
`Unknown action: ${action.type}`
);
}
}
I wouldn’t reach for a reducer because an application has three state variables. I consider it when several updates belong to the same domain and explicit actions make the state transitions easier to follow.
Do You Need Redux?
Not when you’re starting React.
Learn React state first.
Understand props, state ownership, lifting state, Context and reducers before adding an external state library.
Redux becomes useful for certain applications, but adding it before understanding React’s own state model usually makes the learning process harder.
When you reach the point where Redux solves a real problem, use the modern Redux Toolkit approach rather than old Redux boilerplate. I cover that in my Redux Toolkit tutorial.
Adding Routing
Once an application has multiple screens, you’ll probably want URLs such as:
/
/tasks
/tasks/123
/settings
React itself doesn’t provide a client-side router.
React Router is one common option.
The current declarative React Router setup can be installed with:
npm install react-router
Then configure routes:
import {
BrowserRouter,
Routes,
Route
} from 'react-router';
import Home from './pages/Home.jsx';
import Tasks from './pages/Tasks.jsx';
import Settings from './pages/Settings.jsx';
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/tasks"
element={<Tasks />}
/>
<Route
path="/settings"
element={<Settings />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
Use Link for normal in-app navigation:
import { Link } from 'react-router';
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/tasks">
Tasks
</Link>
<Link to="/settings">
Settings
</Link>
</nav>
);
}
React Router now offers several modes ranging from basic declarative routing through data and full framework features. You can see the current APIs in the official React Router documentation.
React Alone or a Framework?
After learning these fundamentals, you’ll eventually need to decide whether your project should remain a client-side React application or use a React framework.
I would consider React with Vite for things such as internal dashboards, authenticated portals and applications that already have a separate backend.
I would start evaluating a framework when I need features such as:
- server rendering
- server-side data access
- integrated routing and data loading
- SEO-focused public pages
- server components
- full-stack application features
I go deeper into that decision in my React vs Next.js guide.
Class Components: Should Beginners Still Learn Them?
You will still encounter class components in older React applications.
class Counter extends React.Component {
state = {
count: 0
};
render() {
return (
<p>
{this.state.count}
</p>
);
}
}
So I wouldn’t pretend they don’t exist.
But I also wouldn’t make them the foundation of a modern beginner tutorial.
I would learn functional components and Hooks first. If you join a project containing older class components later, learn enough of the class lifecycle model to maintain that codebase.
A Project Structure I Would Start With
I wouldn’t create thirty directories before the application has thirty things to organize.
For our small project, this is enough:
src/
│
├── components/
│ ├── TaskForm.jsx
│ ├── TaskItem.jsx
│ └── TaskList.jsx
│
├── hooks/
│ └── useOnlineStatus.js
│
├── pages/
│ ├── Home.jsx
│ ├── Tasks.jsx
│ └── Settings.jsx
│
├── App.jsx
├── main.jsx
└── index.css
As the application becomes larger, I often prefer grouping functionality by feature instead of creating giant global folders for every type of file.
src/
│
├── features/
│ ├── tasks/
│ │ ├── TaskForm.jsx
│ │ ├── TaskList.jsx
│ │ └── taskApi.js
│ │
│ └── account/
│ ├── Profile.jsx
│ └── accountApi.js
│
├── shared/
│ └── components/
│
└── App.jsx
There isn’t one perfect folder structure. I choose a structure that makes related code easy to find.
Common React Mistakes I Would Avoid While Learning
1. Putting Everything in App.jsx
A 700-line App.jsx makes it much harder to understand the component model React is trying to give you.
2. Using useEffect for Derived State
If something can be calculated during rendering, calculate it there first.
3. Mutating State
Prefer producing a new array or object rather than changing the existing state value.
4. Using Array Indexes as Every Key
If list items can be inserted, removed or reordered, use a stable ID when you have one.
5. Adding Redux Immediately
Understand React state before solving state-management problems you don’t have yet.
6. Memorizing Hooks Instead of Understanding State
You don’t need to memorize every hook before building your first application.
7. Copying Old Create React App Tutorials
React’s tooling recommendations have changed. Check current documentation before assuming a setup guide from several years ago is still the right starting point.
8. Optimizing Everything Too Early
You don’t need useMemo, useCallback and memoization around every component simply because those APIs exist.
Build a clear application first. Optimize when you understand what actually needs optimization.
Building the Production Version
For our Vite application, create a production build with:
npm run build
Vite generates the production assets in:
dist/
You can preview that build locally:
npm run preview
If the application is a client-side SPA using routes such as /tasks and /settings, remember that your web server or hosting provider normally needs to fall back to index.html for those application routes.
Otherwise refreshing /tasks may produce a server 404 even though client-side navigation works correctly.
What I Would Learn After This Guide
Once the concepts in this article feel comfortable, I would learn in roughly this order:
Components + JSX
↓
Props
↓
State
↓
Events + Forms
↓
Lists + Keys
↓
Lifting State
↓
Effects
↓
Custom Hooks
↓
Context + Reducers
↓
Routing
↓
API / Data patterns
↓
Testing
↓
Frameworks
↓
Advanced performance
I like this order because each topic solves a problem introduced by the previous stage.
It is much easier than learning useMemo, Redux, Server Components and twenty libraries before you are comfortable passing a prop from one component to another.
A Practical React Learning Checklist
If you can do the following without copying every line from a tutorial, I would say your React fundamentals are becoming solid:
- Create and reuse a component.
- Pass data using props.
- Manage local state with
useState. - Handle button, input and form events.
- Render a list from an array.
- Update arrays and objects without mutation.
- Decide which component should own state.
- Recognize derived data that doesn’t need state.
- Explain when
useEffectis actually needed. - Fetch data and handle loading and failure states.
- Create basic routes.
- Extract reusable logic into a custom Hook.
- Create and deploy a production build.
Once you can do those things, building larger projects is mostly a matter of combining the same ideas at a larger scale.
What I Would Build for Practice
Watching React tutorials is useful for getting started, but most of the learning happens when you have to decide how something should work without being shown the next line.
After this task manager, I would try projects such as:
- a notes application
- a product search interface
- a small expense tracker
- a dashboard using a public API
- a multi-page admin interface with routing
For each project, add one new problem rather than ten new libraries.
For example:
Project 1
Local state
Project 2
Forms + validation
Project 3
API fetching
Project 4
Routing
Project 5
Authentication + shared state
That gives you a reason to learn each concept rather than collecting APIs without understanding when you need them.
Final Thoughts
React has a large ecosystem, and that can make it look more complicated than it needs to be when you’re starting.
You don’t need to understand every React 19 feature, Redux, Next.js, Server Components, React Router, performance optimization and testing on day one.
Start with the component model.
Understand how props move data down, how state changes the UI, how events trigger updates and how React re-renders from those values.
Then learn Effects as a way to synchronize with systems outside React rather than treating them as a general-purpose place to put logic.
Once those ideas make sense, the rest of the React ecosystem becomes much easier to evaluate.
My advice when learning React is simple: build small things, understand why they work, and add complexity only when the application gives you a reason.
Continue Learning
Once you’re comfortable with the fundamentals, these guides are a good next step:




