Understanding React Context
What is React Context?
React Context is a built-in feature of React that allows you to share data between components without explicitly passing props through every level of the component tree (known as "prop drilling"). It's especially useful for data that can be considered "global" for a specific part of your application, such as:
- User authentication status
- Theme preferences
- Language settings
- Form state (which is how TanStack Form uses it)
Think of Context like a broadcasting system that makes specific data available to any component that wants to tune in.
The Problem: Prop Drilling
Without Context, sharing data between components that aren't directly related requires passing props through intermediate components that don't actually use the data.
Example: A Shopping Cart Application
Imagine you're building an e-commerce application with these components:
App
- The main applicationNavbar
- Contains the cart icon and countProductList
- Shows available productsProductItem
- Individual product with "Add to Cart" buttonCartSummary
- Shows cart contentsCheckout
- Process for completing purchase
Without Context, you'd need to pass cart data through props:
// Without Context - Prop Drilling
function App() {
const [cart, setCart] = useState([]);
const addToCart = (product) => {
setCart([...cart, product]);
};
return (
<div>
<Navbar cartCount={cart.length} />
<ProductList products={products} addToCart={addToCart} />
<CartSummary items={cart} />
<Checkout cart={cart} />
</div>
);
}
function ProductList({ products, addToCart }) {
return (
<div>
{products.map((product) => (
<ProductItem key={product.id} product={product} addToCart={addToCart} />
))}
</div>
);
}
function ProductItem({ product, addToCart }) {
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
);
}
function Navbar({ cartCount }) {
return (
<nav>
<div>🛒 {cartCount} items</div>
</nav>
);
}
Notice how we had to pass addToCart
from App
→ ProductList
→ ProductItem
and cartCount
from App
→ Navbar
. This is prop drilling.
The Solution: React Context
React Context solves this by creating a way for components to access shared data without prop drilling.
How to Implement Context
1. Create a Context
const CartContext = React.createContext();
2. Create a Provider Component
The Provider wraps your components and makes the context data available to them.
function CartProvider({ children }) {
const [cart, setCart] = useState([]);
const addToCart = (product) => {
setCart([...cart, product]);
};
// The value object contains all data and functions we want to share
const value = {
cart,
cartCount: cart.length,
addToCart,
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
3. Wrap Your App with the Provider
function App() {
return (
<CartProvider>
<div className="app">
<Navbar />
<ProductList products={products} />
<CartSummary />
<Checkout />
</div>
</CartProvider>
);
}
4. Use the Context in Your Components
Any component inside the Provider can now access the context data directly:
function ProductItem({ product }) {
// Get the addToCart function directly from context
const { addToCart } = React.useContext(CartContext);
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
);
}
function Navbar() {
// Get the cart count directly from context
const { cartCount } = React.useContext(CartContext);
return (
<nav>
<div>🛒 {cartCount} items</div>
</nav>
);
}
function CartSummary() {
// Get the cart items directly from context
const { cart } = React.useContext(CartContext);
return (
<div>
<h2>Your Cart</h2>
{cart.map((item) => (
<div key={item.id}>
{item.name} - ${item.price}
</div>
))}
<p>Total: ${cart.reduce((sum, item) => sum + item.price, 0)}</p>
</div>
);
}
Context in TanStack Form
TanStack Form uses Context in a similar way to provide form state and functionality to form components.
// 1. Create the contexts
const { fieldContext, formContext } = createFormHookContexts();
// 2. Create a custom hook with components that use these contexts
const { useAppForm } = createFormHook({
fieldComponents: {
TextField,
NumberField,
},
formComponents: {
SubmitButton,
},
fieldContext,
formContext,
});
// 3. Use the hook to create a form instance
function MyForm() {
const form = useAppForm({
defaultValues: {
username: "",
email: "",
},
// ...other options
});
// 4. The form component internally uses the contexts to provide data
return (
<form>
{/* Each field gets access to its own field state */}
<form.AppField
name="username"
children={(field) => <field.TextField label="Username" />}
/>
{/* Form-level components access the entire form state */}
<form.AppForm>
<form.SubmitButton />
</form.AppForm>
</form>
);
}
Benefits of Using Context
-
Eliminates Prop Drilling: No need to pass props through intermediate components that don't use them.
-
Cleaner Component API: Components only receive props they actually use.
-
Centralized State Management: State is managed in one place, making it easier to update and maintain.
-
Component Reusability: Components become more reusable because they're not tightly coupled to specific prop structures.
-
Performance Optimization: With careful implementation, you can prevent unnecessary re-renders.
When to Use Context
Context is ideal for:
- Theme data: Light/dark mode and color schemes
- User authentication: User info, permissions, and login state
- Localization/i18n: Language settings and translations
- Form state: As seen in TanStack Form
- App configuration: Settings that affect multiple components
It's best to use Context for data that is truly "global" to a component tree and changes infrequently. For more complex state management, you might consider combining Context with other state management solutions like Redux or Zustand.
Context vs. Props
Props | Context |
---|---|
Explicit data flow | Implicit data flow |
Better for component-specific data | Better for "global" data |
More reusable components | Less prop drilling |
Clear dependencies | Hidden dependencies |
Best for direct parent-child | Best for distant components |
Conclusion
React Context is a powerful tool for sharing data between components without prop drilling. It simplifies your component tree and makes your code more maintainable. In libraries like TanStack Form, Context is a key mechanism that enables the creation of elegant, type-safe forms with minimal boilerplate.
By understanding and properly utilizing Context, you can create more maintainable and efficient React applications, especially when dealing with data that needs to be accessed by many components across your application.