Building Component Libraries for React Native apps

@satya164

Hello everyone. Dzien dobry Krakow! I’m very excited to be speaking here about building component libraries for React Native apps

Hello

👋 My name is Satyajit Sahoo

💻 I am a developer at callstack.com

📢 Follow me @satya164

🎩 I work on reactnativepaper.com & reactnavigation.org

2

@satya164

My name is Satyajit Sahoo and I am a frontend developer at Callstack. You can follow me @satya164 on Twitter, GitHub and Medium. I am a core contributor to react-native-paper and react-navigation.

Challenges

3

@satya164

When I say component library, I refer to a collection of reusable components. By using a component library, you reduce extra boilerplate in your app, and can get productive faster. It's very common to follow a style guide when designing a component library, resulting in a consistent design across your apps.

Today, I'll be talking about my experience in maintaining component libraries and the various challenges involved. Hopefully the things I've learned will be helpful to you.

📱 Cross-platform

👓 Accessibility

📝 Documentation

📦 Tooling

👽 And more...

4

@satya164

Building a library can be very different from building an app. In apps, most of the time we can hardcode certain things. Libraries tend to be more generic, because we don’t know all of the ways the user is going to use the library.

Some of those challenges are cross-platform support, accessibility, good documentation, tooling, and many more.

So let’s dive into it.

Cross-platform

5

@satya164

A promise of React Native is cross-platform development. Lots of people come to React Native, so they can write a single codebase and use it for multiple platforms.

But cross-platform development is not easy. Apart from the design, there are many other fundamental differences between platforms that we need to address when writing cross platform code.

Overflow

hidden (Android) vs visible (iOS, Web)

6

@satya164

Overflow area

Main content

Height of content

One of the biggest example is, overflow behaviour. On Android, overflow: hidden is the default, on iOS, overflow: visible is default. This can be very tricky, so the best way to handle this is to always assume that overflow is hidden by default when working on cross platform components.

In recent versions of React Native, you can actually set overflow to visible on Android, but gestures such as touches won’t work in the overflow region.

Text background

transparent (Android, Web) vs opaque (iOS)

7

@satya164

Text over image

Background is opaque

Another difference is that, on iOS, text elements have an opaque background color by default for improved performance. It works fine on solid backgrounds, but not when you have a background image, for example. You can explicitly set it to transparent to avoid this behaviour when needed.

Touchable

TouchableNativeFeedback (Android) vs TouchableOpacity

8

@satya164

The other difference is the Touchable components. On iOS, touchables such as buttons tend to change the opacity when pressed, while on Android 5 onwards, there’s a ripple effect. Unfortunately React Native doesn’t provide a single touchable which has appropriate behaviour based on the platform, so you’ll need to build your own component which renders the appropriate touchable.

react-native-platform-touchable

For a ready-made solution, checkout react-native-community/react-native-platform-touchable

<Touchable onPress={() => alert('Yay!')}>

<Text>Press me</Text>

</Touchable>

9

@satya164

To avoid repeating this every time, you can use the react-native-platform-touchable library made by our lord and saviour Brent Vatne. It renders a ripple effect on supported Android versions and a opacity effect on iOS and unsupported platforms, such as old Android versions and on web.

In react-native-paper, we have a similar component which renders a ripple effect or a highlight effect based on the platform. We also have a separate touchable implementation for web which renders a ripple effect similar to Android.

Elevation and Shadows

Android

style={{

elevation: 4,

}}

iOS, Web

style={{

shadowColor: 'black',

shadowOffset: { height: 3 },

shadowOpacity: 0.24,

shadowRadius: 4,

}}

10

@satya164

Next difference is that, there’s no support for shadows on Android. Instead, there’s an elevation style which maps to the native elevation property. So what is elevation? Elevation refers to the position of the element in z-axis. The higher the element is, the bigger it’s shadow.

