โ† LINE API 09 / 18 Next: Next.js โ†’

React Basics

Module 4 ยท Frontend ยท โฑ ~25 min
Why this matters: The entire JongYang frontend โ€” the marketplace, shop pages, 5-step booking wizard, and admin panel โ€” is built with React. Understanding components, state, and effects is prerequisite to reading any frontend file.

What React Is

React is a JavaScript library for building user interfaces. The core idea: instead of manually manipulating the DOM (document.getElementById('btn').textContent = '...'), you describe what the UI should look like for a given state, and React updates the DOM for you.

Components: Functions That Return JSX

A React component is just a function that returns JSX (HTML-like syntax that compiles to JavaScript). Here's the simplest possible component:

tsx โ€” simple component
function ShopName({ name }: { name: string }) {
  return <h1 className="text-2xl font-bold">{name}</h1>
}
// Used as: <ShopName name="BKK Sports Club" />
// Renders: <h1 class="text-2xl font-bold">BKK Sports Club</h1>

Three things to notice:

useState: Local State

useState is how a component remembers data between renders. When state changes, React re-renders the component:

tsx โ€” useState example from the admin bookings page
'use client'  // โ† needed because this uses hooks

import { useState } from 'react'

export default function BookingsPage() {
  // useState returns [currentValue, setter]
  const [date, setDate] = useState(todayISO())     // today's date
  const [bookings, setBookings] = useState([])      // empty array initially
  const [loading, setLoading] = useState(true)      // start in loading state
  const [error, setError] = useState(undefined)     // no error initially

  // When setDate("2026-06-25") is called, React re-renders the component
  // with date = "2026-06-25"
  return (
    <input
      type="date"
      value={date}
      onChange={e => setDate(e.target.value)}  // update state on change
    />
  )
}
React's re-render model: Calling a setter (like setDate) schedules a re-render. React calls the function again with the new state values and updates only the DOM parts that changed. This is fast and automatic โ€” you never manually update the DOM.

useEffect: Side Effects

useEffect runs code after the component renders. Use it for fetching data, setting up timers, or subscribing to events:

tsx โ€” useEffect for data loading
useEffect(() => {
  // This runs after the component first renders
  fetchBookings()
}, [fetchBookings])  // โ† dependency array

// Dependency array controls WHEN the effect re-runs:
// []           โ†’ run once on mount only (initial load)
// [fetchBookings] โ†’ re-run whenever fetchBookings changes
// no array     โ†’ re-run after EVERY render (usually wrong)

useCallback: Stable Function References

Functions are recreated on every render. If you put a function in a useEffect dependency array, it causes infinite re-renders (function changes โ†’ effect runs โ†’ state updates โ†’ render โ†’ function changes...). useCallback memoizes the function so its reference stays stable:

tsx โ€” useCallback from admin/bookings/page.tsx
const fetchBookings = useCallback(async () => {
  setLoading(true)
  setError(undefined)
  try {
    const qs = new URLSearchParams()
    if (date) qs.set('booking_date', date)
    if (resourceId) qs.set('resource_id', resourceId)
    const data = await proxyGet<BookingAdmin[]>(`/bookings?${qs}`)
    setBookings(data)
  } catch {
    setError('Failed to load bookings')
  } finally {
    setLoading(false)
  }
}, [date, resourceId, status])  // only recreate if these change

// Now this is stable โ€” won't cause infinite loops
useEffect(() => { fetchBookings() }, [fetchBookings])

Conditional Rendering

React components often show different UI based on state:

tsx โ€” conditional rendering patterns
// Ternary: show one thing or another
{loading ? (
  <div className="animate-spin h-8 w-8 ..." />
) : (
  <table>...</table>
)}

// Short-circuit: only show if condition is true
{error && (
  <p className="text-red-500">{error}</p>
)}

// Three-way
{loading ? <Spinner /> : error ? <Error msg={error} /> : <Table data={bookings} />}

List Rendering

