Codementor Events

Themed components made easy with React Native + Recompose

Published Jul 27, 2018Last updated Jan 23, 2019
Themed components made easy with React Native + Recompose

I’m building a React Native application in my free time. I’m writing this to show how happy I am with the structure I created to theme components.

This is a personal project, so I have no designers. It’s all by myself. So colors always give me headaches. It’s the slowest part of the development.

Also, without proper organization, a React Native application can turn into a chaos. So themes come in handy.

Choosing colors

To ease the pain I use this site to generate colors.

It’s wonderful how beautiful colors pop out from a spacebar hit.

For this tutorial I choose two distinct themes. Each one with text, secondary text, light text, background and primary colors.

It is up to you how many colors to use. For my project, that’s what I am using. For sure, as the app grows, I’ll be back to Coolors to select more colors for my palette.

Before we start

In this tutorial, we will be using React 16.3, React context API and Recompose. We will also create some HOCs ourselves for reuse purposes.

If you don’t know what Recompose is, read the Building HOCs with Recompose post on Medium that explains what it is and how to use it.

If you are not familiar with React Context feature, read this first.

Another important information is regarding the code structure. All my components use recompose for something. So a hypothetical MyComponent component directory structure would be:

┌─ MyComponent/
├─── MyComponent.js 
└─── index.js

MyComponent.js file is the component itself (a functional component). The index.js file is where recompose applies all its HOCs and exports the enhanced component.

Setting up the theme

We need to set a place to hold our themes. In my project I have a file called themes.js. It holds my themes and also exports my React Context Provider and Consumer.

import React from 'react';

const themes = {
  beans: {
    text: '#191923',
    secondaryText: '#818187',
    lightText: '#FBFEF9',
    bg: '#FBFEF9',
    primary: '#FF206E',
  },
  chaid: {
    text: '#222823',
    secondaryText: '#A7A2A9',
    lightText: '#F4F7F5',
    bg: '#F4F7F5',
    primary: '#2274A5',
  },
};

const { Provider, Consumer } = React.createContext(themes.chaid);

export {
  Provider,
  Consumer,
  themes,
};

Notice that I am setting the whole theme object as the context value. This will ease things in the future.

Wrapping with our Provider

Now we will create a component that wraps any children passed with the Provider obtained from the theme.js file.

// AppContainer/AppContainer.js
// @flow
import React from 'react';
import { Provider as ThemeProvider, themes } from '../../theme';

type Props = {
  theme: string,
  children: any
};

const AppContainer = ({
  theme,
  children,
}: Props) => (
  <ThemeProvider value={themes[theme] || themes.beans}>
    {children}
  </ThemeProvider>
);

export default AppContainer;
// AppContainer/index.js
// @flow
import { compose, withState } from 'recompose';
import AppContainer from './AppContainer';

const EnhancedAppContainer = compose(
  withState('theme', 'setTheme', 'chaid'),
)(AppContainer);

export default EnhancedAppContainer;

Notice that I am not using the setTheme from recompose. I don’t have an use case for that yet. I set a default theme in the index.js file and use it. But it is not hard to create a handler in the application to switch themes. Since the focus of this tutorial is to show how to create themed components, I will not cover this.

Also, this approach allow us to test themes just changing the default theme on this component.

Using the container

Our container will wrap our whole application.

// App.js
import React from 'react';
import { NativeRouter } from 'react-router-native';
import { Scenes } from '../../scenes';
import AppContainer from '../AppContainer';

class App extends React.Component {
  render() {
    return (
      <AppContainer>
        <NativeRouter>
          <Scenes />
        </NativeRouter>
      </AppContainer>
    );
  }
}

export default App;

This is the root component that React Native registers (in my case Expo). Another file is taking care of this.

Now the interesting part.

Setting up the required HOCs

As the time of writing, recompose was in version 0.27.1. In the source code there is a fromRenderProps file. It is also documented. But this HOC have not been released yet. So I borrowed the fromRenderProps.js file and created a HOC in my source in the src/hocs directory.

I am not reinventing the wheel here. I am supporting by myself something Recompose will support in the future. Once Recompose supports it, I can remove this HOC.

