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": "[email protected]", "role": "admin"},
        {"id": 2, "name": "Bob", "email": "[email protected]", "role": "user"},
        {"id": 3, "name": "Charlie", "email": "[email protected]", "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: '[email protected]' })}
        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