Global Theming in React Native
Using Styled-Components & Redux Toolkit — Typescript
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
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
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.

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!