As a React Native developer committed to delivering exceptional user experiences, I always strive to optimize the UI by incorporating features like dark mode. This is my journey implementing a seamless dark mode experience.
The Challenge: Consistency in Theme Selection
#When a user selects a preferred theme that doesn't align with the system theme, native components inadvertently adopt the system theme. This inconsistency disrupts the application's appearance.
Step 1: Creating a React Context for the Theme
#import React, { createContext, useState, useContext } from "react";
const ThemeContext = createContext<
{ theme: string; toggleTheme: () => void } | undefined
>(undefined);
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};Step 2: Enabling User Theme Persistence Using Storage
#import React, { createContext, useState, useContext, useEffect } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
const ThemeContext = createContext<
{ theme: string; toggleTheme: () => void } | undefined
>(undefined);
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = async () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
await AsyncStorage.setItem("user-theme", newTheme);
};
useEffect(() => {
const loadTheme = async () => {
const storedTheme = await AsyncStorage.getItem("user-theme");
if (storedTheme) setTheme(storedTheme);
};
loadTheme();
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};Step 3: Integrating the Appearance API
#Incorporating the Appearance API allows us to explicitly inform native components about the user's color scheme preferences.
import { Appearance } from "react-native";
useEffect(() => {
const loadTheme = async () => {
const storedTheme = await AsyncStorage.getItem("user-theme");
if (storedTheme) {
setTheme(storedTheme);
} else {
const systemTheme = Appearance.getColorScheme();
setTheme(systemTheme || "light");
}
};
loadTheme();
}, []);Exploring Theme Variations
#Without Appearance API
#- System: Dark, User: Light means native components follow system (dark), while the app follows user (light) → mismatch.
- System: Light, User: Dark causes the inverse mismatch.
With Appearance API
#- User selection consistently wins, regardless of system theme. Native components stay in sync.
Conclusion
#Implementing dark mode significantly enhances UX. Through Context for theme management, persistence, and the Appearance API for native sync, you get a robust theming system that feels native.
Originally published at saeed.guru/blog/posts/dark-mode-react-native.