So basically, it renders a predefined shadow based on material design guidelines depending on the value you provide. There’s not much to customize. Elevation also affects the position in z-axis similar to z-index, so you have to be careful when using it.

function shadow(elevation) {

return {

elevation,

shadowColor: 'black',

shadowOffset: { width: 0, height: elevation - 1 },

shadowOpacity: 0.24,

shadowRadius: elevation,

}

}

11

@satya164

To handle this inconsistency, I like to have a single shadow function which calculates shadow properties for iOS and web, based on the elevation value. Here’s a very rough implementation in the slide. In react-native-paper, we have a more robust implementation which produces a more accurate result and handles animated values as well.

Notch and StatusBar

  • SafeArea
  • StatusBar.currentHeight

12

@satya164

Awesome App

Handling the notch is always annoying when writing a library. In React Native, the notch is handled by SafeAreaView. Older iPhones and many Android phones don’t have a notch, so we have to handle the statusbar height.

A common example is that, we have a header in the app and we want it to appear below the translucent statusbar. But we also want to add a top padding so that it doesn’t overlap any content such as the header title.

Every app can have a different StatusBar/Notch height based on the platform, iOS version, whether translucent statusbar is enabled on Android or not etc. There’s no single cross-platform API to get the statusbar height. There is a SafeAreaView API, but it only supports iOS 11+, and can be troublesome when you’re animating views containing SafeArea.

const STATUSBAR_HEIGHT_EXPO =

global.__expo && global.__expo.Constants

? global.__expo.Constants.statusBarHeight

: 0;

const STATUSBAR_HEIGHT = Platform.select({

android: STATUSBAR_HEIGHT_EXPO,

ios: Platform.Version >= 11 ? 0 : STATUSBAR_HEIGHT_EXPO,

});

13

@satya164

Currently we have some custom logic in react-native-paper to try and guess the statusbar height. We take into account whether the user is using Expo, Android or iOS, and apply the default heights for those environments. On iOS 11 and above, we use the available SafeAreaView instead. We also have a prop to let the user manually override this value.

This approach is not perfect. The value we guessed might be often wrong, and we are using a constant value even if the StatusBar height can change over time. But this is the best we can do right now.

14

Fortunately there’s this pull request opened by Janic which adds an API to get the height of the Navigation bar at the bottom and the StatusBar on both Android and iOS. So look forward to much easier way of handling Notches and StatusBar in the future.

Platform specific code

Always specify a fallback when there is platform specific code

MyComponent

├── index.android.js

├── index.ios.js

└── index.js

15

@satya164

Sometimes it’s not possible to have a single implementation for all platforms. In those cases, there are different approaches we can use to provide platform specific implementations. One of them is by android, ios or native extensions in the filename.

Sometimes it’s not possible to support all platforms. But when we’re using the file extension approach, we should always provide a fallback implementation, even if it renders nothing. Why? For example, say 9 out of 10 components in the library work on all platforms, but one component has platform specific implementation. If we don’t provide a fallback for this component, it becomes impossible to use the whole library for that unsupported platform.

Metro looks for specific extensions like ios.js or android.js, then more generic native.js, and then just .js. We can take advantage of this and use the most generic extension as the fallback, and have platform specific implementation in the files with more specific extensions.

Platform.select

Use the default key in Platform.select for fallbacks

Platform.select({

ios: {...},

default: {...},

})

16

@satya164

The other way is to use the Platform.select API. When using Platform.select, we can use the default key to provide a fallback.

Platforms

  • Windows
  • TV OS
  • Mac OS
  • More?

17

@satya164

React Native

for Mac OS

React Native

for Samsung

Smart Fridge

So far we talked about some of the differences between iOS, Android and Web, but there are a lot more other platforms. People are building React Native for more and more platforms. There is React Native for web, windows, TV OS, Mac OS, and many more platforms.

