Skip to main content
React is the most popular JavaScript library for building user interfaces. Created by Facebook, React revolutionized frontend development with its component-based architecture and virtual DOM. This comprehensive guide shows you how to build production-ready React SPAs with Mizu as your backend.

Why React?

React has become the industry standard for frontend development: Component-Based - Build encapsulated components that manage their own state, then compose them into complex UIs. Declarative - Design simple views for each state in your application, and React efficiently updates and renders the right components when data changes. Learn Once, Write Anywhere - React doesn’t make assumptions about your tech stack, so you can develop new features without rewriting existing code. Huge Ecosystem - The largest library ecosystem in frontend development. Whatever you need, there’s probably a package for it. Strong TypeScript Support - First-class TypeScript integration for type-safe components and APIs. Industry Adoption - Used by Facebook, Instagram, Netflix, Airbnb, and thousands of other companies.

React vs Other Frameworks

FeatureReactVueSvelteAngular
Bundle Size~45kB~35kB~2kB~150kB
Learning Curve⚠️ Moderateβœ… Easyβœ… Easy❌ Steep
Performance⚑ Fast⚑ Fast⚑⚑ Fastest⚑ Fast
Ecosystemβœ… Huge⚑ Large⚠️ Growing⚑ Large
TypeScriptβœ… Excellentβœ… Excellentβœ… Excellentβœ… Native
Jobsβœ… Most⚑ Many⚠️ Growing⚑ Many
Best forSPAs, large appsAllPerformanceEnterprise
Communityβœ… Largest⚑ Large⚠️ Growing⚑ Large
CompanyMeta (Facebook)IndependentIndependentGoogle
Choose React when:
  • You need the largest ecosystem and community
  • TypeScript integration is important
  • Team has React experience
  • Building a complex, interactive SPA
  • Job market considerations matter
Choose something else when:
  • Bundle size is critical β†’ Use Preact or Svelte
  • Learning curve matters β†’ Try Vue
  • Need full framework β†’ Use Next.js
  • Need SSR β†’ Use Next.js

Quick Start

Create a new React project with the CLI:
mizu new ./my-react-app --template frontend/react
cd my-react-app
make dev
Visit http://localhost:3000 to see your app!

Project Structure

my-react-app/
β”œβ”€β”€ cmd/
β”‚   └── server/
β”‚       └── main.go              # Go entry point
β”œβ”€β”€ app/
β”‚   └── server/
β”‚       β”œβ”€β”€ app.go               # Mizu app configuration
β”‚       β”œβ”€β”€ config.go            # Server configuration
β”‚       └── routes.go            # API routes (Go)
β”œβ”€β”€ frontend/                      # React application
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ main.tsx            # React entry point
β”‚   β”‚   β”œβ”€β”€ App.tsx             # Root component
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ Layout.tsx      # Layout component
β”‚   β”‚   β”‚   β”œβ”€β”€ Header.tsx
β”‚   β”‚   β”‚   └── Footer.tsx
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ Home.tsx        # Home page
β”‚   β”‚   β”‚   β”œβ”€β”€ About.tsx       # About page
β”‚   β”‚   β”‚   └── Users.tsx       # Users page
β”‚   β”‚   β”œβ”€β”€ hooks/              # Custom hooks
β”‚   β”‚   β”‚   β”œβ”€β”€ useUsers.ts
β”‚   β”‚   β”‚   └── useAuth.ts
β”‚   β”‚   β”œβ”€β”€ contexts/           # React contexts
β”‚   β”‚   β”‚   └── AuthContext.tsx
β”‚   β”‚   β”œβ”€β”€ types/              # TypeScript types
β”‚   β”‚   β”‚   └── user.ts
β”‚   β”‚   └── styles/
β”‚   β”‚       └── index.css       # Global styles
β”‚   β”œβ”€β”€ public/
β”‚   β”‚   └── vite.svg           # Public assets
β”‚   β”œβ”€β”€ index.html             # HTML template
β”‚   β”œβ”€β”€ package.json           # npm dependencies
β”‚   β”œβ”€β”€ vite.config.ts         # Vite configuration
β”‚   β”œβ”€β”€ tsconfig.json          # TypeScript config
β”‚   └── tsconfig.node.json     # TypeScript for Vite
β”œβ”€β”€ dist/                       # Built files (after build)
β”œβ”€β”€ go.mod
└── Makefile

How React Works with Mizu

