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
| Feature | React | Vue | Svelte | Angular |
|---|---|---|---|---|
| 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 for | SPAs, large apps | All | Performance | Enterprise |
| Community | β Largest | β‘ Large | β οΈ Growing | β‘ Large |
| Company | Meta (Facebook) | Independent | Independent |
- You need the largest ecosystem and community
- TypeScript integration is important
- Team has React experience
- Building a complex, interactive SPA
- Job market considerations matter
- 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:Copy
mizu new ./my-react-app --template frontend/react
cd my-react-app
make dev
http://localhost:3000 to see your app!
Project Structure
Copy
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:Copy
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 β
βββββββββββββββββββββββββββββββ
- User requests
http://yourdomain.com - Mizu serves
index.htmlfrom embedded FS - Browser loads React bundle
- React hydrates and takes over
- Client-side routing handles navigation
- API calls go to Mizu Go handlers
Backend Setup
app/server/app.go
Copy
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
Copy
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
Copy
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>,
)
- 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
Copy
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
Copy
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:Copy
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>
)
}
Copy
// β Don't do this with previous state
setCount(count + 1)
// β
Use functional update
setCount(prev => prev + 1)
useEffect
Perform side effects:Copy
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>
}
Copy
// 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:Copy
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:Copy
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:Copy
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:Copy
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:Copy
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:Copy
// 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 }
}
Copy
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>
)
}
Copy
// 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)} />
}
Copy
// 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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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:Copy
// 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:Copy
npm install zustand
Copy
// 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' }
)
)
)
Copy
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:Copy
npm install @tanstack/react-query
Copy
// 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>
)
Copy
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:Copy
npm install swr
Copy
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:Copy
npm install react-hook-form
Copy
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>
)
}
Copy
npm install @hookform/resolvers yup
Copy
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
Copy
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>
)
}
Copy
npm install react-error-boundary
Copy
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:Copy
/* 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;
}
Copy
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:Copy
cd frontend
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js:
Copy
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: '#007bff',
secondary: '#6c757d',
}
},
},
plugins: [],
}
src/styles/index.css:
Copy
@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;
}
}
Copy
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:Copy
npm install styled-components
npm install -D @types/styled-components
Copy
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
Copy
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'],
},
})
Copy
import Button from '@components/Button'
import { useUsers } from '@hooks/useUsers'
import Home from '@pages/Home'
tsconfig.json:
Copy
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@pages/*": ["./src/pages/*"],
"@hooks/*": ["./src/hooks/*"]
}
}
}
Development Workflow
Start Development
Copy
# 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
http://localhost:3000
Making Changes
Frontend changes:- Edit any
.tsxfile infrontend/src/ - Save the file
- Browser updates instantly (HMR)
- State is preserved during updates
- Edit
.gofiles - Stop server (Ctrl+C)
- Restart with
go run cmd/server/main.go
Copy
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:Copy
make build
- Runs
npm run buildinfrontend/ - Builds Go binary with
go build - Embeds frontend in binary
./bin/server (single executable)
Run in production:
Copy
MIZU_ENV=production ./bin/server
Build Optimizations
Code splitting:Copy
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>
)
}
Copy
// β Bad - imports entire library
import * as icons from 'react-icons/fa'
// β
Good - imports only what you need
import { FaUser, FaHome } from 'react-icons/fa'
Copy
cd frontend
npm install -D rollup-plugin-visualizer
Copy
// 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:Copy
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:Copy
npm install react-window
Copy
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
Copy
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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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>
)
}
Copy
// 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>
)
}
Copy
// 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: Checkvite.config.ts:
Copy
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: EnsureBrowserRouter (not HashRouter)
Solution 3: Check Mizuβs SPA fallback is enabled
TypeScript Errors
Error: βCannot find module β./Appββ Solution: Check file extensions in imports:Copy
// β
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:Copy
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:- Analyze bundle:
Copy
npm run build
# Check dist/ folder size
- Enable code splitting:
Copy
const Dashboard = lazy(() => import('./Dashboard'))
- Check for duplicate dependencies:
Copy
npm ls react
- 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 practicesChoose 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