Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Learning Objectives
By the end of this module, you’ll understand:
- Local state with useState and useReducer
- Context API patterns
- Prop drilling solutions
- When to use global state
- How mobile-specific concerns shape state decisions
Why State Management Matters in Mobile
Think of state as the short-term memory of your application. Every piece of data that can change — the text in an input field, whether a modal is open, the list of items fetched from an API — lives in state somewhere. The question is never whether you need state management, but where each piece of state should live.
In mobile apps, this decision carries extra weight. Unlike web apps where a page reload clears everything, mobile users expect their app to feel continuous: scroll positions preserved, form inputs remembered, and data cached across navigations. Getting state architecture wrong means janky transitions, lost user input, and excessive network requests that drain battery.
The golden rule: state should live as close to where it is used as possible, and only be lifted higher when multiple components genuinely need it.
Local State
useState Pattern
useState is your default tool for any piece of data that belongs to a single component. If only one screen or one widget needs it, keep it local.
import { useState } from 'react';
function Counter() {
// useState returns a pair: current value and a setter function.
// The argument (0) is the initial value, only used on first render.
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
{/* Use the functional updater form (c => c + 1) when the new value
depends on the previous value. This avoids stale closure bugs,
which are especially common in mobile event handlers that fire
rapidly (like gesture callbacks). */}
<Button title="+" onPress={() => setCount(c => c + 1)} />
</View>
);
}
Mobile tip: Every useState setter triggers a re-render. On resource-constrained mobile devices, avoid storing rapidly-changing values (like scroll positions or gesture coordinates) in useState. Use useRef or Reanimated shared values instead — they update without re-rendering.
useReducer for Complex State
When a component has multiple related state values that change together, useReducer keeps the logic organized. Think of it like a state machine: you dispatch an action describing what happened, and the reducer decides how state changes.
import { useReducer } from 'react';
// Define all possible states upfront. This makes it impossible to end up
// in an invalid state like { loading: true, error: 'something' }.
const initialState = { loading: false, data: null, error: null };
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
// Clear previous errors when starting a new fetch
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
// Usage in a component
function UserProfile({ userId }) {
const [state, dispatch] = useReducer(dataReducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetchUser(userId)
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, [userId]);
if (state.loading) return <ActivityIndicator />;
if (state.error) return <Text>Error: {state.error}</Text>;
return <Text>{state.data?.name}</Text>;
}
When to pick useReducer over useState:
- The next state depends on the previous state in non-trivial ways
- You have more than 3 related state variables
- State transitions need to be predictable and testable (the reducer is a pure function you can unit-test in isolation)
Context API
The Problem: Prop Drilling
Imagine you have a theme value at the top of your app and a button 6 components deep that needs it. Without Context, you would have to pass theme as a prop through every intermediate component — even if those components never use it. This is prop drilling, and it makes code brittle and hard to refactor.
Context solves this by creating a “tunnel” that lets deeply nested components access shared data directly.
Creating Context
import { createContext, useContext, useState, useMemo } from 'react';
// 1. Create the context with a meaningful default (or null + type guard)
const ThemeContext = createContext(null);
// 2. Build a provider that owns and manages the state
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// useMemo prevents the value object from being recreated on every render,
// which would cause every consumer to re-render unnecessarily.
const value = useMemo(() => ({
theme,
isDark: theme === 'dark',
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 3. Create a custom hook with a helpful error message
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error(
'useTheme must be used within a ThemeProvider. ' +
'Wrap your app root in <ThemeProvider> in App.tsx.'
);
}
return context;
}
Context re-render pitfall: When a Context value changes, every component consuming that context re-renders — even if it only uses a property that did not change. For high-frequency updates (like animation values or cursor positions), Context is the wrong tool. Use a dedicated state library like Zustand or Jotai instead, which support selective subscriptions.
When to Use Context vs. a State Library
| Scenario | Use Context | Use a Library (Zustand, etc.) |
|---|
| Theme / locale / auth status | Yes — changes rarely | Also works, but overkill |
| Shopping cart | Maybe — if small app | Yes — many consumers, frequent updates |
| Real-time data (chat, presence) | No — too many re-renders | Yes — selective subscriptions |
| Form state across screens | No | Yes — or React Hook Form |
Derived State: Calculate, Don’t Store
One of the most common state management mistakes is storing values that could be calculated from existing state. Every piece of redundant state is a synchronization bug waiting to happen.
// Bad: storing both items and filteredItems
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]); // Sync risk!
const [searchQuery, setSearchQuery] = useState('');
// Good: derive filteredItems from items + searchQuery
const [items, setItems] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
// useMemo recalculates only when items or searchQuery change
const filteredItems = useMemo(
() => items.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
),
[items, searchQuery]
);
Best Practices
- Start local, lift when needed — Do not reach for global state until two or more unrelated components need the same data.
- Use context for truly global, rarely-changing state — Theme, locale, and authentication status are ideal. Shopping carts and real-time feeds are not.
- Avoid prop drilling with composition — Before adding Context, try restructuring your component tree so that the parent renders the consuming child directly (the “slots” pattern).
- Keep state minimal — Derive computed values with
useMemo rather than storing them separately. Two sources of truth will eventually disagree.
- Profile before optimizing — On mobile, unnecessary re-renders matter more than on web, but the React Profiler should guide your decisions, not assumptions.