When you build a React app with Mizu:
Development Mode
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Vite Dev Server (5173)      β”‚
β”‚ - Hot Module Replacement    β”‚
β”‚ - TypeScript compilation    β”‚
β”‚ - Fast bundling             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Mizu Server (3000)          β”‚
β”‚ - Proxies to Vite           β”‚
β”‚ - Handles /api requests     β”‚
β”‚ - Serves WebSocket for HMR  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Production Mode
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ npm run build               β”‚
β”‚ - Optimizes & minifies      β”‚
β”‚ - Code splitting            β”‚
β”‚ - Tree shaking              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Embedded in Go Binary       β”‚
β”‚ - All files in single bin   β”‚
β”‚ - Served by Mizu            β”‚
β”‚ - No Node.js needed         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
At runtime in production:
  1. User requests http://yourdomain.com
  2. Mizu serves index.html from embedded FS
  3. Browser loads React bundle
  4. React hydrates and takes over
  5. Client-side routing handles navigation
  6. API calls go to Mizu Go handlers

Backend Setup

app/server/app.go

package server

import (
    "embed"
    "io/fs"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/frontend"
)

//go:embed all:../../dist
var distFS embed.FS

func New(cfg *Config) *mizu.App {
    app := mizu.New()

    // API routes come first
    setupRoutes(app)

    // Frontend middleware (handles all non-API routes)
    dist, _ := fs.Sub(distFS, "dist")
    app.Use(frontend.WithOptions(frontend.Options{
        Mode:        frontend.ModeAuto,       // Auto-detect dev/prod
        FS:          dist,                     // Embedded files
        Root:        "./dist",                 // Fallback to filesystem in dev
        DevServer:   "http://localhost:" + cfg.DevPort,  // Vite dev server
        IgnorePaths: []string{"/api"},        // Don't proxy /api to Vite
    }))

    return app
}

app/server/routes.go

package server

import "github.com/go-mizu/mizu"

func setupRoutes(app *mizu.App) {
    // User endpoints
    app.Get("/api/users", handleUsers)
    app.Post("/api/users", createUser)
    app.Get("/api/users/{id}", getUser)
    app.Put("/api/users/{id}", updateUser)
    app.Delete("/api/users/{id}", deleteUser)

    // Auth endpoints
    app.Post("/api/auth/login", handleLogin)
    app.Post("/api/auth/logout", handleLogout)
    app.Get("/api/auth/me", handleMe)
}

func handleUsers(c *mizu.Ctx) error {
    users := []map[string]any{
        {"id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin"},
        {"id": 2, "name": "Bob", "email": "bob@example.com", "role": "user"},
        {"id": 3, "name": "Charlie", "email": "charlie@example.com", "role": "user"},
    }
    return c.JSON(200, users)
}

func createUser(c *mizu.Ctx) error {
    var user map[string]any
    if err := c.BodyJSON(&user); err != nil {
        return c.JSON(400, map[string]string{"error": "Invalid JSON"})
    }

    // Validate
    if user["name"] == nil || user["email"] == nil {
        return c.JSON(400, map[string]string{"error": "Name and email required"})
    }

    // In real app, save to database
    user["id"] = 4
    user["role"] = "user"

    return c.JSON(201, user)
}

func getUser(c *mizu.Ctx) error {
    id := c.Param("id")
    user := map[string]any{
        "id":    id,
        "name":  "User " + id,
        "email": "user" + id + "@example.com",
        "role":  "user",
    }
    return c.JSON(200, user)
}

Frontend Setup

Entry Point

frontend/src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './styles/index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
Why StrictMode?
  • Highlights potential problems in components
  • Warns about deprecated APIs
  • Detects unexpected side effects
  • Only in development, no production overhead

Root Component

frontend/src/App.tsx

import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import About from './pages/About'
import Users from './pages/Users'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

export default App

Layout Component

frontend/src/components/Layout.tsx

import { Link, Outlet } from 'react-router-dom'

export default function Layout() {
  return (
    <div className="app">
      <header>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/about">About</Link>
          <Link to="/users">Users</Link>
        </nav>
      </header>

      <main>
        <Outlet />
      </main>

      <footer>
        <p>Built with Mizu and React</p>
      </footer>
    </div>
  )
}

React Hooks Deep Dive

React Hooks let you use state and other React features in function components.

useState

Manage component state:
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

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

      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    </div>
  )
}
Functional updates:
// ❌ Don't do this with previous state
setCount(count + 1)

// βœ… Use functional update
setCount(prev => prev + 1)

useEffect

Perform side effects:
import { useState, useEffect } from 'react'

