Creating a Cross-Platform Design System for React and React Native with Bit

jo
jonykru2 years ago

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:

Building platform-specific Accordions

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:

React
Manchester United
Barcelona
Paris Saint G.
Liverpool
React Native

You can find all the components here

Turn common code to shared dependencies

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:

Component Breakdown

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 render
  • design-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
CopiedCopy

Creating a Cross-Platform Design System

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:

  • A base Token, common to both
  • A Token extended from the base one to use with React.
  • A Token extended from the base one to use with React Native.

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.

Providing the Design Tokens to a Theme

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.tsx
export 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",
};
CopiedCopy

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:

react-tokens.tsx
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",
};
CopiedCopy

While rnative-tokens.tsx will have only numbers:

rnative-tokens.tsx
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,
};
CopiedCopy

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:

Could not find command 'install'.

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.

Providing the react-tokens to a React Theme provider

In this example, we will show how to create a Theme for React:

web.tsx
import { 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 };
CopiedCopy

We will do the same for the react-native one.

Providing the rnative-tokens to a React-Native Theme provider

In this theme, we will need to pass the option withoutCssVars: true to avoid having a component that renders a <div>.

mobile.tsx
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 };
CopiedCopy

Implementing the accordions: all set, go compose!

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/>;
};
CopiedCopy
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:

Composing a React Web App with our components

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.

Could not find command 'create'.
Could not find command 'install'.
Could not find command 'use'.
Could not find command 'install'.
After incorporating our React Accordion to the code:app.tsx
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 */}
    </>
  );
}
CopiedCopy

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"
CopiedCopy

Once we tag the component, it will deploy to netlify. You can open the website here

Consuming the React Native component in Expo

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
CopiedCopy

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,
    },
  });
CopiedCopy

And run it inside our app:

Source code

Conclusion

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!