Skip to content

React Crash Course for Forma3D Connect

Target Audience: Junior developers with TypeScript knowledge and basic SPA concepts
Goal: Get up to speed to independently work on this project's React frontend


Table of Contents

  1. What is React?
  2. React Core Concepts
  3. Components: The Building Blocks
  4. Props: Passing Data to Components
  5. Hooks: Adding Behavior to Components
  6. Custom Hooks: Reusable Logic
  7. Context: Global State Management
  8. React Query: Server State Management
  9. React Router: Navigation
  10. Project Structure and Conventions
  11. Styling with Tailwind CSS
  12. Testing React Components
  13. Common Patterns in This Codebase
  14. Quick Reference

1. What is React?

React is a JavaScript library for building user interfaces. Unlike traditional web development where you manipulate the DOM directly, React uses a declarative approach:

  • You describe what the UI should look like based on the current state
  • React figures out how to update the DOM efficiently

Key Mental Model

Think of React as a function:

UI = f(state)

Your UI is the result of applying your component functions to the current state. When state changes, React re-runs the relevant functions and updates only what changed in the browser.


2. React Core Concepts

JSX: JavaScript + HTML

JSX lets you write HTML-like syntax in JavaScript:

// This is JSX - it looks like HTML but it's JavaScript
const element = <h1 className="title">Hello, World!</h1>;

// It compiles to this:
const element = React.createElement('h1', { className: 'title' }, 'Hello, World!');

Key differences from HTML: - Use className instead of class (because class is reserved in JS) - Use htmlFor instead of for - All attributes are camelCase: onClick, onChange, tabIndex - Self-closing tags must have a slash: <img />, <input />

Embedding JavaScript in JSX

Use curly braces {} to embed any JavaScript expression:

const name = 'Forma3D';
const count = 5;

return (
  <div>
    <h1>Welcome to {name}</h1>
    <p>You have {count} orders</p>
    <p>That's {count > 0 ? 'great!' : 'zero orders'}</p>
  </div>
);

Rendering Lists

Use .map() to render arrays. Each item needs a unique key prop:

const orders = ['Order A', 'Order B', 'Order C'];

return (
  <ul>
    {orders.map((order, index) => (
      <li key={index}>{order}</li>
    ))}
  </ul>
);

Note: Use a unique ID as the key when available, not the array index. The index is only acceptable when the list never reorders.

Conditional Rendering

Several patterns for showing/hiding content:

// 1. Ternary operator
return isLoading ? <Spinner /> : <Content />;

// 2. Logical AND (renders if truthy)
return hasError && <ErrorMessage />;

// 3. Early return
if (isLoading) {
  return <Spinner />;
}
return <Content />;

3. Components: The Building Blocks

Components are reusable pieces of UI. In this project, we exclusively use functional components.

Basic Component

// A component is just a function that returns JSX
function Greeting() {
  return <h1>Hello!</h1>;
}

// Export for use in other files
export function OrdersPage() {
  return (
    <div>
      <Greeting />
      <p>Your orders will appear here.</p>
    </div>
  );
}

Component Rules

  1. Name must start with uppercase: OrdersPage, not ordersPage
  2. Must return a single root element (use fragments if needed):
// Wrong - multiple root elements
function Wrong() {
  return (
    <h1>Title</h1>
    <p>Content</p>
  );
}

// Correct - wrapped in a fragment
function Correct() {
  return (
    <>
      <h1>Title</h1>
      <p>Content</p>
    </>
  );
}

Real Example from Our Codebase

From apps/web/src/components/layout/root-layout.tsx:

import { Outlet } from 'react-router-dom';
import { Header } from './header';
import { Sidebar } from './sidebar';

export function RootLayout() {
  return (
    <div className="min-h-screen bg-slate-950 text-white">
      <Header />
      <Sidebar />
      <main className="ml-64 pt-16 min-h-screen p-6">
        <Outlet />  {/* Child routes render here */}
      </main>
    </div>
  );
}

4. Props: Passing Data to Components

Props are how you pass data from parent to child components. Think of them as function arguments.

Basic Props

// Define the prop types with an interface
interface GreetingProps {
  name: string;
  age?: number;  // Optional prop (?)
}

// Destructure props in the function signature
function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old</p>}
    </div>
  );
}

// Usage
<Greeting name="John" />
<Greeting name="Jane" age={25} />

