ExoSnip Code Snippets

React Snippets

← Back to Home

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.json

Create 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 dev

Task 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.