Themed React Native app with redux, and styled-components


In this article, I will show how to move from a design to a fully customize-able theme. We will use the three main libraries described below and any additional as the need arises. Luckily, many of us are already familiar with redux, react-native and styled-components.

Design

First, we need to see a design so as to understand what styles we need to apply. If you work with designers or are provided a UI kit, then you are in luck. If not, then it would be important to take a step back and really think about what styles your design is made up of. Especially those that are not explicit.
We will use the design below to create our theme.

From the design above, there are a few things that we can deduce. Having an understanding of how react native styles work, there are a few styles that we can list in our theme.

FontSize, FontFamily, BackgroundColor, Color

There are many aspects of the style, but I will limit myself to just those mentioned above for this example. Using the basic react native stylesheet, we can make the user interface. But first, we will make a simple react-native project:

react-native init themeable
cd themeable 
npm install --save redux react-redux redux-thunk styled-components
react-native run-android
npm start
our app is now ready

We can now add a login screen on the same level as our App.js file.

touch Login.js
import * as React from "react";
import {
  Text,
  View,
  StyleSheet,
  TouchableOpacity,
  Image,
  TextInput
} from "react-native";

export default class Login extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        {/* the header*/}
        <View style={styles.header}>
          <Text style={styles.headerText}>Login</Text>
        </View>
        {/* the body*/}
        <View style={styles.body}>
          <View style={styles.segment}>
            <Image
              style={styles.icon}
              source={{
                uri: "https://img.icons8.com/dusk/50/000000/lock-2.png"
              }}
            />
          </View>
          <View style={styles.segment}>
            <Text style={styles.title}>Login</Text>
            <Text style={styles.description}>
              Please enter your username and password to proceed
            </Text>
          </View>
          <View style={styles.segment}>
            <View style={styles.textInputContainer}>
              <TextInput style={styles.textInput}>Username</TextInput>
            </View>
            <View style={styles.textInputContainer}>
              <TextInput style={styles.textInput}>Password</TextInput>
            </View>
          </View>
        </View>
        {/* the footer*/}
        <View style={styles.footer}>
          <TouchableOpacity style={styles.button}>
            <Text style={styles.buttonText}>Login</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: "column",
    justifyContent: "space-between",
    backgroundColor: "white"
  },
  header: {
    padding: 10,
    backgroundColor: "blue"
  },
  headerText: {
    fontSize: 24,
    color: "white",
    fontFamily: "AvertaDemo-Regular"
  },
  body: {
    flexDirection: "column",
    justifyContent: "space-between",
    alignItems: "stretch",
    backgroundColor: "white",
    paddingTop: 30,
    padding: 20
  },
  segment: {
    paddingTop: 20,
    paddingBottom: 20,
    flexDirection: "column",
    justifyContent: "space-between",
    alignItems: "stretch"
  },
  icon: {
    height: 60,
    width: 60
  },

  title: {
    color: "#3d3d3d",
    fontSize: 30,
    fontFamily: "AvertaDemo-Regular"
  },
  description: {
    color: "#3d3d3d",
    fontSize: 18,
    fontFamily: "AvertaDemo-Regular"
  },
  textInputContainer: {
    borderBottomWidth: 1,
    borderBottomColor: "#e0e0e0"
  },
  textInput: {
    color: "#3d3d3d",
    fontSize: 24,
    paddingTop: 20,
    fontFamily: "AvertaDemo-Regular"
  },
  footer: {
    padding: 20,
    flexDirection: "column",
    justifyContent: "center",
    alignItems: "stretch"
  },
  button: {
    padding: 10,
    backgroundColor: "blue",
    flexDirection: "column",
    justifyContent: "center",
    alignItems: "center",
    elevation: 1,
    borderRadius: 2
  },
  buttonText: {
    fontSize: 18,
    color: "white",
    fontFamily: "Product-Sans-Regular"
  }
});

