Tutorial: Building ExpenseApp with React Native and Expo Chapter 1: Web, HTML, CSS & JavaScript Fundamentals Chapter 2: Orientation & Environment Setup Chapter 3: JSX & Core Components Chapter 4: State, Props & Component Architecture Chapter 5: Lists & Layout with Flexbox Chapter 6: Inputs & Form Handling Chapter 7: React Native Tutorial – StickerSmash with Expo Chapter 8: Advanced JavaScript for React Native Chapter 9: Form Handling & Validation with Formik and Yup Chapter 10: Navigation with Expo Router Chapter 11: Navigation Params & Modals Chapter 12: React Hooks, Side Effects & Data Flow Chapter 13: Global State Management with Context API & AsyncStorage Chapter 14: API Integration, Async Operations & Error Handling Chapter 15: Dynamic Bottom Tabs & Drawer Navigation Chapter 16: Advanced JavaScript – Async Patterns Chapter 17: Device APIs – Camera, Image Picker & Location Chapter 18: Notifications & Permissions Chapter 19: Animations & Microinteractions Chapter 20: Performance Optimization Chapter 21: TypeScript Fundamentals for React Native Chapter 22: Theme Context & Dark Mode Chapter 23: Advanced TypeScript – Navigation Types & API Typings Chapter 24: Expo Router Authentication & Transaction Module Chapter 25: Testing Fundamentals (Jest & React Native Testing Library) Chapter 26: Debugging & Troubleshooting Chapter 27: Code Quality & Refactoring Chapter 28: Build & Deployment with Expo (EAS) Chapter 29: Capstone Build-out – Features, Polish & Production Readiness Chapter 30: Capstone Demo, Final Review & Next Steps

Chapter 22: Theme Context & Dark Mode


In previous chapters, we built several ExpenseApp features using:

  • Navigation
  • Context API
  • AsyncStorage
  • Notifications
  • Device APIs
  • Animations
  • TypeScript

Modern mobile applications also support:

✅ Light Mode ✅ Dark Mode ✅ System Theme Detection

Users now expect applications to automatically adapt to their device theme settings.

Examples:

  • Banking apps support dark mode
  • Social media apps switch themes automatically
  • Expense apps improve readability in dark environments

In this chapter, we will build a simple and reusable Theme Context system for ExpenseApp using:

✅ React Context API ✅ AsyncStorage ✅ useColorScheme() ✅ Dynamic Styles ✅ Theme Toggle Button

The implementation is simplified and optimized from your existing production version for easier learning and understanding.


🎯 What You Will Learn

By the end of this chapter, you will:

  • Understand theming in React Native
  • Create Theme Context
  • Support light and dark themes
  • Detect system theme automatically
  • Save theme using AsyncStorage
  • Create dynamic styles
  • Use reusable theme colors
  • Build a theme toggle button

⚡ Why Themes Matter

Themes improve:

✅ User experience ✅ Accessibility ✅ Readability ✅ Battery efficiency (OLED screens)


🔹 Real ExpenseApp Benefits

Dark mode is useful for:

  • Night usage
  • Reduced eye strain
  • Better visual comfort
  • Modern UI appearance

🔹 Theme Flow

User Changes Theme
          ↓
Theme Context Updates
          ↓
Colors Change
          ↓
UI Re-renders Automatically

🔹 Create Theme Colors

📁 src/constants/theme.ts

export const COLORS = {
    light: {
        background: "#ffffff",
        card: "#f4f4f4",
        text: "#222222",
        primary: "#007bff",
        buttonText: "#ffffff",
    },

    dark: {
        background: "#121212",
        card: "#1e1e1e",
        text: "#ffffff",
        primary: "#4d94ff",
        buttonText: "#ffffff",
    },
};

export const getColors = (mode: "light" | "dark") => {
    return COLORS[mode];
};

🔹 Understanding Theme Colors

We separate:

✅ Light colors

✅ Dark colors

This keeps UI centralized and reusable.


🔹 Create Theme Context

📁 src/contexts/theme-context.tsx

import React, { createContext, useContext, useEffect, useState } from "react";

import AsyncStorage from "@react-native-async-storage/async-storage";

import { useColorScheme } from "react-native";

import { getColors } from "../constants/theme";

type ThemeMode = "light" | "dark" | "system";

type ThemeContextType = {
    mode: ThemeMode;

    theme: "light" | "dark";

    colors: ReturnType<typeof getColors>;

    toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
    const systemTheme = useColorScheme();

    const [mode, setMode] = useState<ThemeMode>("system");

    /* Load Saved Theme */

    useEffect(() => {
        (async () => {
            const saved = await AsyncStorage.getItem("theme");

            if (saved === "light" || saved === "dark" || saved === "system") {
                setMode(saved);
            }
        })();
    }, []);

    /* Resolve Final Theme */

    const theme =
        mode === "system" ? (systemTheme === "dark" ? "dark" : "light") : mode;

    /* Toggle Theme */

    const toggleTheme = async () => {
        const next = theme === "light" ? "dark" : "light";

        setMode(next);

        await AsyncStorage.setItem("theme", next);
    };

    return (
        <ThemeContext.Provider
            value={{
                mode,
                theme,
                colors: getColors(theme),
                toggleTheme,
            }}
        >
            {children}
        </ThemeContext.Provider>
    );
};

export const useTheme = () => {
    const context = useContext(ThemeContext);

    if (!context) {
        throw new Error("useTheme must be used inside ThemeProvider");
    }

    return context;
};

🔹 Why Context API?