Deep down I know, what we really want is React Native for Samsung Smart fridge, but don’t worry, there actually is React Native for Samsung Smart Fridge. It’s called react-native-tizen-dotnet.


So, the main 3 platforms I try to support are iOS and Android and Web. With awesome tools like Expo starting to have web support by default, it’s much easier to support web in your app.

They key to support multiple platforms is to prefer JavaScript modules when possible as opposed to native code. Sometimes JavaScript code doesn’t provide the same experience as native, but when it does work, we can support multiple platforms and make the modules easier to maintain because we have a single implementation instead of 3 different implementations.

The last thing I’ll say is, always test on all supported platforms. No matter how confident we are, there’s always be going to be inconsistencies, and it’s more work to fix if we find the bugs late.

Responsive Design

18

@satya164

Responsive design, it’s a pretty big thing on the web. But we don’t talk about it in React Native often, and it’s ignored in many apps. I see a lot of apps locking their orientation to portrait and it makes me sad.

Responsive Design in Native apps

  • Screen orientation
  • Split screen
  • Free-form window mode

19

@satya164

Orientation is not the only thing we need to keep in mind. There are many other features which won’t work properly if the app is not responsive. For example, split screen mode, where the user can render apps side by side. Another one is free-form window mode, mainly useful for tablet devices and Chromebook. Here the app windows can be freely resized like a desktop app window.

Today in React Native, it’s not easy to build responsive layouts. The available APIs don’t encourage building responsive layouts, and it’s often a magnitudes easier not to make the UI responsive.

  • Avoid Dimensions API
  • Use flexbox when possible
  • If you really need layout, use onLayout
  • Keep in mind that onLayout is async

20

@satya164

But there are some basic rules we can follow to make our layouts responsive, First of all, avoid Dimensions API. The dimensions API provides constants for screen height, width etc. Because these are constants, our layout won’t react to changes in dimensions. Okay, we can address that by using Dimensions.addListener and listening to layout updates, but there is another fundamental flaw with it when building components. The Dimensions API provides layout of the whole screen, no matter how our component is used. A component should render according to its available space, not according to how big the whole screen is. It’s often okay in apps, because we know how the component will be used, but component libraries can be used in many different ways and we need make it more generic.

If you avoid Dimensions and rely on flexbox as much as you can, this becomes a non-issue. But sometimes you do need the layout, for example, to do some calculations or conditional logic., React Native also has an onLayout API that you can pass to a View to get its layout. Just keep in mind that this is asynchronous, and you might need to do something like fading in the content after the layout is available for a better user experience.

21

@satya164

Here’s a little lesson in trickery

I had a similar issue in my tab view library, where I need to know the width of the tabview to render screens side by side. I didn’t want to fade-in the content and wanted to make the wait for the layout invisible to the user. So I did some trickery to achieve this.

22

@satya164

onLayout

StyleSheet.absoluteFill

flex: 1

A

A

B

C

width: layout.width * 3

flex: 1

flex: 1

On the first render, I only render one screen and position it absolutely. This doesn’t require me to specify any width for the screen. When the layout is available, I remove the absolute position and use flexbox to position them. This is entirely transparent to the user.

23

@satya164

As you can see in the GIF, the tabview renders normally and supports changes in its layout, such as orientation. I’m sure you can also come up with more such tricks for your apps.

Accessibility

24

@satya164

Accessibility is another important aspect to focus on. Accessibility simply means building to optimise access. We should be inclusive about giving equal access and opportunities to everyone wherever possible. Many people rely on assistive technologies to use apps and websites. If our apps are not accessible, we’re making it impossible for them to use.

Component libraries are building blocks, so it’s crucial to have them accessible by default. If the building blocks aren’t accessible, the app built with them won’t be either. This is why we are taking accessibility seriously with React Native Paper.


Let’s talk about some of the common disabilities.

Visual impairments

  • Blindness
  • Low-level vision
  • Color blindness

25

@satya164

