Below is one common solution: instead of calling useStyles (and therefore using hooks) in every component, you can “lift” the style calculation into a provider (or an HOC) that makes the computed styles available via a React Context. This way, you only need to wrap your app (or a subtree) once, and then your components can either consume the context or be wrapped by an HOC so that they don’t need to explicitly call your hook every time.
Because React hooks must be called during render inside function components (or custom hooks), you can’t directly export a static “styles” object that responds to dynamic theme and dimensions changes. Instead, you can create a provider that computes the styles (using useThemeStore and useDimensionsStore) and passes them down.
Below is an example implementation:
Creating a Styles Provider and Context
// ThemeStylesContext.tsx
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
import { StyleSheet } from 'react-native';
import { useThemeStore, colors } from './themeStore';
import { useDimensionsStore } from './dimensionsStore';
type StylesType = ReturnType<typeof StyleSheet.create>;
const StylesContext = createContext<StylesType | null>(null);
export const StylesProvider = ({ children }: { children: ReactNode }) => {
const theme = useThemeStore(state => state.theme);
const { width, height } = useDimensionsStore();
const themeColors = colors[theme];
// Memoize the computed styles – these update when theme, width, or height changes.
const styles = useMemo(() => {
return StyleSheet.create({
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>
);
};
// Custom hook for consuming the styles.
export const useThemeStyles = () => {
const styles = useContext(StylesContext);
if (!styles) {
throw new Error('useThemeStyles must be used within a StylesProvider');
}
return styles;
};
Option A: Using a Custom Hook to Consume the Styles
Then in any component, instead of calling useStyles (your previous hook) you can simply call the new hook:
// HomeScreen.tsx
import React from 'react';
import { Button, View, Text, TouchableOpacity } from 'react-native';
import { useThemeStyles } from './ThemeStylesContext';
import { useThemeStore } from './themeStore';
import { StackScreenProps } from '@react-navigation/stack';
type RootStackParamList = {
Home: undefined;
Settings: undefined;
};
function HomeScreen({ navigation }: StackScreenProps<RootStackParamList, 'Home'>) {
const styles = useThemeStyles();
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>
);
}
export default HomeScreen;
Option B: Using a Higher-Order Component (HOC)
If you’d rather not call a hook inside every component, you can create an HOC that injects the computed styles as a prop:
// withThemeStyles.tsx
import React from 'react';
import { useThemeStyles } from './ThemeStylesContext';
export const withThemeStyles = <P extends { styles: ReturnType<typeof useThemeStyles> }>(
Component: React.ComponentType<P>
) => (props: Omit<P, 'styles'>) => {
const styles = useThemeStyles();
return <Component {...(props as P)} styles={styles} />;
};
Then you can wrap your component:
// SettingsScreen.tsx
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { withThemeStyles } from './withThemeStyles';
import { useThemeStore } from './themeStore';
import { StackScreenProps } from '@react-navigation/stack';
type RootStackParamList = {
Home: undefined;
Settings: undefined;
};
type Props = StackScreenProps<RootStackParamList, 'Settings'> & {
styles: ReturnType<typeof useThemeStyles>;
};
function SettingsScreen({ navigation, styles }: Props) {
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 withThemeStyles(SettingsScreen);
Usage in Your App
Wrap your navigation tree (or the portion of your app that needs dynamic styles) with the provider.
// App.tsx
import React from 'react';
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from './HomeScreen';
import SettingsScreen from './SettingsScreen';
import { useThemeStore, colors } from './themeStore';
import { StylesProvider } from './ThemeStylesContext';
type RootStackParamList = {
Home: undefined;
Settings: undefined;
};
const Stack = createStackNavigator<RootStackParamList>();
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>
);
}
Summary
• Due to React’s rules of hooks you can’t simply import a dynamic, reactive styles object from a module without a hook call.
• By calculating your styles in a provider (or HOC) you centralize the hook usage.
• Components then only use a minimal hook (or get styles injected as props) without repeating the full style logic.
This approach maintains performance (with memoization via useMemo) and makes your theme and dimension responsiveness available in every component without cluttering each component with repetitive hook calls.