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¶
- What is React?
- React Core Concepts
- Components: The Building Blocks
- Props: Passing Data to Components
- Hooks: Adding Behavior to Components
- Custom Hooks: Reusable Logic
- Context: Global State Management
- React Query: Server State Management
- React Router: Navigation
- Project Structure and Conventions
- Styling with Tailwind CSS
- Testing React Components
- Common Patterns in This Codebase
- 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¶
- Name must start with uppercase:
OrdersPage, notordersPage - 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
]);
Navigation Components¶
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)¶
- No class components - use functional components only
- No business logic in JSX - extract to hooks or functions
- No direct API calls in components - use custom hooks with React Query
- Hooks contain logic, components render data
- Max ~300 lines per file - break up larger files
- Explicit types - no
anyorunknown
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¶
- Explore the codebase: Start with
apps/web/src/pages/to see full page implementations - Read the hooks: Check
apps/web/src/hooks/to understand data fetching patterns - Study components: Look at
apps/web/src/components/ui/for reusable component patterns - Run the app:
pnpm nx serve weband click around to understand the user flows - Make small changes: Try adding a new filter or modifying a component's styling
- Write tests: Look at
__tests__/folders for testing patterns