Global Theming in React Native

Using Styled-Components & Redux Toolkit — Typescript

Clayton Francis
Nov 26, 2020 · 7 min read
Image for post
Image for post
Photo by Walling on Unsplash

Let’s face it, these days, an app without dark mode belongs in a museum; somewhere between the Dinosaurs and the wheel. As consumers, we want to see cool things, as developers we want to build cool things, and dark mode is cool! Luckily for us, there are a plethora of libraries to help us out. In this article, we’re going to implement a light/dark mode feature which will change depending on your devices theme settings. We will build it with Styled-Components and Redux Toolkit, in a bare-bones React Native App.

Styled-components

Image for post
Image for post
Photo by Juan Ordonez on Unsplash

I think their homepage summarises what makes styled-components special:

“Use the best bits of ES6 and CSS to style your apps without stress 💅🏾” https://styled-components.com

Like RonSeal, it does exactly what it says on the tin, it styles reusable components! And with styled-components, global theming is easy.

Redux Toolkit

Image for post
Image for post
Photo by Aarón Blanco Tejedor on Unsplash

If like me, you hate boilerplate, avoid Redux like the plague, and was scared to take the red pill; worry no more, RTK is the answer to state management headache. It lets you go down the rabbit hole in blissful ignorance. My favourite thing about RTK is that it uses immer (link) under the hood. If you would like to know more, here is a fantastic article on RTK by Ohans Emmanuel: https://www.ohansemmanuel.com/what-is-redux-toolkit/.

What we’re building

We’re going to build the project from scratch, but if you’d like to follow along, you can clone my repo here.

Image for post
Image for post

Let’s get going!

We need to initialise our project and install all the packages that we’re going to use, so from the terminal, type:

npx react-native init globalThemeExample  --template react-native-template-typescript && cd globalThemeExample && yarn add styled-components react-native-appearance @reduxjs/toolkit react-redux reselect && yarn add -D @types/react-redux @types/styled-components && npx react-native run-ios

This will create a new bare-bones React Native app with a Typescript template, navigate to your new project folder, add your dependencies and dev dependencies, then build the app on your iOS simulator.

1. Global Theme

As we’re making a ‘global’ theme, we need global settings. In your project folder, create a new folder called constants, then, in your new folder, create a file called Theme.ts and add the following code:

constants/theme.ts

export const dark = {
background: '#121212',
text: '#F5AD93',
};
export const light = {
background: '#ECF0F3',
text: '#A40000',
};
type Theme = {
background: typeof dark.background | typeof light.background;
text: typeof dark.text | typeof light.text;
};
export interface CustomThemeProps {
theme?: Theme;
}

If your screen was a canvas, this file would be all the colours on your palette board, and will be accessible by all styled-components (once we’re done). You can extend this file as much as you like, but for this example, we only need a background and a text colour. For our text, we’ll go with a nice Dark Candy Apple Red. Notice that the text colour for dark mode has a different Hex code; this is a lighter shade of red (desaturated) and makes it more legible — easier to read.

We’ve gone with dark grey for our background colour as you should never use black for dark mode. If you’d like more info on dark theme design, check out Material UI https://material.io/design/color/dark-theme.html. Also, Eva Design system is a great resource for finding semantic colours, so is worth a mention too — I used it to get the desaturated text colour for our dark mode: https://colors.eva.design/?utm_campaign=eva_colors — home — eva_design website&utm_source=eva_design&utm_medium=referral&utm_content=eva_website_menu.

Now we’ve defined our theme, let’s use it. Create a folder and call it components, create a file inside components and call this one ThemeManager.tsx, then add the following:

components/ThemeManager.tsx

import React from 'react';
import {StatusBar} from 'react-native';
import styled, {ThemeProvider} from 'styled-components/native';
import {CustomThemeProps, light} from '../../constants/theme';const StyledThemeContainer = styled.KeyboardAvoidingView<CustomThemeProps>`
flex: 1;
align-items: center;
justify-content: center;
background: ${(props) => props.theme.background};
`;
export const ThemeManager = ({children}: {
children: React.reactNode
}) => {
return (
<ThemeProvider theme={light}>
<StatusBar barStyle={'light-content'} />
<StyledThemeContainer>{children}</StyledThemeContainer>
</ThemeProvider>
);
};

