State Management
Managing Application State
Explanation
What is State Management?
State management is how applications track and update data over time. As apps grow complex, organizing state becomes critical for maintainability.
State Types
| Type | Scope | Examples | |------|-------|----------| | Local | Single component | Form inputs, UI toggles | | Shared | Multiple components | User data, theme | | Server | Cached API data | Products, users | | URL | Browser address | Filters, pagination |
Demonstration
Example 1: Simple State Store
// Basic observable store
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState() {
return state;
},
setState(newState) {
state = typeof newState === 'function'
? newState(state)
: { ...state, ...newState };
listeners.forEach(listener => listener(state));
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
// Usage
const store = createStore({
user: null,
theme: 'light',
notifications: []
});
// Subscribe to changes
const unsubscribe = store.subscribe(state => {
console.log('State changed:', state);
});
// Update state
store.setState({ theme: 'dark' });
store.setState(state => ({
notifications: [...state.notifications, { id: 1, text: 'Hello' }]
}));
// Get current state
console.log(store.getState().theme); // 'dark'
Example 2: Redux-like Pattern
// Action types
const ActionTypes = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO',
SET_FILTER: 'SET_FILTER'
};
// Action creators
const actions = {
addTodo: (text) => ({
type: ActionTypes.ADD_TODO,
payload: { id: Date.now(), text, completed: false }
}),
toggleTodo: (id) => ({
type: ActionTypes.TOGGLE_TODO,
payload: id
}),
deleteTodo: (id) => ({
type: ActionTypes.DELETE_TODO,
payload: id
}),
setFilter: (filter) => ({
type: ActionTypes.SET_FILTER,
payload: filter
})
};
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case ActionTypes.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case ActionTypes.DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case ActionTypes.SET_FILTER:
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// Store with reducer
function createReduxStore(reducer, initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
dispatch(action) {
state = reducer(state, action);
listeners.forEach(listener => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
// Usage
const store = createReduxStore(todoReducer, {
todos: [],
filter: 'all'
});
store.subscribe(() => {
console.log('State:', store.getState());
});
store.dispatch(actions.addTodo('Learn state management'));
store.dispatch(actions.toggleTodo(store.getState().todos[0].id));
Example 3: React State Management
// Context + useReducer pattern
import React, { createContext, useContext, useReducer } from 'react';
// Context
const TodoContext = createContext();
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, {
id: Date.now(),
text: action.text,
completed: false
}];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
// Provider
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
const value = {
todos,
addTodo: (text) => dispatch({ type: 'ADD', text }),
toggleTodo: (id) => dispatch({ type: 'TOGGLE', id }),
deleteTodo: (id) => dispatch({ type: 'DELETE', id })
};
return (
<TodoContext.Provider value={value}>
{children}
</TodoContext.Provider>
);
}
// Hook
function useTodos() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodos must be used within TodoProvider');
}
return context;
}
// Usage in components
function TodoList() {
const { todos, toggleTodo, deleteTodo } = useTodos();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
function AddTodo() {
const { addTodo } = useTodos();
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
Example 4: Zustand (Modern Approach)
// Zustand-like store
function create(initializer) {
let state;
const listeners = new Set();
const setState = (partial) => {
const nextState = typeof partial === 'function'
? partial(state)
: partial;
if (nextState !== state) {
state = { ...state, ...nextState };
listeners.forEach(listener => listener(state));
}
};
const getState = () => state;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
// Initialize
state = initializer(setState, getState);
return { getState, setState, subscribe };
}
// Store definition
const useStore = create((set, get) => ({
// State
bears: 0,
fish: [],
// Actions
addBear: () => set(state => ({ bears: state.bears + 1 })),
removeBear: () => set(state => ({ bears: Math.max(0, state.bears - 1) })),
addFish: (fish) => set(state => ({
fish: [...state.fish, fish]
})),
// Computed (via getter)
get totalAnimals() {
const state = get();
return state.bears + state.fish.length;
},
// Async action
fetchFish: async () => {
const response = await fetch('/api/fish');
const fish = await response.json();
set({ fish });
},
// Reset
reset: () => set({ bears: 0, fish: [] })
}));
// React hook (simplified)
function useStoreHook(selector) {
const [, forceRender] = React.useState(0);
React.useEffect(() => {
return useStore.subscribe(() => forceRender(n => n + 1));
}, []);
return selector(useStore.getState());
}
// Usage
function BearCounter() {
const bears = useStoreHook(state => state.bears);
const addBear = useStoreHook(state => state.addBear);
return (
<div>
<span>{bears} bears</span>
<button onClick={addBear}>Add bear</button>
</div>
);
}
Example 5: Server State (React Query Pattern)
// Simple query cache
class QueryCache {
constructor() {
this.cache = new Map();
this.listeners = new Map();
}
getQuery(key) {
return this.cache.get(key);
}
setQuery(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
this.notify(key);
}
invalidate(key) {
this.cache.delete(key);
this.notify(key);
}
subscribe(key, listener) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(listener);
return () => this.listeners.get(key).delete(listener);
}
notify(key) {
const listeners = this.listeners.get(key);
if (listeners) {
listeners.forEach(listener => listener());
}
}
}
// Query hook
function useQuery(key, fetcher, options = {}) {
const { staleTime = 5000 } = options;
const [state, setState] = useState({
data: undefined,
error: undefined,
isLoading: true
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
const cached = queryCache.getQuery(key);
// Return cached if fresh
if (cached && Date.now() - cached.timestamp < staleTime) {
setState({ data: cached.data, error: undefined, isLoading: false });
return;
}
setState(s => ({ ...s, isLoading: true }));
try {
const data = await fetcher();
if (!cancelled) {
queryCache.setQuery(key, data);
setState({ data, error: undefined, isLoading: false });
}
} catch (error) {
if (!cancelled) {
setState({ data: undefined, error, isLoading: false });
}
}
};
fetchData();
return () => { cancelled = true; };
}, [key]);
return state;
}
// Usage
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery(
`user-${userId}`,
() => fetch(`/api/users/${userId}`).then(r => r.json())
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
Example 6: URL State
// URL state management
function useURLState(key, defaultValue) {
const [value, setValue] = useState(() => {
const params = new URLSearchParams(window.location.search);
const param = params.get(key);
return param !== null ? JSON.parse(param) : defaultValue;
});
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (value === defaultValue) {
params.delete(key);
} else {
params.set(key, JSON.stringify(value));
}
const newURL = params.toString()
? `${window.location.pathname}?${params}`
: window.location.pathname;
window.history.replaceState({}, '', newURL);
}, [key, value, defaultValue]);
return [value, setValue];
}
// Usage
function ProductList() {
const [filters, setFilters] = useURLState('filters', {
category: 'all',
sort: 'newest'
});
const [page, setPage] = useURLState('page', 1);
return (
<div>
<select
value={filters.category}
onChange={e => setFilters({
...filters,
category: e.target.value
})}
>
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
<button onClick={() => setPage(page + 1)}>
Page {page} - Next
</button>
</div>
);
}
Key Takeaways:
- Choose state location wisely (local vs global)
- Immutability prevents bugs
- Actions make changes predictable
- Server state needs different handling
- URL state for shareable views
Imitation
Challenge 1: Build a Shopping Cart Store
Task: Create a state management system for a shopping cart.
Solution
const createCartStore = () => {
let state = {
items: [],
isOpen: false
};
const listeners = new Set();
const notify = () => listeners.forEach(l => l(state));
return {
getState: () => state,
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
addItem: (product, quantity = 1) => {
const existing = state.items.find(i => i.id === product.id);
if (existing) {
state = {
...state,
items: state.items.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + quantity }
: i
)
};
} else {
state = {
...state,
items: [...state.items, { ...product, quantity }]
};
}
notify();
},
removeItem: (productId) => {
state = {
...state,
items: state.items.filter(i => i.id !== productId)
};
notify();
},
updateQuantity: (productId, quantity) => {
if (quantity <= 0) return cartStore.removeItem(productId);
state = {
...state,
items: state.items.map(i =>
i.id === productId ? { ...i, quantity } : i
)
};
notify();
},
toggleCart: () => {
state = { ...state, isOpen: !state.isOpen };
notify();
},
clear: () => {
state = { ...state, items: [] };
notify();
},
get total() {
return state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
get itemCount() {
return state.items.reduce((sum, item) => sum + item.quantity, 0);
}
};
};
const cartStore = createCartStore();
Practice
Exercise 1: Undo/Redo
Difficulty: Intermediate
Add undo/redo functionality to a store.
Exercise 2: State Persistence
Difficulty: Intermediate
Persist state to localStorage with hydration.
Summary
What you learned:
- Observable store pattern
- Redux-like architecture
- React Context + useReducer
- Modern approaches (Zustand)
- Server and URL state
Next Steps:
- Read: Testing
- Practice: Build a complex app
- Explore: Redux, Zustand, Jotai