tsx โ€” rendering a list of bookings
{bookings.map(b => (
  <tr key={b.id} className="hover:bg-slate-50">
    <td className="px-4 py-3">
      <p>{b.booking_date}</p>
      <p className="text-xs">{fmtTime(b.start_time)}โ€“{fmtTime(b.end_time)}</p>
    </td>
    <td className="px-4 py-3">
      <p className="font-medium">{b.customer_name}</p>
    </td>
  </tr>
))}
// The key={b.id} prop is REQUIRED for lists โ€” React uses it to track which
// item changed when the list updates. Use a stable, unique ID, not an array index.

'use client' โ€” The Directive

Next.js components are server-side by default (they run on the server, no browser needed). When a component uses React hooks (useState, useEffect) or browser APIs (window, document), it must be marked with 'use client':

tsx โ€” why 'use client' is needed
'use client'  // โ† must be FIRST line of the file

import { useState } from 'react'
// Without 'use client', importing useState would crash:
// "hooks cannot be used in server components"

export default function BookingFlow({ shop, resource }: Props) {
  const [step, setStep] = useState('duration')  // needs browser + React runtime
  // ...
}

The BookingFlow wizard uses 'use client' because it has complex interactive state (current step, selected date, countdown timer). The shop page itself is a server component that just renders data โ€” no hooks needed there.

The BookingFlow Step Machine

The booking wizard tracks the current step in state:

tsx โ€” step management from BookingFlow.tsx
type Step = 'service' | 'duration' | 'date' | 'time' | 'details' | 'review' | 'payment' | 'success'

function getSteps(resource: Resource): Step[] {
  if (resource.resource_type === 'asset') {
    return ['duration', 'date', 'time', 'details', 'review', 'payment', 'success']
  }
  if (resource.services.length > 1) {
    return ['service', 'date', 'time', 'details', 'review', 'payment', 'success']
  }
  return ['date', 'time', 'details', 'review', 'payment', 'success']
}
// Asset (court): pick duration โ†’ date โ†’ time โ†’ fill details โ†’ review โ†’ pay
// Staff multi-service: pick service โ†’ date โ†’ time โ†’ fill details โ†’ review โ†’ pay
// Staff single-service: skip service selection, start at date

๐Ÿง  Self-Check Quiz

1. What does calling setBookings(data) do? What happens next in React?

It updates the bookings state variable and schedules a re-render. React calls the component function again with the new value of bookings. The JSX that maps over bookings now maps over the new data. React compares the new output with the previous render (diffing) and only updates the DOM nodes that actually changed.

2. What does the dependency array [date, resourceId, status] mean in useCallback?

The fetchBookings function will only be recreated (new reference) when date, resourceId, or status changes. If none of these change between renders, the same function reference is returned. This prevents the useEffect that depends on fetchBookings from running unnecessarily on every render.

3. Why must every item in a .map() rendered list have a key prop?

React uses the key to identify which list items changed, were added, or were removed between renders. Without keys, React can't tell which booking row corresponds to which data item when the list updates โ€” it would re-render all rows unnecessarily or worse, mix up the DOM state of different rows. The key should be a stable, unique identifier like b.id (not the array index, which changes when items are inserted or removed).

4. Why does BookingFlow.tsx need 'use client' but app/shop/[slug]/page.tsx doesn't?

BookingFlow has interactive state โ€” it tracks the current step, countdown timer, selected date/time, and form values using useState and useEffect. These hooks require a browser environment and the React client runtime. The shop page just fetches data from the database on the server and renders static HTML โ€” no interactivity, no hooks, no browser APIs needed.

5. What's the difference between {loading && <Spinner />} and {loading ? <Spinner /> : <Table />}?

The short-circuit (&&) renders <Spinner /> when loading is true and renders nothing when loading is false. The ternary renders <Spinner /> when loading is true and <Table /> when false. Use && when you want to conditionally show something with no alternative. Use ternary when you want to switch between two different views (loading vs loaded).