React Native By Night: make your app respond gracefully to the user appearance preferences.

Alaa Chakroun
eDonec
Published in
5 min readFeb 5, 2021

Yet another Dark Theme implementation in React Native.

Dark mode has always been a frequently requested feature in mobile applications, and ever since the release of iOS 13 and Android 10 in 2019, it became more and more expected from modern applications to present the option to set an alternative (darker) theme, and even follow the user’s OS theme preference in order to provide a consistent experience throughout different applications.

Fortunately for us, recent versions of React Native provides us with the handy useColorScheme hook, which basically does all the heavy lifting in order to extract the user’s preferred color scheme which is exactly what we need in order to set up our theming. However, before we can start dimming the lights, we have to be made alert of some quirks of this hook.

The quirks:

On Android, the current behavior of the hook is to provide us with the correct color scheme at launch time, but it doesn’t react to the updates. Fortunately we can fix this quite easily by going to the MainActivity.java that’s usually found in android/app/src/main/java/com/myApp/MainActivity.java and modifying it as follows :

After rebuilding the app the useColorScheme hook should update us with the updated values.

On iOS, there isn’t really any feature breaking issues, we just have to keep in mind that while debugging with chrome (or any chromium based debugger like React Native Debugger), the useColorScheme hook always returns the value “light”, so we just have to disable debugging while setting up and working on styling our application (we won’t need it anyways).

The Easy Way :

Let’s get it out of the way first, if you’re using the React Navigation navigation library, most of the footwork of the setup is already made for us, se we might as well take advantage of that.

The react-navigation library provides us with basic Light (DefaultTheme) and Dark (DarkTheme) themes that we can import and make use of in our components by passing the correspondent theme to the theme prop in the NavigationContainer wrapping our navigator, and then accessing the selected theme different color values using the useTheme hook that react-navigation provides us.

And thus our MyText component appears as black in light theme and white in dark theme.

Find out more about overriding the default themes and making your own over at the React-Navigation Documentation Themes Section.

The Not So Easy Way :

What if we don’t use the react-navigation library for our navigation, or if we prefer to include more color attributes than the limited collection that react-navigation offers us (PS: we can actually add more attributes, but our TypeScript compiler won’t be too happy)?

The Theme File :

We have to start first by setting up our themes.ts file where we define our various themes. We can start by listing our base colors that will be used in both appearances.

And then we can create our baseTheme object

We can also choose to include different dark and light shades of our colors, and for that we can use thne tiny npm library tinycolor2 and start updating our baseTheme object.

We created the ratios by which we are going to lighten / darken our base colors using the tinycolor.darken and tiniycolor.lighten functions (we can play around with these values to get our colors to look just right.)

Now that our baseTheme is setup, we can start declaring our light and dark theme by extending our baseTheme using the handy JavaScript spread operator.

And finally we can add different lighter and darker versions of the background to provide the effect of elevation to our components such as Cards and Modals.

By this point our themes file is done and we can start working on implementing these values in our application.

Complete themes.ts file

The useAppearance hook :

In order to access our themes, we have to set up a custom hook that returns the correct theme object depending on the user color preferences using the React Native useColorScheme hook. We then can call our custom hook in our React components to access the returned theme object.

Overriding the device’s color preferences :

Our application now reacts accordingly and shapes itself depending on the operating system’s selected appearance, but it would be great if we provide the user the option to override these settings and force our application to use a specific theme variant (this step also allows us also to use our alternative appearances on devices running older versions of iOS and Android).

To achieve this functionality, we can make use of React’s awesome Context API and create our ThemeContext file.

The ThemeContext we export is a React context that stores the current appearance mode the app is running in, and a function that allows us to update this mode.

By now we should update our main App.tsx file by wrapping everything under the ThemeContext.Provider

We created a stateful variable ‘theme’ to pass along the setTheme function call to our Preferences screen down the line. We should also update our useAppearance hook to make use of the Context value instead of the value returned by the useColorScheme hook.

Updated useAppearance hook

Finally, we can implement the preferences screen where our user will be presented with the option to either follow the device’s color scheme or to force the application to a specific appearance.

By calling the setMode method, we update the “theme” state value in our main App Component, which is then passed along to the context, updates its mode attribute, which causes our useAppearance hook to react to the changes in the context and results in it returning the updated theme value to our various custom components.

Final Touches :

At this stage, everything is working according to the plan, until the users quits out of the app. Our context updates are saved when the app is running but a fresh relaunch of the application resets it to it’s default state resulting in the user getting blinded by our bright default theme and adds the tedious task of resetting the preferences on each use.

You might have seen where we’re going with this, we need to save the user preferences, and initialize our “theme” value in our main App.tsx file using those saved values. Sounds great ? So let’s get it implemented.

In order to persistently save the user preferences, we are going to use the excellent @react-native-community/async-storage library which you’re probably familiar with. It allows us to store and extract persistent data in the user’s device.

The following steps are straight forward:

  • Update our PreferencesScreen to store the values passed along to our setMode method using AsyncStorage under the “@theme” identifier :
Final PreferencesScreen
  • Use the value stored in the AsyncStorage “@theme” identifier to initialize the “theme” state in our main App.tsx file
Final App.tsx file

Final Words :

Implementing various color schemes might appear as a daunting task at first, but depending on the navigation , state management, and UI elements libraries used the whole process may be easier than you thought. The Not So Easy method presented in this article is personally my favorite way of setting things up as it offers the most flexibility and, while implementing it, allows us to learn about the (underrated -in my opinion-) React Context API.

This has been developed by myself at eDonec.

--

--