The children Prop

A special prop that contains nested elements:

interface CardProps {
  children: React.ReactNode;  // ReactNode accepts any valid JSX
  title?: string;
}

function Card({ children, title }: CardProps) {
  return (
    <div className="rounded-xl border border-slate-800 bg-slate-900/50 p-6">
      {title && <h2>{title}</h2>}
      {children}
    </div>
  );
}

// Usage - anything between the tags becomes `children`
<Card title="Orders">
  <p>Your orders appear here</p>
  <button>View All</button>
</Card>

Real Example: Button Component

From apps/web/src/components/ui/button.tsx:

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = 'primary', size = 'md', loading, disabled, children, ...props }, ref) => {
    const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg...';

    const variants = {
      primary: 'bg-cyan-600 text-white hover:bg-cyan-700...',
      secondary: 'bg-slate-700 text-white hover:bg-slate-600...',
      // ...
    };

    return (
      <button
        ref={ref}
        className={clsx(baseStyles, variants[variant], sizes[size], className)}
        disabled={disabled || loading}
        {...props}  // Spread remaining props (onClick, type, etc.)
      >
        {loading && <Spinner />}
        {children}
      </button>
    );
  }
);

Key patterns: - extends ButtonHTMLAttributes<HTMLButtonElement> - inherit all native button props - Default values: variant = 'primary' - forwardRef - allows parent to get a reference to the button element - ...props spread - forward any extra props to the native element


5. Hooks: Adding Behavior to Components

Hooks are functions that let you "hook into" React features. They must: - Start with use (convention) - Be called at the top level of components (not inside loops, conditions, or nested functions) - Be called from React functions (components or custom hooks)

useState: Component State

Stores values that, when changed, cause the component to re-render:

import { useState } from 'react';

function Counter() {
  // Declare state variable `count` with initial value 0
  // `setCount` is the function to update it
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(prev => prev + 1)}>Increment (safer)</button>
    </div>
  );
}

Best Practice: Use the function form setCount(prev => prev + 1) when the new value depends on the previous value.

Real Example: Filter State

From apps/web/src/pages/orders/index.tsx:

export function OrdersPage() {
  const [page, setPage] = useState(1);
  const [status, setStatus] = useState<OrderStatus | ''>('');
  const [search, setSearch] = useState('');

  // These state values are used in the query and UI
  const { data, isLoading } = useOrders({ page, pageSize: 25, status });

  return (
    <div>
      <Select
        value={status}
        onChange={(e) => {
          setStatus(e.target.value as OrderStatus | '');
          setPage(1);  // Reset to page 1 when filter changes
        }}
      />
      {/* ... */}
    </div>
  );
}

useEffect: Side Effects

Runs code after render. Used for things like fetching data, subscriptions, or DOM manipulation:

import { useEffect, useState } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // This runs after every render
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function - runs when component unmounts
    return () => clearInterval(interval);
  }, []);  // Empty dependency array = run once on mount

  return <p>Elapsed: {seconds}s</p>;
}

Dependency Array Patterns:

useEffect(() => {
  // Runs after EVERY render
});

useEffect(() => {
  // Runs ONCE after initial render
}, []);

useEffect(() => {
  // Runs when `userId` or `page` changes
}, [userId, page]);

Real Example: Socket Connection

From apps/web/src/contexts/socket-context.tsx:

useEffect(() => {
  const socketInstance = io(`${SOCKET_URL}/events`, {
    transports: ['websocket'],
    autoConnect: true,
  });

  socketInstance.on('connect', () => setIsConnected(true));
  socketInstance.on('disconnect', () => setIsConnected(false));

  socketInstance.on('order:created', (data) => {
    toast.success(`New order received: #${data.orderNumber}`);
    queryClient.invalidateQueries({ queryKey: ['orders'] });
  });

  setSocket(socketInstance);

  // Cleanup: disconnect when component unmounts
  return () => {
    socketInstance.disconnect();
  };
}, [queryClient]);  // Re-run if queryClient changes

useCallback: Memoized Functions

Prevents creating new function instances on every render:

import { useCallback } from 'react';

