Managing Global Settings in React Native: A Clean Context API Approach



This content originally appeared on DEV Community and was authored by Cathy Lai

How to share app-wide preferences like measurement units across your Expo app

When building a house hunting app, I ran into a common challenge that many React Native developers face: How do you share global settings across your entire application?

In my case, I needed users to choose between square meters and square feet for property measurements. This preference needed to:

  • ✅ Be accessible from any screen
  • ✅ Persist between app sessions
  • ✅ Update in real-time across components
  • ✅ Be type-safe and easy to maintain

Let me show you the elegant solution I implemented using React’s Context API and AsyncStorage.

The Problem

Initially, I had a Settings screen with a local useState hook:

export default function Settings() {
    const [metricUnit, setMetricUnit] = useState<'sqm' | 'sqft'>('sqm');
    // ... UI code
}

The problem? This state was trapped in the Settings component. Other screens couldn’t access it, and the preference would reset every time the app restarted. Not exactly a great user experience.

The Solution: Context API + AsyncStorage

The winning combination uses:

  1. React Context API – for sharing state across components
  2. AsyncStorage – for persisting data between sessions
  3. TypeScript – for type safety

This gives us a global state management solution without adding heavy dependencies like Redux or Zustand.

Step 1: Create the Settings Context

First, create a new file contexts/SettingsContext.tsx:

import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

type MeasurementUnit = 'sqm' | 'sqft';

interface SettingsContextType {
  measurementUnit: MeasurementUnit;
  setMeasurementUnit: (unit: MeasurementUnit) => Promise<void>;
  isLoading: boolean;
}

const SettingsContext = createContext<SettingsContextType | undefined>(undefined);

const STORAGE_KEY = '@househunt_settings';