And we will override our App.js to show the login page.

import React, { Component } from "react";
import Login from "./Login";


export default class App extends Component {
  render() {
    return <Login />;
  }
}
Our Page is close enough

Redux

Now that the View looks fine, we can connect it with a basic redux store. The redux store is where we will create our theme and serve the app.
To set up our store, we will create a folder called store and add two files: our themeReducer a theme file.

Why Redux anyway? Redux will provide us the ability to customize the theme as we desire. This means we don’t have to hard-code the styles. Redux can also change the theme in run-time and will cause all affected components to re-render making style changes immediate.


export const base = {
  FONT_SIZE_TINY: 8,
  FONT_SIZE_SMALL: 12,
  FONT_SIZE_MEDIUM: 14,
  FONT_SIZE_LARGE: 18,
  FONT_SIZE_EXTRA_LARGE: 24,
  FONT_SIZE_MASSIVE: 34,

  FONT_WEIGHT_LIGHT: "200",
  FONT_WEIGHT_MEDIUM: "500",
  FONT_WEIGHT_BOLD: "700",

  PRIMARY_FONT_FAMILY: "AvertaDemo-Regular",
  PRIMARY_FONT_FAMILY_BOLD: "AvertaDemo-ExtraBoldItalic",

  SECONDARY_FONT_FAMILY: "Product-Sans-Regular",
  SECONDARY_FONT_FAMILY_ITALIC: "Product-Sans-Italic"
};

export const darkTheme = {
  PRIMARY_BACKGROUND_COLOR: "#3d3d3d",
  PRIMARY_BACKGROUND_COLOR_LIGHT: "#797979",

  SECONDARY_BACKGROUND_COLOR: "#ffffff",
  SECONDARY_BACKGROUND_COLOR_LIGHT: "#f7f7f7",

  PRIMARY_TEXT_COLOR: "#ffffff",
  PRIMARY_TEXT_COLOR_LIGHT: "#f7f7f7",
  SECONDARY_TEXT_COLOR: "#3d3d3d",
  PRIMARY_TEXT_BACKGROUND_COLOR: "#3d3d3d",
  SECONDARY_TEXT_BACKGROUND_COLOR: "#ffffff"
};
export const lightTheme = {
  PRIMARY_BACKGROUND_COLOR: "#ffffff",
  PRIMARY_BACKGROUND_COLOR_LIGHT: "#f7f7f7",

  SECONDARY_BACKGROUND_COLOR: "#3d3d3d",
  SECONDARY_BACKGROUND_COLOR_LIGHT: "#797979",

  PRIMARY_TEXT_COLOR: "#3d3d3d",
  PRIMARY_TEXT_COLOR_LIGHT: "#797979",
  SECONDARY_TEXT_COLOR: "#ffffff",
  PRIMARY_TEXT_BACKGROUND_COLOR: "#ffffff",
  SECONDARY_TEXT_BACKGROUND_COLOR: "#3d3d3d"
};

