Skip to content

Part 3: Cleanup components

Tutorial

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.

components/ThemedText.tsx
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.

components/ThemedText.tsx
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:

components/ThemedText.tsx
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.

components/ThemedText.tsx
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',
},
}));
components/ThemedText.tsx
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:

components/ThemedText.tsx
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:

components/ThemedText.tsx
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.

components/ThemedText.tsx
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:

components/ThemedView.tsx
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.