ThemeProvider is the component that supplies our theme settings to all child components. As we’re providing the light theme, all child components will render colours from the light theme.

To consume the theme across our entire app we need to add our new ThemeManager component into our App.tsx file:

App.tsx

import React from 'react';
import {SafeAreaView, Text} from 'react-native';
import {ThemeManager} from './components/ThemeManager';
const App = () => {
return (
<ThemeManager>
<SafeAreaView>
<Text>Hello world</Text>
</SafeAreaView>
</ThemeManager>
);
};
export default App;

2. Redux Toolkit

In your project folder, create a folder called ‘state’ and add the following two files:

state/themeMode.slice.ts

import {Appearance} from 'react-native';
import {createSlice} from '@reduxjs/toolkit';
export enum ThemeModeEnum {
LIGHT = 'light',
DARK = 'dark',
}
export const defaultMode = Appearance.getColorScheme() || ThemeModeEnum.LIGHT;const themeModeSlice = createSlice({
name: 'themeMode',
initialState: {
themeMode: defaultMode as ThemeModeEnum,
},
reducers: {
setThemeMode: (state, action: {payload: ThemeModeEnum}) => {
state.themeMode = action.payload;
},
},
});
export const {setThemeMode} = themeModeSlice.actions;export default themeModeSlice.reducer;

We use ‘Appearance’ to get the devices current theme settings and will need this later on when we edit our Theme Manager file.

state/index.ts