function Auth() {
  const [apiKey, setApiKey] = useState<string | null>(null);

  // Without useCallback: new function created every render
  const login = (key: string) => {
    localStorage.setItem('api_key', key);
    setApiKey(key);
  };

  // With useCallback: same function instance unless dependencies change
  const login = useCallback((key: string) => {
    localStorage.setItem('api_key', key);
    setApiKey(key);
  }, []);  // No dependencies = function never changes

  return <LoginForm onLogin={login} />;
}

When to use: When passing functions to child components or using them in dependency arrays.


6. Custom Hooks: Reusable Logic

Custom hooks extract and reuse stateful logic across components. They're just functions that use other hooks.

Creating a Custom Hook

// hooks/use-local-storage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setStoredValue = useCallback((newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);

  return [value, setStoredValue] as const;
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark');
  // ...
}

Real Example: useOrders Hook

From apps/web/src/hooks/use-orders.ts:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../lib/api-client';

interface OrdersQuery {
  page?: number;
  pageSize?: number;
  status?: OrderStatus | '';
}

// Custom hook for fetching orders
export function useOrders(query: OrdersQuery = {}) {
  return useQuery({
    queryKey: ['orders', query],
    queryFn: () => apiClient.orders.list(query),
  });
}

// Custom hook for fetching a single order
export function useOrder(id: string) {
  return useQuery({
    queryKey: ['orders', id],
    queryFn: () => apiClient.orders.get(id),
    enabled: !!id,  // Only fetch if id exists
  });
}