Visual impairments, this includes blind people, people who can’t see properly, different types of color blindness such as deuteranopia, protanopia, tritanopia etc.

Hearing impairments

  • People with low or no hearing ability
  • Language disorders

26

@satya164

Hearing impairments, this includes people who can’t hear properly, people who have trouble processing language, or even people who speak a different language than you do.

Mobility impairments

  • Physical issues
  • Neurological or genetic disorders

27

@satya164

Mobility impairments, this includes people who have limited movement abilities, due to physical, neurological or genetic disorders.

Screen readers

For people with vision impairments

  • TalkBack on Android
  • VoiceOver on iOS and Mac
  • Narrator on Windows
  • Orca on Linux
  • ChromeVox on ChromeOS
  • Many more…

28

@satya164

I’ll mostly talk about visual impairments, because that’s what we deal with most of the time when building apps. People with visual impairments often use screen readers to interact with apps. A screen reader lets them know what’s on the screen and allows them to interact with the app via certain gestures.

Most operating systems have built in screen readers, such as TalkBack on Android, VoiceOver on iOS and Mac, Narrator on Windows, Orca on Linux, ChromeVox on ChromeOS, and more. There are also third party free and commercial screen readers.

Tools to test screen readers

  • Accessibility Inspector on iOS Simulator
  • VoiceOver on iOS Devices
  • TalkBack on Android Emulator and Devices
  • VoiceOver/Narrator/Orca for web apps in the Browser

29

@satya164

The best way to know if our components work with screen readers is to try to use them with screen readers. We can use the built-in screen readers in the OS to test our components on various devices. On iOS simulator, we can use the Accessibility Inspector tool provided with XCode.

30

@satya164

Accessibility inspector will show all of the attached accessibility related info regarding a particular element. For example, here the inspector is showing that our button has the appropriate accessibility role of button and it’s label is undo.

React native provides several APIs to add accessibility related data. Such as accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityStates and more.

31

@satya164

It’s not much, but it’s honest work

There are many things missing and not much we can do right now, but let’s try our best, it’s honest work.

You can of course send a pull request adding the missing APIs and roles. You’ll be my hero if you did that.

What else?

  • Use semantic elements and appropriate roles
  • Use captions for media
  • Make your web app accessible to keyboard users
  • Use enough contrast for colors and test for color blindness

32

@satya164

The easiest way to ensure accessibility is to use semantic elements. For example, if you need a switch, use the switch component from react native. You could build the same switch with views, but you’ll be missing a lot of behaviour, and it’s a lot of work to implement them yourself.

If you have media such as images, provide alt tags. For audio, provide subtitles.

If your app is built for the web, make it work with a keyboard.

Make sure the colors in your app have enough contrast and usable for people with color blindness.

Right-to-left languages

33

@satya164

We just talked about making our components accessible. This also includes people who use a different language, which gets us to right to left languages, known as RTL in short. What is RTL? When we write english or polish, we write and read them from left to right. But there are many languages which are written and read from right to left.

In a region with a RTL language, books open from the right-hand side. Similarly, most UI elements are mirrored when using a RTL language.

34

@satya164

Here is an example screenshot in an Arabic script. Most UI elements are flipped here, the icons are moved to the right, the text is aligned to right instead left etc.

In most cases React Native automatically handles RTL for us when we’re using flexbox. There will be some cases where we might want to handle it manually. React Native provides all the necessary APIs to test and implement these.

import { I18nManager } from 'react-native'

I18nManager.forceRTL(true)

I18nManager.isRTL

You need to restart the app for RTL related changes to take effect.

35

@satya164

The i18nManager forceRTL API changes your app to render in RTL mode so you can see how everything looks when using RTL languages. When you call this API, you’ll need to close your app and open it again.

The i18nManager isRTL API is a boolean that tells us if we’re in RTL mode. We can implement RTL specific UI changes using this API.

Linking

36

@satya164