function Users() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)

  // Run once on mount
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data)
        setLoading(false)
      })
  }, [])  // Empty deps = run once

  // Run when users changes
  useEffect(() => {
    console.log('Users updated:', users.length)
  }, [users])

  // Cleanup
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('Tick')
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return <div>{/* render users */}</div>
}
Common patterns:
// Fetch on param change
useEffect(() => {
  fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(setUser)
}, [id])

// Subscribe to events
useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth)
  }

  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)
}, [])

// Debounced effect
useEffect(() => {
  const timer = setTimeout(() => {
    // Do something with searchTerm
  }, 500)

  return () => clearTimeout(timer)
}, [searchTerm])

useContext

Access context values:
import { createContext, useContext, ReactNode } from 'react'

interface Theme {
  primary: string
  secondary: string
}

const ThemeContext = createContext<Theme>({
  primary: '#007bff',
  secondary: '#6c757d'
})

export function ThemeProvider({ children }: { children: ReactNode }) {
  const theme = {
    primary: '#007bff',
    secondary: '#6c757d'
  }

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  return useContext(ThemeContext)
}

// Usage
function Button() {
  const theme = useTheme()
  return <button style={{ background: theme.primary }}>Click</button>
}

useReducer

Manage complex state:
import { useReducer } from 'react'

interface State {
  count: number
  text: string
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setText'; payload: string }
  | { type: 'reset' }

const initialState: State = {
  count: 0,
  text: ''
}

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 }
    case 'decrement':
      return { ...state, count: state.count - 1 }
    case 'setText':
      return { ...state, text: action.payload }
    case 'reset':
      return initialState
    default:
      return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>

      <input
        value={state.text}
        onChange={e => dispatch({ type: 'setText', payload: e.target.value })}
      />
    </div>
  )
}

useMemo

Memoize expensive calculations:
import { useMemo, useState } from 'react'

function ExpensiveComponent({ items }: { items: number[] }) {
  const [filter, setFilter] = useState(0)

  // Only recalculates when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering...')
    return items.filter(item => item > filter)
  }, [items, filter])

  const total = useMemo(() => {
    console.log('Calculating total...')
    return filteredItems.reduce((sum, item) => sum + item, 0)
  }, [filteredItems])

  return (
    <div>
      <input
        type="number"
        value={filter}
        onChange={e => setFilter(Number(e.target.value))}
      />
      <p>Filtered: {filteredItems.length}</p>
      <p>Total: {total}</p>
    </div>
  )
}

useCallback

Memoize callback functions:
import { useCallback, useState, memo } from 'react'

interface ItemProps {
  id: number
  onDelete: (id: number) => void
}

// Memoized child component
const Item = memo(({ id, onDelete }: ItemProps) => {
  console.log('Rendering item', id)
  return <button onClick={() => onDelete(id)}>Delete {id}</button>
})

function List() {
  const [items, setItems] = useState([1, 2, 3])

  // Without useCallback, this creates a new function on every render
  // causing all Item components to re-render
  const handleDelete = useCallback((id: number) => {
    setItems(prev => prev.filter(item => item !== id))
  }, [])

  return (
    <div>
      {items.map(id => (
        <Item key={id} id={id} onDelete={handleDelete} />
      ))}
    </div>
  )
}

useRef

Reference DOM elements or persist values:
import { useRef, useEffect } from 'react'

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)
  const renderCount = useRef(0)

  useEffect(() => {
    // Focus input on mount
    inputRef.current?.focus()

    // Track renders (doesn't cause re-render)
    renderCount.current++
    console.log('Rendered', renderCount.current, 'times')
  })

  return <input ref={inputRef} />
}

// Storing previous value
function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = useRef(0)

  useEffect(() => {
    prevCount.current = count
  }, [count])

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount.current}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

Custom Hooks

Reuse stateful logic:
// hooks/useUsers.ts
import { useState, useEffect } from 'react'

interface User {
  id: number
  name: string
  email: string
}

