Implementing State Management Libraries
1. Setting Up Redux Toolkit
Example: Basic Redux Toolkit Setup
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
| Configuration Option | Description | Default Value |
|---|---|---|
| reducer | Root reducer function or object mapping slice names to reducers | null |
| middleware | Middleware to add to the store | Pre-configured middleware |
| devTools | Enable Redux DevTools extension | true |
| preloadedState | Initial state for the store | undefined |
Note: Redux Toolkit automatically includes the Redux DevTools extension and pre-configured middleware.
Example: Slice Creation
// features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Setup Steps
- Install
@reduxjs/toolkitandreact-redux - Create a store using
configureStore - Define slices with reducers using
createSlice - Provide the store to your app via
<Provider>
2. Creating Slices
| Property | Description | Example |
|---|---|---|
| name | Name of the slice | "user" |
| initialState | Initial state for the slice | { id: null, name: "", email: "" } |
| reducers | Functions that define how the state changes | { setUserInfo: (state, action) => { ... } } |
Example: Creating a Slice
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: {
id: null,
name: '',
email: ''
},
reducers: {
setUserInfo: (state, action) => {
state.id = action.payload.id;
state.name = action.payload.name;
state.email = action.payload.email;
},
clearUser: (state) => {
state.id = null;
state.name = '';
state.email = '';
}
}
});
export const { setUserInfo, clearUser } = userSlice.actions;
export default userSlice.reducer;
Note: Slices are created using
createSlice which automatically generates action creators and action types based on the reducer names.
3. Using useSelector and useDispatch
| Hook | Description | Usage |
|---|---|---|
| useSelector | Extracts data from the Redux store state | const value = useSelector(state => state.key) |
| useDispatch | Returns a reference to the dispatch function from the Redux store | const dispatch = useDispatch() |
Example: Basic usage of useSelector and useDispatch
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
const Counter = () => {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch({ type: 'counter/increment' })}>
Increment
</button>
</div>
);
};
export default Counter;
Note:
useSelector is optimized to avoid unnecessary re-renders. It only re-renders when the selected value has changed.
Warning: Avoid inline functions in
useSelector as they can cause unnecessary re-renders. Prefer memoized selectors.
Key Points
useSelectoris a React hook that allows you to extract data from the Redux store state.useDispatchis a React hook that returns the Redux store'sdispatchfunction.- These hooks simplify integration of Redux with React components.
4. Implementing Async Actions
| Action Type | Description | Example |
|---|---|---|
| FETCH_DATA_START | Indicates the start of an async data fetch operation | dispatch({ type: 'FETCH_DATA_START' }); |
| FETCH_DATA_SUCCESS | Indicates successful completion of async data fetch | dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }); |
| FETCH_DATA_FAILURE | Indicates failure during async data fetch | dispatch({ type: 'FETCH_DATA_FAILURE', payload: error }); |
Example: Using Redux with Async Actions
import { createAction, createReducer } from '@reduxjs/toolkit';
// Async action creators
const fetchData = createAsyncThunk(
'data/fetch',
async () => {
const response = await fetch('/api/data');
return response.json();
}
);
// Reducer handling async actions
const dataReducer = createReducer(
{ loading: false, data: null, error: null },
{
[fetchData.pending]: (state) => {
state.loading = true;
},
[fetchData.fulfilled]: (state, action) => {
state.loading = false;
state.data = action.payload;
},
[fetchData.rejected]: (state, action) => {
state.loading = false;
state.error = action.error.message;
}
}
);
Example: Using React Context with Async Actions
const DataContext = createContext();
export const DataProvider = ({ children }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Fetch failed:', error);
} finally {
setLoading(false);
}
};
return (
<DataContext.Provider value={{ data, loading, fetchData }}>
{children}
</DataContext.Provider>
);
};
Note: When implementing async actions in React Native, ensure proper error handling and loading states to provide a smooth user experience.
5. Using Zustand for Lightweight State
Example: Basic Store Setup
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
export default useStore
| Property | Description | Usage |
|---|---|---|
| create | Function to create a new store | const useStore = create((set) => ...) |
| set | Function to update store state | set((state) => ({ count: state.count + 1 })) |
| get | Function to read store state (optional) | get().count |
Note: Zustand is a lightweight state management solution that works well with React Native's component model.
Example: Async Actions
import { create } from 'zustand'
const useStore = create((set) => ({
data: null,
loading: false,
fetchData: async () => {
set({ loading: true })
try {
const response = await fetch('https://api.example.com/data')
const result = await response.json()
set({ data: result, loading: false })
} catch (error) {
set({ loading: false })
}
},
}))
export default useStore
Key Features of Zustand
- Minimal API surface
- No provider needed
- Supports middleware and plugins
- Works with React Native's async patterns
6. Using Jotai for Atomic State
Example: Basic Jotai Setup
// Define an atom
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
// Use in a component
function Counter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
| Feature | Description | Notes |
|---|---|---|
| atom() | atom(initialValue) |
Creates a new atom with an initial value |
| useAtom() | useAtom(atom) |
Hook to read and write atom values |
| useAtomValue() | useAtomValue(atom) |
Hook to read atom value only |
| useSetAtom() | useSetAtom(atom) |
Hook to set atom value only |
Note: Jotai atoms are immutable, and updates create new atom instances.
Example: Async Atom
import { atom } from 'jotai'
import { atomWithCache } from 'jotai/cache'
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
const userAtom = atomWithCache(async (get, id) => {
return await fetchUser(id)
})
Example: Derived State
import { atom } from 'jotai'
const countAtom = atom(0)
const doubledAtom = atom(get => get(countAtom) * 2)
function DoubledCounter() {
const [doubled] = useAtom(doubledAtom)
return <div>Doubled: {doubled}</div>
}
Key Features of Jotai
- Atomic state management with fine-grained reactivity
- Supports async atoms and caching
- Minimal API surface with powerful composition
- Works well with React's concurrent rendering
7. Implementing Persist Middleware
| Feature | Description | Implementation |
|---|---|---|
| persistKey | Unique key to identify the persisted state | 'userState' |
| storage | Storage mechanism (e.g., AsyncStorage, Redux Persist) | AsyncStorage |
| whitelist | State keys to persist (optional) | ['user', 'theme'] |
| blacklist | State keys to exclude from persistence (optional) | ['loading', 'errors'] |
Example: Redux Persist Setup
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['user', 'theme'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
});
export const persistor = persistStore(store);
Example: Using with React Navigation
import { NavigationContainer } from '@react-navigation/native';
import { PersistGate } from 'redux-persist/integration/react';
function App() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
</PersistGate>
</Provider>
);
}
Note: Ensure
AsyncStorage is properly configured in your project for persistence to work.
Warning: Large state objects can cause performance issues when persisted. Consider using
whitelist to limit stored keys.
8. Using Recoil for React State
Example: Basic Recoil Setup
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// Define an atom (state)
const countAtom = atom({
key: 'count',
default: 0
});
// Define a selector (computed value)
const doubleCountSelector = selector({
key: 'doubleCount',
get: ({ get }) => {
const count = get(countAtom);
return count * 2;
}
});
// Component using the state
function Counter() {
const [count, setCount] = useRecoilState(countAtom);
const doubleCount = useRecoilValue(doubleCountSelector);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
| Feature | Description | Notes |
|---|---|---|
| atom | atom({ key, default }) |
Defines a piece of state. key must be unique across the app. |
| selector | selector({ key, get }) |
Computes derived state from atoms or other selectors. |
| useRecoilState | useRecoilState(atom) |
Returns [value, setter] for reading and updating atom. |
| useRecoilValue | useRecoilValue(atomOrSelector) |
Returns the value of an atom or selector (read-only). |
Note: Recoil is a state management library built by Facebook, designed to work with React components.
Warning: Ensure all
key values in atoms and selectors are unique to prevent state conflicts.
Key Concepts in Recoil
- Atoms represent the source of truth for state.
- Selectors derive state from atoms or other selectors.
- useRecoilState provides read-write access to atoms.
- useRecoilValue provides read-only access to atoms or selectors.
9. Managing Global UI State
| Library | Core Features | UI State Management | Notes |
|---|---|---|---|
| Redux | Centralized store, actions, reducers | useSelector, useDispatch |
Requires react-redux bindings |
| Zustand | Simple API, middleware support | useStore, create |
Lightweight alternative to Redux |
| Jotai | Atom-based state management | useAtom, atom |
Granular state updates with atomic units |
| Recoil | Atoms, Selectors, persistent state | useRecoilValue, useSetRecoilState |
Developed by Facebook, supports async state |
Example: Zustand UI State Management
import { create } from 'zustand';\n\nconst useUIStore = create((set) => ({\n isModalOpen: false,\n toggleModal: () => set((state) => ({ isModalOpen: !state.isModalOpen })),\n}));\n\nfunction ModalComponent() {\n const { isModalOpen, toggleModal } = useUIStore();\n \n return (\n <div>\n <button onClick={toggleModal}>Toggle Modal</button>\n {isModalOpen && (\n <div className="modal">\n <h2>Modal Content</h2>\n </div>\n )}\n </div>\n );\n}
Example: Jotai UI State Management
import { atom, useAtom } from 'jotai';\n\nconst modalOpenAtom = atom(false);\n\nfunction ModalComponent() {\n const [isModalOpen, setIsModalOpen] = useAtom(modalOpenAtom);\n\n return (\n <div>\n <button onClick={() => setIsModalOpen(!isModalOpen)}>Toggle Modal</button>\n {isModalOpen && (\n <div className="modal">\n <h2>Modal Content</h2>\n </div>\n )}\n </div>\n );\n}
Note: For global UI state like modals, toasts, or navigation, consider using lightweight libraries such as Zustand or Jotai for simpler setup and better performance compared to Redux.
10. Choosing State Management Approach
State Management Options in React Native
- Redux: Centralized store with predictable state updates; requires more boilerplate and setup.
- Context API: Built-in React feature for sharing state across components without prop drilling; suitable for small to medium apps.
- Recoil: Atom-based state management by Facebook; offers fine-grained reactivity and is more flexible than Context API.
- Jotai: Minimalist, atom-based state management; easy to use and highly composable.
- Zustand: Lightweight, simple store with minimal boilerplate; supports middleware and async operations.
- Pinia: Modern, Vue-inspired state management; offers a more intuitive API and better TypeScript support.
| Feature | Redux | Context API | Recoil | Jotai | Zustand | Pinia |
|---|---|---|---|---|---|---|
| Centralized Store | true |
false |
true |
false |
true |
true |
| Reactivity | Manual | Full | Fine-grained | Fine-grained | Manual | Automatic |
| Boilerplate | High | Low | Medium | Low | Very Low | Low |
| TypeScript Support | Good | Good | Excellent | Excellent | Good | Excellent |
| Middleware Support | Yes | No | No | No | Yes | Yes |
Decision Factors
- App Size: For small to medium apps, Context API or Jotai may suffice. Larger apps might benefit from Redux or Zustand.
- Team Experience: Teams familiar with Redux might prefer it; newer teams may find Zustand or Jotai easier to adopt.
- Reactivity Needs: For fine-grained reactivity, Recoil or Jotai are preferred.
- Tooling: Redux has mature tooling like Redux DevTools, while Zustand and Pinia have modern alternatives.