Dark Mode on React Native with Redux
With all of the rage of having Dark Mode for literally every app out there, I think I’m way past due to post an article about it. So this will be about how I managed to get Dark Mode (and basically, Color Themes) working on a React Native app with the help of Redux.
Disclamer: This is by no means a tutorial or a “best practices” post about how to do Dark Mode. It’s more of a diary I’d like to look back to one day, hopefully to realize how naive I was and have a good ol’ nostalgic laugh after.
What I’m Aiming For
…is basically a simple yet fullproof way of getting a React Native app to change its theme colors instantly using Redux. I like the idea of an app that changes modes/themes without restarting.
A Word of Warning
Note that implementing theme colors on an existing codebase may well be a long process, depending on how you have structured your components and styles, since it involves rewriting existing styles and making the theme available everywhere it’s needed. But it will be fun nonetheless!
Colors!
First things first, I need to set up my theme color configurations somewhere in the codebase. Keeping things simple, I’m gonna put them in a themes.js
file in the root project directory. Inside, there are configurations for a default theme and a default dark theme, which can be defined like so:
const defaultColors = {
primary: '#ED8936',
black: '#000000',
white: '#FFFFFF',
'100': '#F7FAFC',
'200': '#EDF2F7',
'300': '#E2E8F0',
'400': '#CBD5E0',
'500': '#A0AEC0',
'600': '#718096',
'700': '#4A5568',
'800': '#2D3748',
'900': '#1A202C',
};const darkColors = {
primary: '#F6AD55',
black: defaultColors.white,
white: defaultColors.black,
'100': defaultColors[900],
'200': defaultColors[800],
'300': defaultColors[700],
'400': defaultColors[600],
'500': defaultColors[500],
'600': defaultColors[400],
'700': defaultColors[300],
'800': defaultColors[200],
'900': defaultColors[100],
};
Fun fact, the gray color scheme above are the default gray colors from my current go-to CSS framework — Tailwind CSS. As you can see, the gray colors (black, white, 100–900) in the darkColors
scheme are actually mirrored from the gray colors in the defaultColors
scheme. Take note that the intermediary colors between black and white are sequenced in order of lightness: 100 (lightest) to 900 (darkest). It means that when getting a 100 shade, it will return a light shade for light (default) theme, and a darker shade for dark theme.
Of course, you can always go a step further and add in lighter/darker shades for the primary color, or shades for success, danger, warning colors, etc. You don’t even have to have gray colors for 100–900 — have fun using subtle colors! — as long as they are sequenced from lightest to darkest.
Both these themes have an orange-y primary color. Next step is to define the names of these themes into an object to export. And also maybe add two other themes in with blue-ish primary color, one light and one dark.
const themes = {
'default': {...defaultColors},
'dark': {...darkColors},
'blue': {...defaultColors, primary: '#4299E1'},
'blue-dark': {...darkColors, primary: '#63B3ED'},
};export default themes;
Finally, create and export a function to get a color based on the color name and theme. Something like this:
export const getThemeColor = (color, theme = 'default') => {
const themeColor = themes[theme][color];
const fallbackColor = themes.default[color]; return themeColor || fallbackColor;
};
The Redux Part
The great thing about Redux is that we can easily inject values directly to our components as long as they are wrapped inside Redux’s <Provider>
. This — at least for me — makes Redux ideal for the job, since the theme configuration would be something that should be accessible by every presentational component.
I know, I know, right about now, Redux is slowly giving way to React Hooks, and hooks would probably be the preferred way of managing application-wide state in the future. But in the meantime, I’ll be sticking with Redux.
Define a state and reducer to store and change the app configuration values, which includes our selected theme name:
// /redux/reducers/config.jsimport {THEME_SET} from '../actions/config';const defaultState = {
theme: 'default',
};export default (state = defaultState, action) => {
switch (action.type) {
case THEME_SET:
return {...state, theme: action.payload};
default:
return state;
}
};
The state above will be mapped to every presentational component to define their style values. For apps utilizing the React Navigation library, or other navigational libraries, map the theme state to the “screen” components.
Of course, sticking to Redux conventions, also define the function and type for dispatching the action to change our theme selection:
// /redux/actions/config.jsexport const THEME_SET = 'THEME_SET';export const setTheme = theme => ({
type: THEME_SET,
payload: theme,
});
The action above will then be mapped to our theme selector component, which may be some kind of an Appearance Settings screen.
Selecting a Theme
Most apps allow their users to change the theme color via the Settings screen, or for more simpler Light/Dark toggle — on the navigation drawer. Basically, it can be anything. For an example, below is a screen with a component that gets, lists and previews theme colors and changes the theme config by pressing on a theme preview, which in turn passes a theme name (default
, dark
, blue
, or blue-dark
) to the redux action.
import themes from './themes'
import {setTheme} from './redux/actions/config'const AppearanceSettings = ({onSelectTheme}) => (
<View style={styles.container}>
<Header title="Appearance" />
<ScrollView>
<Text>Themes</Text>
<Themes themes={themes} onSelect={theme => onSelectTheme(theme)} />
</ScrollView>
</View>
)export default connect({}, {
onSelectTheme: setTheme
});
Now the user can change the theme all they like. But there’s one thing missing: how do we make all our screens and components react to the selected theme? Read on…
Styling based on Selected Theme
Here’s the hard part (if the previous steps weren’t hard enough): getting the screens (and components) to be styled based on the selected theme name. Actually, it’s not as hard for brand new apps. Whatever the case, we can still enjoy the process of getting the colors right on every component. Here goes.
For every presentational React component that are given some kind of styling, they would need to have access to the theme configuration. For React Navigation-based apps, I like to do it by attaching it to the screen components, then passing down either the theme config or the actual computed styles down to child components.
Connect and map the selected theme name to the component props:
export default connect(state => ({
theme: state.config.theme
}), {})(Home);
Now the component can access the theme name inside the theme
prop. Next, define the styles:
import {getThemeColor} from './themes'const getStyles = theme =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: getThemeColor('white', theme),
},
});
It looks like a standard RN StyleSheet
definition, but with a twist:
- It’s now wrapped inside a function that takes a theme name as an argument,
- It now uses
getThemeColor
from thethemes
file to get colors based on the provided theme name. As shown in the first step,getThemeColor
takes in a color name and a theme name as arguments to return a theme color.
The last piece of the puzzle is to get the StyleSheet
object based on the selected theme. Where and when we get the styles depends on how our screen is placed within the application flow relative to the theme selector screen we talked about on the previous step.
To make it more efficient, one might think that we should define our styles inside of the component class constructor, or as an initial side effect defined in a useEffect
hook, and I think that might be true. But that means if the said component has been mounted prior, it won’t be able to instantly update its styling when the user changes the theme.
class Home extends Component {
constructor(props) {
this.styles = getStyles(props.theme);
}
render() {
return (
<View style={this.styles.container}>
...
</View>
);
}
}
Another method is to simply define the styles for every render. Easy peasy.
const Home = ({theme}) => {
const styles = getStyles(theme);
return (
<View style={styles.container}>
...
</View>
);
}
Oh, I almost forgot! The status bar may also need to reflect the selected theme by correctly setting the barStyle
attribute. This can be done by adding a <StatusBar>
component on the root component wrapped by Redux’s <Provider>
:
<Provider store={store}>
...
<Status barStyle={store.getState().config.theme.includes('dark') ? 'light-content' : 'dark-content'} />
</Provider>
Moving Forwards: How to Style
From my (short) experience writing styles with themes, is to base every other theme from a light-colored default theme, and think using the default theme mindset. Let me explain.
Say we want to style a screen with a plain background and a contrasting text on it. Start by defining the container styling with a “white” background, and the text with a “black” (or 800/900 shade) color. Try not to think into what it will look like in dark mode first, since that will add confusion into what color name we want. Then, check that if it visually makes sense in default theme. Later on, we can try switching to a dark theme and see how it looks in comparison.
And that’s it!
As a takeaway, I think color themeing feature in an app should be something thought of and prepared for in the beginnings of an app’s creation, since it involves so much of the app. Some may argue the cost of developing a dark mode or themes outweighs the benefits, and that could also be true to some extent. But I do admit I’m one of those people who looks at dark mode and thinks it’s way cooler than the default light mode.