Funciones de la app

Theme context (claro / oscuro / default)

Un bundle pequeño de ThemeContext para React Native: guarda la elección del usuario, sigue el tema del sistema cuando está en default y expone un hook `useTheme()` con colores resueltos.

Descarga

Este ZIP contiene `ThemeContext/Colors.ts`, `ThemeContext/helper.tsx` y `ThemeContext/ThemeContext.tsx`.

Descargar ThemeContext.zipArchivo ZIP — extrae para obtener `ThemeContext/`

`Colors.ts`

export const darkColors = {
  background: '#121212',
  text: '#FFFFFF',
};

export const lightColors = {
  background: '#FFFFFF',
  text: '#000000',
};

export type ThemeColors = typeof darkColors | typeof lightColors;

`helper.tsx`

import AsyncStorage from '@react-native-async-storage/async-storage';

/**
 * built-in: __DEV__ (Metro / React Native)
 */
export const devLog = (...args: any[]) => {
    if (__DEV__) {
      console.log(...args);
    }
  };
  
  /**
   * npm: @react-native-async-storage/async-storage
   */
  export async function saveToSecureStorage(
    key: string,
    value: string
  ): Promise<void> {
    try {
      await AsyncStorage.setItem(key, value);
    } catch (error) {
      devLog("[AsyncStorage] Error saving data:", error);
    }
  }
  
  /**
   * npm: @react-native-async-storage/async-storage
   */
  export async function getFromSecureStorage(
    key: string
  ): Promise<string | null> {
    try {
      const value = await AsyncStorage.getItem(key);
      return value;
    } catch (error) {
      devLog("[AsyncStorage] Error retrieving data:", error);
      return null;
    }
  }
  
  /**
   * npm: @react-native-async-storage/async-storage
   */
  export async function removeAllKeysFromSecureStorage() {
    try {
      await AsyncStorage.clear();
    } catch (error) {
      devLog("[AsyncStorage] Error removing all data:", error);
      return null;
    }
  }

`ThemeContext.tsx`

import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Appearance, AppState, ColorSchemeName } from 'react-native';

import { darkColors, lightColors, ThemeColors } from './Colors';
import { devLog, getFromSecureStorage, saveToSecureStorage } from './helper';

export type Theme = 'light' | 'dark' | 'default';
export interface ThemeContextType {
  theme: Theme;
  colors: ThemeColors;
  toggleTheme: (value: Theme) => void;
  systemTheme: ColorSchemeName;
  isInitialized: boolean;
}

const THEME_KEY = 'APP_THEME';

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>('default'); // default preference
  const [systemTheme, setSystemTheme] = useState<ColorSchemeName>(Appearance.getColorScheme());
  const [isInitialized, setIsInitialized] = useState(false);

  // Use ref to track if theme change is in progress to prevent multiple simultaneous changes
  const isChangingTheme = useRef(false);

  useEffect(() => {
    const loadTheme = async () => {
      try {
        const savedTheme = await getFromSecureStorage(THEME_KEY);

        if (savedTheme === 'light' || savedTheme === 'dark' || savedTheme === 'default') {
          setTheme(savedTheme);
        } else {
          setTheme('default'); // fallback
        }
      } catch (error) {
        console.error('Failed to load theme:', error);
        setTheme('default');
      } finally {
        setIsInitialized(true);
      }
    };

    loadTheme();

    // Listen to system theme changes only if 'default' is selected
    const appearanceListener = Appearance.addChangeListener(({ colorScheme }) => {
      devLog("System theme changed:", colorScheme);
      setSystemTheme(colorScheme);
    });

    // Listen to app state changes to refresh theme when app becomes active
    const appStateListener = AppState.addEventListener('change', (nextAppState) => {
      if (nextAppState === 'active') {
        devLog("App became active, refreshing system theme");
        // Force refresh the system theme when app becomes active
        const currentSystemTheme = Appearance.getColorScheme();
        setSystemTheme(currentSystemTheme);
      }
    });

    return () => {
      appearanceListener.remove();
      appStateListener?.remove();
    };
  }, []);

  // Optimized toggleTheme function with debouncing
  const toggleTheme = useCallback(async (value: Theme) => {
    // Prevent multiple simultaneous theme changes
    if (isChangingTheme.current) return;

    isChangingTheme.current = true;

    try {
      // Update theme immediately for instant UI response
      setTheme(value);

      // Save to storage in background (non-blocking)
      saveToSecureStorage(THEME_KEY, value).catch((error) => {
        console.error('Failed to save theme:', error);
      });
    } finally {
      // Reset the flag after a short delay to allow for smooth transitions
      setTimeout(() => {
        isChangingTheme.current = false;
      }, 100);
    }
  }, []);

  // Memoized active theme calculation with better dependency tracking
  const activeTheme = useMemo(() => {
    if (!isInitialized) return 'light'; // Default during initialization
    return theme === 'default' ? systemTheme : theme;
  }, [theme, systemTheme, isInitialized]);

  // Memoized colors with stable reference
  const colors = useMemo(() => {
    if (!isInitialized) return lightColors; // Default during initialization
    return activeTheme === 'dark' ? darkColors : lightColors;
  }, [activeTheme, isInitialized]);

  // Memoized context value with stable references
  const contextValue = useMemo(() => ({
    theme,
    colors,
    toggleTheme,
    systemTheme,
    isInitialized
  }), [theme, colors, toggleTheme, systemTheme, isInitialized]);

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = (): ThemeContextType => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

Dependencias

  • @react-native-async-storage/async-storageGuarda la elección del tema en storage (cámbialo por SecureStore si guardas secretos).

Patrocinado

Promoción breve