export function useUsers() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    fetch('/api/users')
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch')
        return res.json()
      })
      .then(data => {
        setUsers(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err)
        setLoading(false)
      })
  }, [])

  const addUser = async (user: Omit<User, 'id'>) => {
    const res = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user)
    })
    const newUser = await res.json()
    setUsers([...users, newUser])
    return newUser
  }

  const deleteUser = async (id: number) => {
    await fetch(`/api/users/${id}`, { method: 'DELETE' })
    setUsers(users.filter(u => u.id !== id))
  }

  const updateUser = async (id: number, updates: Partial<User>) => {
    const res = await fetch(`/api/users/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates)
    })
    const updated = await res.json()
    setUsers(users.map(u => u.id === id ? updated : u))
    return updated
  }

  return { users, loading, error, addUser, deleteUser, updateUser }
}
Usage:
import { useUsers } from './hooks/useUsers'

function UserList() {
  const { users, loading, error, deleteUser } = useUsers()

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => deleteUser(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}
More custom hooks:
// hooks/useDebounce.ts
import { useState, useEffect } from 'react'

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

// Usage
function Search() {
  const [search, setSearch] = useState('')
  const debouncedSearch = useDebounce(search, 500)

  useEffect(() => {
    if (debouncedSearch) {
      // Perform search
      fetch(`/api/search?q=${debouncedSearch}`)
    }
  }, [debouncedSearch])

  return <input value={search} onChange={e => setSearch(e.target.value)} />
}
// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react'

export function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch {
      // Handle error
    }
  }, [key, value])

  return [value, setValue] as const
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')

  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  )
}

React Router

Basic Routing

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<User />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  )
}

Nested Routes

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />

          <Route path="users" element={<UsersLayout />}>
            <Route index element={<UsersList />} />
            <Route path=":id" element={<UserDetail />} />
            <Route path=":id/edit" element={<UserEdit />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

function UsersLayout() {
  return (
    <div>
      <h1>Users</h1>
      <Outlet />  {/* Child routes render here */}
    </div>
  )
}

Route Parameters

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

function UserDetail() {
  const { id } = useParams<{ id: string }>()
  const [searchParams, setSearchParams] = useSearchParams()

  const tab = searchParams.get('tab') || 'profile'

  return (
    <div>
      <h1>User {id}</h1>
      <button onClick={() => setSearchParams({ tab: 'posts' })}>
        View Posts
      </button>
      {tab === 'profile' && <Profile />}
      {tab === 'posts' && <Posts />}
    </div>
  )
}

Programmatic Navigation

import { useNavigate } from 'react-router-dom'

function CreateUser() {
  const navigate = useNavigate()

  const handleSubmit = async (data) => {
    const user = await createUser(data)
    navigate(`/users/${user.id}`)  // Navigate to new user
  }

  return <form onSubmit={handleSubmit}>...</form>
}

// Go back
function BackButton() {
  const navigate = useNavigate()
  return <button onClick={() => navigate(-1)}>Back</button>
}

Protected Routes

import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from './hooks/useAuth'

function ProtectedRoute() {
  const { user } = useAuth()

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

  return <Outlet />
}

function App() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />

      <Route element={<ProtectedRoute />}>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Route>
    </Routes>
  )
}

State Management

Context API

Built-in state management:
// contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'

interface User {
  id: number
  name: string
  email: string
}

interface AuthContextType {
  user: User | null
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  isAuthenticated: boolean
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null)

  const login = async (email: string, password: string) => {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    if (res.ok) {
      const data = await res.json()
      setUser(data.user)
    } else {
      throw new Error('Login failed')
    }
  }

  const logout = () => {
    fetch('/api/auth/logout', { method: 'POST' })
    setUser(null)
  }

  return (
    <AuthContext.Provider value={{
      user,
      login,
      logout,
      isAuthenticated: !!user
    }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Zustand

Lightweight state management:
npm install zustand
// stores/userStore.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface User {
  id: number
  name: string
  email: string
}

interface UserState {
  users: User[]
  loading: boolean
  error: string | null

  fetchUsers: () => Promise<void>
  addUser: (user: Omit<User, 'id'>) => Promise<void>
  deleteUser: (id: number) => Promise<void>
  updateUser: (id: number, updates: Partial<User>) => Promise<void>
}

export const useUserStore = create<UserState>()(
  devtools(
    persist(
      (set, get) => ({
        users: [],
        loading: false,
        error: null,

        fetchUsers: async () => {
          set({ loading: true, error: null })
          try {
            const res = await fetch('/api/users')
            const users = await res.json()
            set({ users, loading: false })
          } catch (error) {
            set({ error: (error as Error).message, loading: false })
          }
        },

        addUser: async (user) => {
          const res = await fetch('/api/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(user)
          })
          const newUser = await res.json()
          set({ users: [...get().users, newUser] })
        },

        deleteUser: async (id) => {
          await fetch(`/api/users/${id}`, { method: 'DELETE' })
          set({ users: get().users.filter(u => u.id !== id) })
        },

        updateUser: async (id, updates) => {
          const res = await fetch(`/api/users/${id}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(updates)
          })
          const updated = await res.json()
          set({
            users: get().users.map(u => u.id === id ? updated : u)
          })
        }
      }),
      { name: 'user-storage' }
    )
  )
)
Usage:
import { useUserStore } from './stores/userStore'
import { useEffect } from 'react'

