Application multilingueLecture vocaleTexte en direct & scanPDF → imagesTheme contextWidget Quick Tools
Fonctionnalités de l'app
Theme context (clair / sombre / défaut)
Un petit bundle ThemeContext pour React Native : stocker le choix de l’utilisateur, suivre le thème système en mode défaut, et exposer un hook `useTheme()` avec les couleurs résolues.
Télécharger
Ce ZIP contient `ThemeContext/Colors.ts`, `ThemeContext/helper.tsx` et `ThemeContext/ThemeContext.tsx`.
Télécharger ThemeContext.zipArchive ZIP — extraire pour obtenir `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;
};
Dépendances
@react-native-async-storage/async-storage— Persiste le thème dans le storage (remplacez par SecureStore si vous stockez des secrets).