Skip to main content

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/toolkit and react-redux
  • Create a store using configureStore
  • Define slices with createSlice
  • Use Provider to wrap your app with the store

5. Creating Redux Store

Example: Basic Redux Store Setup

import { configureStore } from &#x27;@reduxjs/toolkit&#x27;;
import counterReducer from &#x27;./features/counterSlice&#x27;;

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: Basic Action Definition

{type: "INCREMENT", payload: 1}

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
import { useSelector } from 'react-redux';

const theme = useSelector(state => state.theme);
useDispatch Returns a reference to the dispatch function from the Redux store
import { useDispatch } from 'react-redux';

const dispatch = useDispatch();
dispatch({ type: 'SET_THEME', payload: 'dark' });
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 useSelector and useDispatch

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 &#x27;@react-native-async-storage/async-storage&#x27;;

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.

Was this article helpful?