Why Reactive Variables Matter
Managing local state in React applications often requires introducing additional libraries like Redux or Zustand. However, if you're already using Apollo Client for GraphQL data management, reactive variables provide a powerful built-in solution for handling client-side state without adding extra dependencies.
Reactive variables are state containers that integrate seamlessly with Apollo Client's reactivity model. When a reactive variable changes, any component using that variable automatically re-renders with the new value. This makes them ideal for managing UI state, feature flags, user preferences, and other client-only data that doesn't need to be persisted to the server.
Eliminate External State Management Libraries
Before reactive variables, developers often needed Redux, MobX, or similar libraries to manage client state alongside Apollo's server cache. Reactive variables eliminate this complexity by providing a unified state management approach within the Apollo ecosystem. The key advantage is simplicity--a reactive variable requires just one line of code to create, compared to the boilerplate required to set up a Redux store or Context providers.
By leveraging reactive variables, you significantly reduce your application's bundle size. A typical Redux setup with React-Redux and Redux Toolkit adds considerable weight to your JavaScript bundle. Reactive variables, being built directly into Apollo Client, add no additional overhead beyond what you're already including for GraphQL data management. This is particularly valuable for performance-critical applications where every kilobyte matters.
Fine-Grained Reactivity
Unlike React's useState or Context, reactive variables provide fine-grained reactivity. When you update a reactive variable, only the components and queries that depend on that variable re-evaluate. This minimizes unnecessary re-renders and improves application performance, especially in larger applications with complex state dependencies. Components subscribe to specific variables rather than entire context providers, giving you precise control over when updates occur.
Integration with Apollo Cache
Reactive variables work seamlessly with Apollo Client's cache through field policies. You can use them alongside computed fields, derive state from cached data, or trigger cache updates in response to local state changes. This tight integration means you don't need custom logic to keep local state synchronized with your GraphQL data--everything works through Apollo's established reactivity patterns.
For developers working with Next.js applications, integrating Apollo Client's reactive variables with Next.js routing conventions creates a powerful combination for managing both client-side state and page navigation efficiently.
No Extra Dependencies
Built directly into Apollo Client, eliminating the need for Redux, Zustand, or other state management libraries.
Fine-Grained Reactivity
Components only re-render when the specific variables they depend on change, optimizing performance.
Cache Integration
Seamlessly works with Apollo Client's cache through field policies and the @client directive.
TypeScript Support
Full TypeScript type inference ensures type safety across your entire application.
Creating Reactive Variables
Basic Variable Creation
The makeVar function from @apollo/client creates a reactive variable. Call it with an initial value to create a variable that can be read and updated anywhere in your application.
import { makeVar } from '@apollo/client';
// Simple boolean variable
export const isDarkModeVar = makeVar<boolean>(false);
// String variable for user preferences
export const selectedLanguageVar = makeVar<string>('en');
// Object variable for complex state
interface UserPreferences {
notifications: boolean;
newsletter: boolean;
theme: 'light' | 'dark' | 'system';
}
export const userPreferencesVar = makeVar<UserPreferences>({
notifications: true,
newsletter: false,
theme: 'system',
});
// Array variable for lists
export const favoriteIdsVar = makeVar<number[]>([]);
The variable behaves like a function--when called without arguments, it returns the current value; when called with a new value, it updates the variable and triggers updates in all subscribed components. When you call the variable with a new value, Apollo Client automatically notifies all components and queries that depend on this variable. This happens synchronously, so the update is immediate and predictable.
Reading and Updating Variables
Reading and updating reactive variables is straightforward. Call the variable as a function to get or set its value:
// Read current value
const currentMode = isDarkModeVar();
console.log(currentMode); // false
// Update the variable
isDarkModeVar(true);
// Read updated value
console.log(isDarkModeVar()); // true
TypeScript Type Safety
TypeScript provides excellent support for reactive variables through type inference. The generic type parameter in makeVar<T> ensures type safety throughout your application. This is particularly valuable in large codebases where type errors can be caught at compile time rather than runtime.
import { makeVar } from '@apollo/client';
// Type is inferred from initial value
const countVar = makeVar(0);
// countVar is ReactiveVar<number>
// Explicit type for complex objects
interface AppState {
isLoading: boolean;
error: string | null;
data: Record<string, unknown> | null;
}
const appStateVar = makeVar<AppState>({
isLoading: true,
error: null,
data: null,
});
// Type-safe updates
appStateVar({
isLoading: false,
error: null,
data: { users: [] },
});
// TypeScript will catch type mismatches at compile time
// Error: Type 'string' is not assignable to type 'boolean'
// appStateVar({ isLoading: 'true', ... });
When working with complex object types, you can define interfaces that serve as contracts for your reactive variables. This approach ensures that all updates to the variable conform to the expected structure, preventing runtime errors caused by incorrect data shapes. The type system also provides excellent IDE support with autocompletion and inline documentation.
For teams looking to strengthen their TypeScript skills, our guide on using TypeScript with React provides practical examples that complement these reactive variable patterns.
Using Reactive Variables in React Components
The useReactiveVar Hook
The useReactiveVar hook subscribes a component to a reactive variable and returns its current value. The component re-renders whenever the variable changes, ensuring the UI always reflects the current state.
import { useReactiveVar } from '@apollo/client';
import { isDarkModeVar, userPreferencesVar } from './variables';
function ThemeToggle() {
const isDarkMode = useReactiveVar(isDarkModeVar);
const preferences = useReactiveVar(userPreferencesVar);
const toggleTheme = () => {
isDarkModeVar(!isDarkMode);
};
return (
<button onClick={toggleTheme}>
Current theme: {isDarkMode ? 'Dark' : 'Light'}
<br />
Preference: {preferences.theme}
</button>
);
}
The hook handles all the subscription management internally. When the component unmounts, the subscription is automatically cleaned up, preventing memory leaks. This means you don't need to worry about cleanup functions or subscription cleanup in your useEffect hooks.
Modal State Management Example
Here's a practical example showing how to manage modal state with reactive variables. This pattern avoids prop drilling through multiple component layers:
import { useReactiveVar } from '@apollo/client';
import { isModalOpenVar, activeModalVar, modalPropsVar } from './variables';
function ModalContainer() {
const isOpen = useReactiveVar(isModalOpenVar);
const modalType = useReactiveVar(activeModalVar);
const props = useReactiveVar(modalPropsVar);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={() => isModalOpenVar(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{modalType === 'confirm' && <ConfirmModal {...props} />}
{modalType === 'form' && <FormModal {...props} />}
{modalType === 'info' && <InfoModal {...props} />}
<button onClick={() => isModalOpenVar(false)}>Close</button>
</div>
</div>
);
}
// Custom hook for easy modal control
export function useModal() {
const openModal = (type: string, props: Record<string, unknown> = {}) => {
activeModalVar(type);
modalPropsVar(props);
isModalOpenVar(true);
};
const closeModal = () => {
isModalOpenVar(false);
activeModalVar(null);
modalPropsVar({});
};
return { openModal, closeModal };
}
Multiple Variables and Conditional Rendering
Components can subscribe to multiple reactive variables, and they work naturally with React's conditional rendering patterns. Each variable is tracked independently, and the component only re-renders when one of the subscribed variables changes:
import { useReactiveVar } from '@apollo/client';
import { isAuthenticatedVar, currentUserVar, sidebarOpenVar } from './variables';
function Header() {
const isAuthenticated = useReactiveVar(isAuthenticatedVar);
const currentUser = useReactiveVar(currentUserVar);
const sidebarOpen = useReactiveVar(sidebarOpenVar);
return (
<header className={sidebarOpen ? 'sidebar-open' : ''}>
{isAuthenticated ? (
<div>
<span>Welcome, {currentUser.name}</span>
<button onClick={() => isAuthenticatedVar(false)}>
Sign Out
</button>
</div>
) : (
<button onClick={() => isAuthenticatedVar(true)}>
Sign In
</button>
)}
</header>
);
}
Integrating with Apollo Cache
Field Policies and Reactive Variables
Reactive variables can be used within Apollo Client's cache field policies to create computed fields or derive state from cached data. This allows you to use reactive variables as the source of truth for cache reads and writes, creating a unified data layer that combines server and client state.
import { makeVar, InMemoryCache, gql } from '@apollo/client';
export const sortOptionVar = makeVar<'name' | 'date' | 'price'>('name');
export const filterCategoryVar = makeVar<string | null>(null);
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
sortOption: {
read() {
return sortOptionVar();
},
},
filterCategory: {
read() {
return filterCategoryVar();
},
},
filteredProducts: {
read(_, { toReference }) {
const products = toReference(
gql`query GetProducts { products { id name price category date } }`
);
const sortOption = sortOptionVar();
const category = filterCategoryVar();
return applyFiltersAndSort(products, { category, sortOption });
},
},
},
},
},
});
function applyFiltersAndSort<T extends { name: string; price: number; date: string; category: string }>(
items: T[],
options: { category: string | null; sortOption: 'name' | 'date' | 'price' }
): T[] {
let result = [...items];
if (options.category) {
result = result.filter(item => item.category === options.category);
}
return result.sort((a, b) => {
switch (options.sortOption) {
case 'name':
return a.name.localeCompare(b.name);
case 'date':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'price':
return b.price - a.price;
default:
return 0;
}
});
}
Using @client Directive with Reactive Variables
You can query reactive variables alongside your GraphQL data using the @client directive. This allows you to include local state in your data requirements, making components more declarative and keeping all data concerns in one place:
import { gql, useQuery } from '@apollo/client';
const GET_DASHBOARD_DATA = gql`
query GetDashboardData {
products @client {
items {
id
name
price
}
totalCount
}
filterOption @client
sortOption @client
isFiltersOpen @client
}
`;
function Dashboard() {
const { data, loading } = useQuery(GET_DASHBOARD_DATA);
if (loading) return <LoadingSpinner />;
return (
<div>
<ProductList
products={data.products.items}
filter={data.filterOption}
sort={data.sortOption}
isFiltersOpen={data.isFiltersOpen}
/>
</div>
);
}
Creating a Unified Data Layer
By combining reactive variables with Apollo's cache and the @client directive, you create a unified data layer where both server and client state are managed consistently. This approach eliminates the mental overhead of switching between different state management patterns and reduces the complexity of your application's data flow. Components can declare their data requirements declaratively, whether that data comes from your GraphQL server or exists only on the client.
This unified approach is particularly valuable for complex React applications where managing multiple sources of state can quickly become unwieldy. The cache serves as a single source of truth, with reactive variables providing a simple API for client-only state that integrates naturally with your existing GraphQL queries.
Best Practices
Organize Variables in a Central Location
Create a dedicated file or module for reactive variables to provide a single source of truth for client state. This makes it easier to track and manage variables across your application, and helps team members understand what client state exists without searching through multiple files:
// src/apollo/variables.ts
import { makeVar } from '@apollo/client';
// UI State
export const isSidebarOpenVar = makeVar(true);
export const isModalOpenVar = makeVar(false);
export const activeModalVar = makeVar<string | null>(null);
// User Preferences
export const themeVar = makeVar<'light' | 'dark'>('light');
export const languageVar = makeVar('en');
// Application State
export const isInitializedVar = makeVar(false);
export const currentViewVar = makeVar('dashboard');
// Feature Flags
export const featureFlagsVar = makeVar({
newCheckout: false,
darkMode: true,
betaFeatures: false,
});
Use Descriptive Variable Names
Follow a consistent naming convention that indicates the variable is a reactive variable. The Var suffix makes it immediately clear that you're dealing with a reactive variable rather than a regular variable or function:
// Good naming - clear and consistent
export const isLoadingVar = makeVar(false);
export const selectedItemsVar = makeVar<Set<string>>(new Set());
export const filterCriteriaVar = makeVar<FilterCriteria | null>(null);
// Avoid - unclear and inconsistent
export const loading = makeVar(false); // Unclear it's reactive
export const selected = makeVar(new Set()); // Not descriptive
Keep Variables Focused
Each reactive variable should manage a single piece of state. If you find yourself updating many fields at once, consider splitting the variable into multiple focused variables. This improves performance by minimizing unnecessary re-renders:
// Instead of one large object that changes frequently
export const appSettingsVar = makeVar({
theme: 'light',
language: 'en',
notifications: true,
autoSave: false,
});
// Use multiple focused variables
export const themeVar = makeVar<'light' | 'dark'>('light');
export const languageVar = makeVar('en');
export const notificationsEnabledVar = makeVar(true);
export const autoSaveEnabledVar = makeVar(false);
Handle Complex Updates Carefully
When updating reactive variables with complex objects or arrays, create new references to ensure React's change detection works correctly. Never mutate the existing value in place:
import { makeVar } from '@apollo/client';
interface Task {
id: string;
text: string;
completed: boolean;
}
export const tasksVar = makeVar<Task[]>([]);
// Good: Create new array
export const addTask = (task: Task) => {
tasksVar([...tasksVar(), task]);
};
// Good: Create new array with mapping
export const toggleTask = (id: string) => {
tasksVar(
tasksVar().map(task =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
};
// Good: Create new set
export const selectedTaskIdsVar = makeVar<Set<string>>(new Set());
export const toggleSelection = (id: string) => {
const current = selectedTaskIdsVar();
const next = new Set(current);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
selectedTaskIdsVar(next);
};
Performance Optimization Tips
Each useReactiveVar call creates a subscription. While the overhead is minimal, in large applications with many components, be mindful of how many variables each component subscribes to. Consider lifting subscriptions to parent components and passing values down via props when multiple child components need the same variable.
Understanding how reactive variables interact with other React hooks is essential for building efficient applications. Our guide on React useCallback covers complementary patterns for optimizing component performance alongside reactive variable usage.
// Instead of each child subscribing independently
function ParentComponent() {
return (
<>
<Header />
<Sidebar />
<Content />
</>
);
}
function Header() {
const isAuthenticated = useReactiveVar(isAuthenticatedVar);
// ...
}
function Sidebar() {
const isAuthenticated = useReactiveVar(isAuthenticatedVar);
// ...
}
// Better: Parent subscribes once and passes value as prop
function ParentComponent() {
const isAuthenticated = useReactiveVar(isAuthenticatedVar);
return (
<>
<Header isAuthenticated={isAuthenticated} />
<Sidebar isAuthenticated={isAuthenticated} />
<Content isAuthenticated={isAuthenticated} />
</>
);
}
When you need to derive state from reactive variables, use useMemo to avoid recalculating on every render. This is especially important for expensive computations:
import { useReactiveVar, useMemo } from '@apollo/client';
import { filterOptionsVar, productsVar } from './variables';
function FilteredProductList() {
const filter = useReactiveVar(filterOptionsVar);
const products = useReactiveVar(productsVar);
const filteredProducts = useMemo(() => {
return products.filter(product => {
if (filter.category && product.category !== filter.category) {
return false;
}
if (filter.minPrice && product.price < filter.minPrice) {
return false;
}
if (filter.maxPrice && product.price > filter.maxPrice) {
return false;
}
return true;
});
}, [products, filter.category, filter.minPrice, filter.maxPrice]);
return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
Common Use Cases
Feature Flags
Reactive variables excel at managing feature flags, allowing you to toggle features without code changes or app redeploys. This is essential for gradual rollouts, A/B testing, and quickly disabling problematic features:
// src/apollo/variables.ts
export const featureFlagsVar = makeVar({
newOnboardingFlow: false,
darkModeBeta: true,
experimentalAnalytics: false,
stripeIntegration: true,
});
// src/components/OnboardingFlow.tsx
function OnboardingFlow() {
const flags = useReactiveVar(featureFlagsVar);
if (!flags.newOnboardingFlow) {
return <LegacyOnboarding />;
}
return <NewOnboardingFlow />;
}
// src/utils/analytics.ts
function trackEvent(event: string, properties: Record<string, unknown>) {
const flags = featureFlagsVar();
if (flags.experimentalAnalytics) {
experimentalAnalytics.track(event, properties);
} else {
standardAnalytics.track(event, properties);
}
}
UI State Management
Manage UI state like modals, sidebars, and form states with reactive variables. This avoids prop drilling and keeps components clean. Reactive variables are particularly useful for React applications that need to coordinate state across distant components:
// src/apollo/variables.ts
export const modalStateVar = makeVar<{
isOpen: boolean;
component: string | null;
props: Record<string, unknown>;
}>({
isOpen: false,
component: null,
props: {},
});
export const toastQueueVar = makeVar<Toast[]>([]);
// src/hooks/useModal.ts
export function useModal() {
const openModal = (component: string, props: Record<string, unknown> = {}) => {
modalStateVar({
isOpen: true,
component,
props,
});
};
const closeModal = () => {
modalStateVar({
isOpen: false,
component: null,
props: {},
});
};
return { openModal, closeModal };
}
Authentication State Example
Here's a comprehensive example showing how to manage authentication state across your application using reactive variables. This pattern provides a clean separation between authentication logic and UI components:
// src/apollo/variables.ts
import { makeVar } from '@apollo/client';
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user' | 'guest';
avatar?: string;
}
export const isAuthenticatedVar = makeVar(false);
export const currentUserVar = makeVar<User | null>(null);
export const authTokenVar = makeVar<string | null>(null);
export const authLoadingVar = makeVar(false);
export const authErrorVar = makeVar<string | null>(null);
// src/hooks/useAuth.ts
import { useReactiveVar } from '@apollo/client';
import {
isAuthenticatedVar,
currentUserVar,
authTokenVar,
authErrorVar,
User
} from './variables';
export function useAuth() {
const isAuthenticated = useReactiveVar(isAuthenticatedVar);
const currentUser = useReactiveVar(currentUserVar);
const authError = useReactiveVar(authErrorVar);
const login = async (email: string, password: string) => {
authErrorVar(null);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
authTokenVar(data.token);
currentUserVar(data.user);
isAuthenticatedVar(true);
} catch (error) {
authErrorVar('Login failed. Please try again.');
}
};
const logout = () => {
authTokenVar(null);
currentUserVar(null);
isAuthenticatedVar(false);
};
return {
isAuthenticated,
currentUser,
authError,
login,
logout,
};
}
Caching Computed Results
Store expensive computed results in reactive variables to avoid recalculating on every render. Update the cached value when dependencies change. This pattern is valuable for dashboards and data-heavy applications where aggregation operations are costly.
For practical examples of building React dashboards with these patterns, explore our guide on top React dashboard libraries, which demonstrates how reactive variables integrate with UI components in real-world applications.
import { makeVar, useReactiveVar } from '@apollo/client';
interface ProductSummary {
totalProducts: number;
averagePrice: number;
categories: string[];
}
export const productSummaryVar = makeVar<ProductSummary | null>(null);
// Update when products change
export function updateProductSummary(products: Product[]) {
const summary: ProductSummary = {
totalProducts: products.length,
averagePrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
categories: [...new Set(products.map(p => p.category))],
};
productSummaryVar(summary);
}
// Use in component
function ProductSummaryCard() {
const summary = useReactiveVar(productSummaryVar);
if (!summary) return null;
return (
<div className="summary-card">
<p>Total Products: {summary.totalProducts}</p>
<p>Average Price: ${summary.averagePrice.toFixed(2)}</p>
<p>Categories: {summary.categories.join(', ')}</p>
</div>
);
}
Frequently Asked Questions
When should I use reactive variables instead of Redux?
Use reactive variables for small to medium client-side state that needs to be accessed from multiple components. They're ideal for UI state, user preferences, feature flags, and caching computed results. For complex state with many actions, complex state transformations, or middleware needs, consider Redux or Zustand.
Do reactive variables persist across page refreshes?
No, reactive variables are in-memory and reset when the page reloads. For persistence needs, combine reactive variables with localStorage or use Apollo Link for server-side storage.
How do reactive variables differ from React state?
Reactive variables provide fine-grained reactivity across multiple components without prop drilling. Multiple components can subscribe to the same variable and all will update when it changes. React state is component-scoped and requires lifting state up or using Context for cross-component communication.
Can I use reactive variables with Apollo Client 4?
Yes, reactive variables are fully supported in Apollo Client 4. The API is unchanged from Apollo Client 3, and all reactive variable features remain available.
How do I test components using reactive variables?
Test components by directly setting variable values before rendering. Use `import { resetApolloContext }` in test setup to ensure each test starts with clean state. Mock the variables in isolation tests.