Skip to content
Colin Wren
Twitter

React Native Component Library — Making things theme-able

JavaScript, Software Development2 min read

As part of the React Native component library I’ve been building to make maintaining a legacy React Native app easier, I’ve been refactoring the way that styles were handled by the components.

The original complex components had a stylesheet to cover all the smaller components it contained, and when I broke these smaller components out into the component library I simply copied this stylesheet over, and removed any un-used styles by that component.

1import React from 'react';
2import { StyleSheet, View } from 'react-native';
3
4const styles = StyleSheet.create({
5 foo: {
6 height: 1,
7 backgroundColor: '#ff0000',
8 },
9});
10
11const Foo = () => <View style={styles.foo} />;
12
13export default Foo;
An example component with separate stylesheet

This approach led to a stylesheet for each component, and as the original stylesheet never re-used any of the styles it declared there was a lot of duplicated styles.

The team I’m working with expressed that the original styling implementation was a little awkward due to the number of different stylesheets, and they’d have preferred something similar to the CSS stylesheets they were used to working with.

With the need to refactor the implementation I decided a better approach would be to extract all the styles in the code base into one Object and that Object would use a hierarchy that followed the hierarchy in the component library.

With this approach a component located at src/inputs/pokemonPicker/Cell that needed a style object called background would use the inputs.pokemonPicker.Cell.background property from the theme object.

This would then allow me to remove duplicate styles by extracting common values into other style definitions within the theme and updating the components to use those values.

Accessing the theme within the components

One of the main challenges of a moving all the components styles into one object is how to make that object accessible to the components without having to pass that prop to each component and then down to its children.

This is made really easy with the React Context Hook as it allows you to wrap your component in a Higher Order Component (HOC) that will grab the theme from the context and pass it to the wrapped component as a prop.

1import React, { useContext } from 'react';
2
3const ThemeContext = React.createContext();
4
5export const ThemeContextProvider = ({ children, theme }) => (
6 <ThemeContext.Provider value={{ theme }}>
7 {children}
8 </ThemeContext.Provider>
9);
10
11export function withTheme(Component) {
12 return (props) => {
13 const { theme } = useContext(ThemeContext);
14 return <Component {...props} theme={theme} />;
15 };
16}
Context Providing component and withTheme HOC

You then add the context providing component to the top of your component hierarchy and set the theme property that the theme HOC will pass to the component it’s wrapping.

Once you’ve set up the context you then need to add the theme property to the components propTypes definition.

You can set the theme property as required or if you’d like to provide a default value (so you’re not dependent on passing the theme for things like testing) you can set the theme in defaultProps .

One issue I encountered with the defaultProps approach however was in a HOC I had in the library for providing common theming around form inputs, it never received the default theme property so I had to instead set a default value for the theme when I de-structured the props object within the component.

1import React from 'react';
2import { View } from 'react-native';
3import PropTypes from 'prop-types';
4import defaultTheme from '../themes/default';
5
6// Setting default theme via destructuring of props
7const Divider = ({ theme = defaultTheme }) => <View style={theme.Foo} />;
8
9Divider.propTypes = {
10 theme: PropTypes.shape({
11 Foo: PropTypes.shape(),
12 }),
13};
14
15// Setting default theme via defaultProps
16Divider.defaultProps = {
17 theme: defaultTheme,
18};
19
20export default Divider;
Providing a default value to props de-structure or using defaultProps will allow a default theme to be used when the component isn’t wrapped by the withTheme HOC

The benefits of a theme

The biggest benefit of moving from stylesheets to a theme object has been that the code for each component is a lot cleaner now and as the theme object follows the same hierarchy as the component library it’s easier to see the logic behind what the styles represent.

The second is the ability to change the theme.

dark theme picker
The dark mode override object made only 8 changes but the styles changes are drastic

In order to show how easy having a theme allowed for broad stylistic changes to the app, I created an override object that would apply a ‘dark mode’ to the existing theme and used deepmerge to create a new theme.

1import React from 'react';
2import deepmerge from "deepmerge";
3import { ThemeContextProvider } from '../../src';
4import { pokemonPicker } from './common';
5import defaultTheme from "../../src/themes/default";
6
7const darkThemeOverrides = {
8 inputs: {
9 pokemonPicker: {
10 Cell: {
11 rowCell: {
12 backgroundColor: '#494949',
13 },
14 pokemonId: {
15 color: '#ffffff',
16 },
17 },
18 Input: {
19 searchView: {
20 backgroundColor: '#2d2a2b'
21 },
22 selectionColor: {
23 color: '#ffffff',
24 },
25 placeholderTextColor: {
26 color: '#ffffff',
27 },
28 },
29 },
30 },
31 modals: {
32 Modal: {
33 overlay: {
34 backgroundColor: 'rgba(0, 0, 0, 0.8)',
35 },
36 closeImage: {
37 tintColor: '#494949'
38 },
39 modal: {
40 backgroundColor: '#2d2a2b',
41 },
42 },
43 }
44};
45
46const darkTheme = deepmerge(defaultTheme, darkThemeOverrides);
47
48storiesOf('Themed Components', module)
49 .add('Light Theme', () => (
50 <ThemeContextProvider theme={defaultTheme}>
51 {pokemonPicker}
52 </ThemeContextProvider>
53 ))
54 .add('Dark Theme', () => (
55 <ThemeContextProvider theme={darkTheme}>
56 {pokemonPicker}
57 </ThemeContextProvider>
58 ))
Creating a dark mode theme using deepmerge

As the theme is just a JavaScript object this also opens up opportunities within the app to download different themes from a web server and present seasonal or event based style changes to the app.