Documentation Index Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
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
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 Google
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:
User requests http://yourdomain.com
Mizu serves index.html from 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
package server
import (
" embed "
" io/fs "
" github.com/go-mizu/mizu "
" github.com/go-mizu/mizu/frontend "
)
//go:embed all:../../dist
var distFS embed . FS
func New ( cfg * Config ) * mizu . App {
app := mizu . New ()
// API routes come first
setupRoutes ( app )
// Frontend middleware (handles all non-API routes)
dist , _ := fs . Sub ( distFS , "dist" )
app . Use ( frontend . WithOptions ( frontend . Options {
Mode : frontend . ModeAuto , // Auto-detect dev/prod
FS : dist , // Embedded files
Root : "./dist" , // Fallback to filesystem in dev
DevServer : "http://localhost:" + cfg . DevPort , // Vite dev server
IgnorePaths : [] string { "/api" }, // Don't proxy /api to Vite
}))
return app
}
app/server/routes.go
package server
import " github.com/go-mizu/mizu "
func setupRoutes ( app * mizu . App ) {
// User endpoints
app . Get ( "/api/users" , handleUsers )
app . Post ( "/api/users" , createUser )
app . Get ( "/api/users/{id}" , getUser )
app . Put ( "/api/users/{id}" , updateUser )
app . Delete ( "/api/users/{id}" , deleteUser )
// Auth endpoints
app . Post ( "/api/auth/login" , handleLogin )
app . Post ( "/api/auth/logout" , handleLogout )
app . Get ( "/api/auth/me" , handleMe )
}
func handleUsers ( c * mizu . Ctx ) error {
users := [] map [ string ] any {
{ "id" : 1 , "name" : "Alice" , "email" : "alice@example.com" , "role" : "admin" },
{ "id" : 2 , "name" : "Bob" , "email" : "bob@example.com" , "role" : "user" },
{ "id" : 3 , "name" : "Charlie" , "email" : "charlie@example.com" , "role" : "user" },
}
return c . JSON ( 200 , users )
}
func createUser ( c * mizu . Ctx ) error {
var user map [ string ] any
if err := c . BodyJSON ( & user ); err != nil {
return c . JSON ( 400 , map [ string ] string { "error" : "Invalid JSON" })
}
// Validate
if user [ "name" ] == nil || user [ "email" ] == nil {
return c . JSON ( 400 , map [ string ] string { "error" : "Name and email required" })
}
// In real app, save to database
user [ "id" ] = 4
user [ "role" ] = "user"
return c . JSON ( 201 , user )
}
func getUser ( c * mizu . Ctx ) error {
id := c . Param ( "id" )
user := map [ string ] any {
"id" : id ,
"name" : "User " + id ,
"email" : "user" + id + "@example.com" ,
"role" : "user" ,
}
return c . JSON ( 200 , user )
}
Frontend Setup
Entry Point
frontend/src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './styles/index.css'
ReactDOM . createRoot ( document . getElementById ( 'root' ) ! ). render (
< React.StrictMode >
< App />
</ React.StrictMode > ,
)
Why StrictMode?
Highlights potential problems in components
Warns about deprecated APIs
Detects unexpected side effects
Only in development, no production overhead
Root Component
frontend/src/App.tsx
import { BrowserRouter , Routes , Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import About from './pages/About'
import Users from './pages/Users'
function App () {
return (
< BrowserRouter >
< Routes >
< Route path = "/" element = { < Layout /> } >
< Route index element = { < Home /> } />
< Route path = "about" element = { < About /> } />
< Route path = "users" element = { < Users /> } />
</ Route >
</ Routes >
</ BrowserRouter >
)
}
export default App
Layout Component
frontend/src/components/Layout.tsx
import { Link , Outlet } from 'react-router-dom'
export default function Layout () {
return (
< div className = "app" >
< header >
< nav >
< Link to = "/" > Home </ Link >
< Link to = "/about" > About </ Link >
< Link to = "/users" > Users </ Link >
</ nav >
</ header >
< main >
< Outlet />
</ main >
< footer >
< p > Built with Mizu and React </ p >
</ footer >
</ div >
)
}
React Hooks Deep Dive
React Hooks let you use state and other React features in function components.
useState
Manage component state:
import { useState } from 'react'
function Counter () {
const [ count , setCount ] = useState ( 0 )
const [ text , setText ] = useState ( '' )
return (
< div >
< p > Count: { count } </ p >
< button onClick = { () => setCount ( count + 1 ) } > Increment </ button >
< button onClick = { () => setCount ( prev => prev + 1 ) } > Better Increment </ button >
< input
value = { text }
onChange = { e => setText ( e . target . value ) }
/>
</ div >
)
}
Functional updates:
// β Don't do this with previous state
setCount ( count + 1 )
// β
Use functional update
setCount ( prev => prev + 1 )
useEffect
Perform side effects:
import { useState , useEffect } from 'react'
function Users () {
const [ users , setUsers ] = useState ([])
const [ loading , setLoading ] = useState ( true )
// Run once on mount
useEffect (() => {
fetch ( '/api/users' )
. then ( res => res . json ())
. then ( data => {
setUsers ( data )
setLoading ( false )
})
}, []) // Empty deps = run once
// Run when users changes
useEffect (() => {
console . log ( 'Users updated:' , users . length )
}, [ users ])
// Cleanup
useEffect (() => {
const interval = setInterval (() => {
console . log ( 'Tick' )
}, 1000 )
return () => clearInterval ( interval )
}, [])
return < div > { /* render users */ } </ div >
}
Common patterns:
// Fetch on param change
useEffect (() => {
fetch ( `/api/users/ ${ id } ` )
. then ( res => res . json ())
. then ( setUser )
}, [ id ])
// Subscribe to events
useEffect (() => {
function handleResize () {
setWidth ( window . innerWidth )
}
window . addEventListener ( 'resize' , handleResize )
return () => window . removeEventListener ( 'resize' , handleResize )
}, [])
// Debounced effect
useEffect (() => {
const timer = setTimeout (() => {
// Do something with searchTerm
}, 500 )
return () => clearTimeout ( timer )
}, [ searchTerm ])
useContext
Access context values:
import { createContext , useContext , ReactNode } from 'react'
interface Theme {
primary : string
secondary : string
}
const ThemeContext = createContext < Theme >({
primary: '#007bff' ,
secondary: '#6c757d'
})
export function ThemeProvider ({ children } : { children : ReactNode }) {
const theme = {
primary: '#007bff' ,
secondary: '#6c757d'
}
return (
< ThemeContext.Provider value = { theme } >
{ children }
</ ThemeContext.Provider >
)
}
export function useTheme () {
return useContext ( ThemeContext )
}
// Usage
function Button () {
const theme = useTheme ()
return < button style = { { background: theme . primary } } > Click </ button >
}
useReducer
Manage complex state:
import { useReducer } from 'react'
interface State {
count : number
text : string
}
type Action =
| { type : 'increment' }
| { type : 'decrement' }
| { type : 'setText' ; payload : string }
| { type : 'reset' }
const initialState : State = {
count: 0 ,
text: ''
}
function reducer ( state : State , action : Action ) : State {
switch ( action . type ) {
case 'increment' :
return { ... state , count: state . count + 1 }
case 'decrement' :
return { ... state , count: state . count - 1 }
case 'setText' :
return { ... state , text: action . payload }
case 'reset' :
return initialState
default :
return state
}
}
function Counter () {
const [ state , dispatch ] = useReducer ( reducer , initialState )
return (
< div >
< p > Count: { state . count } </ p >
< button onClick = { () => dispatch ({ type: 'increment' }) } > + </ button >
< button onClick = { () => dispatch ({ type: 'decrement' }) } > - </ button >
< button onClick = { () => dispatch ({ type: 'reset' }) } > Reset </ button >
< input
value = { state . text }
onChange = { e => dispatch ({ type: 'setText' , payload: e . target . value }) }
/>
</ div >
)
}
useMemo
Memoize expensive calculations:
import { useMemo , useState } from 'react'
function ExpensiveComponent ({ items } : { items : number [] }) {
const [ filter , setFilter ] = useState ( 0 )
// Only recalculates when items or filter changes
const filteredItems = useMemo (() => {
console . log ( 'Filtering...' )
return items . filter ( item => item > filter )
}, [ items , filter ])
const total = useMemo (() => {
console . log ( 'Calculating total...' )
return filteredItems . reduce (( sum , item ) => sum + item , 0 )
}, [ filteredItems ])
return (
< div >
< input
type = "number"
value = { filter }
onChange = { e => setFilter ( Number ( e . target . value )) }
/>
< p > Filtered: { filteredItems . length } </ p >
< p > Total: { total } </ p >
</ div >
)
}
useCallback
Memoize callback functions:
import { useCallback , useState , memo } from 'react'
interface ItemProps {
id : number
onDelete : ( id : number ) => void
}
// Memoized child component
const Item = memo (({ id , onDelete } : ItemProps ) => {
console . log ( 'Rendering item' , id )
return < button onClick = { () => onDelete ( id ) } > Delete { id } </ button >
})
function List () {
const [ items , setItems ] = useState ([ 1 , 2 , 3 ])
// Without useCallback, this creates a new function on every render
// causing all Item components to re-render
const handleDelete = useCallback (( id : number ) => {
setItems ( prev => prev . filter ( item => item !== id ))
}, [])
return (
< div >
{ items . map ( id => (
< Item key = { id } id = { id } onDelete = { handleDelete } />
)) }
</ div >
)
}
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:
// stores/userStore.ts
import { create } from 'zustand'
import { devtools , persist } from 'zustand/middleware'
interface User {
id : number
name : string
email : string
}
interface UserState {
users : User []
loading : boolean
error : string | null
fetchUsers : () => Promise < void >
addUser : ( user : Omit < User , 'id' >) => Promise < void >
deleteUser : ( id : number ) => Promise < void >
updateUser : ( id : number , updates : Partial < User >) => Promise < void >
}
export const useUserStore = create < UserState >()(
devtools (
persist (
( set , get ) => ({
users: [],
loading: false ,
error: null ,
fetchUsers : async () => {
set ({ loading: true , error: null })
try {
const res = await fetch ( '/api/users' )
const users = await res . json ()
set ({ users , loading: false })
} catch ( error ) {
set ({ error: ( error as Error ). message , loading: false })
}
},
addUser : async ( user ) => {
const res = await fetch ( '/api/users' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( user )
})
const newUser = await res . json ()
set ({ users: [ ... get (). users , newUser ] })
},
deleteUser : async ( id ) => {
await fetch ( `/api/users/ ${ id } ` , { method: 'DELETE' })
set ({ users: get (). users . filter ( u => u . id !== id ) })
},
updateUser : async ( id , updates ) => {
const res = await fetch ( `/api/users/ ${ id } ` , {
method: 'PUT' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( updates )
})
const updated = await res . json ()
set ({
users: get (). users . map ( u => u . id === id ? updated : u )
})
}
}),
{ name: 'user-storage' }
)
)
)
Usage:
import { useUserStore } from './stores/userStore'
import { useEffect } from 'react'
function Users () {
const { users , loading , fetchUsers , deleteUser } = useUserStore ()
useEffect (() => {
fetchUsers ()
}, [])
if ( loading ) return < div > Loading... </ div >
return (
< ul >
{ users . map ( user => (
< li key = { user . id } >
{ user . name }
< button onClick = { () => deleteUser ( user . id ) } > Delete </ button >
</ li >
)) }
</ ul >
)
}
Data Fetching
React Query
Install:
npm install @tanstack/react-query
Setup:
// main.tsx
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient ({
defaultOptions: {
queries: {
refetchOnWindowFocus: false ,
retry: 1 ,
staleTime: 5 * 60 * 1000 , // 5 minutes
},
},
})
ReactDOM . createRoot ( document . getElementById ( 'root' ) ! ). render (
< QueryClientProvider client = { queryClient } >
< App />
< ReactQueryDevtools initialIsOpen = { false } />
</ QueryClientProvider >
)
Usage:
import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query'
interface User {
id : number
name : string
email : string
}
function Users () {
const queryClient = useQueryClient ()
// Fetch users
const { data : users , isLoading , error } = useQuery ({
queryKey: [ 'users' ],
queryFn : async () => {
const res = await fetch ( '/api/users' )
if ( ! res . ok ) throw new Error ( 'Failed to fetch' )
return res . json () as Promise < User []>
}
})
// Create user mutation
const createMutation = useMutation ({
mutationFn : async ( user : Omit < User , 'id' >) => {
const res = await fetch ( '/api/users' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( user )
})
return res . json ()
},
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: [ 'users' ] })
}
})
// Delete user mutation
const deleteMutation = useMutation ({
mutationFn : async ( id : number ) => {
await fetch ( `/api/users/ ${ id } ` , { method: 'DELETE' })
},
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: [ 'users' ] })
}
})
if ( isLoading ) return < div > Loading... </ div >
if ( error ) return < div > Error: { error . message } </ div >
return (
< div >
< ul >
{ users ?. map ( user => (
< li key = { user . id } >
{ user . name }
< button
onClick = { () => deleteMutation . mutate ( user . id ) }
disabled = { deleteMutation . isPending }
>
Delete
</ button >
</ li >
)) }
</ ul >
< button
onClick = { () => createMutation . mutate ({ name: 'New' , email: 'new@example.com' }) }
disabled = { createMutation . isPending }
>
Add User
</ button >
</ div >
)
}
Alternative to React Query:
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 >
)
}
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.5 rem 1 rem ;
border : none ;
border-radius : 4 px ;
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:
Edit any .tsx file in frontend/src/
Save the file
Browser updates instantly (HMR)
State is preserved during updates
Backend changes:
Edit .go files
Stop server (Ctrl+C)
Restart with go run cmd/server/main.go
Or use air for auto-reload:
go install github.com/cosmtrek/air@latest
air
Install browser extension:
Features:
Inspect component tree
View props and state
Track component updates
Profiler for performance
Building for Production
Build the complete app:
This:
Runs npm run build in frontend/
Builds Go binary with go build
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 ,
})
]
})
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:
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:
Analyze bundle:
npm run build
# Check dist/ folder size
Enable code splitting:
const Dashboard = lazy (() => import ( './Dashboard' ))
Check for duplicate dependencies:
Use smaller alternatives:
Replace moment with date-fns
Replace lodash with individual functions
Consider Preact instead of React
When to Choose React
Choose React When:
β
You need the largest ecosystem and community
β
Building a complex, interactive SPA
β
TypeScript integration is important
β
Team has React experience or youβre hiring React developers
β
You need access to thousands of third-party libraries
β
Job market considerations matter (most React jobs)
β
You want industry-standard patterns and practices
Choose Something Else When:
Bundle size is critical β Use Preact (3kB) or Svelte (2kB)
Learning curve matters β Try Vue (easier to learn)
Need SSR/SSG β Use Next.js (React framework)
Need full framework β Use Next.js or Remix
Want simpler reactivity β Try Vue or Svelte
Performance is paramount β Try Svelte
Next Steps
Next.js Guide Full React framework with SSR
API Integration Best practices for API communication
Deployment Build and deploy your app
Vue Guide Try Vue instead of React
React Docs Official React documentation
React Query Powerful data fetching