Theme must be accessible globally.

Using Context API allows:

✅ Global theme access

✅ Automatic UI updates

✅ Cleaner architecture


🔹 useColorScheme()

React Native provides:

useColorScheme();

It detects:

✅ Light mode

✅ Dark mode

from device settings automatically.


🔹 AsyncStorage Integration

We save theme using:

AsyncStorage.setItem();

This preserves theme after application restart.


🔹 Create Dynamic Styles

📁 src/styles/styles.ts

Simplified version using theme context:

import { StyleSheet } from "react-native";

import { useTheme } from "../contexts/theme-context";

export const useStyles = () => {
    const { colors } = useTheme();

    return StyleSheet.create({
        container: {
            flex: 1,
            padding: 20,
            backgroundColor: colors.background,
        },

        title: {
            fontSize: 24,
            fontWeight: "700",
            marginBottom: 20,
            color: colors.text,
            textAlign: "center",
        },

        card: {
            padding: 15,
            borderRadius: 10,
            marginBottom: 10,
            backgroundColor: colors.card,
        },

        text: {
            fontSize: 16,
            color: colors.text,
        },

        button: {
            backgroundColor: colors.primary,
            padding: 12,
            marginBottom: 20,
            alignItems: "center",
            borderRadius: 8,
        },

        buttonText: {
            color: colors.buttonText,

            fontWeight: "600",
        },

        header: {
            height: 60,

            backgroundColor: colors.primary,

            justifyContent: "center",

            alignItems: "center",
        },

        headerText: {
            color: colors.buttonText,

            fontSize: 20,

            fontWeight: "700",
        },
    });
};

🔹 Why Dynamic Styles?

Without dynamic styles:

❌ Colors stay fixed

With dynamic styles:

✅ UI updates automatically when theme changes


🔹 Create App Header

📁 src/components/app-header.tsx

import React from "react";

import { View, Text, Pressable } from "react-native";

import { useTheme } from "../contexts/theme-context";

import { useStyles } from "../styles/styles";

export default function AppHeader() {
    const { theme, toggleTheme } = useTheme();

    const style = useStyles();

    return (
        <View style={styles.header}>
            <Text style={styles.headerText}>ExpenseApp</Text>

            <Pressable
                onPress={toggleTheme}
                style={{
                    position: "absolute",

                    right: 20,
                }}
            >
                <Text
                    style={{
                        color: "#fff",
                    }}
                >
                    {theme === "light" ? "🌙" : "☀️"}
                </Text>
            </Pressable>
        </View>
    );
}

🔹 Why Keep Header Simple?

We intentionally keep the theme switch simple:

✅ Easier to understand

✅ Less styling complexity

✅ Beginner-friendly

Later chapters can introduce:

  • Icons
  • Animated toggles
  • Multiple theme options

🔹 Wrap Root Layout

📁 src/app/_layout.tsx

import { Slot } from "expo-router";

import { ThemeProvider } from "../contexts/theme-context";

export default function Layout() {
    return (
        <ThemeProvider>
            <Slot />
        </ThemeProvider>
    );
}

🔹 Use Theme in Screens

📁 src/app/index.tsx

import React from "react";

import { View, Text } from "react-native";

import AppHeader from "../components/app-header";

import { useStyles } from "../styles/styles";

export default function HomeScreen() {
    const style = useStyles();

    return (
        <View style={styles.container}>
            <AppHeader />

            <View style={styles.card}>
                <Text style={styles.text}>Welcome to ExpenseApp</Text>
            </View>
        </View>
    );
}

🔹 Theme Switching Flow

User Presses Toggle
          ↓
toggleTheme() Executes
          ↓
Theme Context Updates
          ↓
Styles Recalculate
          ↓
UI Automatically Updates

🔹 Light vs Dark Theme

Theme Background Text
Light White Dark
Dark Dark White

🔹 Why Use useStyles Hook?

Using:

useStyles();

helps:

✅ Centralize styles

✅ Dynamically apply colors

✅ Reuse styles everywhere


🔹 Recommended Folder Structure

src
│
├── app
│   ├── _layout.tsx
│   └── index.tsx
│
├── components
│   └── app-header.tsx
│
├── constants
│   └── theme.ts
│
├── contexts
│   └── theme-context.tsx
│
└── styles
    └── styles.ts

⚡ Performance Considerations

✅ Keep themes centralized

✅ Avoid recreating unnecessary objects

✅ Reuse color definitions

✅ Keep styles minimal


🔐 Best Practices

✅ Use Context API for global themes ✅ Store theme in AsyncStorage ✅ Keep colors centralized ✅ Use reusable style hooks ✅ Keep dark mode readable


⚠️ Common Mistakes

❌ Hardcoding colors everywhere ❌ Duplicating styles ❌ Not saving theme preferences ❌ Mixing light/dark logic inside components ❌ Large complex theme objects for small apps


🧪 Practice Exercises

  1. Add System theme support UI
  2. Add separate Settings screen for theme
  3. Add dark mode to TabBar
  4. Add dark mode to Drawer
  5. Add animated theme transition
  6. Add reusable themed button component
  7. Create custom useColors hook

📘 Chapter Summary

In this chapter, you:

✅ Built Theme Context ✅ Added light and dark mode ✅ Used AsyncStorage for persistence ✅ Used useColorScheme() ✅ Created dynamic styles ✅ Built reusable theme architecture ✅ Added theme toggle functionality ✅ Improved ExpenseApp user experience

Need help with your project?
I can help you explore our services or start an inquiry.