Below is one approach to “hiding” the per‑component hook call while still getting updated styles on theme or dimension changes. The key idea is to create a centralized StylesProvider (or HOC) that computes the styles on each update and makes them available via context. Then, components can either use a HOC that injects the styles via props or—if you’re okay with a one‑line context lookup—call a simple useContext hook (which can be hidden in your own custom HOC).
Below is an example using a context provider:
styles.ts
import React, { createContext, useContext, useMemo } from 'react';
import { StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { useThemeStore, colors } from './themeStore';
import { useDimensionsStore } from './dimensionsStore';
interface AppStyles {
screen: StyleProp<ViewStyle>;
container: StyleProp<ViewStyle>;
text: StyleProp<TextStyle>;
header: StyleProp<TextStyle>;
button: StyleProp<ViewStyle>;
buttonText: StyleProp<TextStyle>;
}
const StylesContext = createContext<AppStyles | null>(null);
export const StylesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const theme = useThemeStore((state) => state.theme);
const { width, height } = useDimensionsStore();
const themeColors = colors[theme];
// Memoize styles so they are recalculated only when dependencies change.
const styles = useMemo(() => {
return StyleSheet.create<AppStyles>({
screen: {
flex: 1,
backgroundColor: themeColors.background,
width,
height,
},
container: {
padding: 16,
backgroundColor: themeColors.card,
borderRadius: 8,
margin: 8,
},
text: {
color: themeColors.text,
fontSize: 16,
},
header: {
color: themeColors.primary,
fontSize: 24,
fontWeight: 'bold',
},
button: {
backgroundColor: themeColors.primary,
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: themeColors.background,
fontWeight: 'bold',
},
});
}, [theme, width, height, themeColors]);
return (
<StylesContext.Provider value={styles}>
{children}
</StylesContext.Provider>
);
};
// A custom hook that simply reads from the context.
export const useStyles = (): AppStyles => {
const context = useContext(StylesContext);
if (!context) {
throw new Error('useStyles must be used within a StylesProvider');
}
return context;
};
Now, wrap your app (or at least the navigators/screens that require dynamic styles) with the provider. For example:
App.tsx
import React from 'react';
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createStackNavigator, StackScreenProps } from '@react-navigation/stack';
import { Button, View, Text, TouchableOpacity } from 'react-native';
import { useThemeStore, colors } from './themeStore';
import { StylesProvider, useStyles } from './styles';
type RootStackParamList = {
Home: undefined;
Settings: undefined;
};
const Stack = createStackNavigator<RootStackParamList>();
function HomeScreen({ navigation }: StackScreenProps<RootStackParamList, 'Home'>) {
// Now using styles from context
const styles = useStyles();
const toggleTheme = useThemeStore((state) => state.toggleTheme);
return (
<View style={styles.screen}>
<View style={[styles.container, { marginTop: 40 }]}>
<Text style={styles.header}>Home Screen</Text>
<Text style={styles.text}>Current theme settings</Text>
</View>
<View style={styles.container}>
<Button title="Toggle Theme" onPress={toggleTheme} />
<TouchableOpacity
style={[styles.button, { marginTop: 16 }]}
onPress={() => navigation.navigate('Settings')}
>
<Text style={styles.buttonText}>Go to Settings</Text>
</TouchableOpacity>
</View>
</View>
);
}
function SettingsScreen({ navigation }: StackScreenProps<RootStackParamList, 'Settings'>) {
const styles = useStyles();
const theme = useThemeStore((state) => state.theme);
return (
<View style={styles.screen}>
<View style={styles.container}>
<Text style={styles.header}>Settings Screen</Text>
<Text style={styles.text}>Current Theme: {theme.toUpperCase()}</Text>
<TouchableOpacity
style={[styles.button, { marginTop: 16 }]}
onPress={() => navigation.goBack()}
>
<Text style={styles.buttonText}>Go Back</Text>
</TouchableOpacity>
</View>
</View>
);
}
export default function App() {
const theme = useThemeStore((state) => state.theme);
const navigationTheme = {
...DefaultTheme,
dark: theme === 'dark',
colors: {
...DefaultTheme.colors,
...colors[theme],
},
};
return (
<StylesProvider>
<NavigationContainer theme={navigationTheme}>
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: navigationTheme.colors.card,
},
headerTintColor: navigationTheme.colors.text,
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
</NavigationContainer>
</StylesProvider>
);
}
If you’d like to completely remove the need to call a hook inside every screen, you can wrap your screen components with a higher‑order component that injects the styles automatically. For example:
withDynamicStyles.tsx
import React from 'react';
import { useStyles } from './styles';
export function withDynamicStyles<P extends { styles: ReturnType<typeof useStyles> }>(
Component: React.ComponentType<P>
) {
return (props: Omit<P, 'styles'>) => {
const styles = useStyles();
return <Component {...(props as P)} styles={styles} />;
};
}
Then your screen component can be defined without calling the hook:
import React from 'react';
import { View, Text, Button, TouchableOpacity } from 'react-native';
type HomeScreenProps = {
styles: ReturnType<typeof useStyles>;
navigation: any; // or your proper navigation prop type
};
function HomeScreenBase({ styles, navigation }: HomeScreenProps) {
return (
<View style={styles.screen}>
<View style={[styles.container, { marginTop: 40 }]}>
<Text style={styles.header}>Home Screen</Text>
<Text style={styles.text}>Current theme settings</Text>
</View>
<View style={styles.container}>
<Button title="Toggle Theme" onPress={() => { /* theme toggle logic */ }} />
<TouchableOpacity
style={[styles.button, { marginTop: 16 }]}
onPress={() => navigation.navigate('Settings')}
>
<Text style={styles.buttonText}>Go to Settings</Text>
</TouchableOpacity>
</View>
</View>
);
}
export const HomeScreen = withDynamicStyles(HomeScreenBase);
In summary, because the styles need to update when your theme or dimensions change, you must obtain them at render time. Wrapping this logic in a context provider or a higher‑order component lets you “hide” the per‑component hook calls while still benefiting from dynamic, performant styling.