Context
Share a value down the tree without props, using the React 19 provider shorthand, and the re-render limit that bounds context.
const Ctx = createContext(defaultValue) → <Ctx value={v}>…</Ctx> → const v = useContext(Ctx)Context lets a component read a value from an ancestor without every component in between passing it as a prop. It exists for values a whole section of the app needs: the theme, the signed-in user, the language.
createContext(defaultValue) returns a context object. You wrap part of the tree in that object as a provider and give it a value. Any component below calls useContext with the same context object and receives that value, however deep it sits. The default value is used only when there is no provider above, and it never changes.
Provider shorthand
React 19 lets you render the context object itself as the provider: <ThemeContext value={value}>. The longhand <ThemeContext.Provider value={value}> is the same thing and still works, and most existing code uses it. React Native 0.87 ships React 19, so both forms are available.
Context is the only value in React Native that flows down a tree on its own. It is not a CSS cascade. Styles do not inherit from a parent View to its children; context values do, because components ask for them by name.
The re-render problem
When a provider's value changes, every component reading that context re-renders, and React.memo in between does not stop it. An object literal built during the provider's render is a new object every time, so wrap it in useMemo. For values that change many times a second, context is the wrong tool; use a store with selectors.
Example
// A centred card on a full screen. Tapping Toggle flips the whole screen light/dark.
import { createContext, useContext, useMemo, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
type Theme = { background: string; text: string };
type ThemeValue = { theme: Theme; toggle: () => void };
const light: Theme = { background: "#ffffff", text: "#111111" };
const dark: Theme = { background: "#111111", text: "#f5f5f5" };
// The argument is only a fallback for a component with no provider above it.
const ThemeContext = createContext<ThemeValue>({ theme: light, toggle: () => {} });
function ThemedCard() {
// Reads the nearest provider above. No props were threaded down to get here.
const { theme, toggle } = useContext(ThemeContext);
return (
<View style={[styles.card, { backgroundColor: theme.background }]}>
<Text style={{ color: theme.text }}>Tap to switch theme</Text>
<Pressable onPress={toggle}>
<Text style={{ color: theme.text }}>Toggle</Text>
</Pressable>
</View>
);
}
export default function App() {
const [isDark, setIsDark] = useState(false);
// useMemo keeps one object across renders; a fresh literal re-renders every consumer.
const value = useMemo<ThemeValue>(
() => ({ theme: isDark ? dark : light, toggle: () => setIsDark((d) => !d) }),
[isDark]
);
// React 19 shorthand. Longhand, identical: <ThemeContext.Provider value={value}>
return (
<ThemeContext value={value}>
<View style={[styles.screen, { backgroundColor: isDark ? "#000000" : "#eeeeee" }]}>
<ThemedCard />
</View>
</ThemeContext>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center" },
card: { padding: 20, borderRadius: 12, gap: 8, alignItems: "center" },
});When to use it
- Give every screen the current theme without passing a theme prop through each layout component.
- Expose the signed-in user and a signOut function to any screen under the authenticated part of the tree.
- Pass a reducer's dispatch function down to deeply nested form fields without threading callbacks.
More examples
Splitting the value from the actions
On screen: a line of text with a button under it; tapping the button switches the text between "All items" and "Unread only". Two contexts instead of one means the button, which only dispatches, is not re-rendered when the filter changes. Reach for this split when a provider sits near the root and many of its consumers only need to trigger changes, not read them.
import { createContext, useContext, useMemo, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
type Filters = { onlyUnread: boolean };
type FiltersApi = { toggleUnread: () => void };
const FiltersContext = createContext<Filters>({ onlyUnread: false });
// The actions never change, so this provider's value is built once.
const FiltersApiContext = createContext<FiltersApi>({ toggleUnread: () => {} });
function FilterButton() {
// Reads only the actions, so a filter change does not re-render this component.
const { toggleUnread } = useContext(FiltersApiContext);
return (
<Pressable onPress={toggleUnread} style={styles.button}>
<Text>Toggle unread</Text>
</Pressable>
);
}
function FilterLabel() {
const { onlyUnread } = useContext(FiltersContext);
return <Text>{onlyUnread ? "Unread only" : "All items"}</Text>;
}
export default function FiltersScreen() {
const [onlyUnread, setOnlyUnread] = useState(false);
const filters = useMemo<Filters>(() => ({ onlyUnread }), [onlyUnread]);
const api = useMemo<FiltersApi>(
() => ({ toggleUnread: () => setOnlyUnread((v) => !v) }),
[]
);
return (
<FiltersApiContext value={api}>
<FiltersContext value={filters}>
<View style={styles.screen}>
<FilterLabel />
<FilterButton />
</View>
</FiltersContext>
</FiltersApiContext>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 },
button: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8, backgroundColor: "#e5e5e5" },
});A typed access hook that fails loudly
Nothing is drawn here; this is the pair of files every context ends up as. The throw turns a forgotten provider into an immediate, named error during development instead of a screen that quietly shows a signed-out state, and the null check also narrows the type so callers do not have to test for null themselves.
import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
type User = { id: string; name: string };
type AuthValue = { user: User | null; signIn: (name: string) => void };
// null default: there is no sensible "no provider" value, so reading it is a bug to surface.
const AuthContext = createContext<AuthValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value = useMemo<AuthValue>(
() => ({ user, signIn: (name) => setUser({ id: "u1", name }) }),
[user]
);
return <AuthContext value={value}>{children}</AuthContext>;
}
// Components import this, never AuthContext itself.
export function useAuth(): AuthValue {
const value = useContext(AuthContext);
if (value === null) {
throw new Error("useAuth must be called inside AuthProvider");
}
return value; // narrowed: callers get AuthValue, not AuthValue | null
}
Discussion