export function SettingsProvider({ children }: { children: ReactNode }) {
  const [measurementUnit, setMeasurementUnitState] = useState<MeasurementUnit>('sqm');
  const [isLoading, setIsLoading] = useState(true);

  // Load settings from AsyncStorage on mount
  useEffect(() => {
    loadSettings();
  }, []);

  const loadSettings = async () => {
    try {
      const storedSettings = await AsyncStorage.getItem(STORAGE_KEY);
      if (storedSettings) {
        const settings = JSON.parse(storedSettings);
        setMeasurementUnitState(settings.measurementUnit || 'sqm');
      }
    } catch (error) {
      console.error('Error loading settings:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const setMeasurementUnit = async (unit: MeasurementUnit) => {
    try {
      setMeasurementUnitState(unit);
      const settings = { measurementUnit: unit };
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
    } catch (error) {
      console.error('Error saving settings:', error);
    }
  };

  return (
    <SettingsContext.Provider
      value={{
        measurementUnit,
        setMeasurementUnit,
        isLoading,
      }}
    >
      {children}
    </SettingsContext.Provider>
  );
}

// Custom hook to use the settings context
export function useSettings() {
  const context = useContext(SettingsContext);
  if (context === undefined) {
    throw new Error('useSettings must be used within a SettingsProvider');
  }
  return context;
}

What’s happening here?

  • We create a Context with TypeScript types for safety
  • The Provider loads settings from AsyncStorage on mount
  • When settings change, they’re automatically saved to AsyncStorage
  • We export a custom useSettings() hook for easy consumption

Step 2: Wrap Your App with the Provider

In your root layout file (app/_layout.tsx in Expo Router):

import { SettingsProvider } from "../contexts/SettingsContext";
import "../global.css";

export default function RootLayout() {
  return (
    <SettingsProvider>
      <Tabs screenOptions={{ /* ... */ }}>
        {/* Your screens */}
      </Tabs>
    </SettingsProvider>
  );
}

By wrapping your entire app, every screen can now access the settings context.

Step 3: Update Your Settings Screen

Now update your Settings screen to use the context:

import { useSettings } from "../contexts/SettingsContext";

export default function Settings() {
    const { measurementUnit, setMeasurementUnit } = useSettings();

    return (
        <View className="flex-1 bg-[#FFF9F3] p-5 pt-[60px]">
            <Text className="text-[28px] font-bold text-[#765227] mb-[30px]">
                Settings
            </Text>

            <View className="bg-white rounded-xl p-5 shadow-lg">
                <Text className="text-base font-semibold text-[#8C6D4A] mb-4">
                    Area Measurement
                </Text>

                <TouchableOpacity
                    className="flex-row items-center py-3"
                    onPress={() => setMeasurementUnit('sqm')}
                >
                    <View className="w-6 h-6 rounded-full border-2 border-[#8C6D4A] items-center justify-center mr-3">
                        {measurementUnit === 'sqm' && (
                            <View className="w-3 h-3 rounded-full bg-[#8C6D4A]" />
                        )}
                    </View>
                    <Text className="text-base text-[#4A3C2B]">
                        Square Meters (m²)
                    </Text>
                </TouchableOpacity>

                <TouchableOpacity
                    className="flex-row items-center py-3"
                    onPress={() => setMeasurementUnit('sqft')}
                >
                    <View className="w-6 h-6 rounded-full border-2 border-[#8C6D4A] items-center justify-center mr-3">
                        {measurementUnit === 'sqft' && (
                            <View className="w-3 h-3 rounded-full bg-[#8C6D4A]" />
                        )}
                    </View>
                    <Text className="text-base text-[#4A3C2B]">
                        Square Feet (ft²)
                    </Text>
                </TouchableOpacity>
            </View>
        </View>
    );
}

Notice how clean this is! We simply import useSettings() and destructure what we need.

Step 4: Use It Anywhere

Now any component in your app can access and use the measurement preference:

import { useSettings } from "../contexts/SettingsContext";

function PropertyCard({ property }) {
  const { measurementUnit } = useSettings();

  const displayArea = (area: number) => {
    if (measurementUnit === 'sqft') {
      return `${Math.round(area * 10.764)} ft²`;
    }
    return `${area} m²`;
  };

  return (
    <View>
      <Text>Area: {displayArea(property.area)}</Text>
    </View>
  );
}

The component automatically re-renders when the measurement unit changes. Magic! ✨

Why This Approach Works

🎯 Simple and Standard

No need to learn Redux or other state management libraries. This uses React’s built-in Context API.

💾 Persistent

Settings survive app restarts thanks to AsyncStorage. Users set their preference once and it sticks.

🔒 Type-Safe

TypeScript ensures you can’t accidentally use invalid measurement units or forget to wrap a component in the provider.

📈 Scalable

Need to add more settings like currency, theme, or language? Just extend the context:

interface SettingsContextType {
  measurementUnit: MeasurementUnit;
  currency: Currency;
  theme: Theme;
  setMeasurementUnit: (unit: MeasurementUnit) => Promise<void>;
  setCurrency: (currency: Currency) => Promise<void>;
  setTheme: (theme: Theme) => Promise<void>;
}

⚡ Performant

Context changes only trigger re-renders in components that actually use the context. Components that don’t care about settings won’t re-render.

Bonus: Loading State

Notice the isLoading flag in the context? This is useful for showing a splash screen while settings load:

function App() {
  const { isLoading } = useSettings();

  if (isLoading) {
    return <SplashScreen />;
  }

  return <YourApp />;
}

Alternative Approaches

You might be wondering about other options:

Redux/Redux Toolkit – Overkill for simple settings. Adds complexity and boilerplate.

Zustand – Great alternative! Lighter than Redux, but another dependency to manage.

React Query – Excellent for server state, but not ideal for local app preferences.

Props Drilling – Please no. Your code will thank you for not doing this.

Global Variables – They work but don’t trigger re-renders and are hard to persist.

For global settings, Context API + AsyncStorage hits the sweet spot of simplicity and functionality.

Wrapping Up

Managing global settings doesn’t have to be complicated. With React’s Context API and AsyncStorage, you can create a clean, type-safe, persistent solution in under 100 lines of code.

The pattern I’ve shown works great for:

  • User preferences (measurement units, currency, language)
  • Theme settings (dark mode, color schemes)
  • App configuration (API endpoints, feature flags)
  • Authentication state

Give it a try in your next React Native project!

Have you used a different approach for managing global settings? I’d love to hear about it in the comments below. And if you found this helpful, give it a clap! 👏

Building a house hunting app with React Native and Expo. Follow along for more tips on mobile development!

Tags: #ReactNative #Expo #TypeScript #MobileDevelopment #AsyncStorage


This content originally appeared on DEV Community and was authored by Cathy Lai