March 27, 202613 min read

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 javascript frontend beginners tutorial
Ad 336x280

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

  1. Return a single root element. Wrap everything in a
    or use a fragment <>....
  2. Close all tags. Self-closing tags like must be written as .
  3. Use className instead of class. Since class is a reserved word in JavaScript.
  4. Use camelCase for attributes. onclick becomes onClick, tabindex becomes tabIndex.

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 useState hook 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 to 0
  • 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
This is the core of React. State changes trigger re-renders, and re-renders update the UI.

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 key prop 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

useEffect lets 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 mount

return <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 whenever count changes
  • 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 userId changes.

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.css with 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 TaskItem and FilterBar into their own components
  • JSX -- HTML-like syntax with embedded JavaScript expressions
  • State -- tasks, input, and filter are all state variables managed with useState
  • Events -- onSubmit, onChange, onClick handlers throughout
  • Conditional rendering -- the empty state message uses a ternary and &&
  • Lists and keys -- tasks rendered with .map() and key={task.id}
  • useEffect -- syncing tasks to localStorage whenever they change
  • Props -- ready to refactor: extract child components and pass data via props

Common Mistakes Beginners Make

Mutating state directly. Never do tasks.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 an id field. Calling the state setter function instead of passing it. Writing onClick={setCount(count + 1)} calls the function immediately during render. Use onClick={() => 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
The best way to learn is to build. Pick a project -- a weather app, a note-taking tool, a quiz game -- and build it. You'll hit problems, google them, and learn more from solving real issues than from any tutorial.

For more guides on React, JavaScript, and full-stack development, check out CodeUp.

Ad 728x90