React

React is a JavaScript library for building user interfaces. It uses a component-based architecture where you build complex UIs from small, isolated pieces of code called "components", following a declarative approach.

React is the most widely used frontend library, with a large ecosystem and strong community. It is used by Meta, Vercel, Airbnb, and many more.

Core Concepts

JSX

JSX is a syntax extension to JavaScript that looks like HTML but has the full power of JavaScript. It makes it easier to write and read UI code in React. JSX produces React "elements" that describe what should appear on screen.

// Static element
const element = <h1>Hello, world!</h1>

// JavaScript expressions inside curly braces
const name = 'John Doe'
const greeting = <h1>Hello, {name}</h1>

// Event listeners
const Example = () => (
  <button onClick={() => alert('Hello world')}>Click me</button>
)

Components

Components are the building blocks of React applications — reusable pieces of code that return React elements. Always use function components (class components are legacy and should be avoided).

const Greeting = () => {
  return <div>Hello!</div>
}

// With implicit return
const Greeting = () => <div>Hello!</div>

Props

Props are how components receive data from their parents. They are passed as attributes and follow a one-way data flow — a child can read its props but never modify them.

type GreetingProps = {
  name: string
}

const Greeting = ({ name }: GreetingProps) => {
  return <div>Hello {name}</div>
}

const App = () => {
  return <Greeting name="John" />
}

All React components must act like pure functions with respect to their props.

State

State holds data that may change over the lifetime of a component. When state is updated, React re-renders the component and its children to reflect the new data.

You declare state using the useState hook, which returns the current value and a setter function:

const Counter = () => {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

Hooks

Hooks are functions that let you use React features (state, side effects, context, etc.) from function components. The two most fundamental hooks are:

  • useState — declares a state variable (seen above).
  • useEffect — runs side effects (data fetching, subscriptions, DOM manipulation) after render. It takes a callback and a dependency array that controls when it re-runs.
const Example = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // Runs when `count` changes
    document.title = `You clicked ${count} times`

    return () => {
      // Cleanup: runs before the next effect or when the component unmounts
      console.log('Cleaning up')
    }
  }, [count])

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

React ships many more built-in hooks (useContext, useRef, useMemo, useCallback, etc.). Read about them in the React Hooks reference.

Virtual DOM

React uses a Virtual DOM (VDom) — a lightweight JavaScript representation of the actual DOM. When state changes, React builds a new VDom tree, diffs it against the previous one, and only applies the minimal set of changes to the real DOM.

You can learn more about this in this article from Mosh Hamedani.

Resources

TanStack Query

When building a React app that talks to an API, you quickly run into challenges: loading states, caching, refetching stale data, optimistic updates, etc. TanStack Query (formerly React Query) handles all of this out of the box.

Key characteristics:

  • Automatic caching — query results are cached and shared across components. No need to lift state or use global stores for server data.
  • Background refetching — stale data is shown instantly while fresh data is fetched in the background.
  • Loading and error states — every query returns isLoading, isError, and data, making it trivial to handle UI states.
  • MutationsuseMutation handles create/update/delete operations with built-in support for optimistic updates and cache invalidation.

Quick example:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// Fetching data
const ItemList = () => {
  const { data: items, isLoading } = useQuery({
    queryKey: ['items'],
    queryFn: () => fetch('/api/items').then((res) => res.json()),
  })

  if (isLoading) return <p>Loading...</p>

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

// Creating data
const AddItemForm = () => {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newItem: { name: string }) =>
      fetch('/api/items', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newItem),
      }),
    onSuccess: () => {
      // Refetch the items list after a successful creation
      queryClient.invalidateQueries({ queryKey: ['items'] })
    },
  })

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    mutation.mutate({ name: formData.get('name') as string })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Item name" required />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Adding...' : 'Add Item'}
      </button>
    </form>
  )
}

Resources

Exercise — Wishlist Frontend

After creating the REST API, let's build a React application to consume it. The app should let users view, add, edit, and delete wishlist items.

Steps to get started:

  1. Scaffold a new React project with Vite: bun create vite wishlist-client --template react-ts.
  2. Install TanStack Query: bun add @tanstack/react-query.
  3. Set up a QueryClientProvider at the root of your app (see the quick start guide).
  4. Create a component that lists all items using useQuery to fetch from GET /api/items.
  5. Create a form component that adds a new item using useMutation and invalidates the items query on success.
  6. Add edit and delete functionality for each item.
  7. Handle loading and error states in all views.