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
相关产品推荐
相关产品推荐

