react-context-patterns
This repository contains a complete **Cursor (Claude AI)** infrastructure that provides **auto-activating skills**, **specialized agents**, and **powerful commands** to help you build better applications faster.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
# React Context Patterns
**Skill Version:** 1.0.0
**Stack:** React, TypeScript, Firebase Auth
**Purpose:** Production-ready React Context patterns for state management and authentication
## Overview
This skill provides battle-tested patterns for React Context API, including authentication context with Firebase sync, email verification management, and context performance optimization.
## When to Use This Skill
Load this skill when working on:
- Authentication context setup
- Global state management with Context
- Context performance optimization
- Provider composition
- Custom hooks for context consumption
- Firebase Auth integration with React
## Quick Reference
### Core Patterns
1. **Auth Context** - Firebase authentication state management
2. **Smart Sync** - Only update when data actually changed
3. **Context + Custom Hook** - Enforce proper usage
4. **Type Safety** - Full TypeScript support
5. **Provider Composition** - Multiple contexts
## Core Guidelines
### 1. Authentication Context with Firebase Sync
**Pattern:** Sync Firebase Auth state with Firestore user data, only update when necessary
```typescript
// contexts/AuthContext.tsx
'use client';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { User as FirebaseUser } from 'firebase/auth';
import { onAuthStateChange, signOut } from '@/lib/firebase/auth';
import { createOrUpdateUser, getUserById } from '@/lib/firebase/firestore';
import { User } from '@/types/user';
interface AuthContextType {
user: User | null;
firebaseUser: FirebaseUser | null;
loading: boolean;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Custom hook to use auth context
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
interface AuthProviderProps {
children: React.ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [firebaseUser, setFirebaseUser] = useState<FirebaseUser | null>(null);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChange(async (firebaseUser) => {
setFirebaseUser(firebaseUser);
if (firebaseUser) {
// CRITICAL: Reload to get latest emailVerified status
try {
await firebaseUser.reload();
} catch (error) {
console.error('Error reloading user:', error);
}
// Get current user data from Firestore
const userData = await getUserById(firebaseUser.uid);
// Determine provider
const provider = firebaseUser.providerData[0]?.providerId || 'email';
const authProvider = provider.includes('google') ? 'google' : 'email';
// SMART SYNC: Only update if data actually changed
const emailChanged = userData && userData.email !== firebaseUser.email;
const emailVerifiedChanged = userData && userData.emailVerified !== firebaseUser.emailVerified;
const needsSync = !userData || emailChanged || emailVerifiedChanged;
if (needsSync) {
// Sync Firestore with latest Firebase Auth data
await createOrUpdateUser({
uid: firebaseUser.uid,
email: firebaseUser.email!,
displayName: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
phoneNumber: firebaseUser.phoneNumber,
emailVerified: firebaseUser.emailVerified,
provider: authProvider as any,
});
// Fetch updated user data
const updatedUserData = await getUserById(firebaseUser.uid);
setUser(updatedUserData);
} else {
// Use existing data
setUser(userData);
}
} else {
setUser(null);
}
setLoading(false);
});
return () => unsubscribe();
}, []);
const logout = async () => {
try {
setLoading(true);
await signOut();
} catch (error) {
console.error('Logout error:', error);
throw error;
} finally {
setLoading(false);
}
};
const refreshUser = async () => {
try {
if (firebaseUser) {
// Force reload from Firebase
await firebaseUser.reload();
const provider = firebaseUser.providerData[0]?.providerId || 'email';
const authProvider = provider.includes('google') ? 'google' : 'email';
// Update Firestore
await createOrUpdateUser({
uid: firebaseUser.uid,
email: firebaseUser.email!,
displayName: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
phoneNumber: firebaseUser.phoneNumber,
emailVerified: firebaseUser.emailVerified,
provider: authProvider as any,
});
// Refetch user data
const userData = await getUserById(firebaseUser.uid);
setUser(userData);
}
} catch (error) {
console.error('Refresh user error:', error);
}
};
const value: AuthContextType = {
user,
firebaseUser,
loading,
logout,
refreshUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
```
**Key Features:**
- ✅ Smart sync - only updates when data changed
- ✅ Email verification status tracking
- ✅ Reload Firebase user to get latest status
- ✅ Manual refresh capability
- ✅ Type-safe with TypeScript
- ✅ Custom hook enforces proper usage
### 2. Context with Error Boundary
```typescript
// contexts/AppContext.tsx
'use client';
import React, { createContext, useContext, useState, useCallback } from 'react';
interface AppContextType {
error: Error | null;
setError: (error: Error | null) => void;
clearError: () => void;
showNotification: (message: string, type: 'success' | 'error' | 'info') => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
};
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [error, setErrorState] = useState<Error | null>(null);
const setError = useCallback((error: Error | null) => {
setErrorState(error);
if (error) {
console.error('App Error:', error);
// Could send to error tracking service here
}
}, []);
const clearError = useCallback(() => {
setErrorState(null);
}, []);
const showNotification = useCallback((
message: string,
type: 'success' | 'error' | 'info'
) => {
// Implementation for toast/notification
console.log(`[${type.toUpperCase()}] ${message}`);
}, []);
return (
<AppContext.Provider value={{ error, setError, clearError, showNotification }}>
{children}
</AppContext.Provider>
);
};
```
### 3. Provider Composition Pattern
```typescript
// contexts/index.tsx
'use client';
import React from 'react';
import { AuthProvider } from './AuthContext';
import { AppProvider } from './AppContext';
import { ThemeProvider } from './ThemeContext';
/**
* Compose all providers in one place
* Benefits:
* - Clean layout.tsx
* - Easy to add new providers
* - Proper provider ordering
*/
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<AppProvider>
<ThemeProvider>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
</AppProvider>
);
};
// Usage in layout.tsx
import { AppProviders } from '@/contexts';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AppProviders>
{children}
</AppProviders>
</body>
</html>
);
}
```
### 4. Performance-Optimized Context
**Problem:** Context re-renders all consumers even if they don't use changed values
**Solution:** Split contexts by concern and use memoization
```typescript
// contexts/UserContext.tsx
'use client';
import React, { createContext, useContext, useMemo } from 'react';
import { User } from '@/types/user';
// Split into data and actions
interface UserDataContextType {
user: User | null;
loading: boolean;
}
interface UserActionsContextType {
updateProfile: (data: Partial<User>) => Promise<void>;
refreshUser: () => Promise<void>;
}
const UserDataContext = createContext<UserDataContextType | undefined>(undefined);
const UserActionsContext = createContext<UserActionsContextType | undefined>(undefined);
// Separate hooks for data and actions
export const useUserData = () => {
const context = useContext(UserDataContext);
if (!context) throw new Error('useUserData must be used within UserProvider');
return context;
};
export const useUserActions = () => {
const context = useContext(UserActionsContext);
if (!context) throw new Error('useUserActions must be used within UserProvider');
return context;
};
export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// Memoize data context value
const dataValue = useMemo(
() => ({ user, loading }),
[user, loading]
);
// Memoize action context value
const actionsValue = useMemo(
() => ({
updateProfile: async (data: Partial<User>) => {
// Implementation
},
refreshUser: async () => {
// Implementation
},
}),
[] // Actions don't change
);
return (
<UserDataContext.Provider value={dataValue}>
<UserActionsContext.Provider value={actionsValue}>
{children}
</UserActionsContext.Provider>
</UserDataContext.Provider>
);
};
// Usage - components only re-render when their used values change
function UserProfile() {
const { user } = useUserData(); // Only re-renders when user changes
return <div>{user?.name}</div>;
}
function UserActions() {
const { updateProfile } = useUserActions(); // Never re-renders
return <button onClick={() => updateProfile({ name: 'New' })}>Update</button>;
}
```
### 5. Lazy Context Initialization
**Use Case:** Context with expensive initialization
```typescript
// contexts/ConfigContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
interface Config {
features: Record<string, boolean>;
settings: Record<string, any>;
}
interface ConfigContextType {
config: Config | null;
loading: boolean;
error: Error | null;
}
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
export const useConfig = () => {
const context = useContext(ConfigContext);
if (!context) throw new Error('useConfig must be used within ConfigProvider');
return context;
};
export const ConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [config, setConfig] = useState<Config | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// Lazy initialization - only runs once on mount
async function loadConfig() {
try {
// Expensive operation (API call, complex calculation, etc.)
const response = await fetch('/api/config');
const data = await response.json();
setConfig(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
loadConfig();
}, []); // Empty deps - only run once
const value = useMemo(
() => ({ config, loading, error }),
[config, loading, error]
);
return (
<ConfigContext.Provider value={value}>
{children}
</ConfigContext.Provider>
);
};
```
### 6. Context with LocalStorage Persistence
```typescript
// contexts/PreferencesContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
interface Preferences {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
interface PreferencesContextType {
preferences: Preferences;
updatePreferences: (updates: Partial<Preferences>) => void;
}
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
export const usePreferences = () => {
const context = useContext(PreferencesContext);
if (!context) throw new Error('usePreferences must be used within PreferencesProvider');
return context;
};
const STORAGE_KEY = 'user-preferences';
const DEFAULT_PREFERENCES: Preferences = {
theme: 'light',
language: 'en',
notifications: true,
};
export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [preferences, setPreferences] = useState<Preferences>(DEFAULT_PREFERENCES);
const [isClient, setIsClient] = useState(false);
// Load from localStorage on client-side only
useEffect(() => {
setIsClient(true);
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
setPreferences(JSON.parse(stored));
} catch (error) {
console.error('Failed to parse preferences:', error);
}
}
}, []);
// Save to localStorage whenever preferences change
useEffect(() => {
if (isClient) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
}
}, [preferences, isClient]);
const updatePreferences = useCallback((updates: Partial<Preferences>) => {
setPreferences(prev => ({ ...prev, ...updates }));
}, []);
const value = useMemo(
() => ({ preferences, updatePreferences }),
[preferences, updatePreferences]
);
return (
<PreferencesContext.Provider value={value}>
{children}
</PreferencesContext.Provider>
);
};
```
## Best Practices
### Do's ✅
- **Always provide custom hooks** for context consumption
- **Throw errors** if context used outside provider
- **Memoize context values** to prevent unnecessary re-renders
- **Split contexts** by concern (data vs actions)
- **Use TypeScript** for type safety
- **Handle loading states** properly
- **Implement cleanup** in useEffect
- **Reload Firebase user** to get latest emailVerified status
- **Only sync when data changed** (smart sync pattern)
### Don'ts ❌
- **Don't put all state** in one context
- **Don't forget useMemo** for context values
- **Don't use context** for frequently changing values
- **Don't skip error boundaries** for critical contexts
- **Don't access localStorage** during SSR
- **Don't update Firestore** on every auth state change
## Performance Tips
1. **Split Contexts**
- Separate rarely-changing data from frequently-changing data
- Split data and actions into different contexts
2. **Memoization**
- Always memoize context values with useMemo
- Memoize action functions with useCallback
3. **Conditional Rendering**
- Show loading states during initialization
- Defer expensive operations until needed
4. **Smart Updates**
- Only update when data actually changed
- Use shallow comparison for objects
## Common Patterns
### Loading State Pattern
```typescript
if (loading) return <LoadingSpinner />;
if (!user) return <LoginPrompt />;
return <Dashboard user={user} />;
```
### Error Boundary Pattern
```typescript
if (error) return <ErrorDisplay error={error} />;
```
### Refresh Pattern
```typescript
const { refreshUser } = useAuth();
// After email verification
await sendEmailVerification();
await refreshUser(); // Reload user data
```
---
**Last Updated:** 2025-01-10
**Maintained By:** Development Team