Back to catalog

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