React turns 12 in 2026 and it is still the gravitational center of front-end development. The library crossed more than 130 million weekly npm downloads in June 2026 (npm registry data) and the facebook/react repository holds over 245,000 GitHub stars, making it one of the most-depended-upon packages on the internet. Yet for newcomers, the React ecosystem of 2026 looks nothing like the tutorials still floating around from 2021. Create React App is gone, class components are a historical footnote, and React 19 introduced a batch of new primitives that change how forms, data fetching, and metadata are handled.
This React tutorial is a complete, hands-on, project-based guide written for June 2026. Instead of disconnected snippets, you will build one real, working application from an empty folder to a polished, deployable single-page app: a media search tool that queries a live public API, manages loading and error states, debounces search input, and persists favorites to the browser. Along the way you will learn the modern React fundamentals every professional uses daily – components, JSX, props, state, hooks, effects, and the React 19 features that are now standard.
Everything below uses React 19.2 (the current stable line, with patch 19.2.7 shipping in June 2026) scaffolded with Vite, which is the officially recommended way to start a React project now that Create React App has been sunset. No prior React experience is required; you only need to be comfortable with basic JavaScript. By the end you will have a genuinely useful app and the mental model to keep building. Let’s get into it.
Why Learn React in 2026?
Before writing a line of code, it is worth understanding why React remains the default choice for so many teams – and why this React tutorial focuses on it over the alternatives. React is a JavaScript library for building user interfaces out of components: small, reusable, self-contained pieces of UI that describe what the screen should look like for a given state. When the state changes, React efficiently re-renders only what changed. That declarative model – you describe the destination, not the step-by-step DOM manipulation – is what made React dominant and what every modern framework has since imitated.
The ecosystem signals are unambiguous. React is consistently among the top two most-used web technologies in Stack Overflow’s 2025 Developer Survey, it backs production apps at Meta, Netflix, Airbnb, and Shopify, and it is the foundation under meta-frameworks like Next.js and Remix. Learning React is not just learning a library – it is learning the vocabulary (components, props, hooks, state) that nearly every front-end job posting now assumes. If you can build with React, the jump to React Native for mobile or Next.js for full-stack work is incremental rather than a fresh start.
React 19, the major release that shipped stable on December 5, 2024, also moved the library forward in ways that matter for beginners. New hooks such as useActionState and useOptimistic bake common patterns (form submission state, optimistic UI) directly into React, the use() API simplifies reading async values, and components can now render document metadata like <title> and <meta> tags natively. The experimental React Compiler promises to remove much of the manual memoization (useMemo, useCallback) that historically tripped up learners. In short: 2026 is the easiest time yet to learn React, provided you learn the modern version. That is exactly what this guide teaches.
Prerequisites and Required Versions
A clean toolchain prevents most beginner frustration. This React tutorial was tested in June 2026 against the exact versions in the table below. You do not need to match every patch number, but you do need a current Node.js LTS, because Vite’s build tooling depends on it.
| Tool | Version used (June 2026) | Why it matters |
|---|---|---|
| Node.js | 22 LTS (20+ minimum) | Runs Vite’s dev server and build. Use an active LTS line. |
| npm | 10.x (bundled with Node) | Installs dependencies. Yarn or pnpm also work. |
| React | 19.2.7 | The stable major line; includes all React 19 features. |
| react-dom | 19.2.7 | Renders React to the browser DOM. Must match React’s version. |
| Vite | 8.x | Scaffolds and serves the project; replaces Create React App. |
| VS Code | Latest | Recommended editor with the best React extension support. |
You should already understand JavaScript basics: variables (const/let), functions and arrow functions, arrays and the .map() / .filter() methods, objects and destructuring, template literals, and the async/await syntax for promises. React leans heavily on these, especially array .map() for rendering lists and destructuring for reading props. If any of those feel shaky, spend an hour on them first – it will pay off immediately.
Install two browser extensions before you start: the React Developer Tools for Chrome, Firefox, or Edge, which lets you inspect the component tree, props, and state live; and in VS Code, the ES7+ React/Redux snippets and Prettier extensions for autoformatting. Confirm Node is installed by opening a terminal and running node --version – you should see v22.x.x or any v20+ number. If you see “command not found,” install Node from the official site first. The free public API we will consume (TVMaze) requires no key, no signup, and no credit card, which keeps this React tutorial entirely self-contained.
Step 1: Scaffold a React Project with Vite
Because Create React App was officially deprecated on February 14, 2025 in the React team’s “Sunsetting Create React App” announcement, every fresh React project in 2026 should start with a build tool like Vite. Vite (French for “fast”) gives you instant dev server startup, near-instant hot module replacement, and an optimized production build with almost no configuration. Open your terminal, navigate to where you keep projects, and run the scaffolder:
# Create a new React + JavaScript project with Vite
npm create vite@latest react-show-finder -- --template react
# Move into the project and install dependencies
cd react-show-finder
npm install
# Start the development server
npm run dev
The --template react flag selects the React + JavaScript starter. If you prefer TypeScript (recommended for larger projects), use --template react-ts instead; the concepts in this React tutorial are identical, only with type annotations added. After npm run dev finishes, Vite prints a local URL – typically http://localhost:5173/. Open it in your browser and you should see the spinning Vite + React logo starter page.
VITE v8.0.16 ready in 312 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
Take a moment to explore the generated structure. The important files are index.html (the single HTML page, with a <div id="root"> mount point), src/main.jsx (the entry point that mounts React into that root), src/App.jsx (your top-level component), and package.json (dependencies and scripts). Unlike old Create React App projects, the index.html lives at the project root and loads main.jsx as an ES module directly – that is part of why Vite is so fast.
Pitfall #1 – wrong Node version. If npm create vite@latest errors with a message about an unsupported engine, your Node.js is too old. Vite 8 requires a modern LTS (Node 20 or newer). Run node --version, and if it is below 20, install the current LTS and try again. This single mistake causes more “it won’t even start” support questions than anything else for beginners.
Step 2: Understand Components, JSX, and the Entry Point
Open src/main.jsx. This is where React attaches itself to the page. In React 19 the entry point uses createRoot from react-dom/client, wraps your app in <StrictMode> (which surfaces bugs during development by intentionally double-invoking certain functions), and renders your top-level <App /> component into the DOM node with id root:
// src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Now look at App.jsx. A React component is just a JavaScript function whose name starts with a capital letter and which returns markup. That markup looks like HTML but is actually JSX – a syntax extension that lets you write UI inside JavaScript. JSX is not a string and not HTML; it compiles to React.createElement calls. Replace the contents of App.jsx with a minimal component so you start from a clean slate:
// src/App.jsx
function App() {
const appName = 'Show Finder'
return (
<div className="app">
<h1>{appName}</h1>
<p>Search thousands of TV shows and films.</p>
</div>
)
}
export default App
Three JSX rules trip up every beginner. First, use className instead of class, because class is a reserved JavaScript keyword. Second, a component must return a single root element – wrap siblings in a parent <div> or an empty <>...</> fragment. Third, embed JavaScript expressions with curly braces { }, as with {appName} above. You can put any expression inside braces – a variable, a function call, a ternary – but not a statement like an if or a for loop. Save the file and the browser updates instantly thanks to hot module replacement; you should see your “Show Finder” heading.
Step 3: Pass Data with Props and Render Lists
Props (short for “properties”) are how a parent component passes data down to a child. They are read-only: a child must never modify its own props. This one-way data flow – data flows down, events flow up – is the core of React’s predictability. Let’s create a reusable ShowCard component that displays a single show, then render several of them. Create a new folder src/components and a file ShowCard.jsx:
// src/components/ShowCard.jsx
function ShowCard({ name, genres, rating, image }) {
return (
<article className="card">
{image && <img src={image} alt={name} width="120" />}
<h3>{name}</h3>
<p>{genres.join(', ') || 'Unknown genre'}</p>
<span className="rating">
⭐ {rating ? rating.toFixed(1) : 'N/A'}
</span>
</article>
)
}
export default ShowCard
Notice the function signature destructures props directly: { name, genres, rating, image } instead of props. That is the idiomatic 2026 style. To render a list of shows, use JavaScript’s array .map() method to transform an array of data objects into an array of JSX elements. The critical detail – and the source of countless bugs – is the key prop. Every element in a mapped list needs a stable, unique key so React can track items efficiently across re-renders. Update App.jsx to render a hard-coded list for now:
// src/App.jsx
import ShowCard from './components/ShowCard'
const SAMPLE = [
{ id: 1, name: 'Stranger Things', genres: ['Drama', 'Sci-Fi'], rating: 8.4 },
{ id: 2, name: 'The Bear', genres: ['Comedy', 'Drama'], rating: 8.6 },
{ id: 3, name: 'Severance', genres: ['Sci-Fi', 'Thriller'], rating: 8.7 },
]
function App() {
return (
<div className="app">
<h1>Show Finder</h1>
<div className="grid">
{SAMPLE.map((show) => (
<ShowCard
key={show.id}
name={show.name}
genres={show.genres}
rating={show.rating}
/>
))}
</div>
</div>
)
}
export default App
Pitfall #2 – using the array index as a key. A tempting shortcut is key={index}, but it causes subtle, hard-to-debug rendering glitches whenever the list is reordered, filtered, or items are inserted. Always use a stable unique identifier from your data (here, show.id). Reserve index keys only for static lists that never change order.
Step 4: Add Interactivity with the useState Hook
Static UI is only half of React. State is data that changes over time and, when it changes, triggers a re-render so the UI stays in sync. The useState hook is the foundation of every React app. It returns a pair: the current value and a setter function to update it. The naming convention is [thing, setThing]. Let’s add a search input whose value is controlled by React – a “controlled component,” where state is the single source of truth for the input’s value:
// src/App.jsx (excerpt)
import { useState } from 'react'
function App() {
const [query, setQuery] = useState('')
return (
<div className="app">
<h1>Show Finder</h1>
<input
type="text"
placeholder="Search for a show..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p>You typed: {query}</p>
</div>
)
}
Type in the box and watch the “You typed” paragraph update on every keystroke. That round trip – event fires, setQuery updates state, component re-renders with the new value – is the heartbeat of React. The onChange handler reads e.target.value and pushes it into state; because value={query}, the input always displays exactly what state holds.
Pitfall #3 – mutating state directly. Never write query = 'new value' or push into a state array with list.push(item). React detects changes by comparing references, so you must always create a new value and pass it to the setter. For arrays, use setList([...list, item]); for objects, setUser({ ...user, name: 'Sam' }). Direct mutation leaves the UI stale because React never sees that anything changed. When new state depends on old state, prefer the updater form: setCount((c) => c + 1), which is safe even when multiple updates are batched together.
Step 5: Fetch Live Data with the useEffect Hook
Real apps talk to servers. The useEffect hook lets a component run side effects – data fetching, subscriptions, manual DOM work – after it renders. We will call the free TVMaze API (no key required) to fetch shows. The classic pattern fetches once when the component first mounts by passing an empty dependency array []. Here is a self-contained example that loads a default set of shows on startup:
// src/App.jsx (data-fetching version)
import { useState, useEffect } from 'react'
import ShowCard from './components/ShowCard'
function App() {
const [shows, setShows] = useState([])
useEffect(() => {
async function loadShows() {
const res = await fetch('https://api.tvmaze.com/search/shows?q=star')
const data = await res.json()
setShows(data.map((item) => item.show))
}
loadShows()
}, []) // empty array = run once on mount
return (
<div className="app">
<h1>Show Finder</h1>
<div className="grid">
{shows.map((show) => (
<ShowCard
key={show.id}
name={show.name}
genres={show.genres}
rating={show.rating?.average}
image={show.image?.medium}
/>
))}
</div>
</div>
)
}
export default App
The dependency array is the most important and most misunderstood part of useEffect. It tells React when to re-run the effect: [] means “only once, after the first render”; [query] means “re-run whenever query changes”; and omitting the array entirely means “run after every render,” which usually creates an infinite loop when the effect also sets state. Note the optional-chaining operators (show.rating?.average, show.image?.medium) – TVMaze sometimes returns shows with no rating or image, and ?. safely returns undefined instead of throwing.
Pitfall #4 – infinite re-render loops. If you call a state setter inside useEffect without a correct dependency array, the setter triggers a re-render, which re-runs the effect, which calls the setter again – forever, freezing the browser tab. The fix is almost always supplying the right dependencies. In React 19’s StrictMode you will also notice effects run twice in development; that is intentional and only happens in dev, designed to expose effects that aren’t safely repeatable.
Step 6: Handle Loading and Error States Gracefully
A fetch can be slow or fail. Production-quality React always tracks three states: loading, error, and success. We model this with two extra pieces of state and conditional rendering. This is one of the most important real-world patterns in any React tutorial, because users notice a frozen blank screen immediately. Refactor the effect to set and clear these flags:
// src/App.jsx (with loading + error handling)
const [shows, setShows] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
async function loadShows() {
try {
setIsLoading(true)
setError(null)
const res = await fetch('https://api.tvmaze.com/search/shows?q=star')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setShows(data.map((item) => item.show))
} catch (err) {
setError('Could not load shows. Please try again.')
} finally {
setIsLoading(false)
}
}
loadShows()
}, [])
Now render different UI for each state using conditional rendering. React has several idioms: the logical && operator for “show this only if true,” and the ternary ? : for either/or. Combine them so the user always sees something meaningful:
return (
<div className="app">
<h1>Show Finder</h1>
{isLoading && <p className="status">Loading shows…</p>}
{error && <p className="status error">{error}</p>}
{!isLoading && !error && (
<div className="grid">
{shows.map((show) => (
<ShowCard key={show.id} name={show.name}
genres={show.genres} rating={show.rating?.average}
image={show.image?.medium} />
))}
</div>
)}
</div>
)
Pitfall #5 – the falsy zero trap. When using && for conditional rendering, never put a number on the left side: {shows.length && <List />} will literally render the text 0 when the array is empty, because 0 is falsy but still a renderable value. Use an explicit comparison instead – {shows.length > 0 && <List />} – or a ternary. This bug appears constantly in beginner code reviews.
Step 7: Build a Search Form and Debounce It
Let’s wire the search box to the API so typing actually searches. The naive approach – fetching on every keystroke – hammers the API and produces a flickering, race-condition-prone UI. The professional fix is debouncing: wait until the user stops typing for a moment before firing the request. We implement this with useEffect, a setTimeout, and a cleanup function that cancels the pending timer on each new keystroke:
// Debounced search effect
const [query, setQuery] = useState('star')
useEffect(() => {
if (!query.trim()) return
const timer = setTimeout(async () => {
try {
setIsLoading(true)
setError(null)
const url = `https://api.tvmaze.com/search/shows?q=${encodeURIComponent(query)}`
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setShows(data.map((item) => item.show))
} catch {
setError('Search failed. Try a different term.')
} finally {
setIsLoading(false)
}
}, 400) // wait 400ms after the last keystroke
return () => clearTimeout(timer) // cleanup cancels the old timer
}, [query])
The returned cleanup function is the secret. Every time query changes, React first runs the cleanup from the previous effect (clearing the old timer) and then runs the new effect (starting a fresh timer). The result: only the timer from your last keystroke survives to actually fetch. Always remember to encodeURIComponent user input before putting it in a URL – without it, searches containing spaces or special characters break. This same cleanup pattern is how you cancel subscriptions, remove event listeners, and abort fetches in real apps.
Step 8: Extract Logic into a Custom Hook
As App.jsx grows, fetching logic clutters the component. Custom hooks let you extract stateful logic into a reusable function whose name starts with use. This is one of React’s most powerful patterns and a frequent topic in any serious React tutorial. Create src/hooks/useShowSearch.js and move the search logic there:
// src/hooks/useShowSearch.js
import { useState, useEffect } from 'react'
export function useShowSearch(query) {
const [shows, setShows] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!query.trim()) {
setShows([])
return
}
const timer = setTimeout(async () => {
try {
setIsLoading(true)
setError(null)
const url = `https://api.tvmaze.com/search/shows?q=${encodeURIComponent(query)}`
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setShows(data.map((item) => item.show))
} catch {
setError('Search failed. Try a different term.')
} finally {
setIsLoading(false)
}
}, 400)
return () => clearTimeout(timer)
}, [query])
return { shows, isLoading, error }
}
Now your component becomes dramatically cleaner – it just declares state for the query and consumes the hook. This separation of concerns (UI in the component, data logic in the hook) scales beautifully and makes the logic testable and reusable across components:
// src/App.jsx (using the custom hook)
import { useState } from 'react'
import { useShowSearch } from './hooks/useShowSearch'
import ShowCard from './components/ShowCard'
function App() {
const [query, setQuery] = useState('star')
const { shows, isLoading, error } = useShowSearch(query)
return (
<div className="app">
<h1>Show Finder</h1>
<input value={query} placeholder="Search for a show..."
onChange={(e) => setQuery(e.target.value)} />
{isLoading && <p className="status">Loading…</p>}
{error && <p className="status error">{error}</p>}
{!isLoading && !error && (
<div className="grid">
{shows.map((s) => (
<ShowCard key={s.id} name={s.name} genres={s.genres}
rating={s.rating?.average} image={s.image?.medium} />
))}
</div>
)}
</div>
)
}
export default App
Step 9: Share State Globally with useContext
Passing props through many layers of components (“prop drilling”) gets tedious. The useContext hook, paired with createContext, lets you share values – a theme, the current user, a favorites list – with any descendant without threading props through every level. Let’s build a favorites feature with context. Create src/context/FavoritesContext.jsx:
// src/context/FavoritesContext.jsx
import { createContext, useContext, useState } from 'react'
const FavoritesContext = createContext(null)
export function FavoritesProvider({ children }) {
const [favorites, setFavorites] = useState([])
function toggleFavorite(show) {
setFavorites((prev) =>
prev.some((f) => f.id === show.id)
? prev.filter((f) => f.id !== show.id)
: [...prev, show]
)
}
return (
<FavoritesContext.Provider value={{ favorites, toggleFavorite }}>
{children}
</FavoritesContext.Provider>
)
}
// Custom hook for ergonomic access
export function useFavorites() {
return useContext(FavoritesContext)
}
Wrap your app in the provider inside main.jsx (or at the top of App), then any component can call useFavorites() to read the list or toggle an item. The toggleFavorite function shows immutable update patterns in action: filter to remove, spread [...prev, show] to add – never mutating prev directly. This is the same architecture, scaled down, that powers global state in production React apps before teams reach for heavier libraries like Redux or Zustand.
Step 10: Persist Favorites to localStorage
Favorites should survive a page refresh. The browser’s localStorage persists string data, and we sync it with React state using two effects: one to load on mount, one to save whenever favorites change. Update the provider:
// Inside FavoritesProvider
const [favorites, setFavorites] = useState(() => {
// Lazy initializer: read localStorage only once, on first render
const saved = localStorage.getItem('favorites')
return saved ? JSON.parse(saved) : []
})
useEffect(() => {
localStorage.setItem('favorites', JSON.stringify(favorites))
}, [favorites]) // save whenever the list changes
The lazy initializer – passing a function to useState instead of a value – is an important optimization: the function runs only on the first render, so we read and parse localStorage exactly once rather than on every render. The second effect serializes the array to JSON and writes it whenever favorites changes. Refresh the page and your favorites are still there. Wrap JSON.parse in a try/catch in production code, because corrupted storage data would otherwise crash the initial render.
Pitfall #6 – stale closures. A subtle bug occurs when an effect or event handler “captures” an old value of state because the dependency array is wrong. If a callback inside useEffect references favorites but you forget to list it as a dependency, the callback keeps seeing the value from when the effect first ran. The fixes are: list every reactive value the effect uses in the dependency array, or use the functional updater form of setters (setFavorites((prev) => ...)) so you never need to read the current value directly. The React Compiler and the eslint-plugin-react-hooks linter both help catch these automatically.
Step 11: Use React 19 Features – Actions and useActionState
React 19 introduced Actions, a built-in pattern for handling async operations triggered by forms, with automatic pending, error, and optimistic state. The new useActionState hook removes the boilerplate of manually tracking “is this form submitting?” Suppose we add a small form that lets users submit a rating note. With useActionState, React manages the pending state for you:
// src/components/NoteForm.jsx (React 19 Actions)
import { useActionState } from 'react'
function NoteForm() {
const [state, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const note = formData.get('note')
// Simulate an async save (e.g. to an API)
await new Promise((r) => setTimeout(r, 800))
if (!note) return { error: 'Note cannot be empty' }
return { message: `Saved: ${note}` }
},
{ message: '' }
)
return (
<form action={submitAction}>
<input name="note" placeholder="Add a note..." />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving…' : 'Save'}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.message && <p>{state.message}</p>}
</form>
)
}
export default NoteForm
Notice there is no manual useState for isPending – React derives it automatically from the action’s lifecycle, disabling the button while the async function runs. The form’s action prop now accepts a function directly. This is a meaningful simplification over the pre-19 pattern, where you tracked loading state by hand. React 19 also adds useOptimistic for showing instant UI updates before the server confirms, and the use() hook for reading promises and context conditionally – both worth exploring once these fundamentals click.
Step 12: Style the App and Add Document Metadata
Let’s make the app presentable. Vite supports plain CSS out of the box; create or edit src/index.css. A simple responsive grid and card styling go a long way:
/* src/index.css */
:root { font-family: system-ui, sans-serif; }
body { margin: 0; background: #0f1117; color: #e6e6e6; }
.app { max-width: 960px; margin: 0 auto; padding: 2rem; }
input { width: 100%; padding: 0.75rem; font-size: 1rem;
border-radius: 8px; border: 1px solid #333; background: #1a1d27;
color: #fff; margin-bottom: 1.5rem; }
.grid { display: grid; gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.card { background: #1a1d27; border-radius: 12px; padding: 1rem;
text-align: center; }
.card img { border-radius: 8px; }
.status { text-align: center; color: #888; }
.error { color: #ff6b6b; }
React 19 lets you render document metadata directly from any component – no extra library needed. Tags like <title> and <meta> placed inside a component are automatically hoisted to the document <head>. This is a genuinely new capability that earlier React versions lacked:
// Inside App, React 19 hoists these into <head> automatically
function App() {
return (
<div className="app">
<title>Show Finder – Search TV & Film</title>
<meta name="description" content="Search thousands of shows." />
<h1>Show Finder</h1>
{/* ...rest of the app... */}
</div>
)
}
For larger projects, consider a utility-first CSS framework – our Tailwind CSS tutorial pairs perfectly with React and is the most popular styling approach in the 2026 ecosystem. CSS Modules (.module.css files) are another built-in Vite option that scopes class names to a single component, preventing global style collisions.
Step 13: Build for Production and Deploy
Your app is feature-complete. The final step is producing an optimized build and shipping it. Vite bundles, minifies, tree-shakes, and hashes your assets with a single command:
# Create an optimized production build in /dist
npm run build
# Preview the production build locally before deploying
npm run preview
The output lands in a dist/ folder containing static HTML, CSS, and hashed JavaScript bundles. A typical build summary looks like this:
vite v8.0.16 building for production...
✓ 42 modules transformed.
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-a1b2c3d4.css 1.89 kB │ gzip: 0.78 kB
dist/assets/index-e5f6g7h8.js 188.21 kB │ gzip: 59.04 kB
✓ built in 1.34s
Because the build is fully static, you can deploy it free to Vercel, Netlify, Cloudflare Pages, or GitHub Pages. With Vercel or Netlify, connect your Git repository and the platform auto-detects Vite, runs npm run build, and serves dist/ on a global CDN – zero config. That completes the project: a fully working, deployable React app built from scratch. If you want to add server-side rendering, routing, and data loading next, a meta-framework is the natural progression – see our Next.js tutorial, which builds on everything you just learned.
React Hooks Reference Table
Hooks are the heart of modern React. You used several in this React tutorial; here is a concise reference to the core built-in hooks every developer should know, plus when to reach for each. The full list lives in the official React hooks reference.
| Hook | Purpose | When to use it |
|---|---|---|
useState | Local component state | Any value that changes and should re-render the UI. |
useEffect | Side effects after render | Data fetching, subscriptions, timers, manual DOM work. |
useContext | Read shared context | Theme, auth, or global state without prop drilling. |
useRef | Mutable value / DOM ref | Focus an input, store a value that shouldn’t re-render. |
useReducer | Complex state logic | When multiple sub-values update together via actions. |
useMemo | Memoize a computed value | Expensive calculations you don’t want to repeat. |
useCallback | Memoize a function | Stable callbacks passed to optimized child components. |
useActionState | Async action + pending state | React 19 form submissions with built-in loading state. |
useOptimistic | Optimistic UI updates | React 19; show a result instantly before the server replies. |
Advanced tip – the React Compiler. The experimental React Compiler, which the team has been rolling out through 2025–2026, automatically memoizes components and values at build time. When it stabilizes, it will make manual useMemo and useCallback largely unnecessary for performance. For now, write straightforward code first and only reach for memoization hooks when the React DevTools Profiler shows a real bottleneck – premature memoization adds complexity without measurable benefit.
Common Pitfalls and Troubleshooting Guide
Most React errors a beginner hits fall into a handful of categories. This troubleshooting table maps the symptom to the cause and the fix – bookmark it, because you will see most of these in your first month of learning React.
| Symptom / Error | Likely cause | Fix |
|---|---|---|
| “Each child in a list should have a unique key prop” | Missing or duplicate key in a .map() | Add key={item.id} using a stable unique value. |
| Browser tab freezes / “Too many re-renders” | State setter called during render or bad effect deps | Move setters into handlers/effects; fix the dependency array. |
| UI doesn’t update after changing data | State was mutated directly | Create a new array/object; use spread or functional updater. |
| “Objects are not valid as a React child” | Rendering an object instead of a string/number | Render a property, e.g. {user.name}, not {user}. |
A literal 0 appears in the UI | {count && ...} with count of zero | Use {count > 0 && ...} or a ternary. |
| “Cannot read properties of undefined” | Accessing data before it loads | Add a loading guard or optional chaining data?.field. |
| Effect runs twice in development | StrictMode double-invokes effects in dev | Expected; make effects idempotent. It won’t happen in production. |
| Input is read-only / won’t type | value set without an onChange handler | Add onChange to update state (controlled input). |
| Styles or imports not found | Wrong relative path or missing extension | Check the import path; component files use .jsx. |
| CORS error on fetch | API blocks browser cross-origin requests | Use an API that allows CORS (TVMaze does) or a proxy. |
Pitfall #7 – forgetting the cleanup function. Effects that set up timers, event listeners, or subscriptions must return a cleanup function, or you leak memory and stack duplicate listeners. Whenever you write addEventListener, setInterval, or open a WebSocket inside useEffect, return a function that tears it down. Pitfall #8 – calling hooks conditionally. Hooks must be called at the top level of a component, never inside an if, loop, or nested function. React relies on hooks being called in the same order every render; the eslint-plugin-react-hooks linter (enabled by default in the Vite template) flags violations automatically.
Advanced Tips for Going Further
Once the fundamentals click, a few patterns separate beginner React from professional React. First, colocate state as low as possible. Beginners often lift everything to the top component; instead, keep state in the smallest component that needs it, and only lift it when two siblings must share it. This keeps re-renders narrow and components reusable.
Second, reach for a data-fetching library as your app grows. Manual useEffect fetching, as we did here for learning, becomes repetitive and lacks caching. Libraries like TanStack Query (formerly React Query) and SWR handle caching, background refetching, retries, and deduplication with a single hook – they are the de facto standard for server state in 2026. Keep your hand-rolled hook for understanding; use a library for production.
Third, add TypeScript early. It catches the “cannot read property of undefined” class of bugs before runtime and makes refactoring fearless. Scaffolding with --template react-ts costs nothing and pays back quickly; our TypeScript tutorial covers the essentials. Fourth, learn a router – React Router or the routing built into meta-frameworks – once you need multiple pages. Fifth, write tests with Vitest and React Testing Library, which query the DOM the way users do. Finally, profile before optimizing: the React DevTools Profiler shows exactly which components re-render and why, so you fix real bottlenecks instead of guessing. For a different component model worth studying, compare React’s virtual DOM approach with the compiler-first design covered in our SvelteKit tutorial.
React vs. the Alternatives in 2026
React is dominant but not alone. Understanding where it sits helps you choose the right tool and recognize transferable concepts. The table below summarizes the major front-end options as of June 2026. Every one of them borrowed React’s component-and-state model, so the mental framework you built in this React tutorial transfers directly.
| Framework | Model | Best for | Learning curve |
|---|---|---|---|
| React 19 | Virtual DOM, hooks | Largest ecosystem, jobs, flexibility | Moderate |
| Vue 3 | Reactive, template-based | Gentle onboarding, clear structure | Gentle |
| Svelte 5 | Compiler, no virtual DOM | Smallest bundles, simple syntax | Gentle |
| Angular | Full framework, opinionated | Large enterprise teams | Steep |
| SolidJS | Fine-grained reactivity | Maximum runtime performance | Moderate |
The pragmatic takeaway: React wins on ecosystem size, job availability, and the breadth of its community, which is exactly why it anchors this guide. Vue and Svelte are excellent and arguably easier to learn – explore our Vue.js tutorial if you want a comparison point. But because React’s concepts (components, props, one-way data flow, hooks) are now the industry’s shared vocabulary, learning React first makes every other framework easier to pick up later. And when you are ready for mobile, those same skills carry into our React Native tutorial with minimal new learning.
Frequently Asked Questions
Is React hard to learn for beginners in 2026?
React has a moderate learning curve. The hardest concepts for newcomers are the dependency array in useEffect, immutable state updates, and JSX rules. But because class components and many legacy patterns are gone, modern React is simpler than it was five years ago. If you know JavaScript fundamentals – especially array methods, destructuring, and async/await – you can build a real app like the one in this React tutorial within a few days of focused practice.
Should I use Vite or Create React App?
Use Vite. Create React App was officially deprecated by the React team on February 14, 2025, and is no longer recommended for new projects. Vite is faster, more actively maintained, and is the de facto community standard for single-page React apps in 2026. For full-stack apps that need server-side rendering, a meta-framework like Next.js is the recommended path instead of a plain Vite SPA.
What is the difference between React 18 and React 19?
React 19, stable since December 5, 2024, added Actions and the useActionState, useOptimistic, and use() APIs, made ref a regular prop (reducing the need for forwardRef), added native document metadata support, and improved how stylesheets and async scripts load. It also paved the way for the React Compiler. Most React 18 code runs unchanged on React 19; the new features are additive and worth adopting gradually.
Do I need to learn class components?
No. As of 2026, function components with hooks are the standard for all new React code. Class components still work and you may encounter them in older codebases, so it helps to recognize the syntax, but you should write everything new with function components and hooks – exactly as this React tutorial does throughout.
How long does it take to learn React?
With solid JavaScript already in place, most learners grasp the core – components, props, state, and effects – in one to two weeks of daily practice, and can build production-quality apps within two to three months. The fastest route is project-based learning: build, break, and fix real apps rather than only reading. Completing and then extending the Show Finder app here is a strong start.
What is the best way to manage state in a React app?
Start with useState for local state and useContext for a few global values, as shown above. For server data (anything fetched from an API), use TanStack Query or SWR. Only reach for a dedicated global-state library like Zustand or Redux Toolkit when you have genuinely complex, app-wide client state – most apps never need it. Choosing the lightest tool that solves the problem is the mark of experienced React work.
Why does my useEffect run twice?
In development, React’s StrictMode intentionally mounts, unmounts, and remounts components once to help you catch effects that aren’t safely repeatable (for example, effects that don’t clean up). This double-invocation happens only in development, never in the production build. If running twice causes a visible problem, it usually means your effect is missing a cleanup function or isn’t idempotent – that is a real bug StrictMode is helping you find.
Is React still worth learning with so many alternatives?
Yes. React crossed more than 130 million weekly npm downloads in 2026 and remains among the top two most-used web technologies in the Stack Overflow Developer Survey. It has by far the largest ecosystem, the most job openings, and the deepest pool of learning resources. Even if you eventually prefer Vue or Svelte, the concepts you learn in React transfer directly, and React fluency is the single most marketable front-end skill.
Conclusion: Your Next Steps
You started this React tutorial with an empty folder and finished with a complete, deployable application: a media search app that scaffolds with Vite, renders components and lists, manages state with hooks, fetches and debounces live API data, handles loading and error states, shares and persists favorites with context and localStorage, and uses React 19’s useActionState and native metadata. More importantly, you now hold the mental model – components, props, one-way data flow, state, and effects – that underpins every React app and most modern front-end frameworks.
From here, the highest-leverage moves are: rebuild this app from memory to cement the patterns, then extend it (add a favorites page, pagination, a detail view per show). After that, add TypeScript, learn a router, adopt TanStack Query for data fetching, and graduate to a meta-framework when you need routing and server rendering. Keep the official react.dev learn guide and the React 19 release notes open as references – they are the authoritative, up-to-date source. Build relentlessly, and React will quickly stop feeling like magic and start feeling like a tool you reach for without thinking.
Related Coverage
- Next.js Tutorial: Build a Full-Stack App in 13 Steps [2026]
- Vue.js Tutorial: Build a Full-Stack App with Pinia [2026]
- SvelteKit Tutorial: Build a Full-Stack App in 13 Steps [2026]
- TypeScript Tutorial for Beginners: Complete Guide [2026]
- Tailwind CSS Tutorial: Build a Dashboard with v4 [2026]
- React Native Tutorial: Build a Mobile App [2026]
Sofia Lindström
Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.
View all articles