// Custom hook for mutations
export function useCancelOrder() {
  const queryClient = useQueryClient();
  const { apiKey } = useAuth();

  return useMutation({
    mutationFn: (orderId: string) => apiClient.orders.cancel(orderId, apiKey),
    onSuccess: () => {
      // Invalidate and refetch orders after cancellation
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

Usage in a component:

function OrdersPage() {
  const { data, isLoading, error } = useOrders({ page: 1 });
  const cancelOrder = useCancelOrder();

  if (isLoading) return <LoadingPage />;
  if (error) return <ErrorPage message={error.message} />;

  return (
    <div>
      {data.orders.map(order => (
        <OrderRow 
          key={order.id} 
          order={order}
          onCancel={() => cancelOrder.mutate(order.id)}
        />
      ))}
    </div>
  );
}

7. Context: Global State Management

Context provides a way to pass data through the component tree without prop drilling.

When to Use Context

  • Authentication state
  • Theme settings
  • User preferences
  • Any data needed by many components at different levels

Creating a Context

import { createContext, useContext, useState, type ReactNode } from 'react';

// 1. Define the shape of the context
interface AuthContextType {
  isAuthenticated: boolean;
  apiKey: string | null;
  login: (key: string) => void;
  logout: () => void;
}

// 2. Create the context with undefined as default
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// 3. Create the Provider component
export function AuthProvider({ children }: { children: ReactNode }) {
  const [apiKey, setApiKey] = useState<string | null>(() => {
    return localStorage.getItem('api_key');
  });

  const login = useCallback((key: string) => {
    localStorage.setItem('api_key', key);
    setApiKey(key);
  }, []);

  const logout = useCallback(() => {
    localStorage.removeItem('api_key');
    setApiKey(null);
  }, []);

  const value: AuthContextType = {
    isAuthenticated: !!apiKey,
    apiKey,
    login,
    logout,
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// 4. Create a custom hook for consuming the context
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Using the Context

// Wrap your app with the Provider (in app.tsx)
function App() {
  return (
    <AuthProvider>
      <SocketProvider>
        <RouterProvider router={router} />
      </SocketProvider>
    </AuthProvider>
  );
}

// Use the hook anywhere in the tree
function Navbar() {
  const { isAuthenticated, logout } = useAuth();

  return (
    <nav>
      {isAuthenticated && (
        <button onClick={logout}>Logout</button>
      )}
    </nav>
  );
}

8. React Query: Server State Management

React Query (TanStack Query) handles all server state: fetching, caching, synchronization, and updates.

Why React Query?

  • Automatic caching and background refetching
  • Loading and error states built-in
  • Automatic retries
  • Pagination and infinite scroll support
  • Optimistic updates
  • Devtools for debugging

Setup

From apps/web/src/app.tsx:

import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './lib/query-client';

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        {/* Your app */}
      </AuthProvider>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Fetching Data with useQuery

import { useQuery } from '@tanstack/react-query';

function OrdersPage() {
  const { 
    data,           // The fetched data
    isLoading,      // True during initial load
    isError,        // True if query failed
    error,          // The error object
    isFetching,     // True during any fetch (including background)
    refetch,        // Function to manually refetch
  } = useQuery({
    queryKey: ['orders', { page: 1, status: 'PENDING' }],  // Unique key for caching
    queryFn: () => apiClient.orders.list({ page: 1 }),    // Function that returns a promise
    staleTime: 5000,     // Consider data fresh for 5 seconds
    refetchOnWindowFocus: true,  // Refetch when window regains focus
  });

  if (isLoading) return <Spinner />;
  if (isError) return <Error message={error.message} />;

  return <OrderList orders={data.orders} />;
}

Mutations with useMutation

For creating, updating, or deleting data:

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

function CancelOrderButton({ orderId }: { orderId: string }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (id: string) => apiClient.orders.cancel(id),
    onSuccess: () => {
      // Invalidate queries to refetch fresh data
      queryClient.invalidateQueries({ queryKey: ['orders'] });
      toast.success('Order cancelled');
    },
    onError: (error) => {
      toast.error(`Failed: ${error.message}`);
    },
  });

  return (
    <Button
      onClick={() => mutation.mutate(orderId)}
      loading={mutation.isPending}
      disabled={mutation.isPending}
    >
      Cancel Order
    </Button>
  );
}

Query Keys

Query keys are used for: - Caching data - Invalidating/refetching related queries - Deduplicating requests

// Simple key
queryKey: ['orders']

// Key with parameters - different params = different cache
queryKey: ['orders', { page: 1, status: 'PENDING' }]

// Nested key for related data
queryKey: ['orders', orderId, 'print-jobs']

9. React Router: Navigation

React Router handles client-side navigation in SPAs.

Route Configuration

From apps/web/src/router.tsx:

import { createBrowserRouter, Navigate } from 'react-router-dom';

export const router = createBrowserRouter([
  {
    path: '/login',
    element: <LoginPage />,
  },
  {
    path: '/',
    element: (
      <ProtectedRoute>
        <RootLayout />
      </ProtectedRoute>
    ),
    children: [
      { index: true, element: <DashboardPage /> },  // Matches exactly "/"
      { path: 'orders', element: <OrdersPage /> },
      { path: 'orders/:id', element: <OrderDetailPage /> },  // Dynamic segment
      { path: 'mappings', element: <MappingsPage /> },
      { path: 'mappings/new', element: <NewMappingPage /> },
    ],
  },
  { path: '*', element: <NotFoundPage /> },  // Catch-all
]);
import { Link, useNavigate } from 'react-router-dom';

function Navigation() {
  const navigate = useNavigate();

  return (
    <nav>
      {/* Declarative navigation */}
      <Link to="/orders">Orders</Link>
      <Link to={`/orders/${orderId}`}>View Order</Link>

      {/* Programmatic navigation */}
      <button onClick={() => navigate('/orders')}>Go to Orders</button>
      <button onClick={() => navigate(-1)}>Go Back</button>
    </nav>
  );
}

Reading URL Parameters

import { useParams, useSearchParams } from 'react-router-dom';

// For route: /orders/:id
function OrderDetailPage() {
  const { id } = useParams<{ id: string }>();

  // id is the value from the URL, e.g., "abc123"
  const { data } = useOrder(id!);

  return <OrderDetail order={data} />;
}

// For query strings: /orders?page=2&status=PENDING
function OrdersPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const page = Number(searchParams.get('page')) || 1;
  const status = searchParams.get('status') || '';

  // Update query string
  const goToPage = (newPage: number) => {
    setSearchParams({ page: String(newPage), status });
  };
}

Outlet for Nested Routes

The Outlet component renders child routes:

function RootLayout() {
  return (
    <div>
      <Header />
      <Sidebar />
      <main>
        <Outlet />  {/* Child route components render here */}
      </main>
    </div>
  );
}

Protected Routes

From our codebase:

function ProtectedRoute({ children }: { children: ReactNode }) {
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

10. Project Structure and Conventions

Directory Structure

apps/web/src/
├── app.tsx              # Root component with providers
├── router.tsx           # Route configuration
├── main.tsx             # Entry point
├── styles.css           # Global styles
├── components/          # Reusable components
│   ├── layout/          # Layout components (header, sidebar)
│   ├── orders/          # Order-specific components
│   └── ui/              # Generic UI components (button, card, etc.)
├── contexts/            # React contexts
│   ├── auth-context.tsx
│   └── socket-context.tsx
├── hooks/               # Custom hooks
│   ├── use-orders.ts
│   ├── use-mappings.ts
│   └── use-dashboard.ts
├── lib/                 # Utilities and configuration
│   ├── api-client.ts    # API wrapper
│   ├── query-client.ts  # React Query config
│   └── constants.ts     # App constants
├── pages/               # Page components (route handlers)
│   ├── dashboard.tsx
│   ├── orders/
│   │   ├── index.tsx    # /orders
│   │   └── [id].tsx     # /orders/:id
│   └── mappings/
│       ├── index.tsx
│       └── new.tsx
└── test/                # Test utilities
    ├── setup.ts
    ├── test-utils.tsx
    └── mocks/

Naming Conventions

Type Convention Example
Components PascalCase OrdersPage, Button
Files kebab-case use-orders.ts, auth-context.tsx
Hooks camelCase, starts with use useOrders, useAuth
Constants SCREAMING_SNAKE_CASE ORDER_STATUS_COLORS
Interfaces PascalCase OrdersQuery, ButtonProps

Project Rules (from .cursorrules)

  1. No class components - use functional components only
  2. No business logic in JSX - extract to hooks or functions
  3. No direct API calls in components - use custom hooks with React Query
  4. Hooks contain logic, components render data
  5. Max ~300 lines per file - break up larger files
  6. Explicit types - no any or unknown

11. Styling with Tailwind CSS

This project uses Tailwind CSS for styling. Instead of writing CSS files, you apply utility classes directly in JSX.

Basic Usage

function Card() {
  return (
    <div className="rounded-xl border border-slate-800 bg-slate-900/50 p-6">
      <h2 className="text-lg font-semibold text-white">Title</h2>
      <p className="mt-2 text-sm text-slate-400">Description</p>
    </div>
  );
}

Common Patterns

// Spacing
<div className="p-4">Padding all sides</div>
<div className="px-4 py-2">Padding horizontal/vertical</div>
<div className="mt-4 mb-2">Margin top/bottom</div>
<div className="space-y-4">Gap between children (vertical)</div>

// Flexbox
<div className="flex items-center justify-between gap-4">
  <span>Left</span>
  <span>Right</span>
</div>

// Grid
<div className="grid grid-cols-3 gap-4">
  <div>1</div>
  <div>2</div>
  <div>3</div>
</div>

// Responsive
<div className="p-2 md:p-4 lg:p-6">
  {/* p-2 on mobile, p-4 on medium screens, p-6 on large */}
</div>

// Hover/Focus states
<button className="bg-cyan-600 hover:bg-cyan-700 focus:ring-2">
  Click me
</button>

// Conditional classes with clsx
import { clsx } from 'clsx';

<div className={clsx(
  'base-classes',
  isActive && 'active-classes',
  variant === 'primary' && 'primary-classes'
)}>

Color Palette

Our app uses the Slate color palette for dark theme:

bg-slate-950  - Darkest (main background)
bg-slate-900  - Card backgrounds
bg-slate-800  - Borders, dividers
text-white    - Primary text
text-slate-400 - Secondary text
text-slate-500 - Muted text
bg-cyan-600   - Primary accent

12. Testing React Components

We use Vitest and React Testing Library for testing.

Basic Component Test

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from './button';

describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('applies variant classes', () => {
    render(<Button variant="danger">Delete</Button>);
    expect(screen.getByRole('button')).toHaveClass('bg-red-600');
  });
});

Testing Hooks

import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useOrders } from './use-orders';

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return function Wrapper({ children }: { children: ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <AuthProvider>{children}</AuthProvider>
      </QueryClientProvider>
    );
  };
}

