Dynamic Theming
Light/dark mode with custom theme support
This guide covers advanced configuration options for RNC Theme, including custom themes, internationalization, and performance optimizations.
RNC Theme provides a comprehensive theming solution for React Native applications with built-in support for:
Dynamic Theming
Light/dark mode with custom theme support
Internationalization
Multi-language support with auto-detection
Toast System
Configurable toast notifications
⚡ Performance
Optimized with memoization and lazy loading
The RNCProvider
is the core component that provides theme context to your entire application. Here are all available configuration options:
import { RNCProvider } from 'rnc-theme';import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function RootLayout() { return ( <GestureHandlerRootView> <RNCProvider defaultTheme="system"> {/* Your app content */} </RNCProvider> </GestureHandlerRootView> );}
import { RNCProvider } from 'rnc-theme';import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function RootLayout() { return ( <GestureHandlerRootView> <RNCProvider // Theme Configuration defaultTheme="system" // 'light' | 'dark' | 'system'
// Toast Configuration toast={{ maxToasts: 4, // Maximum number of toasts position: "bottom", // 'top' | 'bottom' }}
// Internationalization i18nConfig={{ defaultLocale: "en", // Default locale translations: { en: { /* English translations */ }, id: { /* Indonesian translations */ }, }, }}
// Bottom Sheet Configuration bottomSheetProps={{}}
// Theme Colors customLightTheme={{}} customDarkTheme={{}} > {/* Your app content */} </RNCProvider> </GestureHandlerRootView> );}
Option | Type | Default | Description |
---|---|---|---|
defaultTheme | 'light' | 'dark' | 'system' | 'system' | Initial theme mode |
toast | ToastConfig | {} | Toast notification settings |
i18nConfig | I18nConfig | undefined | Internationalization configuration |
bottomSheetProps | BottomSheetProps | {} | Bottom sheet component props |
customLightTheme | ThemeConfig | {} | Custom light theme override |
customDarkTheme | ThemeConfig | {} | Custom dark theme override |
Import the required hooks
import { useTheme, themeRegistry } from 'rnc-theme';
Access theme in your component
const ThemeSetup = () => { const { theme, themeMode, setThemeMode } = useTheme();
return ( <View style={{ backgroundColor: theme.colors.background, }}> <Text style={{ color: theme.colors.text }}> Current theme: {themeMode} </Text> </View> );};
Handle theme changes
const handleThemeChange = (newTheme: 'light' | 'dark' | 'system') => { setThemeMode(newTheme);};
Register custom themes using the theme registry for advanced customization:
import { themeRegistry, CustomThemeConfigFactory } from 'rnc-theme';import { useMemo } from 'react';
const customThemeConfig: CustomThemeConfigFactory = useMemo( () => (isDark: boolean) => ({ colors: { primary: isDark ? '#FF6B6B' : '#4ECDC4', secondary: isDark ? '#FFE66D' : '#45B7D1', background: isDark ? '#1a1a1a' : '#f8f9fa', surface: isDark ? '#2d2d2d' : '#ffffff', text: isDark ? '#ffffff' : '#333333', textSecondary: isDark ? '#b0b0b0' : '#666666', border: isDark ? '#404040' : '#e0e0e0', error: '#FF5252', warning: '#FF9800', success: '#4CAF50', info: '#2196F3', muted: isDark ? '#666666' : '#999999', accent: isDark ? '#FF6B6B' : '#4ECDC4', destructive: '#FF5252', }, components: { height: { xs: 32, sm: 36, md: 40, lg: 44, xl: 48, }, padding: { xs: 8, sm: 12, md: 16, lg: 20, xl: 24, }, borderRadius: { xs: 4, sm: 4, md: 8, lg: 16, xl: 24, full: 9999, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, }, }), []);
// Register the custom themethemeRegistry.registerPreset('custom', customThemeConfig);
interface ThemeColors { primary: string; secondary: string; background: string; surface: string; text: string; textSecondary: string; border: string; error: string; warning: string; success: string; info: string; muted: string; accent: string; destructive: string;}
interface ComponentConfig { height: { xs: number; sm: number; md: number; lg: number; xl: number; }; padding: { xs: number; sm: number; md: number; lg: number; xl: number; }; borderRadius: { xs: number; sm: number; md: number; lg: number; xl: number; full: number; };}
interface SpacingConfig { xs: number; sm: number; md: number; lg: number; xl: number; xxl: number;}
import { RNCProvider } from 'rnc-theme';import * as Localization from 'expo-localization';
const i18nConfig: I18nConfig = { translations: { en: { title: 'Internationalization Demo', subtitle: 'Language switching example', currentLanguage: 'Current Language', switchLanguage: 'Switch Language', greeting: 'Hello, World!', description: 'This is a demonstration of the i18n functionality in the rnc-theme library.', features: { title: 'Features', dynamicSwitching: 'Dynamic language switching', persistentStorage: 'Persistent language storage', customConfig: 'Custom configuration support', autoDetection: 'Auto locale detection', }, buttons: { english: 'English', indonesian: 'Indonesian', french: 'French', }, }, id: { title: 'Demo Internasionalisasi', subtitle: 'Contoh pergantian bahasa', currentLanguage: 'Bahasa Saat Ini', switchLanguage: 'Ganti Bahasa', greeting: 'Halo, Dunia!', description: 'Ini adalah demonstrasi fungsionalitas i18n dalam library rnc-theme.', features: { title: 'Fitur', dynamicSwitching: 'Pergantian bahasa dinamis', persistentStorage: 'Penyimpanan bahasa persisten', customConfig: 'Dukungan konfigurasi kustom', autoDetection: 'Deteksi lokal otomatis', }, buttons: { english: 'Inggris', indonesian: 'Indonesia', french: 'Prancis', }, }, fr: { title: "Démo d'Internationalisation", subtitle: 'Exemple de changement de langue', currentLanguage: 'Langue Actuelle', switchLanguage: 'Changer de Langue', greeting: 'Bonjour le Monde!', description: 'Ceci est une démonstration de la fonctionnalité i18n dans la bibliothèque rnc-theme.', features: { title: 'Fonctionnalités', dynamicSwitching: 'Changement de langue dynamique', persistentStorage: 'Stockage de langue persistant', customConfig: 'Support de configuration personnalisée', autoDetection: 'Détection automatique des paramètres régionaux', }, buttons: { english: 'Anglais', indonesian: 'Indonésien', french: 'Français', }, }, }, supportedLocales: ['en', 'id', 'fr'], defaultLocale: 'en', autoDetectLocale: true,};
const App = () => ( <RNCProvider i18nConfig={i18nConfig}> {/* Your app content */} </RNCProvider>);
import { useLanguage } from 'rnc-theme';
const MyComponent = () => { const { i18n: { t } } = useLanguage();
return ( <View> <Text>{t('title')}</Text> <Text>{t('features.title')}</Text> </View> );};
// In translationsconst translations = { welcome: 'Welcome, {{name}}!', itemCount: 'You have {{count}} items',};
// In componentconst MyComponent = () => { const { i18n: { t } } = useLanguage();
return ( <View> <Text>{t('welcome', { name: 'John' })}</Text> <Text>{t('itemCount', { count: 5 })}</Text> </View> );};
import { useLanguage } from 'rnc-theme';
const LanguageSwitcher = () => { const { locale, setLocale, i18n } = useLanguage();
const handleLanguageChange = async (newLocale: string) => { setIsLoading(true); await setLocale(newLocale); setTimeout(() => setIsLoading(false), 300); // Small delay for smooth transition };
const languages = [ { code: 'en', name: i18n.t('buttons.english') }, { code: 'id', name: i18n.t('buttons.indonesian') }, { code: 'fr', name: i18n.t('buttons.french') }, ];
return ( <View> <Text>Current: {locale}</Text> {{languages.map((language) => ( <Button key={language.code} variant={locale === language.code ? 'primary' : 'outline'} size="md" onPress={() => handleLanguageChange(language.code)} disabled={isLoading} style={styles.languageButton} > <ButtonText variant={locale === language.code ? 'primary' : 'outline'} > {language.name} </ButtonText> </Button> ))} </View> );};
Configure toast notifications at the provider level:
<RNCProvider toast={{ maxToasts: 3, // Maximum number of visible toasts position: "bottom", // 'top' | 'bottom' }}> {/* Your app content */}</RNCProvider>
import { useToast } from 'rnc-theme';
const MyComponent = () => { const { toast } = useToast();
const showBasicToast = () => { toast({ title: 'Success!', description: 'Your action was completed successfully.', }); };
const showErrorToast = () => { toast({ variant: 'error', title: 'Error occurred', description: 'Something went wrong. Please try again.', }); };
return ( <View> <Button onPress={showBasicToast}>Show Success</Button> <Button onPress={showErrorToast}>Show Error</Button> </View> );};
const {toastAsync} = useToast()
const showAsyncWithCustomMessages = async () => {if (isLoading) return;
setIsLoading(true);try { await toastAsync( { title: 'Syncing Data', loadingText: 'Synchronizing with server...', }, async () => { // Simulasi sync process await new Promise((resolve) => setTimeout(resolve, 2500)); return { syncedItems: 150 }; } );} catch (error) { console.log('Sync failed:', error);} finally { setIsLoading(false);}};
Default
Standard informational toast
Success
Success confirmation toast
Warning
Warning or caution toast
Error
Error or failure toast
Implement lazy loading for better performance:
import { lazy, Suspense } from 'react';import { Spinner } from 'rnc-theme';
// Lazy load heavy componentsconst HeavyComponent = lazy(() => import('./HeavyComponent'));const AnotherHeavyComponent = lazy(() => import('./AnotherHeavyComponent'));
const App = () => ( <View> <Suspense fallback={<Spinner size="lg" />}> <HeavyComponent /> </Suspense>
<Suspense fallback={<Spinner size="md" />}> <AnotherHeavyComponent /> </Suspense> </View>);
import { memo, useMemo, useCallback } from 'react';import { useThemedStyles, Theme } from 'rnc-theme';import { StyleSheet } from 'react-native';
const OptimizedComponent = memo(({ data }) => { // useThemedStyles includes memoization const styles = useThemedStyles(createStyles);
// Memoize expensive calculations const processedData = useMemo(() => { return data.map(item => ({ ...item, processed: true, timestamp: Date.now() })); }, [data]);
// Memoize callbacks to prevent child re-renders const handlePress = useCallback((id: string) => { console.log('Item pressed:', id); }, []);
const handleLongPress = useCallback((id: string) => { console.log('Item long pressed:', id); }, []);
return ( <View style={styles.container}> {processedData.map(item => ( <TouchableOpacity key={item.id} style={styles.item} onPress={() => handlePress(item.id)} onLongPress={() => handleLongPress(item.id)} > <Text style={styles.text}>{item.name}</Text> <Text style={styles.subtitle}>{item.description}</Text> </TouchableOpacity> ))} </View> );});
const createStyles = (theme: Theme) => StyleSheet.create({ container: { backgroundColor: theme.colors.background, padding: theme.spacing.md, flex: 1, }, item: { backgroundColor: theme.colors.surface, padding: theme.spacing.sm, marginBottom: theme.spacing.xs, borderRadius: theme.components.borderRadius.md, borderWidth: 1, borderColor: theme.colors.border, }, text: { color: theme.colors.text, fontSize: 16, fontWeight: '600', }, subtitle: { color: theme.colors.textSecondary, fontSize: 14, marginTop: theme.spacing.xs, },});
// Set display name for debuggingOptimizedComponent.displayName = 'OptimizedComponent';
export default OptimizedComponent;
import { Platform } from 'react-native';
const iosSpecificConfig = { components: { height: { xs: Platform.OS === 'ios' ? 34 : 32, sm: Platform.OS === 'ios' ? 38 : 36, md: Platform.OS === 'ios' ? 42 : 40, lg: Platform.OS === 'ios' ? 46 : 44, xl: Platform.OS === 'ios' ? 50 : 48, }, borderRadius: { xs: Platform.OS === 'ios' ? 6 : 4, sm: Platform.OS === 'ios' ? 8 : 6, md: Platform.OS === 'ios' ? 10 : 8, lg: Platform.OS === 'ios' ? 14 : 12, xl: Platform.OS === 'ios' ? 18 : 16, }, padding: { xs: Platform.OS === 'ios' ? 10 : 8, sm: Platform.OS === 'ios' ? 14 : 12, md: Platform.OS === 'ios' ? 18 : 16, lg: Platform.OS === 'ios' ? 22 : 20, xl: Platform.OS === 'ios' ? 26 : 24, }, },};
const iosColors = { primary: '#007AFF', // iOS Blue secondary: '#5AC8FA', // iOS Light Blue success: '#34C759', // iOS Green warning: '#FF9500', // iOS Orange error: '#FF3B30', // iOS Red background: '#F2F2F7', // iOS Light Gray surface: '#FFFFFF',};
const androidSpecificConfig = { colors: { // Material Design 3 colors primary: Platform.OS === 'android' ? '#6750A4' : '#007AFF', secondary: Platform.OS === 'android' ? '#625B71' : '#5AC8FA', success: Platform.OS === 'android' ? '#4CAF50' : '#34C759', warning: Platform.OS === 'android' ? '#FF9800' : '#FF9500', error: Platform.OS === 'android' ? '#F44336' : '#FF3B30', }, components: { // Android-specific elevation elevation: Platform.OS === 'android' ? { xs: 1, sm: 2, md: 4, lg: 8, xl: 12, } : undefined, // Android ripple effect ripple: Platform.OS === 'android' ? { color: 'rgba(0, 0, 0, 0.12)', borderless: false, } : undefined, },};
const androidShadowStyles = (theme: Theme) => StyleSheet.create({ card: { backgroundColor: theme.colors.surface, borderRadius: theme.components.borderRadius.md, ...(Platform.OS === 'android' ? { elevation: 4, } : { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, }), },});
Organize your theme-related files for better maintainability:
Always prefer theme tokens over hardcoded values:
const createStyles = () => StyleSheet.create({ container: { backgroundColor: '#FFFFFF', // Hardcoded color padding: 16, // Hardcoded spacing borderRadius: 12, // Hardcoded radius borderWidth: 1, borderColor: '#E0E0E0', // Hardcoded border }, text: { color: '#333333', // Hardcoded text color fontSize: 16, },});
const createStyles = (theme: Theme) => StyleSheet.create({ container: { backgroundColor: theme.colors.background, padding: theme.spacing.md, borderRadius: theme.components.borderRadius.lg, borderWidth: 1, borderColor: theme.colors.border, }, text: { color: theme.colors.text, fontSize: 16, // OK for standard sizes },});
Use memoization for expensive computations
const expensiveValue = useMemo(() => computeExpensiveValue(data), [data]);
Memoize callbacks to prevent unnecessary re-renders
const handlePress = useCallback(() => { // Handle press}, [dependency]);
Use useThemedStyles
for dynamic styling
const styles = useThemedStyles(createStyles); // Auto-memoized
Implement lazy loading for heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
Ensure your themed components are accessible:
const createAccessibleStyles = (theme: Theme) => StyleSheet.create({ button: { backgroundColor: theme.colors.primary, padding: theme.spacing.md, borderRadius: theme.components.borderRadius.md, minHeight: 44, // Minimum touch target size }, buttonText: { color: theme.colors.surface, textAlign: 'center', fontSize: 16, fontWeight: '600', },});
const AccessibleButton = ({ onPress, children }) => { const styles = useThemedStyles(createAccessibleStyles);
return ( <TouchableOpacity style={styles.button} onPress={onPress} accessible={true} accessibilityRole="button" accessibilityLabel={typeof children === 'string' ? children : 'Button'} > <Text style={styles.buttonText}>{children}</Text> </TouchableOpacity> );};
Theme Not Persisting
Problem: Theme changes don’t persist between app restarts Solution: Check AsyncStorage permissions and ensure the provider is at the root level
// Ensure RNCProvider wraps your entire app<RNCProvider> <App /></RNCProvider>
Styles Not Updating
Problem: Component styles don’t update when theme changes
Solution: Use useThemedStyles
instead of static styles
// ❌ Static styles won't updateconst styles = StyleSheet.create({...});// ✅ Dynamic styles update with themeconst styles = useThemedStyles(createStyles);
Performance Issues
Problem: App feels sluggish with theme changes Solution: Implement proper memoization and lazy loading
// Memoize expensive operationsconst memoizedStyles = useThemedStyles(createStyles);const memoizedData = useMemo(() => processData(data), [data]);
TypeScript Errors
Problem: TypeScript complains about theme types Solution: Update type definitions and use proper imports
import { Theme } from 'rnc-theme';const createStyles = (theme: Theme) => StyleSheet.create({ // Your styles});
Monitor theme performance with React DevTools Profiler: