Learn React from Scratch: A Practical Beginner Guide

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…

learn-reactjs
guide.md READY

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:

  • const and let
  • functions and arrow functions
  • objects and arrays
  • destructuring
  • spread syntax
  • map(), filter() and find()
  • template literals
  • modules using import and export
  • 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 useEffect is 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:

Share this guideLinkedInPost

ARTICLE TOOLKIT

Save or share this guide

Keep the reference nearby or send it to a teammate solving the same problem.

Share this guideLinkedInPost

QUALITY NOTE

Written from practical development experience and reviewed for clarity. Found an outdated step?

Report a correction →

Jaydip Barad

WRITTEN BY

Jaydip Barad

Senior full-stack developer sharing production-tested lessons from 14+ years of building backend systems, WordPress platforms and modern JavaScript applications.

Node.jsTypeScriptWordPressArchitecture
Previous guide
Next guide

THE PRACTICAL DEVELOPER LETTER

Get useful engineering lessons without the noise.

New tutorials, architecture notes and tools worth knowing—delivered occasionally.




    Occasional practical tutorials. Unsubscribe any time.