Managing App State
1. Using Context API
| Property | Description | Usage |
|---|---|---|
| createContext | Creates a new context object | const MyContext = createContext(defaultValue); |
| Provider | Component to wrap children and provide values to them | <MyContext.Provider value={value}>...</MyContext.Provider> |
| useContext | Hook to consume values from a context | const value = useContext(MyContext); |
Example: Basic Context Usage
// Create context
const ThemeContext = createContext("light");
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumer component
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}
Note: Context is ideal for global state like themes, user preferences, or language settings.
Warning: Avoid overusing Context as it can make components harder to test and debug.
2. Creating Global State
Example: Using React Context for Global State
import React, { createContext, useContext, useReducer } from 'react';
// Create context
const GlobalStateContext = createContext();
// Initial state
const initialState = {
user: null,
theme: 'light',
};
// Reducer function
const globalReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'TOGGLE_THEME':
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
default:
return state;
}
};
// Provider component
export const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(globalReducer, initialState);
return (
<GlobalStateContext.Provider value={{ state, dispatch }}>
{children}
</GlobalStateContext.Provider>
);
};
// Custom hook to use global state
export const useGlobalState = () => {
const context = useContext(GlobalStateContext);
if (!context) {
throw new Error('useGlobalState must be used within a GlobalStateProvider');
}
return context;
};
| Property | Description | Usage |
|---|---|---|
| initialState | Initial state object for the global store | Define default values for all state properties |
| globalReducer | Function that updates state based on actions | Handles all state mutations in a predictable way |
| GlobalStateProvider | Wrapper component to provide state to children | Wrap your app or parts of it with this provider |
| useGlobalState | Custom hook to access global state and dispatch | Use in any component that needs to read or update state |
Note: For more complex state management, consider using libraries like Redux or Zustand.
Example: Using Zustand for Global State
import { create } from 'zustand';
// Create a store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Using the store in a component
const Counter = () => {
const { count, increment, decrement, reset } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
};
State Management Options in Expo
- React Context + useReducer: Built-in, good for simple state
- Zustand: Lightweight, easy to use, great for small to medium apps
- Redux: Powerful and scalable, but more boilerplate
- AsyncStorage: For persisting state to disk (e.g., user preferences)
3. Using useReducer Hook
The useReducer hook is a powerful alternative to useState for managing complex state logic in React applications. It's particularly useful when state updates depend on the previous state or involve multiple sub-values.
| Property | Description | Usage |
|---|---|---|
| reducer | A function that takes the current state and an action, returning the new state. | (state, action) => newState |
| initialState | The initial state value for the reducer. | Any valid JavaScript value (object, array, primitive) |
| action | An object that describes the change to the state. | Must have a type property and optional payload |
Example: Basic useReducer usage
import React, { useReducer } from 'react';<br><br>const initialState = { count: 0 };<br><br>function reducer(state, action) {<br> switch (action.type) {<br> case 'INCREMENT':<br> return { count: state.count + 1 };<br> case 'DECREMENT':<br> return { count: state.count - 1 };<br> default:<br> throw new Error();<br> }<br>}<br><br>function Counter() {<br> const [state, dispatch] = useReducer(reducer, initialState);<br><br> return (<br> <div><br> Count: {state.count}<br /><br> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button><br> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button><br> </div><br> );<br>}
Example: useReducer with payload
import React, { useReducer } from 'react';<br><br>const initialState = { todos: [] };<br><br>function todoReducer(state, action) {<br> switch (action.type) {<br> case 'ADD_TODO':<br> return {<br> todos: [...state.todos, action.payload]<br> };<br> case 'REMOVE_TODO':<br> return {<br> todos: state.todos.filter(todo => todo.id !== action.payload)<br> };<br> default:<br> return state;<br> }<br>}<br><br>function TodoList() {<br> const [state, dispatch] = useReducer(todoReducer, initialState);<br><br> const addTodo = (text) => {<br> dispatch({<br> type: 'ADD_TODO',<br> payload: { id: Date.now(), text }<br> });<br> };<br><br> const removeTodo = (id) => {<br> dispatch({ type: 'REMOVE_TODO', payload: id });<br> };<br><br> return (<br> <div><br> {state.todos.map(todo => (<br> <div key={todo.id}><br> {todo.text}<br /><br> <button onClick={() => removeTodo(todo.id)}>Remove</button><br> </div><br> ))}<br> </div><br> );<br>}
Note: Unlike
useState, useReducer does not provide a direct way to update state. Instead, you must dispatch actions to describe the desired change.
4. Implementing Redux
Example: Setting up Redux with Expo
// Install required packages
// npm install @reduxjs/toolkit react-redux
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
// Define a slice
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
},
})
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
| Feature | Description | Notes |
|---|---|---|
| configureStore | function |
Creates a Redux store with the provided reducer |
| createSlice | function |
Generates reducer and action creators for a slice of state |
| initialState | object |
Initial state for the slice |
| reducers | object |
Map of reducer functions to handle state changes |
Note: Redux Toolkit simplifies Redux setup and is the recommended approach for new projects.
Warning: For large-scale apps, consider using
createAsyncThunk for handling async logic.
Redux Setup Steps
- Install
@reduxjs/toolkitandreact-redux - Create a store using
configureStore - Define slices with
createSlice - Use
Providerto wrap your app with the store
5. Creating Redux Store
Example: Basic Redux Store Setup
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
| Property | Description | Values |
|---|---|---|
| reducer | Root reducer function or object with slice reducers | Function | Object |
| middleware | Middleware to enhance store functionality | Array<Middleware> |
| devTools | Enable Redux DevTools extension | true | false |
Note: In Expo projects, ensure
@reduxjs/toolkit is installed and properly configured.
6. Defining Actions and Reducers
Actions and reducers are core concepts in state management using Redux Toolkit with Expo. Actions describe the event that occurred, while reducers specify how the state changes in response to those actions.
| Action | Type | Description |
|---|---|---|
| type | string |
Unique identifier for the action |
| payload | any |
Data associated with the action |
Example: Action Creator Using Redux Toolkit
import { createAction } from "@reduxjs/toolkit";
const increment = createAction("INCREMENT", (amount) => ({
payload: amount
}));
Example: Reducer Definition
import { createReducer } from "@reduxjs/toolkit";
const counterReducer = createReducer(0, {
[increment.type]: (state, action) => state + action.payload
});
Note: The
createReducer function from Redux Toolkit simplifies reducer creation by allowing you to define state transitions in a more readable way.
7. Using Redux Hooks
| Hook | Description | Usage |
|---|---|---|
| useSelector | Extracts data from the Redux store state | |
| useDispatch | Returns a reference to the dispatch function from the Redux store | |
Note: Redux Toolkit's
createSlice and configureStore simplify state management in Expo apps.8. Implementing Redux Toolkit
| Feature | Description | Usage |
|---|---|---|
| configureStore | Creates a Redux store with middleware and reducer setup | import { configureStore } from '@reduxjs/toolkit' |
| createSlice | Generates actions, reducers, and selectors for a slice of state | const counterSlice = createSlice({ name: 'counter', initialState, reducers }) |
| createAsyncThunk | Handles async logic and returns a thunk for dispatching actions | const fetchUser = createAsyncThunk('user/fetch', async (id) => { ... }) |
Example: Basic Store Setup
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
const store = configureStore({
reducer: {
counter: counterReducer,
},
})
export default store
Example: Creating a Slice
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
},
})
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
Example: Async Action with createAsyncThunk
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return await response.json()
} catch (error) {
return rejectWithValue(error.message)
}
}
)
const userSlice = createSlice({
name: 'user',
initialState: {
data: null,
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = true
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = false
state.error = action.payload
})
},
})
export default userSlice.reducer
Note: Redux Toolkit simplifies Redux setup by including
immer and redux-thunk by default, reducing boilerplate.
Warning: Avoid mutating state directly in reducers. Redux Toolkit uses
immer under the hood, but it's still good practice to treat state as immutable.
Redux Toolkit Overview
- NEW Simplifies Redux setup with
configureStore - NEW Reduces boilerplate with
createSlice - NEW Handles async logic with
createAsyncThunk - NEW Integrates with React via
useSelectoranduseDispatch
9. Using Zustand
Example: Basic Zustand Store Setup
// store.js
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() | Creates a new store instance | const useStore = create((set) => ({ ... })) |
| set() | Updates store state | set((state) => ({ count: state.count + 1 })) |
| get() | Reads current state values | const count = useStore.getState().count |
Note: Zustand is a lightweight state management solution that works well in Expo apps.
Example: Async Actions with Zustand
// store.js
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 and Expo
10. Implementing MobX
MobX is a state management library that helps manage and react to state changes in Expo applications. It provides a simple and efficient way to handle application state.
| Feature | Description | Usage |
|---|---|---|
| observable | Marks a value as observable, making it reactive to changes. | const observableValue = observable(0); |
| computed | Creates a computed value that is derived from observable values. | const doubleValue = computed(() => observableValue * 2); |
| action | Defines a function that modifies observable state. | const increment = action(() => observableValue++); |
Example: Basic MobX Setup
import { observable, computed, action } from 'mobx';
import { observer } from 'mobx-react-lite';
// Define a store
const store = observable({
count: 0,
get doubleCount() {
return this.count * 2;
},
increment: action(function() {
this.count++;
})
});
// Use in a component
const CounterComponent = observer(() => (
<div>
<p>Count: {store.count}</p>
<p>Double: {store.doubleCount}</p>
<button onPress={() => store.increment()}>Increment</button>
</div>
));
Note: MobX requires
mobx-react-lite for React integration. Ensure you have the correct version compatible with your Expo SDK.
Warning: MobX is not officially supported in Expo Go. You must build your app with a custom development client or EAS Build to use MobX.
11. Using React Query
Example: Fetching and Caching Data
import { useQuery } from 'react-query';
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetchUser(userId)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {data.name}!</div>;
}
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
| Property | Description | Default |
|---|---|---|
| queryKey | Unique identifier for the query | Required |
| queryFn | Function that fetches the data | Required |
| enabled | Whether the query should run | true |
| staleTime | How long data is considered fresh (ms) | 1000 * 60 * 5 |
| cacheTime | How long to keep data in cache (ms) | 1000 * 60 * 5 |
Note: React Query automatically handles caching, refetching, and background updates.
Example: Mutating Data
import { useMutation, useQueryClient } from 'react-query';
function UpdateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Invalidate and refetch queries
queryClient.invalidateQueries('user');
}
});
return (
<button onClick={() => mutation.mutate({ id: 1, name: 'New Name' })}>
Update User
</button>
);
}
async function updateUser({ id, name }) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
return response.json();
}
Key Features
- Automatic Caching - Data is cached and reused across components
- Background Refetching - Automatically refetches stale data
- Optimistic Updates - Update UI before server response
- Error Recovery - Handles network errors gracefully
12. Persisting State
Example: Using AsyncStorage to persist user preferences
import AsyncStorage from '@react-native-async-storage/async-storage';
const saveUserPreference = async (key, value) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error("Failed to save preference", error);
}
};
const loadUserPreference = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error("Failed to load preference", error);
return null;
}
};
// Usage
saveUserPreference("theme", "dark");
const theme = await loadUserPreference("theme");
| Method | Description | Notes |
|---|---|---|
| setItem(key, value) | Saves a key-value pair to storage | Value must be a string. Use JSON.stringify for objects |
| getItem(key) | Retrieves a value by key | Returns null if key doesn't exist. Use JSON.parse to parse objects |
| removeItem(key) | Deletes a key-value pair | Does not throw if key doesn't exist |
| getAllKeys() | Retrieves all stored keys | Useful for debugging or batch operations |
Note: AsyncStorage is not suitable for sensitive data like passwords or tokens. Use secure storage solutions like
expo-secure-store for such cases.
Warning: AsyncStorage operations are asynchronous and must be awaited or handled with
.then(). Blocking the main thread can cause performance issues.