Lab 3: React Basics
Components, State, and Props
Points: 100
Overview
This lab introduces the fundamentals of React. React is a JavaScript library for building user interfaces. Understanding React is essential because Next.js is built on top of it.
Learning Objectives
By completing this lab, you will:
- Understand what React components are and how they work
- Manage state with
useState - Handle user events (clicks, form inputs)
- Pass data between components with props
- Render lists of items
- Apply the “lifting state up” pattern
Prerequisites
- Completed Lab 2
- Understanding of component-based architecture
Getting Started
Click the button below to accept the assignment and create your repository:
Conceptual Foundation: How React Components Work
Before writing code, it’s important to understand what a React component really is. This mental model will make everything else in this lab—and later in Next.js—much easier to understand.
What Is a React Component?
At its core:
A React component is just a JavaScript function that returns UI.
Example:
function Greeting() {
return <h2>Welcome to React Basics!</h2>
}This is not a template or special syntax. It is a normal JavaScript function.
When React sees <Greeting /> in your code, it:
- Calls the
Greeting()function - Takes the JSX it returns
- Displays that JSX on the screen
You can think of this as:
Greeting()producing UI.
Components Are Re-Executed on Every Render
A critical mental model in React is this:
React re-runs your component function every time state changes.
For example:
function App() {
const [count, setCount] = useState(0)
return <p>Count: {count}</p>
}When setCount() is called:
- React stores the new state
- React re-runs
App() - React compares the old UI to the new UI
- Only the changed parts of the DOM are updated
This is why:
- You never manually update the DOM
- UI is always a function of state
Why Component Names Must Be Capitalized
React uses capitalization to distinguish components from HTML.
<Greeting /> // React component → calls Greeting()
<greeting /> // HTML element → looks for <greeting> tagRule:
- Capital letter → React component
- Lowercase → HTML element
Props vs State
| Concept | Description |
|---|---|
| Props | Data passed into a component |
| State | Data managed inside a component |
| Props | Read-only |
| State | Can change over time |
Data Flow in React
Data flows down, events flow up
Parents pass data to children via props. Children notify parents of events via callback functions. Parents update state. React re-renders the UI.
Part 1: Setting Up
What is Vite?
Vite is a build tool that creates a development environment for modern JavaScript projects. It provides:
- Fast hot module replacement (changes appear instantly)
- A development server
- Optimized production builds
Step 1: Create a New React Project
Open your terminal and run:
npx create-vite reactYou’ll see prompts:
? Select a framework: › React
? Select a variant: › JavaScript
Step 2: Install and Run
cd react
npm install
npm run devStep 3: Open in Browser
Go to http://localhost:5173/
You should see the default Vite + React welcome page with a counter.
Step 4: Open in Your Code Editor
Open the react folder in VS Code (or your preferred editor). The key files are:
react/
├── src/
│ ├── App.jsx ← Main component (we'll edit this)
│ ├── App.css ← Styles
│ └── main.jsx ← Entry point (renders App)
├── index.html ← HTML template
└── package.json ← Dependencies
Part 2: Your First Component
What is a Component?
A React component is a JavaScript function that returns JSX (HTML-like syntax). Components are the building blocks of React apps.
Step 5: Simplify App.jsx
Open react/src/App.jsx and replace everything with:
import { useState } from 'react'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>
Click
</button>
<p>Count: {count}</p>
</div>
)
}
export default AppSave the file. Your browser will automatically update.
What’s Happening?
| Code | Explanation |
|---|---|
import { useState } from 'react' |
Import the useState hook from React |
const [count, setCount] = useState(0) |
Create a state variable count starting at 0 |
onClick={() => setCount(count + 1)} |
When clicked, update count to count + 1 |
{count} |
Display the current value of count |
Key concept: When you call setCount(), React re-renders the component with the new value.
Part 3: Adding Another Component
Step 6: Create a Greeting Component
Components can be combined together. Add a Greeting component above App:
import { useState } from 'react'
import './App.css'
// This is a simple component
function Greeting() {
return <h2>Welcome to React Basics!</h2>
}
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<Greeting />
<button onClick={() => setCount(count + 1)}>
Click
</button>
<p>Count: {count}</p>
</div>
)
}
export default AppWhat’s Happening?
Greetingis a component (a function returning JSX)<Greeting />includes it like an HTML tag- Components must start with a capital letter
Part 4: Components in Separate Files
Real apps put components in separate files. Let’s create a reusable TaskItem component.
Step 7: Create the Components Folder
Create a new folder: react/src/components/
Step 8: Create TaskItem.jsx
Create the file react/src/components/TaskItem.jsx:
function TaskItem({ title, completed, onToggle }) {
return (
<div style={{
padding: '10px',
margin: '5px 0',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: completed ? '#e8f5e9' : '#fff'
}}>
<input
type="checkbox"
checked={completed}
onChange={onToggle}
/>
<span style={{
marginLeft: '10px',
textDecoration: completed ? 'line-through' : 'none'
}}>
{title}
</span>
</div>
)
}
export default TaskItemWhat’s Happening?
| Code | Explanation |
|---|---|
{ title, completed, onToggle } |
These are props - data passed from the parent |
completed ? '#e8f5e9' : '#fff' |
Conditional: green background if completed, white if not |
onChange={onToggle} |
When checkbox changes, call the parent’s function |
export default TaskItem |
Makes the component available to import elsewhere |
Part 5: Putting It All Together
Step 9: Update App.jsx with Task List
Replace App.jsx with:
import { useState } from 'react'
import './App.css'
import TaskItem from './components/TaskItem'
function Greeting() {
return <h2>Welcome to React Basics!</h2>
}
function App() {
const [count, setCount] = useState(0)
// State: an array of task objects
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn React basics', completed: false },
{ id: 2, title: 'Understand useState', completed: true },
{ id: 3, title: 'Create components', completed: false },
])
// Function to toggle a task
const toggleTask = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
))
}
return (
<div className="App">
<Greeting />
<div style={{ marginBottom: '30px' }}>
<h3>Counter Example</h3>
<button onClick={() => setCount(count + 1)}>
Click
</button>
<p>Count: {count}</p>
</div>
<div>
<h3>Task List Example</h3>
{tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
completed={task.completed}
onToggle={() => toggleTask(task.id)}
/>
))}
</div>
</div>
)
}
export default AppWhat’s Happening?
Rendering a list with .map():
{tasks.map(task => (
<TaskItem key={task.id} ... />
))}.map()loops through the array- Returns a
TaskItemfor each task key={task.id}helps React track items efficiently
Updating state immutably:
setTasks(tasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
))- Creates a new array (don’t modify the original)
{ ...task, completed: !task.completed }copies the task and flips completed- This is called an immutable update
Why is toggleTask in App, not TaskItem?
This is a key React pattern called “lifting state up”.
┌─────────────────────────────────────┐
│ App (Parent) │
│ ┌─────────────────────────────┐ │
│ │ tasks = [...] │ │ ← State lives here
│ │ toggleTask = (id) => {...} │ │ ← Function lives here
│ └─────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ TaskItem │ │ TaskItem │ │ ← Children receive
│ │ (props) │ │ (props) │ │ data via props
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Why this pattern?
Single source of truth: The
tasksarray lives in one place (App). If TaskItem could modify its own state, we’d have copies of data in multiple places that could get out of sync.TaskItem is “dumb”: It only knows how to display a task and call a function when clicked. It doesn’t know about other tasks or how to update the array.
Reusability: TaskItem can be used anywhere. It just needs
title,completed, andonToggleprops.Data flows down, events flow up:
- App passes data DOWN to TaskItem via props
- TaskItem sends events UP by calling
onToggle - App handles the event and updates state
- React re-renders with new data
The alternative (and why it’s bad):
// DON'T DO THIS - TaskItem managing its own state
function TaskItem({ title, initialCompleted }) {
const [completed, setCompleted] = useState(initialCompleted)
// Now TaskItem has its own copy of completed
// This gets out of sync with the parent's data!
}Part 6: Adding New Tasks
Now let’s add the ability to create new tasks. This demonstrates handling form input.
Step 10: Update App.jsx with Add Task Form
Replace App.jsx with:
import { useState } from 'react'
import './App.css'
import TaskItem from './components/TaskItem'
function Greeting() {
return <h2>Welcome to React Basics!</h2>
}
function App() {
const [count, setCount] = useState(0)
// State for the task list
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn React basics', completed: false },
{ id: 2, title: 'Understand useState', completed: true },
{ id: 3, title: 'Create components', completed: false },
])
// State for the new task input field
const [newTaskTitle, setNewTaskTitle] = useState('')
// Function to toggle a task
const toggleTask = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
))
}
// Function to add a new task
const addTask = () => {
// Don't add empty tasks
if (newTaskTitle.trim() === '') return
const newTask = {
id: Date.now(), // Simple way to generate unique IDs
title: newTaskTitle,
completed: false
}
setTasks([...tasks, newTask]) // Add to end of array
setNewTaskTitle('') // Clear the input
}
return (
<div className="App">
<Greeting />
<div style={{ marginBottom: '30px' }}>
<h3>Counter Example</h3>
<button onClick={() => setCount(count + 1)}>
Click
</button>
<p>Count: {count}</p>
</div>
<div>
<h3>Task List Example</h3>
{/* Add Task Form */}
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="Enter a new task..."
style={{ padding: '8px', marginRight: '10px', width: '200px' }}
/>
<button onClick={addTask}>
Add Task
</button>
</div>
{/* Task List */}
{tasks.map(task => (
<TaskItem
key={task.id}
title={task.title}
completed={task.completed}
onToggle={() => toggleTask(task.id)}
/>
))}
</div>
</div>
)
}
export default AppWhat’s New?
Controlled input:
const [newTaskTitle, setNewTaskTitle] = useState('')
<input
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
/>| Concept | Explanation |
|---|---|
value={newTaskTitle} |
The input displays whatever is in state |
onChange={(e) => ...} |
When user types, update the state |
e.target.value |
The current text in the input field |
This is called a controlled component - React state controls what the input shows.
Adding to an array:
setTasks([...tasks, newTask])...tasksspreads all existing tasksnewTaskis added at the end- Creates a new array (immutable update)
Generating unique IDs:
id: Date.now()Date.now()returns milliseconds since 1970- Good enough for simple apps (in real apps, use UUID or database IDs)
Summary
| Concept | What It Does | Example |
|---|---|---|
| Component | Reusable UI piece | function Greeting() { return <h2>Hi</h2> } |
| JSX | HTML-like syntax in JavaScript | <div className="App"> |
| State | Data that can change | const [count, setCount] = useState(0) |
| Props | Data passed to child components | <TaskItem title="Learn React" /> |
| Events | Responding to user actions | onClick={() => setCount(count + 1)} |
| Lists | Rendering arrays | {items.map(item => <Item key={item.id} />)} |
Exercises
Try extending the app:
- Delete a task: Add a delete button to TaskItem that removes the task from the list
- Show count: Display “3 tasks, 1 completed” above the task list
- Filter tasks: Add buttons to show All / Active / Completed tasks
- Edit a task: Double-click a task title to edit it inline
Connection to Next.js
Everything you learned here works exactly the same in Next.js:
- Components are identical
- useState works the same
- Props work the same
Next.js adds: file-based routing, server-side rendering, and API routes. But the React fundamentals you learned here are the foundation.