describe('useOrders', () => {
  it('fetches orders', async () => {
    const { result } = renderHook(() => useOrders(), {
      wrapper: createWrapper(),
    });

    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true);
    });

    expect(result.current.data?.orders).toHaveLength(2);
  });
});

User Interactions

import userEvent from '@testing-library/user-event';

it('calls onClick when clicked', async () => {
  const handleClick = vi.fn();
  const user = userEvent.setup();

  render(<Button onClick={handleClick}>Click me</Button>);

  await user.click(screen.getByRole('button'));

  expect(handleClick).toHaveBeenCalledTimes(1);
});

13. Common Patterns in This Codebase

Pattern 1: Page Component Structure

export function OrdersPage() {
  // 1. State
  const [page, setPage] = useState(1);
  const [status, setStatus] = useState<OrderStatus | ''>('');

  // 2. Data fetching
  const { data, isLoading, error } = useOrders({ page, status });

  // 3. Error handling
  if (error) {
    return <Card className="bg-red-500/10">Error: {error.message}</Card>;
  }

  // 4. Render
  return (
    <div className="space-y-6">
      {/* Header */}
      <div>
        <h1 className="text-2xl font-bold text-white">Orders</h1>
      </div>

      {/* Filters */}
      <Card padding="sm">
        <Select value={status} onChange={(e) => setStatus(e.target.value)} />
      </Card>

      {/* Content */}
      <Card padding="none">
        {isLoading ? (
          <LoadingPage />
        ) : !data?.orders.length ? (
          <EmptyState title="No orders found" />
        ) : (
          <>
            <OrderTable orders={data.orders} />
            <Pagination page={page} total={data.total} onPageChange={setPage} />
          </>
        )}
      </Card>
    </div>
  );
}

