React Snippets
About React
React is a UI library for building interactive interfaces from reusable components. This track starts with foundations and ends with a small project-style feature set.
Project Structure for a Learning React App
Use a predictable structure so state logic, UI, and tests stay easy to find as your app grows.
react-task-lab/
src/
components/
TaskForm.jsx
TaskList.jsx
Pomodoro.jsx
hooks/
useLocalStorage.js
usePomodoro.js
state/
tasksReducer.js
App.jsx
main.jsx
tests/
TaskList.test.jsx
package.jsonCreate the Project with Vite
Create, install dependencies, and start a modern React development server.
npm create vite@latest react-task-lab -- --template react
cd react-task-lab
npm install
npm run devTask State with useReducer (src/state/tasksReducer.js)
useReducer keeps task updates explicit and easier to scale than chained setState calls.
export const initialTasks = []
export function tasksReducer(state, action) {
switch (action.type) {
case 'task/added':
return [...state, { id: crypto.randomUUID(), text: action.text, done: false }]
case 'task/toggled':
return state.map((task) =>
task.id === action.id ? { ...task, done: !task.done } : task
)
case 'task/removed':
return state.filter((task) => task.id !== action.id)
default:
return state
}
}Task Form Component (src/components/TaskForm.jsx)
Keep input state local and dispatch clean actions to your reducer.
import { useState } from 'react'
export default function TaskForm({ dispatch }) {
const [text, setText] = useState('')
const onSubmit = (event) => {
event.preventDefault()
if (!text.trim()) return
dispatch({ type: 'task/added', text: text.trim() })
setText('')
}
return (
<form onSubmit={onSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type='submit'>Add Task</button>
</form>
)
}Task List Component (src/components/TaskList.jsx)
Render list state declaratively and keep actions near UI controls.
export default function TaskList({ tasks, dispatch }) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type='checkbox'
checked={task.done}
onChange={() => dispatch({ type: 'task/toggled', id: task.id })}
/>
{task.text}
</label>
<button onClick={() => dispatch({ type: "task/removed", id: task.id })}>
Delete
</button>
</li>
))}
</ul>
)
}Persist Tasks with localStorage (src/hooks/useLocalStorage.js)
A reusable hook keeps persistence logic out of UI components.
import { useEffect, useState } from 'react'
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}Compose the App (src/App.jsx)
Wire reducer + persistence together, then render focused child components.
import { useReducer, useEffect } from 'react'
import TaskForm from './components/TaskForm'
import TaskList from './components/TaskList'
import { initialTasks, tasksReducer } from './state/tasksReducer'
import { useLocalStorage } from './hooks/useLocalStorage'
export default function App() {
const [savedTasks, setSavedTasks] = useLocalStorage('tasks', initialTasks)
const [tasks, dispatch] = useReducer(tasksReducer, savedTasks)
useEffect(() => setSavedTasks(tasks), [tasks, setSavedTasks])
return (
<>
<h1>Task Lab</h1>
<TaskForm dispatch={dispatch} />
<TaskList tasks={tasks} dispatch={dispatch} />
</>
)
}Mini Project: Pomodoro Hook (src/hooks/usePomodoro.js)
Small project idea: build a focus timer. Start with a pure timing hook so UI stays simple.
import { useEffect, useState } from 'react'
export function usePomodoro(seconds = 1500) {
const [remaining, setRemaining] = useState(seconds)
const [running, setRunning] = useState(false)
useEffect(() => {
if (!running) return
const id = setInterval(() => setRemaining((s) => Math.max(0, s - 1)), 1000)
return () => clearInterval(id)
}, [running])
return { remaining, running, setRunning, reset: () => setRemaining(seconds) }
}Mini Project: Pomodoro UI (src/components/Pomodoro.jsx)
Connect hook state to controls and keep formatting logic close to rendering.
import { usePomodoro } from '../hooks/usePomodoro'
export default function Pomodoro() {
const { remaining, running, setRunning, reset } = usePomodoro(25 * 60)
const mm = String(Math.floor(remaining / 60)).padStart(2, "0")
const ss = String(remaining % 60).padStart(2, "0")
return (
<section>
<h2>Pomodoro</h2>
<p>{mm}:{ss}</p>
<button onClick={() => setRunning((v) => !v)}>{running ? "Pause" : "Start"}</button>
<button onClick={reset}>Reset</button>
</section>
)
}Component Test with React Testing Library
Start testing behavior by user interactions, not implementation details.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import TaskForm from '../src/components/TaskForm'
test('dispatches task/added after submit', async () => {
const user = userEvent.setup()
const dispatch = vi.fn()
render(<TaskForm dispatch={dispatch} />)
await user.type(screen.getByRole('textbox'), 'Read docs')
await user.click(screen.getByRole('button', { name: /add task/i }))
expect(dispatch).toHaveBeenCalledWith({ type: 'task/added', text: 'Read docs' })
})Next Learning Upgrades
After the starter and mini project, level up with production patterns.
1. Add routing with React Router for task views and settings.
2. Add optimistic UI when syncing tasks to an API.
3. Add accessibility checks (keyboard nav, aria labels).
4. Add form validation with zod + react-hook-form.
5. Add end-to-end tests with Playwright.