ADR-0015: React 18 as Frontend Framework
Status
Accepted - 2025-01-26
Context
TVL Platform needs a frontend framework for the web application with the following requirements:
Business Requirements
- Fast time-to-market (build MVP.0 in < 3 months)
- Rich user interfaces (complex forms, calendars, dashboards)
- Large hiring pool (easy to find developers)
Technical Requirements
- Component-based architecture (reusable UI)
- TypeScript support (type safety)
- Server-side rendering (SEO, performance)
- Excellent developer experience (hot reload, debugging)
- Rich ecosystem (UI libraries, state management, routing)
Constraints
- TypeScript 5.3+ (ADR-0005)
- Next.js 14 planned for meta-framework (ADR-0016)
- Must work with Tailwind CSS (ADR-0017)
- Team has React experience
Decision
React 18 as the frontend framework for the web application.
Rationale
- Industry Standard: Most popular frontend framework (40%+ market share)
- TypeScript Support: First-class TypeScript support
- Rich Ecosystem: 100,000+ React packages on npm
- Hiring: Largest talent pool (React is #2 most wanted framework - Stack Overflow 2024)
- Next.js Compatibility: Next.js built on React (seamless integration)
Alternatives Considered
Alternative 1: Vue 3
Rejected
Pros:
- Simpler learning curve
- Good TypeScript support
- Excellent documentation
- Composition API similar to React Hooks
Cons:
- Smaller Ecosystem: Fewer libraries and components
- Smaller Hiring Pool: Harder to find Vue developers
- Less Momentum: React has more growth and investment
- Team Unfamiliar: Team has React experience, not Vue
Decision: React larger ecosystem and hiring pool.
Alternative 2: Svelte / SvelteKit
Rejected
Pros:
- Smallest bundle size (compiles to vanilla JS)
- Excellent performance (no virtual DOM)
- Simple syntax (less boilerplate)
- Growing momentum
Cons:
- Tiny Ecosystem: Far fewer libraries and components
- Very Small Hiring Pool: Very hard to find Svelte developers
- Immature Ecosystem: Newer, less battle-tested
- TypeScript Support: Not as mature as React
Decision: Too risky for production app, small hiring pool.
Alternative 3: Angular
Rejected
Pros:
- Full framework (includes routing, state, forms)
- Excellent TypeScript support
- Enterprise-proven
Cons:
- Heavy: Large bundle size, slow builds
- Steep Learning Curve: RxJS, dependency injection
- Declining Popularity: Losing market share to React/Vue
- Team Unfamiliar: Team has React experience, not Angular
Decision: React more flexible and lighter weight.
Alternative 4: Solid.js
Rejected
Pros:
- Fastest performance (fine-grained reactivity)
- React-like API (easy migration)
- Small bundle size
- No virtual DOM
Cons:
- Tiny Ecosystem: Very few libraries
- Extremely Small Hiring Pool: Almost impossible to hire
- Immature: New framework (2021)
- Risk: Not proven at scale
Decision: Too new and risky, no hiring pool.
Consequences
Positive
- 
Developer Experience - React DevTools (excellent debugging)
- Fast Refresh (instant hot reload)
- Great error messages
- Excellent IDE support (VS Code, Cursor)
 
- 
Ecosystem - UI libraries (Radix, Headless UI, Material UI)
- State management (Zustand, Redux, Jotai)
- Forms (React Hook Form, Formik)
- Routing (React Router, Next.js)
- Testing (React Testing Library)
 
- 
Performance - Concurrent features (Suspense, transitions)
- Server Components (Next.js 14)
- Streaming SSR
- Automatic batching
 
- 
Hiring - Largest React developer pool
- Easy to train JavaScript developers
- Many bootcamps teach React
 
- 
TypeScript - Excellent type inference
- @types/reactcomprehensive
- JSX type checking
 
Negative
- 
Learning Curve - Hooks can be confusing (useEffect, useMemo)
- Mitigation: Team already knows React
 
- 
Boilerplate - More code than Vue/Svelte for same result
- Mitigation: Use libraries (Shadcn UI reduces boilerplate)
 
- 
Choice Overload - Too many libraries (decision fatigue)
- Mitigation: ADRs document our choices
 
- 
Bundle Size - React runtime ~40KB (larger than Svelte)
- Mitigation: Next.js automatic code splitting
 
React 18 Features Used
1. Concurrent Features
import { Suspense, lazy } from 'react';
// Lazy load components
const BookingCalendar = lazy(() => import('./BookingCalendar'));
function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <BookingCalendar />
    </Suspense>
  );
}
2. Automatic Batching
// React 18 automatically batches these updates
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Only one re-render (React 17 would re-render twice)
}
3. Transitions
import { useTransition } from 'react';
function SearchForm() {
  const [isPending, startTransition] = useTransition();
  const [searchQuery, setSearchQuery] = useState('');
  function handleChange(e) {
    // Mark as low-priority update
    startTransition(() => {
      setSearchQuery(e.target.value);
    });
  }
  return (
    <input value={searchQuery} onChange={handleChange} />
    {isPending && <Spinner />}
  );
}
4. Server Components (Next.js 14)
// app/bookings/page.tsx (Server Component by default)
async function BookingsPage() {
  // Fetch on server (no client-side fetch needed)
  const bookings = await db.query.bookings.findMany();
  return (
    <div>
      {bookings.map(booking => (
        <BookingCard key={booking.id} booking={booking} />
      ))}
    </div>
  );
}
Component Patterns
1. Functional Components with Hooks
import { useState, useEffect } from 'react';
export function BookingForm() {
  const [guestName, setGuestName] = useState('');
  const [errors, setErrors] = useState<string[]>([]);
  useEffect(() => {
    // Validate on mount
    validateForm();
  }, []);
  return (
    <form>
      <input
        value={guestName}
        onChange={(e) => setGuestName(e.target.value)}
      />
      {errors.length > 0 && (
        <div className="text-red-500">{errors.join(', ')}</div>
      )}
    </form>
  );
}
2. Custom Hooks
// hooks/useBooking.ts
export function useBooking(bookingId: string) {
  const [booking, setBooking] = useState<Booking | null>(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetchBooking(bookingId).then(setBooking).finally(() => setLoading(false));
  }, [bookingId]);
  return { booking, loading };
}
// Usage
function BookingDetails({ bookingId }: { bookingId: string }) {
  const { booking, loading } = useBooking(bookingId);
  if (loading) return <Spinner />;
  return <div>{booking.guestName}</div>;
}
3. Compound Components
// components/Tabs.tsx
export function Tabs({ children }: { children: React.ReactNode }) {
  const [activeTab, setActiveTab] = useState(0);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
}
Tabs.List = function TabsList({ children }) {
  return <div className="flex gap-2">{children}</div>;
};
Tabs.Tab = function Tab({ index, children }) {
  const { activeTab, setActiveTab } = useTabsContext();
  return (
    <button
      className={activeTab === index ? 'active' : ''}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
};
// Usage
<Tabs>
  <Tabs.List>
    <Tabs.Tab index={0}>Overview</Tabs.Tab>
    <Tabs.Tab index={1}>Details</Tabs.Tab>
  </Tabs.List>
</Tabs>
TypeScript Integration
// types/booking.ts
export interface Booking {
  id: string;
  guestName: string;
  checkIn: Date;
  checkOut: Date;
  totalCents: number;
  status: 'pending' | 'confirmed' | 'cancelled';
}
// components/BookingCard.tsx
interface BookingCardProps {
  booking: Booking;
  onConfirm?: (id: string) => void;
}
export function BookingCard({ booking, onConfirm }: BookingCardProps) {
  return (
    <div>
      <h3>{booking.guestName}</h3>
      <p>{booking.status}</p>
      {onConfirm && (
        <button onClick={() => onConfirm(booking.id)}>
          Confirm
        </button>
      )}
    </div>
  );
}
Testing with React Testing Library
// BookingCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { BookingCard } from './BookingCard';
describe('BookingCard', () => {
  const mockBooking: Booking = {
    id: '123',
    guestName: 'John Doe',
    checkIn: new Date('2025-02-01'),
    checkOut: new Date('2025-02-05'),
    totalCents: 50000,
    status: 'pending'
  };
  it('should render booking details', () => {
    render(<BookingCard booking={mockBooking} />);
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('pending')).toBeInTheDocument();
  });
  it('should call onConfirm when button clicked', () => {
    const onConfirm = vi.fn();
    render(<BookingCard booking={mockBooking} onConfirm={onConfirm} />);
    fireEvent.click(screen.getByText('Confirm'));
    expect(onConfirm).toHaveBeenCalledWith('123');
  });
});
Performance Optimization
1. React.memo (Prevent Re-renders)
export const BookingCard = React.memo(function BookingCard({ booking }) {
  return <div>{booking.guestName}</div>;
});
2. useMemo (Expensive Calculations)
function BookingList({ bookings }: { bookings: Booking[] }) {
  const totalRevenue = useMemo(() => {
    return bookings.reduce((sum, b) => sum + b.totalCents, 0);
  }, [bookings]);
  return <div>Total: ${totalRevenue / 100}</div>;
}
3. useCallback (Stable Function References)
function BookingList({ onSelect }: { onSelect: (id: string) => void }) {
  const handleClick = useCallback((id: string) => {
    onSelect(id);
  }, [onSelect]);
  return <BookingCard onClick={handleClick} />;
}
Validation Checklist
- React 18 installed
-  TypeScript types configured (@types/react)
- React DevTools installed (browser extension)
- Functional components with hooks (no class components)
- Custom hooks for reusable logic
- React Testing Library configured
- Performance optimizations (memo, useMemo, useCallback)
- Concurrent features enabled (Suspense)