Pattern 2: Reusable UI Components

interface BadgeProps {
  variant: 'success' | 'warning' | 'error' | 'default';
  children: ReactNode;
}

export function Badge({ variant, children }: BadgeProps) {
  const variants = {
    success: 'bg-green-500/10 text-green-400',
    warning: 'bg-yellow-500/10 text-yellow-400',
    error: 'bg-red-500/10 text-red-400',
    default: 'bg-slate-500/10 text-slate-400',
  };

  return (
    <span className={clsx('px-2 py-1 rounded-full text-xs font-medium', variants[variant])}>
      {children}
    </span>
  );
}

Pattern 3: API Client Organization

// lib/api-client.ts
export const apiClient = {
  orders: {
    list: (params) => request('/api/v1/orders', { params }),
    get: (id) => request(`/api/v1/orders/${id}`),
    cancel: (id, apiKey) => request(`/api/v1/orders/${id}/cancel`, { method: 'PUT' }, apiKey),
  },
  mappings: {
    list: (params) => request('/api/v1/product-mappings', { params }),
    // ...
  },
};

Pattern 4: Custom Hook for API Calls

// 1. Query hook (for fetching)
export function useOrders(query: OrdersQuery = {}) {
  return useQuery({
    queryKey: ['orders', query],
    queryFn: () => apiClient.orders.list(query),
  });
}

// 2. Mutation hook (for modifying)
export function useCancelOrder() {
  const queryClient = useQueryClient();
  const { apiKey } = useAuth();

  return useMutation({
    mutationFn: (orderId: string) => apiClient.orders.cancel(orderId, apiKey),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

14. Quick Reference

Essential Imports

// React core
import { useState, useEffect, useCallback, useContext, createContext } from 'react';
import type { ReactNode } from 'react';

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

// React Router
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';

// Utilities
import { clsx } from 'clsx';
import { format } from 'date-fns';
import toast from 'react-hot-toast';

// Project
import { apiClient } from '../lib/api-client';
import { useAuth } from '../contexts/auth-context';

Cheat Sheet

What you want How to do it
Store component state const [value, setValue] = useState(initial)
Fetch data const { data } = useQuery({ queryKey, queryFn })
Modify data const { mutate } = useMutation({ mutationFn })
Navigate programmatically const navigate = useNavigate()
Read URL param const { id } = useParams()
Access auth state const { apiKey } = useAuth()
Conditional class className={clsx('base', condition && 'extra')}
Show toast toast.success('Message') or toast.error('Error')
Format date format(new Date(date), 'MMM d, yyyy')

Running the App

# Start development server
pnpm nx serve web

# Run tests
pnpm nx test web

# Build for production
pnpm nx build web

Next Steps

  1. Explore the codebase: Start with apps/web/src/pages/ to see full page implementations
  2. Read the hooks: Check apps/web/src/hooks/ to understand data fetching patterns
  3. Study components: Look at apps/web/src/components/ui/ for reusable component patterns
  4. Run the app: pnpm nx serve web and click around to understand the user flows
  5. Make small changes: Try adding a new filter or modifying a component's styling
  6. Write tests: Look at __tests__/ folders for testing patterns

Resources