Going Pro: Job-Ready React NativeExtra· 45 min read

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:

A Context provider holds shared auth state for the whole app
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:

Any screen reads or changes the shared auth state with useAuth
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:

  1. createContext(null) makes an empty shared box, AuthContext, that any component can later read from.
  2. The AuthProvider holds the real state: a user (starting null) plus login and logout functions, and it puts all three into the context via value={{ user, login, logout }}.
  3. You wrap your whole app once in <AuthProvider>, so every screen underneath sits inside that shared box.
  4. A screen calls the tiny useAuth() helper (which is just useContext(AuthContext)) to pull user, login and logout — no props passed down by hand.
  5. On first draw user is null, so HomeScreen shows "Please log in".
  6. The user taps "Log in"; its onPress runs login('Aanya'), which calls setUser({ name: 'Aanya' }) inside the provider.
  7. Changing that state re-renders everyone reading the context, so HomeScreen now sees user.name and shows "Hi Aanya" — and tapping "Log out" sets user back to null.

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):

Install Zustand
npx expo install zustand

Note: 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:

A Zustand store: state plus the actions that change it, in one place
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:

Read just the slice you need with a selector function
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 ContextZustand
Built in?YesNo (tiny library)
BoilerplateMore (provider + hook)Very little
Provider needed?Yes, wrap the appNo
Best forA 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?

Answer: Global state lives outside any one screen so multiple screens can read and update shared data (auth, cart, theme) without passing props through every layer.

✍️ Practice

  1. Build a Zustand useCounterStore with count, increment and reset, and use it in two separate screens.
  2. Add a theme value ("light"/"dark") to a Context or Zustand store and read it in a child component.

🏠 Homework

  1. Add app-wide auth with Zustand: a store with user, login and logout, and two screens that both read and change it.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →