Home >Web Front-end >JS Tutorial >How to build type-enforced UI components in React Native using @shopify/restyle
It’s been quite some time since I wrote a technical post on my blog, and here’s a new one about building type-enforced UI components in React Native with @shopify/restyle and expo.
@shopify/restyle is a powerful styling library for React Native that brings type safety and consistency to your UI components. Unlike traditional styling approaches, Restyle allows you to create a centralized theme configuration that enforces design system principles across your entire application.
npx create-expo-app@latest
cd /path/to/project npx expo install @shopify/restyle
Create a theme.tsx file to define your design system:
touch theme.tsx
import {createTheme} from '@shopify/restyle'; const palette = { purpleLight: '#8C6FF7', purplePrimary: '#5A31F4', purpleDark: '#3F22AB', greenLight: '#56DCBA', greenPrimary: '#0ECD9D', greenDark: '#0A906E', black: '#0B0B0B', white: '#F0F2F3', }; const theme = createTheme({ colors: { mainBackground: palette.white, cardPrimaryBackground: palette.purplePrimary, }, spacing: { s: 8, m: 16, l: 24, xl: 40, }, textVariants: { header: { fontWeight: 'bold', fontSize: 34, }, body: { fontSize: 16, lineHeight: 24, }, defaults: { // We can define a default text variant here. }, }, }); export type Theme = typeof theme; export default theme;
Update your app/_layout.tsx:
import { DarkTheme, DefaultTheme } from "@react-navigation/native"; import { useFonts } from "expo-font"; import { Stack } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; import { StatusBar } from "expo-status-bar"; import { useEffect } from "react"; import "react-native-reanimated"; import { ThemeProvider } from "@shopify/restyle"; import theme from "@/theme"; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const [loaded] = useFonts({ SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), }); useEffect(() => { if (loaded) { SplashScreen.hideAsync(); } }, [loaded]); if (!loaded) { return null; } return ( <ThemeProvider theme={theme}> <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="+not-found" /> </Stack> <StatusBar> <h2> Creating Reusable Components </h2> <h3> Text Component </h3> <pre class="brush:php;toolbar:false">touch components/Text.tsx
// In components/Text.tsx import {createText} from '@shopify/restyle'; import {Theme} from '../theme'; export const Text = createText<Theme>();
Let’s use it in our home screen
import { Text } from "@/components/Text"; import { SafeAreaView } from "react-native-safe-area-context"; export default function HomeScreen() { return ( <SafeAreaView> <Text margin="m" variant="header"> This is the Home screen. Built using @shopify/restyle. </Text> </SafeAreaView> ); }
As you can see in the code above, we’re passing margin as a “m” instead of a number. We’re getting the value from our theme.tsxfile.
// ./theme.tsx const theme = createTheme({ spacing: { s: 8, m: 16, // margin="m" l: 24, xl: 40, }, textVariants: { header: { // our text header variant fontWeight: 'bold', fontSize: 34, }, body: { fontSize: 16, lineHeight: 24, }, }, // ...rest of code }, });
This how our the home page view will looks
Let’s build a Skeleton Loader card
touch components/SkeletonLoader.tsx
// components/SkeletonLoader.tsx import { BackgroundColorProps, createBox, createRestyleComponent, createVariant, spacing, SpacingProps, VariantProps, } from "@shopify/restyle"; import { Theme } from "@/theme"; import { View } from "react-native"; const Box = createBox<Theme>(); type Props = SpacingProps<Theme> & VariantProps<Theme, "cardVariants"> & BackgroundColorProps<Theme> & React.ComponentProps<typeof View>; const CardSkeleton = createRestyleComponent<Props, Theme>([ spacing, createVariant({ themeKey: "cardVariants" }), ]); const SkeletonLoader = () => { return ( <CardSkeleton variant="elevated"> <Box backgroundColor="cardPrimaryBackground" height={20} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} > </Box> <Box backgroundColor="cardPrimaryBackground" height={100} marginBottom="s" width="90%" overflow="hidden" borderRadius={"m"} > </Box> <Box backgroundColor="cardPrimaryBackground" height={50} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} > </Box> </CardSkeleton> ); }; export default SkeletonLoader;
const Box = createBox<Theme>();
type Props = SpacingProps<Theme> & VariantProps<Theme, "cardVariants"> & BackgroundColorProps<Theme> & React.ComponentProps<typeof View>; const CardSkeleton = createRestyleComponent<Props, Theme>([ spacing, createVariant({ themeKey: "cardVariants" }), ]);
// components/SkeletonLoader.tsx export const SkeletonLoader = () => { return ( <CardSkeleton variant="elevated"> <Box backgroundColor="cardPrimaryBackground" height={20} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} ></Box> <Box backgroundColor="cardPrimaryBackground" height={100} marginBottom="s" width="90%" overflow="hidden" borderRadius={"m"} ></Box> <Box backgroundColor="cardPrimaryBackground" height={50} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} ></Box> </CardSkeleton> ); };
We have one thing left to make it working, update theme.tsxfile to have cardVariants
const theme = createTheme({ colors: { // Add Black Color to use it later on black: palette.black, }, // Add Border Radius Variants borderRadii: { s: 4, m: 10, l: 25, xl: 75, }, // Add Card Variants cardVariants: { elevated: { shadowColor: "black", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, borderRadius: "m", }, defaults: { padding: "m", borderRadius: "m", }, }, });
That’s great, but let’s animation to our component
// components/SkeletonLoader.tsx const ShimmerAnimation = () => { const shimmerTranslate = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.loop( Animated.timing(shimmerTranslate, { toValue: 1, duration: 1500, useNativeDriver: true, }) ).start(); }, [shimmerTranslate]); const translateX = shimmerTranslate.interpolate({ inputRange: [0, 1], outputRange: [-300, 300], }); return ( <Animated.View > <p>and let’s use it in our Skeleton Loader Component<br> </p> <pre class="brush:php;toolbar:false">// components/SkeletonLoader.tsx export const SkeletonLoader = () => { return ( <CardSkeleton variant="elevated"> <Box backgroundColor="cardPrimaryBackground" height={20} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} > <ShimmerAnimation /> </Box> <Box backgroundColor="cardPrimaryBackground" height={100} marginBottom="s" width="90%" overflow="hidden" borderRadius={"m"} > <ShimmerAnimation /> </Box> <Box backgroundColor="cardPrimaryBackground" height={50} marginBottom="s" width="70%" overflow="hidden" borderRadius={"m"} > <ShimmerAnimation /> </Box> </CardSkeleton> ); };
And here’s the full component code:
// components/SkeletonLoader.tsx import { useEffect, useRef } from "react"; import { Animated } from "react-native"; import { BackgroundColorProps, createBox, createRestyleComponent, createVariant, spacing, SpacingProps, VariantProps, } from "@shopify/restyle"; import { Theme } from "@/theme"; import { View } from "react-native"; const Box = createBox<Theme>(); const ShimmerAnimation = () => { const shimmerTranslate = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.loop( Animated.timing(shimmerTranslate, { toValue: 1, duration: 1500, useNativeDriver: true, }) ).start(); }, [shimmerTranslate]); const translateX = shimmerTranslate.interpolate({ inputRange: [0, 1], outputRange: [-300, 300], }); return (Et voila, we made a skeleton loader card using @shopify/restyle using
Support for dark mode
Let’s start with adding dark theme configuration, in your theme.tsxfile
// theme.tsx export const darkTheme: Theme = { ...theme, colors: { ...theme.colors, mainBackground: palette.white, cardPrimaryBackground: palette.purpleDark, greenPrimary: palette.purpleLight, }, textVariants: { ...theme.textVariants, defaults: { ...theme.textVariants.header, color: palette.purpleDark, }, },Add our dark theme configuration in our app layout by adding it to our layout.tsx file
npx create-expo-app@latest
- Based on the color schema, use the default light theme or in dark mode use the darkTheme config defined in theme.tsx file
// app/_layout.tsx import theme, { darkTheme } from "@/theme"; //... rest of the code <ThemeProvider theme={colorSchema === "dark" ? darkTheme : theme}> <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name=" not-found" /> </Stack> <StatusBar> <p>Here’s it dark and light mode.</p> <p><img src="https://img.php.cn/upload/article/000/000/000/173310152340178.jpg" alt="How to build type-enforced UI components in React Native using @shopify/restyle"></p> <p><img src="https://img.php.cn/upload/article/000/000/000/173310152557660.jpg" alt="How to build type-enforced UI components in React Native using @shopify/restyle"></p> <p>Et voila, we managed to create type-enforced UI component using @shopify/restyle package</p> <p>Thank you :)</p>The above is the detailed content of How to build type-enforced UI components in React Native using @shopify/restyle. For more information, please follow other related articles on the PHP Chinese website!