You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo(TS/React Native)中AsyncStorage重载后无法保存主题

主题状态无法在应用重启后从AsyncStorage恢复的问题

我有一个用于存储应用中theme变量状态的Context,初始值设为empty。切换主题时AsyncStorage能正确更新,但重载或重启Expo应用后,之前设置的主题不会被保留。


相关代码文件

AppSettingsContext.tsx

import React, {createContext, useState, useEffect} from "react";
import { AppSettingsContextStruct, Themes } from "../../types";
import AsyncStorage from "@react-native-async-storage/async-storage";

const fetchThemeFromStorage = async () => {
    return AsyncStorage.getItem("THEME")
    .then((value) => {
        if (value === `"LIGHT"`) {
            return Themes.LIGHT;
        };
        if (value === `"DARK"`) {
            return Themes.DARK;
        };
        if (value === `"empty"` || "empty") {
            return Themes.EMPTY;
        };
        
        })
    .catch((err) => {
        console.log(err);
        return Themes.LIGHT;
    });
};

const defaultVal: AppSettingsContextStruct = {
    theme: Themes.EMPTY,
    setTheme: () => {},
}

type CtxProps = {
    children: JSX.Element
}

const AppSettingsContext = createContext(defaultVal);

const AppSettingsContextProvider:React.FC<CtxProps> = (props) => {
    const [theme, setTheme] = useState<Themes>(Themes.EMPTY);

    useEffect(() => {
        const asyncWrap = async () => {
            const storageTheme = await fetchThemeFromStorage();
            return {
                theme: storageTheme,
            }
        }
        asyncWrap().then((value) => {
            console.log("asyncWrap ran:", value)
            if (value.theme === JSON.stringify(Themes.EMPTY)) {
                setTheme(Themes.LIGHT);
            };
        })

    }, []);

    useEffect(() => {
        AsyncStorage.setItem("THEME", JSON.stringify(theme)).then(() => {
                console.log(" > ASYNC STORAGE UPDATED THEME ->", theme)
            });
    },[theme]);

    return (
        <AppSettingsContext.Provider value={{theme,setTheme}}>
            {props.children}
        </AppSettingsContext.Provider>
    )
}

export {AppSettingsContext, AppSettingsContextProvider}

App.tsx

import { NavigationContainer } from '@react-navigation/native';
import RootNavigator from './navigator/RootNavigator';
import {UserContextProvider} from "./components/contexts/UserContext"
import {AppSettingsContextProvider} from "./components/contexts/AppSettingsContext"

export default function App() {
      return ( 
        <UserContextProvider>
          <AppSettingsContextProvider> 
            <NavigationContainer>
              <RootNavigator />
            </NavigationContainer>
          </AppSettingsContextProvider>
        </UserContextProvider>
      );
}

useAppSettingsContext.ts

import { useContext } from "react"
import { AppSettingsContext } from "../components/contexts/AppSettingsContext"

export const useAppSettingsContext = () => {
    const context = useContext(AppSettingsContext);
    if (context === undefined) {
        throw new Error("useUserContext was used outside of its Provider");
    }
    return context
}

SettingsToggleComponent.tsx

import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import { useEffect, useState } from 'react';
import { TouchableOpacity, Text, StyleSheet, View } from "react-native"
import { useAppSettingsContext } from '../../hooks/useAppSettingsContext';
import { Themes } from '../../types';
import AsyncStorage from '@react-native-async-storage/async-storage';
type Props = {
    id: string;
    settingName: string;
    iconName: string;
    sendActionLower: Function;
};


const SettingsToggleComponent: React.FC<Props> = ( {id, settingName, sendActionLower} ) => {
    const {
        theme, setTheme
    } = useAppSettingsContext();
    const [isLight, setIsLight] = useState<boolean>(true);
    useEffect(() => {
        if (theme === Themes.LIGHT) {
            setIsLight(true);
        } else if (theme === Themes.DARK) {
            setIsLight(false);
        } else {
            setIsLight(false);
        }
    }, [])

    return (
        <TouchableOpacity  onPress={() => {
            //changing asyncStorage will cause rerender at root? -> false
            if (theme === Themes.LIGHT) {
                setIsLight(false)
                setTheme(Themes.DARK)
            } else {
                setIsLight(true)
                setTheme(Themes.LIGHT)
            }

        }}>
            <View style={styles.settingLayout}>
            {/* Make sure that the possible names for the icons are used instead of random icon names */}
                <View style={styles.iconContainer}>
                    <MaterialCommunityIcons name="theme-light-dark" size={24} color="#5a5a5a" />
                </View>
                <View style={styles.textContainer}>
                    <View style={styles.textContainerLayout}>
                        <Text style={styles.settingName}>{settingName}</Text>
                        <View style={styles.arrowSettingValueContainer}>
                            {
                                isLight?
                                <Ionicons name="ios-sunny-sharp" size={24} color="#5A5A5A" />
                                : <Ionicons name="ios-moon-sharp" size={24} color="#5A5A5A" />
                            }
                        </View>
                    </View>
                </View>
            </View>
        </TouchableOpacity>
    )
}

问题分析与修复方案

1. fetchThemeFromStorage逻辑错误

判断empty的条件value === "empty" || "empty"永远为真,因为字符串"empty"是真值,无论存储的实际值是什么,都会返回Themes.EMPTY,覆盖了正确的存储值。同时读取存储时没有处理JSON.stringify带来的引号问题。

修复后的fetchThemeFromStorage:

const fetchThemeFromStorage = async () => {
    try {
        const value = await AsyncStorage.getItem("THEME");
        if (!value) return Themes.LIGHT;
        // 移除JSON.stringify添加的引号
        const parsedValue = value.replace(/"/g, "");
        if (parsedValue === Themes.LIGHT) return Themes.LIGHT;
        if (parsedValue === Themes.DARK) return Themes.DARK;
        if (parsedValue === Themes.EMPTY) return Themes.EMPTY;
        return Themes.LIGHT;
    } catch (err) {
        console.log(err);
        return Themes.LIGHT;
    }
};

2. 初始化主题的逻辑错误

将value.theme与JSON.stringify(Themes.EMPTY)比较是错误的:value.theme是枚举值(如"empty"),而JSON.stringify(Themes.EMPTY)是"\"empty\"",两者永远不相等,导致不会设置正确的存储主题。

修复后的初始化useEffect:

useEffect(() => {
    const loadTheme = async () => {
        const storageTheme = await fetchThemeFromStorage();
        console.log("Loaded theme from storage:", storageTheme);
        setTheme(storageTheme === Themes.EMPTY ? Themes.LIGHT : storageTheme);
    };
    loadTheme();
}, []);

3. 主题切换组件的状态同步问题

SettingsToggleComponent中的useEffect依赖为空数组,只会在组件挂载时执行一次,当Context中的theme变化时,isLight状态不会更新,导致图标显示与实际主题不一致。

修复后的组件内useEffect:

useEffect(() => {
    setIsLight(theme === Themes.LIGHT);
}, [theme]); // 添加theme作为依赖,主题变化时同步更新isLight

4. 冗余存储操作优化

每次theme变化都会写入存储,包括初始值EMPTY的情况,可添加判断避免不必要的存储:

useEffect(() => {
    if (theme === Themes.EMPTY) return;
    AsyncStorage.setItem("THEME", JSON.stringify(theme))
        .then(() => console.log(" > ASYNC STORAGE UPDATED THEME ->", theme))
        .catch(err => console.log("Failed to save theme:", err));
}, [theme]);

内容的提问来源于stack exchange,提问作者Fergus Johnson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:25:34