import {configureStore} from '@reduxjs/toolkit';
import themeModeReducer from './themeMode.slice';
export const store = configureStore({
reducer: {
themeMode: themeModeReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

This is where we add our reducers to our store, in our case, just themeModeReducer. To change the state of our theme, we need to know what we’re changing it from (current state). For this we will use createSelector from reselect:

selectors/getThemeMode.ts

import {createSelector} from 'reselect';
import {RootState} from '../state';
export const getThemeMode = createSelector(
(state: RootState) => state,
(state) => state.themeMode,
);

Now we can retrieve the current state of our theme, but we also need to update state. For that, we will make our own custom dispatch hook:

Utils/useAppDispatch.ts

import {useDispatch} from 'react-redux';
import {AppDispatch} from '../state';
export const useAppDispatch = () => useDispatch<AppDispatch>();

Finally, we need to add our reducer to the App file:

App.tsx

import React from 'react';
import {SafeAreaView, Text} from 'react-native';
import {Provider} from 'react-redux';
import {ThemeManager} from './components/ThemeManager';
import {store} from './state';
const App = () => (
<Provider store={store}>
<ThemeManager>
<SafeAreaView>
<Text>Hello world</Text>
</SafeAreaView>
</ThemeManager>
</Provider>
);
export default App;

3. Theme Mode Switch

So that our switch component can control the theme, we need to go back to our Theme Manager file and change it to this:

components/ThemeManager.tsx

import React from 'react';
import {StatusBar} from 'react-native';
import styled, {ThemeProvider} from 'styled-components/native';
import {useSelector} from 'react-redux';
import {CustomThemeProps, light, dark} from '../../constants/theme';
import {getThemeMode} from '../../selectors/getThemeMode';
import {ThemeModeEnum} from '../../state/themeMode.slice';
const StyledThemeContainer = styled.KeyboardAvoidingView<CustomThemeProps>`
flex: 1;
align-items: center;
justify-content: center;
background: ${(props) => props.theme.background};
`;
const {DARK, LIGHT} = ThemeModeEnum;export const ThemeManager = ({children}: {children: React.ReactNode}) => {const {themeMode} = useSelector(getThemeMode);const providedTheme = () => {
if (themeMode === DARK) {
return dark;
}
if (themeMode === LIGHT) {
return light;
}
};
return (
<ThemeProvider theme={providedTheme}>
<StatusBar
barStyle={themeMode === DARK ? 'light-content' : 'dark-content'}
/>
<StyledThemeContainer>{children}</StyledThemeContainer>
</ThemeProvider>
);

Now it’s time to build our switch:

components/ThemeModeSwitch.tsx

import React from 'react';
import {Switch} from 'react-native';
import {useSelector} from 'react-redux';
import {getThemeMode} from '../../selectors/getThemeMode';
import {useAppDispatch} from '../../utils/useAppDispatch';
import {ThemeModeEnum, setThemeMode} from '../../state/themeMode.slice';
import {
StyledSwitchWrapper,
StyledThemeContainer,
StyledToggleText,
} from './ThemeModeSwitch.styles';
const {LIGHT, DARK} = ThemeModeEnum;export const ThemeModeSwitch = () => {
const {themeMode} = useSelector(getThemeMode);
const dispatch = useAppDispatch();
return (
<StyledThemeContainer>
<StyledSwitchWrapper>
<StyledToggleText>Dark mode</StyledToggleText>
<Switch
value={themeMode === DARK}
onValueChange={(value) => {
dispatch(setThemeMode(value ? DARK : LIGHT));
}}
/>
</StyledSwitchWrapper>
</StyledThemeContainer>
);
};

components/ThemeModeSwitch.styles.ts

import styled from 'styled-components/native';
import {CustomThemeProps} from '../../constants/theme';
export const StyledThemeContainer = styled.View`
flex: 1;
flex-direction: row;
align-items: center;
justify-content: center;
width: 90%;
padding: 30px 0;
`;
export const StyledSwitchWrapper = styled.View`
flex-direction: row;
justify-content: space-between;
width: 90%;
`;
export const StyledToggleText = styled.Text<CustomThemeProps>`
color: ${(props) => props.theme.text};
font-size: 20px;

Let’s add our new switch to our app file and test it out!

App.tsx

import React from 'react';
import {SafeAreaView} from 'react-native';
import {Provider} from 'react-redux';
import {ThemeManager} from './components/ThemeManager';
import {store} from './state';
import {ThemeModeSwitch} from './components/ThemeModeSwitch';
const App = () => (
<Provider store={store}>
<ThemeManager>
<SafeAreaView>
<ThemeModeSwitch />
</SafeAreaView>
</ThemeManager>
</Provider>
);
export default App;

4. Responsive Theme

All that’s left to do now is make our theme respond to the devices theme settings.

ThemeManager.tsx

import React, {useEffect} from 'react';
import {StatusBar, Appearance} from 'react-native';
import styled, {ThemeProvider} from 'styled-components/native';
import {useSelector} from 'react-redux';
import {CustomThemeProps, light, dark} from '../../constants/theme';
import {getThemeMode} from '../../selectors/getThemeMode';
import {ThemeModeEnum, setThemeMode} from '../../state/themeMode.slice';
import {useAppDispatch} from '../../utils/useAppDispatch';
const StyledThemeContainer = styled.KeyboardAvoidingView<CustomThemeProps>`
flex: 1;
align-items: center;
justify-content: center;
background: ${(props) => props.theme.background};
`;
const {DARK, LIGHT} = ThemeModeEnum;export const ThemeManager = ({children}: {children: React.ReactNode}) => {const {themeMode} = useSelector(getThemeMode);
const dispatch = useAppDispatch();
const providedTheme = () => {
if (themeMode === DARK) {
return dark;
}
if (themeMode === LIGHT) {
return light;
}
};
useEffect(() => {
const subscription = Appearance.addChangeListener(({colorScheme}) => {
dispatch(setThemeMode(colorScheme as ThemeModeEnum));
});
return () => subscription.remove();
}, [dispatch]);
return (
<ThemeProvider theme={providedTheme}>
<StatusBar
barStyle={themeMode === DARK ? 'light-content' : 'dark-content'}
/>
<StyledThemeContainer>{children}</StyledThemeContainer>
</ThemeProvider>
);
};

And, voila! Close your app, change your device theme settings, pop back over to your app and marvel in your brilliance!

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store