This HOC extracts the render props from a component and map it as normal props. React context consumer receives the context value via render props. This HOC will be useful to create another HOC. I called it withTheme.js and stored it in the same src/hocs directory. Here is what it does:

import { compose, defaultProps, setDisplayName } from 'recompose';
import fromRenderProps from './fromRenderProps';
import { Consumer as ThemeConsumer } from '../theme';

export default compose(
  setDisplayName('withTheme'),
  defaultProps({
    theme: {},
  }),
  fromRenderProps(ThemeConsumer, theme => ({ theme })),
);

It just maps the theme render prop from the consumer to normal props.

Using our new HOC

The first good example is the View. I have two versions: a themed and an unthemed. The first one is good to be used as containers where the background color of your theme should be used. The other one can be used in layouts.

// View/View.js
// @flow
import React from 'react';
import { View as RNView } from 'react-native';

type ViewProps = {
  children: React.ReactNode,
};

const View = ({
  children,
  ...props
}: ViewProps) => (
  <RNView {...props}>
    {children}
  </RNView>
);

export default View;
// View/ThemedView.js
// @flow
import React from 'react';
import { View as RNView, StyleSheet } from 'react-native';

type Props = {
  children: React.ReactNode,
  style: ?Object,
};

const getStyles = props => StyleSheet.create({
  view: {
    backgroundColor: props.backgroundColor,
  },
});

const ThemedView = ({
  children,
  style,
  ...props
}: Props) => {
  const styles = getStyles(props);
  return (
    <RNView
      style={[styles.view, style]}
      {...props}
    >
      {children}
    </RNView>
  );
};

export default ThemedView;
// View/index.js
import { compose, withProps, branch, renderComponent } from 'recompose';
import View from './View';
import ThemedView from './ThemedView';
import { withTheme } from '../../../hocs';

const EnhancedView = compose(
  withTheme,
  branch(
    props => props.themed,
    compose(
      withProps(props => ({
        backgroundColor: props.theme.bg,
      })),
      renderComponent(ThemedView),
    ),
  ),
)(View);

export default EnhancedView;

In the index.js file we:

  1. use our withTheme HOC as first in order;
  2. (optional) define some props with nice names to use inside our components;
  3. (optional) define a branch to render a ThemedView if a themed prop exists. The default else is the wrapped component, as per recompose documentation.

If you want to skip those two optional steps, you can just use the theme prop inside your component.

// View/ThemedView.js
// @flow
import React from 'react';
import { View as RNView } from 'react-native';

type Props = {
  children: React.ReactNode,
  theme: Object,
  style: ?Object,
};

const ThemedView = ({
  children,
  style,
  theme,
  ...props
}: Props) => (
  <RNView
    style={[{ backgroundColor: theme.bg }, style]}
    {...props}
  >
    {children}
  </RNView>
);

export default ThemedView;

Using our themed component

To test it out I defined a theme with two basic colors:

const themes = {
  chaid: {
    text: 'white',
    bg: 'steelblue',
  },
};

// ...

Now the tests.

Consider as RNView and RNText the react-native package components. View and Text are from my source code.

  1. Default React Native Text and View
<RNView style={styles.container}>
  <RNText>Hey Hey!</RNText>
</RNView>

a1.png

  1. Default React Native Text and our custom View unthemed
<View style={styles.container}>
  <RNText>Hey Hey!</RNText>
</View>

a2.png
The same result as above. It renders a simple react-native View.

  1. Our custom themed View and a custom themed Text.
<View themed style={styles.container}>
  <Text>Hey Hey!</Text>
</View>

a3.png
Much better.

The custom themed Text component is defined using the same techniques described in the above sections.


Wrapping up

There are some packages for theming for React Native on npm. react-native-theme, for example.

I did not use any of those because I thought I could learn a lot by creating my own theme architecture and my own components. And yup! It worked. I learned a lot from this.

This is not only applicable for React Native. By some small refactoring you’ll be able to run it with React on the web too.

If you have any suggestion, found a bug in the text, or just want to share your thoughts, please leave a comment. I’ll be pleased to answer.

Thank you for reading! 😃

Originally published on Medium and adapted for Codementor.

Discover and read more posts from Andrey Luiz
get started