External State Management Library Integration
1. Redux Toolkit (RTK) Integration Patterns
| Feature | API | Description | Use Case |
|---|---|---|---|
| configureStore | configureStore({ reducer }) |
Create Redux store with good defaults (DevTools, middleware) | Single store setup with automatic configuration |
| createSlice | createSlice({ name, initialState, reducers }) |
Generate action creators and reducers automatically | Reduce boilerplate, use Immer for immutability |
| createAsyncThunk | createAsyncThunk('name', async (arg) => ...) |
Handle async logic with pending/fulfilled/rejected actions | API calls with automatic loading/error state |
| createEntityAdapter | createEntityAdapter() |
Normalized state with CRUD reducers and selectors | Managing collections of entities (users, posts, etc.) |
| createSelector | createSelector([input], (data) => result) |
Memoized selectors for derived state | Compute derived data without re-renders |
| RTK Query | createApi({ endpoints }) |
Data fetching and caching solution | Replace manual async thunks with auto-caching API layer |
| useSelector | useSelector(state => state.slice) |
Extract data from Redux store in components | Subscribe to specific state slices |
| useDispatch | const dispatch = useDispatch() |
Get dispatch function for triggering actions | Update state via action creators |
Example: Complete Redux Toolkit setup with slice
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, history: [] },
reducers: {
increment: (state) => {
state.value += 1; // Immer allows "mutations"
state.history.push({ action: 'increment', timestamp: Date.now() });
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
state.history = [];
}
}
});
export const { increment, decrement, incrementByAmount, reset } =
counterSlice.actions;
export default counterSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
// App.js
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
// Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from './counterSlice';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
Example: Async thunk for API calls
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (page, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users?page=${page}`);
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: false,
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
// Component usage
function UserList() {
const { entities, loading, error } = useSelector(state => state.users);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchUsers(1));
}, [dispatch]);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<ul>
{entities.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Note: Redux Toolkit is the recommended way to use Redux. It includes Immer for immutable
updates, Redux Thunk for async, and Redux DevTools by default. Use RTK Query for data fetching instead of
manually writing thunks.
2. Zustand Store Implementation and Usage
| Feature | API | Description | Benefits |
|---|---|---|---|
| create | create((set, get) => ({ ... })) |
Create a hook-based store | No providers, minimal boilerplate, auto-generates hooks |
| set | set({ field: value }) |
Merge updates into state | Partial updates, can be used with functions |
| get | get() |
Read current state in actions | Access state without React rendering |
| Selectors | useStore(state => state.field) |
Subscribe to specific state slices | Automatic re-render optimization |
| Middleware | persist, devtools, immer |
Extend store with additional functionality | Persist to storage, debug, immutable updates |
| Transient Updates | set({ ... }, true) |
Update without triggering subscribers | Temporary state that doesn't cause re-renders |
| subscribe | store.subscribe(listener) |
Listen to state changes outside React | Integration with non-React code |
| Shallow Equality | useStore(selector, shallow) |
Shallow comparison for object selectors | Prevent re-renders when object content unchanged |
Example: Complete Zustand store with actions
import { create } from 'zustand';
// Create store
const useStore = create((set, get) => ({
// State
bears: 0,
fish: 0,
// Actions
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
// Action using get() to read current state
eatFish: () => set((state) => ({
fish: state.fish - 1,
bears: state.bears + 0.1
})),
// Async action
fetchInitialData: async () => {
const response = await fetch('/api/wildlife');
const data = await response.json();
set({ bears: data.bears, fish: data.fish });
},
// Computed getter
getTotalAnimals: () => {
const state = get();
return state.bears + state.fish;
}
}));
// Component usage - subscribe to entire store
function AllCounts() {
const { bears, fish } = useStore();
return <div>Bears: {bears}, Fish: {fish}</div>;
}
// Component usage - subscribe to specific field
function BearCounter() {
const bears = useStore(state => state.bears);
return <h1>{bears} bears</h1>;
}
// Component usage - access actions
function Controls() {
const increasePopulation = useStore(state => state.increasePopulation);
const removeAllBears = useStore(state => state.removeAllBears);
return (
<div>
<button onClick={increasePopulation}>Add Bear</button>
<button onClick={removeAllBears}>Remove All</button>
</div>
);
}
Example: Zustand with persistence and DevTools
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const useStore = create(
devtools(
persist(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
// Immer allows "mutations"
state.todos.push({
id: Date.now(),
text,
completed: false
});
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}),
removeTodo: (id) => set((state) => {
state.todos = state.todos.filter(t => t.id !== id);
})
})),
{ name: 'todo-storage' } // localStorage key
)
)
);
// Slice pattern - multiple stores
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null })
}));
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
}))
}));
// Use multiple stores in component
function App() {
const user = useUserStore(state => state.user);
const cartItems = useCartStore(state => state.items);
return <div>{user?.name} has {cartItems.length} items</div>;
}
Note: Zustand is lightweight (~1KB), requires no Provider, and supports TypeScript excellently.
Use selectors to optimize re-renders. Combine with middleware for persistence, DevTools, and Immer integration.
3. Jotai Atomic State Management
| Concept | API | Description | Use Case |
|---|---|---|---|
| Atom | atom(initialValue) |
Primitive unit of state (like React state) | Small, independent pieces of state |
| useAtom | const [value, setValue] = useAtom(atom) |
Read and write atom value | useState-like API for atoms |
| useAtomValue | const value = useAtomValue(atom) |
Read-only access to atom | Subscribe to value without write capability |
| useSetAtom | const setValue = useSetAtom(atom) |
Write-only access to atom | Update without subscribing to value |
| Derived Atoms | atom((get) => get(otherAtom) * 2) |
Compute value from other atoms | Derived/computed state with automatic dependencies |
| Async Atoms | atom(async (get) => await fetch(...)) |
Atoms with async read function | Data fetching with Suspense integration |
| Write-only Atoms | atom(null, (get, set, arg) => ...) |
Atoms for actions without state | Action creators that update other atoms |
| atomFamily | atomFamily((param) => atom(...)) |
Create atoms dynamically based on parameters | Parameterized atoms (e.g., user atoms by ID) |
Example: Basic Jotai atoms and derived state
import { atom, useAtom, useAtomValue } from 'jotai';
// Primitive atoms
const countAtom = atom(0);
const nameAtom = atom('Guest');
// Derived atom (read-only)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
// Derived atom with dependencies
const greetingAtom = atom((get) => {
const name = get(nameAtom);
const count = get(countAtom);
return `Hello ${name}, count is ${count}`;
});
// Write-only action atom
const incrementAtom = atom(
null, // no read
(get, set, amount) => {
set(countAtom, get(countAtom) + amount);
}
);
// Component usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
const increment = useSetAtom(incrementAtom);
return (
<div>
<p>Count: {count}, Double: {doubleCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => increment(5)}>+5</button>
</div>
);
}
function Greeting() {
const greeting = useAtomValue(greetingAtom);
return <h1>{greeting}</h1>;
}
// Provider (optional, for scoping)
import { Provider } from 'jotai';
function App() {
return (
<Provider>
<Counter />
<Greeting />
</Provider>
);
}
Example: Async atoms with Suspense
import { atom, useAtomValue } from 'jotai';
import { Suspense } from 'react';
// Async atom for fetching user
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
const userId = get(userIdAtom);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
// Derived async atom
const userPostsAtom = atom(async (get) => {
const user = await get(userAtom);
const response = await fetch(`/api/users/${user.id}/posts`);
return response.json();
});
function UserProfile() {
const user = useAtomValue(userAtom); // Suspends while loading
return <div>{user.name}</div>;
}
function UserPosts() {
const posts = useAtomValue(userPostsAtom); // Suspends
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
<UserPosts />
</Suspense>
);
}
// atomFamily for parameterized atoms
import { atomFamily } from 'jotai/utils';
const todoAtomFamily = atomFamily((id) =>
atom(async () => {
const res = await fetch(`/api/todos/${id}`);
return res.json();
})
);
function Todo({ id }) {
const todo = useAtomValue(todoAtomFamily(id));
return <div>{todo.title}</div>;
}
Note: Jotai provides bottom-up atomic state management. Atoms are defined globally but values
are stored per Provider scope. Excellent TypeScript support, built-in Suspense integration, and minimal
boilerplate.
4. Valtio Proxy-based State Management
| Feature | API | Description | Characteristics |
|---|---|---|---|
| proxy | const state = proxy({ ... }) |
Create mutable proxy state object | Mutate directly, auto-detects changes with Proxies |
| useSnapshot | const snap = useSnapshot(state) |
Get immutable snapshot for rendering | Automatic re-render on used properties |
| subscribe | subscribe(state, callback) |
Listen to state changes | React to mutations outside components |
| snapshot | const snap = snapshot(state) |
Get snapshot outside React | Read immutable version of mutable state |
| derive | derive({ computed: (get) => ... }) |
Create derived/computed properties | Automatically recompute when dependencies change |
| ref | ref(value) |
Mark value as non-reactive | Store objects that shouldn't trigger updates |
| Nested Proxies | Automatic for objects/arrays | Deep reactivity for nested structures | No need to spread or copy nested objects |
| Render Optimization | Auto-tracks property access | Re-render only when accessed properties change | Fine-grained reactivity without selectors |
Example: Basic Valtio state with mutations
import { proxy, useSnapshot } from 'valtio';
// Create mutable state
const state = proxy({
count: 0,
text: 'Hello',
nested: {
value: 42
},
todos: []
});
// Mutate state directly (outside React)
function increment() {
state.count++; // Direct mutation!
}
function addTodo(text) {
state.todos.push({ // Array mutation
id: Date.now(),
text,
completed: false
});
}
function toggleTodo(id) {
const todo = state.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed; // Nested mutation
}
}
// Component usage
function Counter() {
const snap = useSnapshot(state);
return (
<div>
<p>{snap.count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
function TodoList() {
const snap = useSnapshot(state);
return (
<ul>
{snap.todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text} {todo.completed ? '✓' : ''}
</li>
))}
</ul>
);
}
// Only re-renders when count changes (not when todos change)
function CountDisplay() {
const snap = useSnapshot(state);
return <div>Count: {snap.count}</div>;
}
Example: Derived state and computed properties
import { proxy, useSnapshot, derive } from 'valtio';
const state = proxy({
items: [
{ id: 1, name: 'Apple', price: 1.2, quantity: 5 },
{ id: 2, name: 'Banana', price: 0.8, quantity: 3 }
],
taxRate: 0.1
});
// Add derived/computed properties
const derived = derive({
subtotal: (get) => {
const items = get(state).items;
return items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
},
tax: (get) => {
return get(derived).subtotal * get(state).taxRate;
},
total: (get) => {
return get(derived).subtotal + get(derived).tax;
}
});
function Cart() {
const snap = useSnapshot(state);
const derivedSnap = useSnapshot(derived);
return (
<div>
{snap.items.map(item => (
<div key={item.id}>
{item.name}: ${item.price} × {item.quantity}
</div>
))}
<hr />
<div>Subtotal: ${derivedSnap.subtotal.toFixed(2)}</div>
<div>Tax: ${derivedSnap.tax.toFixed(2)}</div>
<div>Total: ${derivedSnap.total.toFixed(2)}</div>
</div>
);
}
// Update quantity
function updateQuantity(id, quantity) {
const item = state.items.find(i => i.id === id);
if (item) {
item.quantity = quantity;
// derived values automatically recompute
}
}
Note: Valtio enables mutable-style updates with immutable snapshots for React. Use for
TypeScript projects needing simple, direct mutations. Automatic fine-grained reactivity without manual
selectors. Consider Vue-like developer experience.
5. Recoil Experimental State Management
| Concept | API | Description | Status |
|---|---|---|---|
| atom | atom({ key, default }) |
Unit of state with unique key | BETA Requires unique string key |
| selector | selector({ key, get }) |
Derived/computed state from atoms | Pure function of atoms/selectors |
| useRecoilState | const [value, setValue] = useRecoilState(atom) |
Read and write atom (like useState) | Similar to React hooks API |
| useRecoilValue | const value = useRecoilValue(atom) |
Read-only access to atom/selector | Subscribe without write capability |
| useSetRecoilState | const setValue = useSetRecoilState(atom) |
Write-only access to atom | Update without subscribing |
| atomFamily | atomFamily({ key, default: (param) => ... }) |
Create atoms dynamically | Parameterized atoms for collections |
| selectorFamily | selectorFamily({ key, get: (param) => ... }) |
Parameterized selectors | Derived state with parameters |
| Async Selectors | get: async ({ get }) => await ... |
Async data fetching in selectors | BETA Experimental feature |
Example: Recoil atoms and selectors
import {
atom,
selector,
useRecoilState,
useRecoilValue,
RecoilRoot
} from 'recoil';
// Atoms (must have unique keys)
const textState = atom({
key: 'textState',
default: ''
});
const listState = atom({
key: 'listState',
default: []
});
// Selector (derived state)
const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
}
});
// Filtered list selector
const filteredListState = selector({
key: 'filteredListState',
get: ({ get }) => {
const list = get(listState);
const filter = get(textState);
return list.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
}
});
// Component usage
function TextInput() {
const [text, setText] = useRecoilState(textState);
const charCount = useRecoilValue(charCountState);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<div>Character Count: {charCount}</div>
</div>
);
}
function FilteredList() {
const filteredItems = useRecoilValue(filteredListState);
return (
<ul>
{filteredItems.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
// App requires RecoilRoot
function App() {
return (
<RecoilRoot>
<TextInput />
<FilteredList />
</RecoilRoot>
);
}
Example: Async selectors and atom families
import { atom, selector, selectorFamily, atomFamily } from 'recoil';
// Async selector for current user
const currentUserQuery = selector({
key: 'currentUserQuery',
get: async () => {
const response = await fetch('/api/current-user');
return response.json();
}
});
// Atom family for user data by ID
const userState = atomFamily({
key: 'userState',
default: selectorFamily({
key: 'userState/default',
get: (userId) => async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
})
});
// Selector family for user posts
const userPostsQuery = selectorFamily({
key: 'userPostsQuery',
get: (userId) => async ({ get }) => {
// Can depend on other atoms/selectors
const user = get(userState(userId));
const response = await fetch(`/api/users/${userId}/posts`);
return response.json();
}
});
// Component usage with Suspense
import { Suspense } from 'react';
function CurrentUser() {
const user = useRecoilValue(currentUserQuery);
return <div>{user.name}</div>;
}
function UserProfile({ userId }) {
const user = useRecoilValue(userState(userId));
const posts = useRecoilValue(userPostsQuery(userId));
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<RecoilRoot>
<Suspense fallback={<div>Loading...</div>}>
<CurrentUser />
<UserProfile userId={1} />
</Suspense>
</RecoilRoot>
);
}
Warning: Recoil is experimental and development has slowed. Consider Jotai as a more actively
maintained alternative with similar atomic state patterns. Recoil requires unique string keys for all atoms
which can be cumbersome.
6. Custom State Management Library Creation
| Approach | Implementation | Complexity | Features |
|---|---|---|---|
| useSyncExternalStore | useSyncExternalStore(subscribe, getSnapshot) |
Low - React 18 built-in | Sync external state with React, concurrent-safe |
| Context + useReducer | createContext + Provider |
Low - Uses React primitives | Simple global state, requires Provider wrapping |
| Observable Pattern | subscribe/unsubscribe listeners |
Medium - Manual subscription management | Pub/sub system, works outside React |
| Proxy-based | new Proxy(target, handler) |
Medium - Requires Proxy knowledge | Auto-tracking, mutable-style updates |
| Immer Integration | produce(state, draft => ...) |
Low - Library dependency | Immutable updates with mutable syntax |
| Middleware Support | compose(middleware1, middleware2) |
High - Complex composition | Extensibility via plugins (logging, persist, devtools) |
| Selector System | createSelector(deps, compute) |
Medium - Memoization logic | Derived state with dependency tracking |
| Time Travel | history = [states], pointer |
High - History management | Undo/redo, debugging, replay |
Example: Custom store with useSyncExternalStore
import { useSyncExternalStore } from 'react';
// Create simple store
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
const getState = () => state;
const setState = (newState) => {
state = typeof newState === 'function'
? newState(state)
: newState;
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
}
// Create store instance
const counterStore = createStore({ count: 0 });
// Custom hook
function useCounter() {
const state = useSyncExternalStore(
counterStore.subscribe,
counterStore.getState
);
const increment = () => {
counterStore.setState(s => ({ count: s.count + 1 }));
};
const decrement = () => {
counterStore.setState(s => ({ count: s.count - 1 }));
};
return { count: state.count, increment, decrement };
}
// Component usage
function Counter() {
const { count, increment, decrement } = useCounter();
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Example: Store with selectors and computed values
function createStoreWithSelectors(initialState) {
let state = initialState;
const listeners = new Set();
const selectorCache = new Map();
const getState = () => state;
const setState = (updater) => {
const newState = typeof updater === 'function'
? updater(state)
: updater;
if (newState !== state) {
state = newState;
selectorCache.clear(); // Invalidate cache
listeners.forEach(listener => listener());
}
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
// Selector with memoization
const createSelector = (selector) => {
return () => {
const cacheKey = selector.toString();
if (!selectorCache.has(cacheKey)) {
selectorCache.set(cacheKey, selector(state));
}
return selectorCache.get(cacheKey);
};
};
return { getState, setState, subscribe, createSelector };
}
// Usage
const store = createStoreWithSelectors({
users: [
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 25 }
]
});
// Define selectors
const selectAdultUsers = store.createSelector(
state => state.users.filter(u => u.age >= 18)
);
const selectUserCount = store.createSelector(
state => state.users.length
);
// Custom hook with selector
function useStoreSelector(selector) {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState())
);
}
function UserList() {
const adults = useStoreSelector(selectAdultUsers);
const count = useStoreSelector(selectUserCount);
return (
<div>
<p>Total users: {count}</p>
{adults.map(u => <div key={u.id}>{u.name}</div>)}
</div>
);
}
Example: Store with middleware support
function createStoreWithMiddleware(initialState, middlewares = []) {
let state = initialState;
const listeners = new Set();
// Compose middleware
const composedMiddleware = middlewares.reduceRight(
(next, middleware) => middleware(next),
(newState) => {
state = newState;
listeners.forEach(l => l());
}
);
const getState = () => state;
const setState = (updater) => {
const newState = typeof updater === 'function'
? updater(state)
: updater;
composedMiddleware(newState);
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
}
// Middleware examples
const logger = (next) => (state) => {
console.log('Previous state:', state);
next(state);
console.log('New state:', state);
};
const persist = (key) => (next) => (state) => {
next(state);
localStorage.setItem(key, JSON.stringify(state));
};
const devtools = (next) => (state) => {
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
window.__REDUX_DEVTOOLS_EXTENSION__.send('UPDATE', state);
}
next(state);
};
// Create store with middleware
const store = createStoreWithMiddleware(
{ count: 0 },
[logger, persist('app-state'), devtools]
);
// Hook
function useStore() {
return useSyncExternalStore(
store.subscribe,
store.getState
);
}
Note: Custom stores are useful for learning or specific needs. Use useSyncExternalStore (React
18+) for concurrent-safe external state. For production, prefer established libraries (Zustand, Jotai) unless
you have unique requirements.
Section 14 Key Takeaways
- Redux Toolkit - Official Redux with minimal boilerplate, Immer, and RTK Query for data fetching
- Zustand - Lightweight (1KB), no Provider, hook-based with middleware support
- Jotai - Atomic bottom-up state, Suspense support, minimal API similar to Recoil
- Valtio - Proxy-based mutable updates with immutable snapshots, fine-grained reactivity
- Recoil - Experimental atomic state from Meta, consider Jotai as alternative
- Custom stores - Use useSyncExternalStore for concurrent-safe external state integration