function Users() {
  const { users, loading, fetchUsers, deleteUser } = useUserStore()

  useEffect(() => {
    fetchUsers()
  }, [])

  if (loading) return <div>Loading...</div>

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => deleteUser(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}

Data Fetching

React Query

Install:
npm install @tanstack/react-query
Setup:
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      retry: 1,
      staleTime: 5 * 60 * 1000, // 5 minutes
    },
  },
})

ReactDOM.createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
)
Usage:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

interface User {
  id: number
  name: string
  email: string
}

function Users() {
  const queryClient = useQueryClient()

  // Fetch users
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users')
      if (!res.ok) throw new Error('Failed to fetch')
      return res.json() as Promise<User[]>
    }
  })

  // Create user mutation
  const createMutation = useMutation({
    mutationFn: async (user: Omit<User, 'id'>) => {
      const res = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(user)
      })
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    }
  })

  // Delete user mutation
  const deleteMutation = useMutation({
    mutationFn: async (id: number) => {
      await fetch(`/api/users/${id}`, { method: 'DELETE' })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    }
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <div>
      <ul>
        {users?.map(user => (
          <li key={user.id}>
            {user.name}
            <button
              onClick={() => deleteMutation.mutate(user.id)}
              disabled={deleteMutation.isPending}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>

      <button
        onClick={() => createMutation.mutate({ name: 'New', email: 'new@example.com' })}
        disabled={createMutation.isPending}
      >
        Add User
      </button>
    </div>
  )
}

SWR

Alternative to React Query:
npm install swr
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

function Users() {
  const { data, error, isLoading, mutate } = useSWR('/api/users', fetcher)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error</div>

  return (
    <div>
      {data.map(user => <div key={user.id}>{user.name}</div>)}
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  )
}

Form Handling

React Hook Form

Install:
npm install react-hook-form
Basic usage:
import { useForm } from 'react-hook-form'

interface FormData {
  name: string
  email: string
  age: number
}

function UserForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>()

  const onSubmit = async (data: FormData) => {
    const res = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })

    if (res.ok) {
      console.log('User created!')
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('name', {
          required: 'Name is required',
          minLength: { value: 2, message: 'Minimum 2 characters' }
        })}
        placeholder="Name"
      />
      {errors.name && <span>{errors.name.message}</span>}

      <input
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'Invalid email'
          }
        })}
        placeholder="Email"
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="number"
        {...register('age', {
          required: 'Age is required',
          min: { value: 18, message: 'Must be 18+' }
        })}
        placeholder="Age"
      />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit">Submit</button>
    </form>
  )
}
With validation library:
npm install @hookform/resolvers yup
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'

const schema = yup.object({
  name: yup.string().required().min(2),
  email: yup.string().email().required(),
  age: yup.number().positive().integer().min(18).required(),
}).required()

function UserForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema)
  })

  return <form>{/* same as above */}</form>
}

Error Handling

Error Boundaries

import { Component, ReactNode, ErrorInfo } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo)
    // Log to error reporting service
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div>
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  )
}
Using react-error-boundary:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset app state
      }}
    >
      <MyComponent />
    </ErrorBoundary>
  )
}

Styling

CSS Modules

Built into Vite:
/* Button.module.css */
.button {
  background: #007bff;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:hover {
  background: #0056b3;
}

.button.primary {
  background: #28a745;
}

.button.danger {
  background: #dc3545;
}
import styles from './Button.module.css'

interface ButtonProps {
  variant?: 'primary' | 'danger'
  children: ReactNode
  onClick?: () => void
}

function Button({ variant, children, onClick }: ButtonProps) {
  const className = [
    styles.button,
    variant && styles[variant]
  ].filter(Boolean).join(' ')

  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  )
}

Tailwind CSS

Install:
cd frontend
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configure tailwind.config.js:
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: '#007bff',
        secondary: '#6c757d',
      }
    },
  },
  plugins: [],
}
Add directives to src/styles/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn {
    @apply px-4 py-2 rounded font-medium transition-colors;
  }

  .btn-primary {
    @apply bg-blue-500 text-white hover:bg-blue-600;
  }
}
Usage:
function Button({ children }) {
  return (
    <button className="btn btn-primary">
      {children}
    </button>
  )
}