This one is every React native developer’s favorite, linking. If a library includes native code, we often need to link it to use it. But unfortunately, it doesn’t work properly with many libraries.

For libraries with native code, make it easy to link. Library which can be linked with one command are awesome:

react-native link awesome-library

37

@satya164

So, if you’re building a library, always test that it works properly with react-native link. It’ll save many developers countless hours of struggle.

38

@satya164

Fortunately, thanks to these amazing people, soon we’ll be able to have ability to link libraries without any work on the user’s part.

Documentation

39

@satya164

Documentation is one of the most important parts of a library. Good documentation makes happy users. However, writing and maintaining the documentation can be a lot of work.

Approaches

  • Extract code comments with React docgen
  • Write documentation in separate markdown files
  • Document props with code comments + detailed guides in markdown files

40

@satya164

I love to automate things that can be automated. With the limited time I have, the less things I have to worry about, the better. I like generating documentation from code because it reduces the amount of work we have to do as maintainers, and there’s way less chance of the documentation getting out of date from the actual code.

It’s true that a lot of generated documentation is really bad. In my opinion, it’s not because they are generated, but because they don’t receive much attention. Even though I’m a fan of documentation generation, I like to write detailed comments for specific props so it’s actually understandable. Some topics require more detailed guides, which I like to put them in their own files.

For React Native Paper, we follow this hybrid approach, where we extract documentation for props from code comments and flow types using react docgen, as well as write guides for broader topics.

type Props = {

/**

* Content of the label in the button.

*/

children: ReactNode,

/**

* Function to execute when the user presses the button.

*/

onPress?: () => void,

};

41

@satya164

Our code comments look kind of like this. There are flow or typescript types with comments that will be extracted to the documentation pages.

Tools

42

@satya164

There are many tools to help you with writing documentation. Docusaurus lets you write your documentation in markdown files and publish them as a website. Docz lets you write documentation in mdx files with helpers for document generation. At callstack, we have our own tool called component docs, which supports markdown, mdx and even vanilla react components. It also supports documentation generation for react components with react docgen. It generates static HTML pages which we deploy to GitHub pages.

Online playgrounds

43

@satya164

When writing documentation, another important thing is to provide runnable playgrounds. This lets users quickly try out components, and also makes it easier to report issues. For playgrounds running on web, you can configure React Live with react-native-web, or use something like React Native Web player. For React Native Paper, we use Expo Snack which lets users run the components directly on their device.

We also have an expo app as the example app in the repo. We use it to preview components while we work on them.

44

@satya164

Each of our code snippets in the usage examples have a “Try on Snack” link which loads that example in a full editor on Snack. It’s pretty easy to implement too, you can pass the full code in a query param to the snack website and it’ll load it in the editor.

Static types

45

@satya164

Static types are great. They help me by providing autocompletion and checking errors in my code.

46

@satya164

When a library doesn’t have static types

This is how I feel when a library doesn’t have type definitions. You should definitely use them in your project.

So, there are 2 popular static type checkers, Flow and TypeScript. Both have their own advantages. I’ll quickly compare both so we know which one is better for building libraries.

Flow

  • Breaking changes in each release
  • React Native versions coupled to Flow versions
  • No good workflow for libraries

47

@satya164

Flow is not 1.0 yet. It has breaking changes in each release. It means it’s a lot of work to keep it working every release. Due to this reason, React Native versions are coupled to specific Flow versions.

There is also no good workflow for supporting multiple Flow versions except publishing on flow-typed. This would’ve been okay if there weren’t breaking changes each release, but right now the reality is that it’s not always possible to support multiple Flow versions with a single definition.

Even if you’re willing to publish definitions to flow-typed, it’s a lot of work, and Flow doesn’t support extracting type definitions from your existingcode.

None of this would be a big issue in apps. But when working with libraries, we need to support different kind of projects which will have different versions of Flow.

