State Management
Manage application state with React Context and React Query.
Overview
EZ-Console uses a combination of React Context for global state and React Query (TanStack Query) for server state management. This approach provides a clean separation between client-side state and server-side data.
State Management Strategy
Client State vs Server State
- Client State: UI state, form data, user preferences (use React Context or useState)
- Server State: Data from API (use React Query)
React Context for Global State
Authentication Context
The framework provides an AuthContext for authentication state:
import { useAuth } from 'ez-console';
export const MyComponent: React.FC = () => {
const { user, token, loading, login, logout } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Please login</div>;
}
return (
<div>
<p>Welcome, {user.username}</p>
<button onClick={logout}>Logout</button>
</div>
);
};
Creating Custom Context
// src/contexts/AppContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AppContextType {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<AppContext.Provider value={{ theme, setTheme }}>
{children}
</AppContext.Provider>
);
};
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
};
React Query for Server State
Basic Query
import { useQuery } from 'react-query';
import { apiGet } from 'ez-console';
export const ProductList: React.FC = () => {
const { data, isLoading, error } = useQuery(
'products',
() => apiGet('/products')
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data?.data.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
};
Query with Parameters
import { useQuery } from 'react-query';
import { apiGet } from 'ez-console';
export const ProductDetail: React.FC<{ id: string }> = ({ id }) => {
const { data, isLoading } = useQuery(
['product', id],
() => apiGet(`/products/${id}`),
{
enabled: !!id, // Only fetch if id exists
}
);
if (isLoading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
};
Mutations
import { useMutation, useQueryClient } from 'react-query';
import { apiPost, apiPut, apiDelete } from 'ez-console';
import { message } from 'antd';
export const ProductForm: React.FC = () => {
const queryClient = useQueryClient();
const createMutation = useMutation(
(data: Product) => apiPost('/products', data),
{
onSuccess: () => {
queryClient.invalidateQueries('products');
message.success('Product created');
},
onError: (error) => {
message.error(error.message);
},
}
);
const handleSubmit = (values: Product) => {
createMutation.mutate(values);
};
return (
<Form onFinish={handleSubmit}>
{/* Form fields */}
</Form>
);
};
Using useRequest Hook
EZ-Console provides useRequest from ahooks for simpler API calls:
import { useRequest } from 'ahooks';
import { apiGet, apiDelete } from 'ez-console';
export const ProductList: React.FC = () => {
const { data, loading, run: fetchProducts } = useRequest(
() => apiGet('/products'),
{
refreshDeps: [], // Dependencies to refetch
}
);
const { run: deleteProduct } = useRequest(
(id: string) => apiDelete(`/products/${id}`),
{
manual: true,
onSuccess: () => {
fetchProducts(); // Refetch after delete
},
}
);
return (
<div>
{loading ? (
<div>Loading...</div>
) : (
data?.data.map(product => (
<div key={product.id}>
{product.name}
<button onClick={() => deleteProduct(product.id)}>Delete</button>
</div>
))
)}
</div>
);
};
Local Component State
useState Hook
For component-specific state:
import { useState } from 'react';
export const ProductForm: React.FC = () => {
const [name, setName] = useState('');
const [price, setPrice] = useState(0);
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="number"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
/>
</form>
);
};
useReducer Hook
For complex state logic:
import { useReducer } from 'react';
interface State {
products: Product[];
loading: boolean;
error: string | null;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Product[] }
| { type: 'FETCH_ERROR'; payload: string };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, products: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
export const ProductList: React.FC = () => {
const [state, dispatch] = useReducer(reducer, {
products: [],
loading: false,
error: null,
});
// Use dispatch to update state
};
State Patterns
Lifting State Up
// Parent component
export const ProductPage: React.FC = () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<div>
<ProductList onSelect={setSelectedId} />
{selectedId && <ProductDetail id={selectedId} />}
</div>
);
};
// Child component
export const ProductList: React.FC<{ onSelect: (id: string) => void }> = ({
onSelect,
}) => {
return (
<div>
{products.map(product => (
<div key={product.id} onClick={() => onSelect(product.id)}>
{product.name}
</div>
))}
</div>
);
};
State with URL
import { useSearchParams } from 'react-router-dom';
export const ProductList: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const handleCategoryChange = (newCategory: string) => {
setSearchParams({ category: newCategory });
};
return (
<div>
<Select value={category} onChange={handleCategoryChange}>
<Option value="all">All</Option>
<Option value="electronics">Electronics</Option>
</Select>
</div>
);
};
Best Practices
1. Choose the Right Tool
- useState: Simple component state
- useReducer: Complex state logic
- Context: Global state shared across components
- React Query: Server state and caching
2. Minimize Context Usage
// ✅ Good: Only global state in context
const AppContext = createContext({
user: null,
theme: 'light',
});
// ❌ Bad: Everything in context
const AppContext = createContext({
user: null,
products: [],
cart: [],
filters: {},
// Too much state
});
3. Use React Query for Server State
// ✅ Good: Use React Query
const { data } = useQuery('products', fetchProducts);
// ❌ Bad: Manual state management
const [products, setProducts] = useState([]);
useEffect(() => {
fetchProducts().then(setProducts);
}, []);
4. Optimize Re-renders
// ✅ Good: Memoize callbacks
const handleClick = useCallback(() => {
// Handler logic
}, [dependencies]);
// ✅ Good: Memoize components
const ProductCard = React.memo(({ product }) => {
return <div>{product.name}</div>;
});
Related Topics
- API Integration - API calls and data fetching
- Routing & Navigation - State with navigation
- Forms & Validation - Form state management
Need help? Ask in GitHub Discussions.