ai react tutorial 2026-08-30

By Xah Lee. Date: .
work in progress

write a react js tutorial

ai answer

React JS Tutorial (2026)

A practical beginner-to-intermediate guide. You will set up a modern React app with Vite, learn components, props, state, effects, and finish with a small task tracker you can run locally.

Official docs: https://react.dev


1. What React is

React is a JavaScript library for building user interfaces. You describe the UI as components (functions that return markup). When data changes, React updates only the parts of the page that need to change.

Core ideas:

React does not include routing, data fetching, or a backend. For those you add libraries or use a framework such as Next.js. This tutorial stays on client-side React so you learn the core first.


2. Prerequisites

Check your install:

node -v
npm -v

3. Create a project (Vite, not Create React App)

Create React App is deprecated. Use Vite.

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

Open the URL Vite prints (usually http://localhost:5173).

Useful scripts:

Command What it does
npm run dev Start the development server with hot reload
npm run build Production build into dist/
npm run preview Preview the production build locally

JavaScript template is used here so beginners can focus on React, not TypeScript. You can later use --template react-ts.


4. Project structure

A typical Vite + React app looks like this:

my-react-app/
├── index.html          # page shell; Vite injects your JS here
├── package.json
├── vite.config.js
└── src/
    ├── main.jsx        # mounts React onto the page
    ├── App.jsx         # root component
    ├── App.css
    └── index.css

src/main.jsx is the entry point:

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>
);

createRoot attaches React to the <div id="root"> in index.html. StrictMode helps catch mistakes in development; it does not affect production.


5. JSX

JSX looks like HTML, but it is JavaScript. A few rules:

function Greeting() {
  const name = "Ada";
  const year = new Date().getFullYear();

  return (
    <>
      <h1 className="title">Hello, {name}</h1>
      <p>The year is {year}.</p>
    </>
  );
}

JSX expressions must produce a value. You can use ternary operators and &&, but not raw if statements inside { }.


6. Components

A component is a function whose name starts with a capital letter and returns JSX.

function Header() {
  return (
    <header>
      <h1>Task Tracker</h1>
    </header>
  );
}

export default function App() {
  return (
    <div>
      <Header />
      <p>Welcome.</p>
    </div>
  );
}

Put reusable pieces in their own files and export them:

// src/Header.jsx
export default function Header() {
  return <h1>Task Tracker</h1>;
}
// src/App.jsx
import Header from "./Header.jsx";

export default function App() {
  return <Header />;
}

7. Props

Props are arguments passed to a component, like HTML attributes.

function UserCard({ name, role }) {
  return (
    <article>
      <h2>{name}</h2>
      <p>{role}</p>
    </article>
  );
}

export default function App() {
  return (
    <>
      <UserCard name="Ada Lovelace" role="Mathematician" />
      <UserCard name="Grace Hopper" role="Computer scientist" />
    </>
  );
}

Notes:

function Button({ label = "Click me" }) {
  return <button>{label}</button>;
}

8. State with useState

State is data that belongs to a component and can change over time. Changing state triggers a re-render.

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times.</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Rules:

setCount((previous) => previous + 1);

That avoids stale values when updates happen quickly.


9. Events

React events use camelCase: onClick, onChange, onSubmit.

