Part 3: Cleanup components
Now that our screens are adapted, let’s refactor the default components.
This is where we’ll see the true power of Unistyles in cleaning up component logic.
We’ll focus on ThemedText
and ThemedView
. The other files can be removed.
After cleaning up, your components folder should look like this:
Directoryapp/
- …
Directorycomponents/
Directoryui/
- IconSymbol.ios.tsx
- IconSymbol.tsx
- TabBarBackground.ios.tsx
- TabBarBackground.tsx
- ThemedText.tsx
- ThemedView.tsx
ThemedText
The default ThemedText
component is a perfect candidate for a Unistyles refactor. It contains conditional style logic directly in the JSX - a pattern we can significantly improve.
First, let’s swap the StyleSheet
import and remove unnecessary useThemeColor
hook.
import { StyleSheet, Text, type TextProps } from 'react-native';import { Text, type TextProps } from 'react-native';import { StyleSheet } from 'react-native-unistyles';import { useThemeColor } from '@/hooks/useThemeColor';
The original component used the useThemeColor
hook to get a color based on the current theme.
We’ll replace this imperative logic with a dynamic function in our stylesheet.
A dynamic function is a Unistyles feature that allows a style to accept arguments.
Let’s create one called textColor
to handle the lightColor
and darkColor
props.
export function ThemedText({ style, lightColor, darkColor, type = 'default', ...rest}: ThemedTextProps) { const color = useThemeColor({ light: lightColor, dark: darkColor });
return ( <Text style={[ { color }, styles.textColor(lightColor, darkColor), type === 'default' ? styles.default : undefined, type === 'title' ? styles.title : undefined, type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, type === 'subtitle' ? styles.subtitle : undefined, type === 'link' ? styles.link : undefined, style, ]} {...rest} /> );}
const styles = StyleSheet.create({ default: { fontSize: 16, lineHeight: 24, }, textColor: (lightColor?: string, darkColor?: string) => ({ // todo }), defaultSemiBold: { fontSize: 16, lineHeight: 24, fontWeight: '600', }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', },});
To implement this, we need to know the current color scheme. Unistyles provides access to this via the runtime object (which we’ll alias as rt).
What’s unique compared to React Native StyleSheet
is that with Unistyles your StyleSheet
can be converted to a function that receives both the theme
and the rt
as arguments.
First argument - theme
is the current, always up-to-date theme object.
Second argument - rt
is the runtime object, containing useful device metadata, including rt.colorScheme
.
Because we are accessing a runtime value, Unistyles is smart enough to know this style depends on the color scheme and will automatically update it when it changes - without re-rendering the component!
Let’s complete our dynamic function:
export function ThemedText({ style, lightColor, darkColor, type = 'default', ...rest}: ThemedTextProps) { return ( <Text style={[ styles.textColor(lightColor, darkColor), type === 'default' ? styles.default : undefined, type === 'title' ? styles.title : undefined, type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, type === 'subtitle' ? styles.subtitle : undefined, type === 'link' ? styles.link : undefined, style, ]} {...rest} /> );}
const styles = StyleSheet.create({ const styles = StyleSheet.create((theme, rt) => ({ default: { fontSize: 16, lineHeight: 24, }, textColor: (lightColor: string, darkColor: string) => ({ // todo color: rt.colorScheme === 'dark' ? darkColor : lightColor, }), defaultSemiBold: { fontSize: 16, lineHeight: 24, fontWeight: '600', }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', }, }) }));
Next, let’s tackle the chain of conditional checks for the type prop. This is a classic use case for variants. Variants allow you to move all of this style logic out of your component and into the stylesheet.
export function ThemedText({ style, lightColor, darkColor, type = 'default', ...rest}: ThemedTextProps) { return ( <Text style={[ styles.textColor(lightColor, darkColor), type === 'default' ? styles.default : undefined, type === 'title' ? styles.title : undefined, type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined, type === 'subtitle' ? styles.subtitle : undefined, type === 'link' ? styles.link : undefined, style, ]} {...rest} /> );}
const styles = StyleSheet.create((theme, rt) => ({ default: { fontSize: 16, lineHeight: 24, }, textColor: (lightColor?: string, darkColor?: string) => ({ color: rt.colorScheme === 'dark' ? darkColor : lightColor, }), defaultSemiBold: { fontSize: 16, lineHeight: 24, fontWeight: '600', }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', },}));
export function ThemedText({ style, lightColor, darkColor, type = 'default', ...rest}: ThemedTextProps) { styles.useVariants({ type })
return ( <Text style={[ styles.textColor(lightColor, darkColor), styles.textType, style, ]} {...rest} /> );}
const styles = StyleSheet.create((theme, rt) => ({ textType: { variants: { type: { default: { fontSize: 16, lineHeight: 24, }, defaultSemiBold: { fontSize: 16, lineHeight: 24, fontWeight: '600', }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', }, } } }, textColor: (lightColor?: string, darkColor?: string) => ({ color: rt.colorScheme === 'dark' ? darkColor : lightColor, }),}));
Notice how much cleaner the component is! We simply pass the type
prop to the useVariants
hook, and Unistyles applies the correct styles from our variants block.
To make this component perfectly type-safe, we can use the UnistylesVariants
helper type. It automatically infers all possible variant props from your stylesheet.
Currently, our component has following props:
export type ThemedTextProps = TextProps & { lightColor?: string; darkColor?: string; type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';};
Instead of specifying type
prop manually, we can use UnistylesVariants
generic type:
import { StyleSheet } from 'react-native-unistyles'; import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
export type ThemedTextProps = TextProps & { export type ThemedTextProps = TextProps & UnistylesVariants<typeof styles> & { lightColor?: string; darkColor?: string; type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';};
export function ThemedText({ style, lightColor, darkColor, type = 'default', type, ...rest}: ThemedTextProps) {
Our component is clean, but we can make it even better! Why are we passing lightColor
and darkColor
as props when Unistyles already has access to our app theme?
Let’s remove those props and use the theme object directly.
import { Text, type TextProps } from 'react-native';import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
export type ThemedTextProps = TextProps & UnistylesVariants<typeof styles> export type ThemedTextProps = TextProps & UnistylesVariants<typeof styles> & { lightColor?: string; darkColor?: string; };
export function ThemedText({ style, lightColor, darkColor, ...rest}: ThemedTextProps) { return ( <Text style={[ styles.textColor(lightColor, darkColor), styles.textColor, styles.textType, style, ]} {...rest} /> );}
const styles = StyleSheet.create(theme => ({ const styles = StyleSheet.create((theme, rt) => ({ textColor: (lightColor?: string, darkColor?: string) => ({ color: rt.colorScheme === 'dark' ? darkColor : lightColor, }), textColor: { color: theme.colors.typography }, textType: { variants: { type: { default: { fontSize: 16, lineHeight: 24, }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', color: theme.colors.link }, } } }}));
Why did we remove the extra code?
We no longer pass as props lightColor
and darkColor
because those colours now come straight from the theme (typography color). When you change the colorScheme
or update the theme, Unistyles automatically injects the new values into your StyleSheet
, so there’s nothing to manage manually. Keeping all theming logic inside the StyleSheet
avoids duplicated work and makes the code easier to maintain.
For the same reason, the dynamic function
is no longer needed - we’ve replaced it with a regular style object.
This is the final, fully refactored ThemedText
component. It’s declarative, type-safe, and completely decoupled from style logic.
ThemedView - your turn!
Now is the time to refactor the ThemedView
component. This one is much simpler.
Based on what you’ve learned, try refactoring it yourself to use the theme.colors.background
property.
Once you’re done, check your work against the solution below:
import { View, type ViewProps } from 'react-native';import { StyleSheet } from 'react-native-unistyles';
export type ThemedViewProps = ViewProps;
export function ThemedView({ style, ...otherProps }: ThemedViewProps) { return <View style={[styles.container, style]} {...otherProps} />;}
const styles = StyleSheet.create(theme => ({ container: { backgroundColor: theme.colors.background, }}));
Constants and hooks
Lastly, we can remove the constants
and hooks
folders, as they are now redundant.
Your final project structure should be clean and organized.
Directoryapp/
Directory(tabs)/
- index.tsx
- explore.tsx
- _layout.tsx
- +not_found.tsx
- _layout.tsx
Directoryassets/
Directoryfonts/
- …
Directoryimages/
- …
Directorycomponents/
Directoryui/
- IconSymbol.ios.tsx
- IconSymbol.tsx
- TabBarBackground.ios.tsx
- TabBarBackground.tsx
- ThemedText.tsx
- ThemedView.tsx
If you run the app now, it should look and function correctly, but its internal styling logic is now far more powerful and maintainable.