React Tutorial: Build Your First App from Scratch
Learn React from zero. Set up with Vite, understand components, props, state, hooks, and build a complete task manager app step by step.
React is the most widely used frontend library in the world. It powers Facebook, Instagram, Netflix, Airbnb, and thousands of other applications you use daily. If you're learning web development and want to build interactive user interfaces, React is the skill that will get you hired.
But here's the thing -- React has a reputation for being confusing when you're starting out. JSX looks weird, the component model feels unfamiliar, and "state management" sounds intimidating. The reality is that React is built on a few simple ideas, and once those click, everything else follows naturally.
This tutorial takes you from zero to building a complete task manager app. We'll cover every concept along the way, with code you can run at each step.
What React Is and Why It Exists
Before React, building interactive web pages meant manually updating the DOM (the browser's internal representation of your page). You'd write code like document.getElementById('counter').textContent = newValue every time something changed. For simple pages, that works. For complex apps with dozens of interactive elements, it becomes a nightmare of tangled updates and bugs.
React solves this with a simple idea: describe what your UI should look like for a given state, and React figures out what needs to change in the DOM. You don't tell the browser how to update -- you tell React what the result should be, and it handles the rest.
This is called declarative rendering, and it makes building complex UIs dramatically simpler.
Setting Up Your Project with Vite
We'll use Vite (pronounced "veet") to set up our React project. Vite is a modern build tool that's fast and simple.
You need Node.js installed first. If you don't have it, download it from nodejs.org (get the LTS version).
Open your terminal and run:
npx create-vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
Open http://localhost:5173 in your browser. You should see a Vite + React starter page. That's your app running.
Project Structure
The important files:
my-react-app/
src/
App.jsx # Your main component
main.jsx # Entry point -- renders App into the page
App.css # Styles for App
index.css # Global styles
index.html # The single HTML page
package.json # Dependencies and scripts
React apps are single-page applications -- there's one index.html file, and React dynamically renders everything inside it.
JSX: HTML in Your JavaScript
Open src/App.jsx. You'll see something that looks like HTML inside a JavaScript function. That's JSX -- a syntax extension that lets you write HTML-like code in JavaScript.
function App() {
return (
<div>
<h1>Hello, React</h1>
<p>This is my first component.</p>
</div>
);
}
export default App;
JSX is not HTML. It gets compiled to JavaScript function calls behind the scenes. But you can think of it as HTML with superpowers -- you can embed JavaScript expressions inside it using curly braces:
function App() {
const name = "Sarah";
const year = new Date().getFullYear();
return (
<div>
<h1>Hello, {name}</h1>
<p>The year is {year}</p>
<p>2 + 2 = {2 + 2}</p>
</div>
);
}
Anything inside {} is JavaScript that gets evaluated and inserted into the output.
JSX Rules to Remember
- Return a single root element. Wrap everything in a or use a fragment
<>...>.- Close all tags. Self-closing tags like
must be written as.- Use
classNameinstead ofclass. Sinceclassis a reserved word in JavaScript.- Use camelCase for attributes.
onclickbecomesonClick,tabindexbecomestabIndex.Components: The Building Blocks
A React component is just a function that returns JSX. That's it. Here's a component:
function Greeting() { return <h2>Welcome to my app</h2>; }You use it like an HTML tag:
function App() { return ( <div> <Greeting /> <Greeting /> <Greeting /> </div> ); }This renders three "Welcome to my app" headings. Components let you break your UI into reusable pieces. Instead of one giant file, you build small components and compose them together.
Organizing Components
Create a file
src/Greeting.jsx:function Greeting() { return <h2>Welcome to my app</h2>; }export default Greeting;
Then import it in
App.jsx:import Greeting from './Greeting';function App() {
return (
<div>
<Greeting />
</div>
);
}Each component lives in its own file. This keeps your code organized as the app grows.
Props: Passing Data to Components
Components become useful when they can receive data. Props (short for properties) let you pass data from a parent component to a child:
function Greeting({ name, role }) { return ( <div> <h2>Hello, {name}</h2> <p>Role: {role}</p> </div> ); }function App() {
return (
<div>
<Greeting name="Alice" role="Developer" />
<Greeting name="Bob" role="Designer" />
<Greeting name="Carol" role="Manager" />
</div>
);
}Props are read-only. A component should never modify its own props -- it receives them and uses them to render output. Think of props as function arguments.
You can pass any JavaScript value as a prop:
<UserCard name="Alice" age={28} isAdmin={true} hobbies={["coding", "reading"]} style={{ color: "blue" }} />Strings use quotes. Everything else uses curly braces.
State with useState: Making Things Interactive
Props are for data coming in. State is for data that changes over time inside a component.
React provides the
useStatehook to add state to a component:import { useState } from 'react';function Counter() {
const [count, setCount] = useState(0);return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}Let's break this down:
useState(0)creates a state variable initialized to0- It returns an array with two elements: the current value (
count) and a function to update it (setCount) - When you call
setCount(newValue), React re-renders the component with the new value - The UI automatically updates -- you don't touch the DOM
Multiple State Variables
A component can have as many state variables as it needs:
function SignupForm() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [agreed, setAgreed] = useState(false);return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<label>
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
I agree to the terms
</label>
</form>
);
}Handling Events
React events work like HTML events, but with camelCase names and function references instead of strings:
function Button() { function handleClick() { alert('Button clicked!'); }return <button onClick={handleClick}>Click me</button>;
}Common events:
onClick,onChange,onSubmit,onKeyDown,onMouseEnter,onFocus.The event handler receives an event object:
function SearchBox() { function handleKeyDown(event) { if (event.key === 'Enter') { console.log('User pressed Enter'); } }return <input onKeyDown={handleKeyDown} placeholder="Search..." />;
}For forms, prevent the default browser submission:
function LoginForm() { const [email, setEmail] = useState('');function handleSubmit(event) {
event.preventDefault();
console.log('Submitting:', email);
}return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Log in</button>
</form>
);
}Conditional Rendering
Show different things based on conditions. There are several patterns:
If/else with early return
function Dashboard({ isLoggedIn }) { if (!isLoggedIn) { return <p>Please log in to continue.</p>; }return <p>Welcome back to your dashboard.</p>;
}Ternary operator in JSX
function Greeting({ isLoggedIn }) { return ( <div> {isLoggedIn ? <p>Welcome back!</p> : <p>Please log in.</p>} </div> ); }Logical AND for conditional display
function Notification({ count }) { return ( <div> <h1>Messages</h1> {count > 0 && <span>You have {count} new messages</span>} </div> ); }Lists and Keys
Render arrays of data using
.map():function FruitList() { const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}The
keyprop is required when rendering lists. It helps React track which items changed, were added, or removed. Use a unique identifier -- never use the array index as a key if the list can be reordered.For objects:
function UserList() { const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, { id: 3, name: 'Carol', email: 'carol@example.com' }, ];return (
<div>
{users.map((user) => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
);
}useEffect: Side Effects and Data Fetching
useEffectlets you run code after a component renders. It's used for things like fetching data, setting up timers, or updating the document title.import { useState, useEffect } from 'react';function Timer() {
const [seconds, setSeconds] = useState(0);useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);// Cleanup function -- runs when the component unmounts
return () => clearInterval(interval);
}, []); // Empty array = run once on mountreturn <p>Elapsed: {seconds} seconds</p>;
}The second argument (the dependency array) controls when the effect runs:
[]-- run once after first render[count]-- run after first render AND whenevercountchanges- No array -- run after every render (usually not what you want)
Fetching Data
A common use case:
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);useEffect(() => {
setLoading(true);
fetch(https://jsonplaceholder.typicode.com/users/${userId})
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then((data) => {
setUser(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [userId]);if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}This fetches user data when the component mounts and whenever
userIdchanges.Build: A Complete Task Manager App
Let's put it all together. We'll build a task manager with add, complete, and delete functionality.
Replace the contents of
src/App.jsx:import { useState, useEffect } from 'react'; import './App.css';function App() {
const [tasks, setTasks] = useState(() => {
const saved = localStorage.getItem('tasks');
return saved ? JSON.parse(saved) : [];
});
const [input, setInput] = useState('');
const [filter, setFilter] = useState('all');useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);function addTask(e) {
e.preventDefault();
const text = input.trim();
if (!text) return;setTasks([
...tasks,
{ id: Date.now(), text, completed: false },
]);
setInput('');
}function toggleTask(id) {
setTasks(
tasks.map((task) =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}function deleteTask(id) {
setTasks(tasks.filter((task) => task.id !== id));
}const filteredTasks = tasks.filter((task) => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});const activeCount = tasks.filter((t) => !t.completed).length;
return (
<div className="app">
<h1>Task Manager</h1><form onSubmit={addTask} className="add-form">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form><div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All ({tasks.length})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active ({activeCount})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed ({tasks.length - activeCount})
</button>
</div><ul className="task-list">
{filteredTasks.map((task) => (
<li key={task.id} className={task.completed ? 'done' : ''}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
/>
<span>{task.text}</span>
</label>
<button onClick={() => deleteTask(task.id)}>Delete</button>
</li>
))}
</ul>{filteredTasks.length === 0 && (
<p className="empty">
{filter === 'all'
? 'No tasks yet. Add one above.'
:No ${filter} tasks.}
</p>
)}
</div>
);
}export default App;
Replace
src/App.csswith some basic styles:.app { max-width: 500px; margin: 2rem auto; padding: 1rem; font-family: system-ui, sans-serif; }.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}.add-form input {
flex: 1;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}.add-form button {
padding: 0.5rem 1rem;
background: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}.filters button {
padding: 0.25rem 0.75rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 4px;
cursor: pointer;
}.filters button.active {
background: #2563eb;
color: white;
border-color: #2563eb;
}.task-list {
list-style: none;
padding: 0;
}.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e5e7eb;
}.task-list li.done span {
text-decoration: line-through;
color: #9ca3af;
}.empty {
text-align: center;
color: #6b7280;
font-style: italic;
}What This App Demonstrates
Every concept we covered shows up in this app:
- Components -- the entire app is a component, and you could extract
TaskItemandFilterBarinto their own components - JSX -- HTML-like syntax with embedded JavaScript expressions
- State --
tasks,input, andfilterare all state variables managed withuseState - Events --
onSubmit,onChange,onClickhandlers throughout - Conditional rendering -- the empty state message uses a ternary and
&& - Lists and keys -- tasks rendered with
.map()andkey={task.id} - useEffect -- syncing tasks to
localStoragewhenever they change - Props -- ready to refactor: extract child components and pass data via props
Common Mistakes Beginners Make
Mutating state directly. Never dotasks.push(newTask). Always create a new array:setTasks([...tasks, newTask]). React only re-renders when it detects a new state value, and mutating the existing object won't trigger that. Forgetting the dependency array in useEffect. Without it, your effect runs after every single render. If the effect updates state, you get an infinite loop. Using array index as key.key={index}causes bugs when items are reordered or deleted. Use a stable unique identifier like anidfield. Calling the state setter function instead of passing it. WritingonClick={setCount(count + 1)}calls the function immediately during render. UseonClick={() => setCount(count + 1)}to wrap it in an arrow function. Fetching data without cleanup. If a component unmounts before a fetch completes, you get a "can't update state on unmounted component" warning. Use a cleanup flag or AbortController in your useEffect.What's Next
You now have a working foundation in React. Here's where to go from here:
- React Router -- add multi-page navigation to your app without full page reloads
- Custom hooks -- extract reusable logic (like localStorage sync) into your own hooks
- Context API -- share state across components without prop drilling
- React Query or SWR -- better data fetching with caching, retries, and loading states
- TypeScript -- add type safety to catch bugs before they happen
For more guides on React, JavaScript, and full-stack development, check out CodeUp.
Ad 728x90 - Close all tags. Self-closing tags like