Many teams use both React and React Native to drive their web and mobile development, offering a unique way to have UI/UX consistency across two very different platforms. One of the problems that developer teams encounter in synchronizing between these platforms is how to share code between them easily.
What sounds like a trivial problem can become a hustle when scaling up and adding more elements to the ecosystem. More elements usually mean more maintenance. It is natural to ask ourselves how to achieve this by using a scalable and maintainable architecture while also being flexible enough to allow variations within the ecosystem, especially when styling.
This guide will teach us how to build a consistent Design System with React and React Native. We will show how to share design tokens, logic, and types while making a design system that aims to provide consistency between the two platforms.
In case you prefer to watch rather than read, you can see the whole process here:
To show how we can achieve it, we will build two accordions, one for React and another for React Native. To ensure consistent UI, we'll create a set of objects that hold design tokens and compose them into themes.
Here are the two components:
You can find all the components here
With Bit, we can manage shared code between React and React Native as components and use it as dependencies for both platforms' respective design system elements. In our example, we are building two accordions, one for React and another for React Native. We want each accordion to contain the minimum amount of platform-specific code (since their APIs are different) and will outsource all the common parts.
Looking at the dependencies graph, we can see how the base-tokens are consumed by other components and provided to different themes. Both Accordions will share a Types component and the internal state management (hooks).
React Dependencies:
React Native Dependencies:
As mentioned, we want to have as much code outside the platform-specific implementations as possible, meaning that hooks, types, themes, and design tokens will be part of different components.
That means that we will need to build the following components:
api/accordion
: To share types. For example, the props of these accordions.api/accordion-items
: To share the items the accordions will need to renderdesign-tokens/base-tokens
: A basic design-token object.design-tokens/react-tokens
: An extension of design-tokens/base-tokens
with platform specific styles for React.design-tokens/rnative-tokens
: An extension of design-tokens/base-tokens
with platform specific styles for React Native.base-ui/hooks/use-open
: A hook that will control the open / close state of the accordion.base-ui/hooks/use-select
: A hook that will control the selected item of the accordion.theme/web
: A component that will share the react-tokens
through the Context Api, and provide a hook to consume it.theme/mobile
: A component that will share the rnative-tokens
through the Context Api, and provide a hook to consume it.base-ui/react/accordion
: A React Accordion.base-ui/react-native/accordion
: A React Native Accordion.share-react-react-native --> scope
├── api --> namespace
│ ├── accordion --> component
│ └── accordion-items --> component
├── design-tokens --> namespace
│ ├── base-tokens --> component
│ ├── react-tokens --> component
│ └── rnative-tokens --> component
├── base-ui --> namespace
│ ├── hooks --> namespace
│ │ ├── use-open --> component
│ │ └── use-select --> component
│ ├── react --> namespace
│ │ └── accordion --> component
│ └── react-native --> namespace
│ └── accordion --> component
└── theme --> namespace
├── web --> component
└── mobile --> component
The post Design Tokens in React and How We Use Them showed us how we could use them in React and Bit. Now let's add React Native!
We will apply the same pattern of creating a context and a hook to inject our tokens into our components. The trick to making it work with React and React Native is that we are going to have three kinds of tokens:
Providing it to two different Themes, we will have two sets of platform-specific tokens that share a common ground.
React and React Native don't share the same styling properties and types. While styles
in React are defined by CSS.Properties
, in React Native it is defined by ViewStyle | TextStyle | ImageStyle
. In practice, that means that React Native has a more restricted type resolution. For example, while in React, many properties comply with the Globals
type, allowing others to inherit,
in React Native, there isn't such a possibility. Another subject to mention is units. React Native units are unitless, as specified in the documentation:
All dimensions in React Native are unitless and represent density-independent pixels.
In React, there is more freedom: you can use px
, rem
, em
.
Back to our tokens, that means that we can't have a property like margin: 10px
or backgroundColor: "inherit"
because it won't work in React Native. To overcome this issue, we will have a base object that shares the most common values and other Platform specific that adds more properties.
Now that we have set the ground and architecture for this system let's look at the code, starting with the three tokens objects. The base one would look something like this:
base-tokens.tsxexport interface BaseTokensProps {
primaryColor: string;
secondaryColor: string;
borderColor: string;
borderStyle: "solid" | "dotted" | "dashed" | undefined;
}
export const baseTokens: BaseTokensProps = {
primaryColor: "red",
secondaryColor: "blue",
borderColor: "green",
borderStyle: "solid",
};
Now, we can extend it on design-tokens/react-tokens
and design-tokens/rnative-tokens
For example, react-tokens.tsx
will have some values in px
:
import { baseTokens } from "@learnbit-react/web-mobile-design-system.design-tokens.base-tokens";
import type { BaseTokensProps } from "@learnbit-react/web-mobile-design-system.design-tokens.base-tokens";
export interface ReactTokensProps extends BaseTokensProps {
spacing: string;
fontSize: string;
borderWidth: string;
}
export const reactTokens: ReactTokensProps = {
...baseTokens,
spacing: "15px",
fontSize: "18px",
borderWidth: "3px",
};
While rnative-tokens.tsx
will have only numbers:
import { baseTokens } from "@learnbit-react/web-mobile-design-system.design-tokens.base-tokens";
import type { BaseTokensProps } from "@learnbit-react/web-mobile-design-system.design-tokens.base-tokens";
export interface RNativeTokensProps extends BaseTokensProps {
spacing: number;
fontSize: number;
borderWidth: number;
}
export const rNativeTokens: RNativeTokensProps = {
...baseTokens,
primaryColor: "purple",
secondaryColor: "gray",
spacing: 10,
fontSize: 12,
borderWidth: 3,
};
These will be our token definitions. Pay attention to how both definitions extend the base one.
If your IDE autocompletes the location using relative imports, you can quickly fix it by running `bit link --rewire`
For our theme, we will use a pre-existing component that creates a Theme from an object. For that, we will need to install it in our workspace:
We need to have an object with the properties of our theme and call the createTheme
function, providing it as an argument. The result will be an object with a hook to use those values and a component that injects it using the React Context.
In this example, we will show how to create a Theme for React:
web.tsximport { createTheme } from "@teambit/base-react.theme.theme-provider";
import { reactTokens } from "@learnbit-react/web-mobile-design-system.design-tokens.react-tokens";
import type { ReactTokensProps } from "@learnbit-react/web-mobile-design-system.design-tokens.react-tokens";
const theme = createTheme<ReactTokensProps>({
theme: reactTokens,
});
const { useTheme, ThemeProvider } = theme;
export { useTheme, ThemeProvider };
We will do the same for the react-native one.
In this theme, we will need to pass the option withoutCssVars: true
to avoid having a component that renders a <div>
.
import { createTheme } from "@teambit/base-react.theme.theme-provider";
import { rNativeTokens } from "@learnbit-react/web-mobile-design-system.design-tokens.rnative-tokens";
import type { RNativeTokensProps } from "@learnbit-react/web-mobile-design-system.design-tokens.rnative-tokens";
const theme = createTheme<RNativeTokensProps>({
theme: rNativeTokens,
withoutCssVars: true,
});
const { useTheme, ThemeProvider } = theme;
export { useTheme, ThemeProvider };
Now that the Themes are ready let's provide them to the Accordions. All we need to do is to import those components that we created. Both of our Accordions will have the same skeleton, where we consume the hooks and return a mapped list, represented and abstracted by a fragment in the following snippet.
import { useTheme } from '@learnbit-react/web-mobile-design-system.theme.web // or .mobile in the react-native one!
import { useOpen } from '@learnbit-react/web-mobile-design-system.hooks.use-open';
import { useSelect } from '@learnbit-react/web-mobile-design-system.hooks.use-select';
import type { AccordionProps } from '@learnbit-react/web-mobile-design-system.api.accordion';
const GenericAccordionTemplate = ({ elementList } : AccordionProps) => {
const { isOpen, toggleOpen } = useOpen();
const { selectedId, setSelection } = useSelect();
const {someValueToken, someValueToken} = useTheme();
return <div_or_View style={{someProp: someValueToken}}>My styled element<div_or_View/>;
};
Beware that the React and React Native Accordion don't use the same useTheme hook. Each one uses a different one to comply with the proper styling types.
You can look at both accordions implementations on the following cards:
Components are great. Now that we have the accordion let's create a React App, which is also a component. We can deploy apps at the same time we snapshot them by running bit tag
. Isn't that cool?
Apps can have a deploy function. In this case, I am going to use the Netlify Deployer. You can learn more about how to create apps with Bit here.
import React from "react";
import { Routes, Route } from "react-router-dom";
import { Accordion } from "@learnbit-react/web-mobile-design-system.base-ui.react.accordion";
import { Item } from "@learnbit-react/web-mobile-design-system.api.accordion";
export function AccordionApp() {
return (
<>
{/* header component */}
<Routes>
<Route
path="/"
element={
<Accordion
elementList={[
new Item("Asia", "01").toObject(),
new Item("Africa", "02").toObject(),
new Item("North America", "03").toObject(),
new Item("South America", "04").toObject(),
new Item("Antarctica", "05").toObject(),
new Item("Australia / Oceania", "06").toObject(),
new Item("Europe", "07").toObject(),
]}
/>
}
/>
<Route path="/about">{/* about page component */}</Route>
</Routes>
{/* footer component */}
</>
);
}
And configuring the app in accordion.react-app.ts
, just a bit tag
will snapshot it and also deploy it:
bit tag apps/react/accordion -m "First deploy"
Once we tag the component, it will deploy to netlify. You can open the website here
expo init my-new-project
cd my-new-project
yarn install @learnbit-react/web-mobile-design-system.base-ui.react-native.accordion @learnbit-react/web-mobile-design-system.api.accordion
We can add the recently created component to the app.js
file:
import {Accordion} from '@learnbit-react/web-mobile-design-system.base-ui.react-native.accordion';
import { Item } from '@learnbit-react/web-mobile-design-system.api.accordion';
import {(StyleSheet, View)} from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Accordion
elementList={[
new Item('Asia', '01').toObject(),
new Item('Africa', '02').toObject(),
new Item('North America', '03').toObject(),
new Item('South America', '04').toObject(),
new Item('Antarctica', '05').toObject(),
new Item('Australia / Oceania', '06').toObject(),
new Item('Europe', '07').toObject(),
]}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'flex-start',
marginTop: 50,
},
});
And run it inside our app:
By using design tokens and Theme Providers, we were able to create platform-specific themes for our applications. Using Bit, we split elements into different components, writing them once but consuming them on both accordions, maintaining a unique source of truth and the code maintenance to a minimum. We also documented and showed usage examples of each component, scaling our apps further and quicker, even across multiple platforms like React and React Native. Lastly, we were also able to create production ready applications.
That is it! Thanks for reading!