TypeScript

  • Stable and less breaking changes
  • CLI extracts definitions from source code
  • IDE provides intellisense even when user doesn’t use TypeScript

48

@satya164

TypeScript is stable and rarely has breaking changes. The TypeScript CLI can extract type definitions from the source code to publish separately. Even if the library users don’t use TypeScript, their IDE can still provide intellisense if your library has TypeScript definitions.

There are of course many other differences, but these are the main things that stand out from a library development point of view. To me, TypeScript is a clear winner for libraries. We used to use Flow in React Navigation, but due to all the issues we removed it, and we want to migrate to TypeScript soon (pull request welcome by the way).

What’s more? Metro supports compiling TypeScript by default in the generated app, so there’s no reason not to use TypeScript.

Code quality

49

@satya164

Now, let’s talk about the most important part, which is, should you use semicolons or not?

Just kidding, it’s about tabs vs spaces.

50

@satya164

All kidding aside, tools like ESLint are very useful to find potential issues and bad patterns in our code. Prettier let’s us focus on the code and forget about the formatting.

For testing, Jest, detox and react-native-testing library are great.

I like to use husky to run some checks such as ESLint when commiting code. I also use commitlint to ensure my commits match the conventional commits specification. It’s important to me, because then I can automatically generate changelogs from my commit history.

51

@satya164

Then I use circleci to run all these checks for every commit and pull request. It’s a great feeling seeing those green check marks.

Publishing

52

@satya164

So we have made sure of everything. How do we get this amazing library into the hand of users? It’s time to publish the package to npm.

What to publish?

  • Source code for Metro (React Native)
  • ES modules build for bundlers like Webpack
  • CommonJS build for everything else
  • Type definition files

53

@satya164

Our libraries have a lot of files. Code, configuration, documentation, tests and many more. But we don’t want to publish all the files to keep the install size minimal. For a React Native app, we should publish, the source code for metro, a build with JavaScript modules and a common js build for using on the web, SSR etc.

If we’re using a type checker, we should also publish the type definitions.

"main": "lib/index.js",

"module": "esm/index.js",

"react-native": "src/index.js",

"types": "typings/index.d.ts",

54

@satya164

When publishing different builds, we can add various keys in package.json to let the bundlers know which one to pick.

Use the files field

"files": [

"lib/",

"esm/",

"src/",

"types/"

]

55

@satya164

There are 2 ways to tell npm what to publish, npmignore field where we list the files we don’t want to publish, and the files field to list the files we want to publish. I personally prefer to use the files key to keep things explicit.

@react-native-community/bob

bob build

Build files for all of the targets

bob init

Configure the package to use bob automatically when publishing to npm

56

@satya164

So as you can see, it’s lot of things to keep track of. But you can use this CLI called bob to simplify this process.

57

@satya164

If you have a React Native library, all you need to do is install @react-native-community/bob, and run yarn bob init to configure your project.

Automate publish with release-it

github.com/release-it/release-it

  • Generates a changelog
  • Tags and creates release on GitHub (or Gitlab)
  • Increments version according to semver
  • Publishes to npm

58

@satya164

Publishing can be a lot of work. We have to create a tag, create a release on GitHub, publish a changelog and finally publish the package to npm.

I use a tool called release-it which automates all of these.

59

@satya164

You can even use it on the CI to publish a new version automatically on every commit to master. There is another tool called semantic release which is very similar.

One last thing…

60

@satya164

Before I finish the talk, I just wanted to say, maintaining a component library can be exhausting. Keep it easy to maintain.

Keep it easy to maintain

It’s easy to add a feature, but hard to remove it

Sometimes, it’s okay to write custom components, not everything needs to be in the library.

61

@satya164

Remember, it’s easy to add a new feature, but hard to remove it. Don’t put everything in your component library. It’s okay to write custom components for specific use cases.

Thank you

62

@satya164

Building Component Libraries - Google Slides