DEV Community

Cathy Lai
Cathy Lai

Posted on

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

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)