Global State with Context & Zustand
useState only lives inside one component — real apps need app-wide state (auth, cart, theme) any screen can read and change.
What you will learn
- Explain when local state is not enough
- Share state across screens with React Context
- Manage global state cleanly with a Zustand store
The problem with local state
So far every useState lives inside one component. That is fine for a counter, but real apps have data that many screens share: whether the user is logged in, the items in a cart, the chosen theme. If that data lives in one screen, every other screen that needs it must have it passed down through props — screen after screen — which gets painful fast. People call this prop drilling: handing a value down through layers that do not even use it, just to reach the one that does.
Global state is data that lives outside any single screen, in a shared place every component can read and update directly. Two common tools: React Context (built in) and Zustand (a tiny, popular library). Almost every paid course and job expects you to know at least one.
Option 1 — React Context
Context is React’s built-in way to share a value with every component below it, without passing props by hand. You create a context, wrap your app in its Provider (which holds the value), and any screen reads it with the useContext hook. Here is a simple auth context that tracks whether someone is logged in:
import { createContext, useContext, useState } from 'react';
// 1) Create the context
const AuthContext = createContext(null);
// 2) A provider that holds the shared state
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (name) => setUser({ name });
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// 3) A tiny hook so screens read it easily
function useAuth() {
return useContext(AuthContext);
}Note: Output:
No screen output yet — this is the plumbing. Any screen wrapped by AuthProvider can now call useAuth() to read user or call login / logout, no matter how deep it sits.
You wrap your whole app once in the provider, then any screen uses it:
export default function App() {
return (
<AuthProvider>
<HomeScreen />
</AuthProvider>
);
}
function HomeScreen() {
const { user, login, logout } = useAuth();
return (
<View>
<Text>{user ? 'Hi ' + user.name : 'Please log in'}</Text>
<Button title="Log in" onPress={() => login('Aanya')} />
<Button title="Log out" onPress={logout} />
</View>
);
}Note: Output:
First the screen shows "Please log in". Tap "Log in" -> it changes to "Hi Aanya". Tap "Log out" -> back to "Please log in".
No props were passed to HomeScreen — it pulled user, login and logout straight from the shared context.
The whole Context flow, step by step
Following the auth example above from app start to a tap of "Log in", in order:
createContext(null)makes an empty shared box,AuthContext, that any component can later read from.- The
AuthProviderholds the real state: auser(startingnull) plusloginandlogoutfunctions, and it puts all three into the context viavalue={{ user, login, logout }}. - You wrap your whole app once in
<AuthProvider>, so every screen underneath sits inside that shared box. - A screen calls the tiny
useAuth()helper (which is justuseContext(AuthContext)) to pulluser,loginandlogout— no props passed down by hand. - On first draw
userisnull, so HomeScreen shows "Please log in". - The user taps "Log in"; its
onPressrunslogin('Aanya'), which callssetUser({ name: 'Aanya' })inside the provider. - Changing that state re-renders everyone reading the context, so HomeScreen now sees
user.nameand shows "Hi Aanya" — and tapping "Log out" setsuserback tonull.
Option 2 — Zustand (less boilerplate)
Context works, but writing a provider, a context and a hook for every piece of shared state gets wordy. Zustand is a tiny library that gives you a global store (a single object holding your state plus the functions that change it) with almost no ceremony — and no provider to wrap. It is a favourite in modern courses for exactly this reason.
Install it (one-time):
npx expo install zustandNote: Output: (Adds the zustand package. Nothing on screen yet.)
You define a store once with create. Inside, set is the function Zustand gives you to update the state. Here is the same auth idea as a store:
import { create } from 'zustand';
const useAuthStore = create((set) => ({
user: null,
login: (name) => set({ user: { name } }),
logout: () => set({ user: null }),
}));Note: Output:
(No screen output — this defines the store.) user starts as null; login and logout call set to replace it. That is the whole store.
Any component reads the store directly — no provider needed anywhere:
function HomeScreen() {
const user = useAuthStore((s) => s.user);
const login = useAuthStore((s) => s.login);
const logout = useAuthStore((s) => s.logout);
return (
<View>
<Text>{user ? 'Hi ' + user.name : 'Please log in'}</Text>
<Button title="Log in" onPress={() => login('Aanya')} />
<Button title="Log out" onPress={logout} />
</View>
);
}Note: Output:
Identical behaviour to the Context version: "Please log in" -> tap Log in -> "Hi Aanya".
The (s) => s.user part is a selector: it picks only the slice this screen cares about, so the screen re-renders only when that slice changes — a nice performance win.
Context vs Zustand — which to reach for
| React Context | Zustand | |
|---|---|---|
| Built in? | Yes | No (tiny library) |
| Boilerplate | More (provider + hook) | Very little |
| Provider needed? | Yes, wrap the app | No |
| Best for | A few simple shared values (theme, auth) | App-wide state used across many screens |
A common rule of thumb: reach for Context for one or two simple shared values, and for Zustand (or Redux Toolkit in larger teams) once several screens share state that changes often.
Tip: You may also hear about Redux Toolkit, the most popular choice on big teams. It works on the same idea — one shared store — but with more structure (actions and reducers). Learn the Context/Zustand mental model first; Redux Toolkit will then feel familiar.
Watch out: Do not put everything in global state. Data only one screen uses (a text box’s current value, a toggle) should stay in local useState. Global state is for things genuinely shared across screens — overusing it makes apps harder to follow.
Q. What problem does global state (Context or Zustand) mainly solve?
✍️ Practice
- Build a Zustand
useCounterStorewithcount,incrementandreset, and use it in two separate screens. - Add a
themevalue ("light"/"dark") to a Context or Zustand store and read it in a child component.
🏠 Homework
- Add app-wide auth with Zustand: a store with
user,loginandlogout, and two screens that both read and change it.