export const colorOptions = {
  orange: {
    PRIMARY_COLOR_FAINT: "#FFF3E0",
    PRIMARY_COLOR_LIGHT: "#FFB74D",
    PRIMARY_COLOR: "#FF9800",
    PRIMARY_COLOR_BOLD: "#EF6C00",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  red: {
    PRIMARY_COLOR_FAINT: "#FFEBEE",
    PRIMARY_COLOR_LIGHT: "#E57373",
    PRIMARY_COLOR: "#F44336",
    PRIMARY_COLOR_BOLD: "#C62828",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  blue: {
    PRIMARY_COLOR_FAINT: "#E3F2FD",
    PRIMARY_COLOR_LIGHT: "#64B5F6",
    PRIMARY_COLOR: "#2196F3",
    PRIMARY_COLOR_BOLD: "#1565C0",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  cyan: {
    PRIMARY_COLOR_FAINT: "#E0F7FA",
    PRIMARY_COLOR_LIGHT: "#4DD0E1",
    PRIMARY_COLOR: "#00BCD4",
    PRIMARY_COLOR_BOLD: "#00838F",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  teal: {
    PRIMARY_COLOR_FAINT: "#E0F2F1",
    PRIMARY_COLOR_LIGHT: "#4DB6AC",
    PRIMARY_COLOR: "#009688",
    PRIMARY_COLOR_BOLD: "#00695C",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  gray: {
    PRIMARY_COLOR_FAINT: "#FAFAFA",
    PRIMARY_COLOR_LIGHT: "#E0E0E0",
    PRIMARY_COLOR: "#9E9E9E",
    PRIMARY_COLOR_BOLD: "#424242",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  purlple: {
    PRIMARY_COLOR_FAINT: "#EDE7F6",
    PRIMARY_COLOR_LIGHT: "#9575CD",
    PRIMARY_COLOR: "#673AB7",
    PRIMARY_COLOR_BOLD: "#4527A0",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  },
  green: {
    PRIMARY_COLOR_FAINT: "#E8F5E9",
    PRIMARY_COLOR_LIGHT: "#81C784",
    PRIMARY_COLOR: "#4CAF50",
    PRIMARY_COLOR_BOLD: "#2E7D32",
    PRIMARY_FOREGROUND_COLOR: "#ffffff"
  }
};
import { base, darkTheme, lightTheme, colorOptions } from "./theme";

const initialState = {
  theme: { ...base, ...lightTheme, ...colorOptions.blue }
};

const themeReducer = (state = initialState, action) => {
  switch (action.type) {
    case "ACTION_TYPE":
      return;
    default:
      return state;
  }
};

export default themeReducer;

We also need to connect our app to the store. Add this code to App.js

import React, { Component } from "react";
import Login from "./Login";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk"

import themeReducer from "./store/themeReducer";
const store = createStore(combineReducers({themeReducer}), applyMiddleware(thunk));

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Login />
      </Provider>
    );
  }
}

As you can see, the theme is provided in the initial state of the reducer by combining all theme objects into one. I know the theme.js looks complicated, but it is simple. Here is an illustration.

At the bottom, we have the base, the styles that can never change such as FontSize, FontFamily.

Then we have our themes. Primarily Dark and light. They mostly affect BackgroundColor and Color.

Finally, we have our color options. They affects the colors as well, but for specific elements such as buttons and headers.


Styled Components
Now that the View can access the theme as a prop. Let us make use of the theme with styled components. I will convert the user interfaces into styled components and substitute the fixed values for the theme props so that they are changeable.


import * as React from "react";

import { connect } from "react-redux";
import styled, { ThemeProvider } from "styled-components";

const Container = styled.View`
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  background-color: ${props => props.theme.PRIMARY_BACKGROUND_COLOR};
`;

const Header = styled.View`
  padding-top: 10;
  padding-bottom: 10;
  padding-left: 10;
  padding-right: 10;
  background-color: ${props => props.theme.PRIMARY_COLOR};
`;

const HeaderText = styled.Text`
  font-size: 24;
  color: ${props => props.theme.PRIMARY_FOREGROUND_COLOR};
  font-family: ${props => props.theme.PRIMARY_FONT_FAMILY};
`;

const Body = styled.View`
  flex-direction: column;
  justify-content: space-between;
  align-items: stretch;
  background-color: ${props => props.theme.PRIMARY_BACKGROUND_COLOR};
  padding-top: 30;
  padding-bottom: 30;
  padding-left: 30;
  padding-right: 30;
`;

const Segment = styled.View`
  padding-top: 10;
  padding-bottom: 10;
  flex-direction: column;
  justify-content: space-between;
  align-items: stretch;
`;

const Icon = styled.Image`
  height: 60;
  width: 60;
`;
const Title = styled.Text`
  color: ${props => props.theme.PRIMARY_TEXT_COLOR};
  font-size: ${props => props.theme.FONT_SIZE_MASSIVE};
  font-family: ${props => props.theme.PRIMARY_FONT_FAMILY};
`;

const Description = styled.Text`
  color: ${props => props.theme.PRIMARY_TEXT_COLOR};
  font-size: ${props => props.theme.FONT_SIZE_MEDIUM};
  font-family: ${props => props.theme.PRIMARY_FONT_FAMILY};
  padding-top: 20;
`;

const TextInputContainer = styled.View`
  border-bottom-width: 1;
  border-bottom-color: #e0e0e0;
`;

const TextInput = styled.TextInput`
  color: ${props => props.theme.PRIMARY_TEXT_COLOR};
  font-size: ${props => props.theme.FONT_SIZE_MEDIUM};
  font-family: ${props => props.theme.PRIMARY_FONT_FAMILY};
  padding-top: 20;
`;

const Footer = styled.View`
  padding-top: 20;
  padding-bottom: 20;
  padding-left: 20;
  padding-right: 20;
  flex-direction: column;
  justify-content: center;
  align-items: stretch;
  background-color: ${props => props.theme.PRIMARY_BACKGROUND_COLOR};
`;

const Button = styled.TouchableOpacity`
  padding-top: 10;
  padding-bottom: 10;
  padding-left: 10;
  padding-right: 10;
  flex-direction: column;
  justify-content: center;
  align-items: stretch;
  elevation: 1
  border-radius: 2;
  
  background-color:${props => props.theme.PRIMARY_COLOR};
`;

const ButtonText = styled.Text`
  text-align: center;
  color: ${props => props.theme.PRIMARY_FOREGROUND_COLOR};
  font-family: ${props => props.theme.PRIMARY_FONT_FAMILY};
  font-size: ${props => props.theme.FONT_SIZE_LARGE};
`;

class Login extends React.Component {
  render() {
    return (
      <ThemeProvider theme={this.props.theme}>
        <Container>
          <Header>
            <HeaderText>Login</HeaderText>
          </Header>
          <Body>
            <Segment>
              <Icon
                source={{
                  uri: "https://img.icons8.com/dusk/50/000000/lock-2.png"
                }}
              />
            </Segment>

            <Segment>
              <Title>Login</Title>
              <Description>
                Please enter your username and password to continue
              </Description>
            </Segment>

            <Segment>
              <TextInputContainer>
                <TextInput>Username</TextInput>
              </TextInputContainer>
              <TextInputContainer>
                <TextInput>Password</TextInput>
              </TextInputContainer>
            </Segment>
          </Body>

          <Footer>
            <Button>
              <ButtonText>Login</ButtonText>
            </Button>
          </Footer>
        </Container>
      </ThemeProvider>
    );
  }
}

const mapStateToProps = state => ({
  theme: state.themeReducer.theme
});

export default connect(mapStateToProps)(Login);

We have converted each of the styles from the styles and created a styled component out from it.

The same app but styled

To make changes in our theme, we can dispatch actions to redux that cause this change. To keep this article short, I will directly edit the initial state in our reducer:

// light-blue
const initialState = {theme: { ...base, ...lightTheme, ...colorOptions.blue }};
// light-orange
const initialState = {theme: { ...base, ...lightTheme, ...colorOptions.orange }};
// dark-blue
const initialState = {theme: { ...base, ...darkTheme, ...colorOptions.blue }};
//dark-orange
const initialState = {theme: { ...base, ...darkTheme, ...colorOptions.oranger }};

Thank you for reading through this mess. Please give your suggestions on how to do this better.

The repository can be found here: Themeable react native (Part 2) — Changing themes

Here are some more useful resources:

To persist the theme even after a user closes the app:

Some more RN libraries

Good stuff? Consider sharing it!
Default image
Evans Munene
I pour milk before my cereal!