function Card() {
  return (
    <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
      <h2 className="text-2xl font-bold mb-4">Title</h2>
      <p className="text-gray-600">Content</p>
    </div>
  )
}

Styled Components

Install:
npm install styled-components
npm install -D @types/styled-components
import styled from 'styled-components'

const Button = styled.button<{ variant?: 'primary' | 'danger' }>`
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: ${props => {
    switch (props.variant) {
      case 'primary': return '#007bff'
      case 'danger': return '#dc3545'
      default: return '#6c757d'
    }
  }};
  color: white;

  &:hover {
    opacity: 0.9;
  }
`

const Card = styled.div`
  background: white;
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`

function MyComponent() {
  return (
    <Card>
      <h1>Title</h1>
      <Button variant="primary">Click me</Button>
    </Card>
  )
}

Vite Configuration

frontend/vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [
    react({
      // Enable Fast Refresh
      fastRefresh: true,
      // Babel options
      babel: {
        plugins: [
          // Add babel plugins if needed
        ]
      }
    })
  ],

  // Path aliases
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@pages': path.resolve(__dirname, './src/pages'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
    }
  },

  // Development server
  server: {
    port: 5173,
    strictPort: true,
    hmr: {
      clientPort: 3000,  // Mizu's port for HMR WebSocket
    },
    // Proxy API requests in development (alternative to Mizu proxy)
    // proxy: {
    //   '/api': {
    //     target: 'http://localhost:3000',
    //     changeOrigin: true,
    //   }
    // }
  },

  // Production build
  build: {
    outDir: '../dist',
    emptyOutDir: true,
    sourcemap: false, // Enable for debugging

    // Code splitting
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'router': ['react-router-dom'],
          // Split large libraries
          'query': ['@tanstack/react-query'],
        },
      },
    },

    // Chunk size warnings
    chunkSizeWarningLimit: 500,

    // Minification
    minify: 'esbuild', // or 'terser' for better compression
  },

  // Optimize deps
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom'],
  },
})
Then use path aliases in components:
import Button from '@components/Button'
import { useUsers } from '@hooks/useUsers'
import Home from '@pages/Home'
Update tsconfig.json:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@pages/*": ["./src/pages/*"],
      "@hooks/*": ["./src/hooks/*"]
    }
  }
}

Development Workflow

Start Development

# Using Makefile (recommended)
make dev

# Or manually
# Terminal 1: React dev server
cd frontend && npm run dev

# Terminal 2: Mizu server
go run cmd/server/main.go
Visit http://localhost:3000

Making Changes

Frontend changes:
  1. Edit any .tsx file in frontend/src/
  2. Save the file
  3. Browser updates instantly (HMR)
  4. State is preserved during updates
Backend changes:
  1. Edit .go files
  2. Stop server (Ctrl+C)
  3. Restart with go run cmd/server/main.go
Or use air for auto-reload:
go install github.com/cosmtrek/air@latest
air

React DevTools

Install browser extension: Features:
  • Inspect component tree
  • View props and state
  • Track component updates
  • Profiler for performance

Building for Production

Build the complete app:
make build
This:
  1. Runs npm run build in frontend/
  2. Builds Go binary with go build
  3. Embeds frontend in binary
Output: ./bin/server (single executable) Run in production:
MIZU_ENV=production ./bin/server

Build Optimizations

Code splitting:
import { lazy, Suspense } from 'react'

const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/dashboard" element={
        <Suspense fallback={<div>Loading...</div>}>
          <Dashboard />
        </Suspense>
      } />
      <Route path="/settings" element={
        <Suspense fallback={<div>Loading...</div>}>
          <Settings />
        </Suspense>
      } />
    </Routes>
  )
}
Tree shaking:
// ❌ Bad - imports entire library
import * as icons from 'react-icons/fa'

// βœ… Good - imports only what you need
import { FaUser, FaHome } from 'react-icons/fa'
Bundle analysis:
cd frontend
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    })
  ]
})

Performance Optimization

React.memo

Prevent unnecessary re-renders:
import { memo } from 'react'

interface UserCardProps {
  user: { id: number; name: string }
  onDelete: (id: number) => void
}

const UserCard = memo(({ user, onDelete }: UserCardProps) => {
  console.log('Rendering user', user.id)

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={() => onDelete(user.id)}>Delete</button>
    </div>
  )
})

// Custom comparison
const UserCardOptimized = memo(
  UserCard,
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id
  }
)

Virtual Lists

For large lists, use windowing:
npm install react-window
import { FixedSizeList } from 'react-window'

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  )

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  )
}

Lazy Load Images

import { useState, useEffect, useRef } from 'react'

function LazyImage({ src, alt }) {
  const [isLoaded, setIsLoaded] = useState(false)
  const [isInView, setIsInView] = useState(false)
  const imgRef = useRef<HTMLImageElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setIsInView(true)
        observer.disconnect()
      }
    })

    if (imgRef.current) {
      observer.observe(imgRef.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <img
      ref={imgRef}
      src={isInView ? src : undefined}
      alt={alt}
      onLoad={() => setIsLoaded(true)}
      style={{ opacity: isLoaded ? 1 : 0, transition: 'opacity 0.3s' }}
    />
  )
}

Real-World Example: Task Manager

Complete task management app:

Backend

// app/server/routes.go
type Task struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
    CreatedAt string `json:"created_at"`
}

var tasks = []Task{
    {ID: 1, Title: "Learn React", Completed: true, CreatedAt: "2025-01-01"},
    {ID: 2, Title: "Build app", Completed: false, CreatedAt: "2025-01-02"},
}

func setupRoutes(app *mizu.App) {
    app.Get("/api/tasks", handleTasks)
    app.Post("/api/tasks", createTask)
    app.Put("/api/tasks/{id}", updateTask)
    app.Delete("/api/tasks/{id}", deleteTask)
}

func handleTasks(c *mizu.Ctx) error {
    return c.JSON(200, tasks)
}

func createTask(c *mizu.Ctx) error {
    var task Task
    if err := c.BodyJSON(&task); err != nil {
        return c.JSON(400, map[string]string{"error": "Invalid JSON"})
    }

    task.ID = len(tasks) + 1
    task.CreatedAt = time.Now().Format("2006-01-02")
    tasks = append(tasks, task)

    return c.JSON(201, task)
}

Frontend Store

// stores/taskStore.ts
import { create } from 'zustand'

interface Task {
  id: number
  title: string
  completed: boolean
  created_at: string
}

interface TaskState {
  tasks: Task[]
  filter: 'all' | 'active' | 'completed'
  loading: boolean

  filteredTasks: () => Task[]
  activeCount: () => number

  fetchTasks: () => Promise<void>
  addTask: (title: string) => Promise<void>
  toggleTask: (id: number) => Promise<void>
  deleteTask: (id: number) => Promise<void>
  setFilter: (filter: 'all' | 'active' | 'completed') => void
}

export const useTaskStore = create<TaskState>((set, get) => ({
  tasks: [],
  filter: 'all',
  loading: false,

  filteredTasks: () => {
    const { tasks, filter } = get()
    switch (filter) {
      case 'active':
        return tasks.filter(t => !t.completed)
      case 'completed':
        return tasks.filter(t => t.completed)
      default:
        return tasks
    }
  },

  activeCount: () => get().tasks.filter(t => !t.completed).length,

  fetchTasks: async () => {
    set({ loading: true })
    const res = await fetch('/api/tasks')
    const tasks = await res.json()
    set({ tasks, loading: false })
  },

  addTask: async (title) => {
    const res = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, completed: false })
    })
    const task = await res.json()
    set({ tasks: [...get().tasks, task] })
  },

  toggleTask: async (id) => {
    const task = get().tasks.find(t => t.id === id)
    if (!task) return

    await fetch(`/api/tasks/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...task, completed: !task.completed })
    })

    set({
      tasks: get().tasks.map(t =>
        t.id === id ? { ...t, completed: !t.completed } : t
      )
    })
  },

  deleteTask: async (id) => {
    await fetch(`/api/tasks/${id}`, { method: 'DELETE' })
    set({ tasks: get().tasks.filter(t => t.id !== id) })
  },

  setFilter: (filter) => set({ filter })
}))

Components

// App.tsx
import { useEffect } from 'react'
import { useTaskStore } from './stores/taskStore'
import TaskForm from './components/TaskForm'
import TaskFilters from './components/TaskFilters'
import TaskList from './components/TaskList'
import './App.css'

function App() {
  const fetchTasks = useTaskStore(state => state.fetchTasks)
  const loading = useTaskStore(state => state.loading)

  useEffect(() => {
    fetchTasks()
  }, [])

  if (loading) return <div className="loading">Loading tasks...</div>

  return (
    <div className="app">
      <header>
        <h1>Task Manager</h1>
      </header>

      <main>
        <TaskForm />
        <TaskFilters />
        <TaskList />
      </main>
    </div>
  )
}

export default App
// components/TaskForm.tsx
import { useState } from 'react'
import { useTaskStore } from '../stores/taskStore'

export default function TaskForm() {
  const [title, setTitle] = useState('')
  const addTask = useTaskStore(state => state.addTask)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!title.trim()) return

    await addTask(title)
    setTitle('')
  }

  return (
    <form onSubmit={handleSubmit} className="task-form">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="What needs to be done?"
        className="task-input"
      />
      <button type="submit" className="btn-add">Add Task</button>
    </form>
  )
}
// components/TaskFilters.tsx
import { useTaskStore } from '../stores/taskStore'

export default function TaskFilters() {
  const filter = useTaskStore(state => state.filter)
  const setFilter = useTaskStore(state => state.setFilter)
  const activeCount = useTaskStore(state => state.activeCount())

  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => setFilter('all')}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => setFilter('active')}
      >
        Active ({activeCount})
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => setFilter('completed')}
      >
        Completed
      </button>
    </div>
  )
}
// components/TaskList.tsx
import { useTaskStore } from '../stores/taskStore'

export default function TaskList() {
  const filteredTasks = useTaskStore(state => state.filteredTasks())
  const toggleTask = useTaskStore(state => state.toggleTask)
  const deleteTask = useTaskStore(state => state.deleteTask)

  if (filteredTasks.length === 0) {
    return <p className="empty">No tasks found</p>
  }

  return (
    <ul className="task-list">
      {filteredTasks.map(task => (
        <li key={task.id} className={task.completed ? 'completed' : ''}>
          <input
            type="checkbox"
            checked={task.completed}
            onChange={() => toggleTask(task.id)}
          />
          <span className="task-title">{task.title}</span>
          <span className="task-date">{task.created_at}</span>
          <button
            onClick={() => deleteTask(task.id)}
            className="btn-delete"
          >
            Γ—
          </button>
        </li>
      ))}
    </ul>
  )
}

Troubleshooting

HMR Not Working

Symptom: Changes don’t appear in browser Cause: HMR WebSocket not connecting through proxy Solution: Check vite.config.ts:
export default defineConfig({
  server: {
    hmr: {
      clientPort: 3000  // Must match Mizu port
    }
  }
})

White Screen / Blank Page

Symptom: Production build shows blank page Cause: Base path mismatch or routing issues Solution 1: Check console for errors Solution 2: Ensure BrowserRouter (not HashRouter) Solution 3: Check Mizu’s SPA fallback is enabled

TypeScript Errors

Error: β€œCannot find module ’./App’” Solution: Check file extensions in imports:
// βœ… Correct
import App from './App.tsx'
import App from './App'  // Also works if tsx is in resolve.extensions

// ❌ Wrong
import App from './App.ts'

404 on Page Refresh

Symptom: Direct URL works, but refresh gives 404 Cause: SPA fallback not configured Solution: Mizu handles this automatically, but ensure:
app.Use(frontend.WithOptions(frontend.Options{
    Mode: frontend.ModeAuto,
    Root: "./dist",
    Index: "index.html",  // Important for SPA fallback
}))

Large Bundle Size

Problem: Bundle is too large Solution:
  1. Analyze bundle:
npm run build
# Check dist/ folder size
  1. Enable code splitting:
const Dashboard = lazy(() => import('./Dashboard'))
  1. Check for duplicate dependencies:
npm ls react
  1. Use smaller alternatives:
  • Replace moment with date-fns
  • Replace lodash with individual functions
  • Consider Preact instead of React

When to Choose React

Choose React When:

βœ… You need the largest ecosystem and community βœ… Building a complex, interactive SPA βœ… TypeScript integration is important βœ… Team has React experience or you’re hiring React developers βœ… You need access to thousands of third-party libraries βœ… Job market considerations matter (most React jobs) βœ… You want industry-standard patterns and practices

Choose Something Else When:

  • Bundle size is critical β†’ Use Preact (3kB) or Svelte (2kB)
  • Learning curve matters β†’ Try Vue (easier to learn)
  • Need SSR/SSG β†’ Use Next.js (React framework)
  • Need full framework β†’ Use Next.js or Remix
  • Want simpler reactivity β†’ Try Vue or Svelte
  • Performance is paramount β†’ Try Svelte

Next Steps

Next.js Guide

Full React framework with SSR

API Integration

Best practices for API communication

Deployment

Build and deploy your app

Vue Guide

Try Vue instead of React

React Docs

Official React documentation

React Query

Powerful data fetching