function SearchBox() {
  const [query, setQuery] = useState("");

  function handleChange(event) {
    setQuery(event.target.value);
  }

  function handleSubmit(event) {
    event.preventDefault();
    alert(`Searching for: ${query}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={handleChange} placeholder="Search" />
      <button type="submit">Go</button>
    </form>
  );
}

event.preventDefault() stops the browser from reloading the page on form submit.


10. Conditional rendering

function Inbox({ messages }) {
  if (messages.length === 0) {
    return <p>No messages.</p>;
  }

  return (
    <p>
      You have {messages.length} {messages.length === 1 ? "message" : "messages"}.
    </p>
  );
}

Common patterns:

{isLoading && <p>Loading…</p>}
{error ? <p>{error}</p> : <List items={items} />}

Do not render 0 by accident. {count && <Badge />} will show 0 when count is 0. Prefer {count > 0 && <Badge />}.


11. Lists and keys

Render arrays with .map(). Give each item a stable unique key.

const people = [
  { id: "a1", name: "Ada" },
  { id: "b2", name: "Grace" },
];

function PeopleList() {
  return (
    <ul>
      {people.map((person) => (
        <li key={person.id}>{person.name}</li>
      ))}
    </ul>
  );
}

Keys help React match items when the list changes. Do not use the array index as a key if items can be reordered, inserted, or deleted.


12. Effects with useEffect

Use useEffect when a component needs to talk to something outside React: the browser, a timer, localStorage, or a network request.

import { useEffect, useState } from "react";

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return <p>Window width: {width}px</p>;
}

Fetching example:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setUser(data);
      });

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (!user) return <p>Loading…</p>;
  return <h2>{user.name}</h2>;
}

The cancelled flag avoids updating state after the component unmounts or after userId changes mid-request.


13. Rules of Hooks

  1. Only call Hooks at the top level of a component or custom Hook — not inside loops, conditions, or nested functions.
  2. Only call Hooks from React functions (components or custom Hooks).
// Bad
if (loggedIn) {
  const [name, setName] = useState("");
}

// Good
const [name, setName] = useState("");
if (loggedIn) {
  // use name here
}

14. Lifting state up

If two children need the same data, put the state in their closest shared parent and pass it down as props.

function FilterableList() {
  const [query, setQuery] = useState("");
  const items = ["React", "Vite", "JavaScript"];
  const visible = items.filter((item) =>
    item.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div>
      <SearchInput value={query} onChange={setQuery} />
      <Results items={visible} />
    </div>
  );
}

function SearchInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder="Filter"
    />
  );
}

function Results({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

This is the standard React data flow: state down, events up.


15. Mini project: Task Tracker

Replace src/App.jsx with the following. You can keep Vite’s CSS or use the styles at the end.

import { useEffect, useState } from "react";
import "./App.css";

const STORAGE_KEY = "react-tutorial-tasks";

function uid() {
  return crypto.randomUUID();
}

export default function App() {
  const [tasks, setTasks] = useState(() => {
    const saved = localStorage.getItem(STORAGE_KEY);
    return saved ? JSON.parse(saved) : [
      { id: uid(), text: "Learn JSX", done: true },
      { id: uid(), text: "Build a component", done: false },
    ];
  });
  const [text, setText] = useState("");
  const [filter, setFilter] = useState("all");

  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  }, [tasks]);

  function addTask(event) {
    event.preventDefault();
    const trimmed = text.trim();
    if (!trimmed) return;

    setTasks((current) => [
      { id: uid(), text: trimmed, done: false },
      ...current,
    ]);
    setText("");
  }

  function toggleTask(id) {
    setTasks((current) =>
      current.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  }

  function deleteTask(id) {
    setTasks((current) => current.filter((task) => task.id !== id));
  }

  const visible = tasks.filter((task) => {
    if (filter === "active") return !task.done;
    if (filter === "done") return task.done;
    return true;
  });

  const remaining = tasks.filter((task) => !task.done).length;

  return (
    <main className="app">
      <h1>Task Tracker</h1>
      <p className="muted">{remaining} task{remaining === 1 ? "" : "s"} left</p>

      <form onSubmit={addTask} className="row">
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Add a task"
        />
        <button type="submit">Add</button>
      </form>

      <div className="filters">
        {["all", "active", "done"].map((value) => (
          <button
            key={value}
            type="button"
            className={filter === value ? "active" : ""}
            onClick={() => setFilter(value)}
          >
            {value}
          </button>
        ))}
      </div>

      {visible.length === 0 ? (
        <p className="muted">Nothing here.</p>
      ) : (
        <ul className="list">
          {visible.map((task) => (
            <li key={task.id} className={task.done ? "done" : ""}>
              <label>
                <input
                  type="checkbox"
                  checked={task.done}
                  onChange={() => toggleTask(task.id)}
                />
                <span>{task.text}</span>
              </label>
              <button type="button" onClick={() => deleteTask(task.id)}>
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}

Optional styles for src/App.css:

.app {
  max-width: 28rem;
  margin: 3rem auto;
  font-family: system-ui, sans-serif;
}

.muted {
  color: #666;
}

.row {
  display: flex;
  gap: 0.5rem;
}

.row input {
  flex: 1;
  padding: 0.5rem;
}

.filters {
  display: flex;
  gap: 0.5rem;
  margin: 1rem 0;
}

.filters .active {
  font-weight: 700;
}

.list {
  list-style: none;
  padding: 0;
}

.list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.4rem 0;
  border-bottom: 1px solid #eee;
}

.done span {
  text-decoration: line-through;
  color: #888;
}

What this app practices:


16. How React rendering works (short version)

  1. A component function runs and returns JSX.
  2. React compares that tree with the previous one (reconciliation).
  3. It updates the DOM only where something actually changed.

That is why you treat state as immutable: React needs to see a new array or object to know something changed.

// Bad — mutates the same array
tasks.push(newTask);
setTasks(tasks);

// Good — new array
setTasks([...tasks, newTask]);

17. Common mistakes

Mistake Fix
Using Create React App for a new project Use Vite or a framework
Mutating state Copy arrays/objects, then set state
Missing key on lists Use a stable id
Calling Hooks inside if Move Hooks to the top
Forgetting preventDefault on forms The page will reload
Fetching without cleanup Ignore or abort stale responses
Using index as a key on editable lists Items get the wrong state when reordered

18. What to learn next

Once this feels comfortable:

  1. Custom Hooks — extract repeated logic (useLocalStorage, useFetch)
  2. React Router — multiple pages in a SPA
  3. Context — share data without prop-drilling (theme, auth)
  4. Forms at scaleuseActionState in React 19, or a library like React Hook Form
  5. A framework — Next.js or React Router’s framework mode when you need routing, data loading, and deployment conventions
  6. TypeScript — catch prop and state mistakes at compile time

Start here: https://react.dev/learn


If you want a follow-up, I can turn this into a TypeScript version, add React Router pages, or expand the task tracker into a multi-file app with components split out.