Appearance
全局状态管理
在完成了收藏和搜索功能后,我们需要进一步优化应用的状态管理。在这一节中,我们将学习如何使用 Redux Toolkit 实现全局状态管理,提升应用的性能和可维护性。
1. Redux Toolkit 回顾
Redux Toolkit 是官方推荐的 Redux 开发工具集,它简化了 Redux 的使用,提供了以下核心功能:
- configureStore - 简化 store 的创建
- createSlice - 简化 reducer 的创建
- createAsyncThunk - 处理异步操作
- createEntityAdapter - 管理规范化的数据
2. 优化全局状态结构
让我们优化我们的全局状态结构,使其更加清晰和可维护:
2.1 重构 store.js
js
// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import newsReducer from './slices/newsSlice';
import searchReducer from './slices/searchSlice';
import favoritesReducer from './slices/favoritesSlice';
import userReducer from './slices/userSlice';
import settingsReducer from './slices/settingsSlice';
export const store = configureStore({
reducer: {
news: newsReducer,
search: searchReducer,
favorites: favoritesReducer,
user: userReducer,
settings: settingsReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export default store;2.2 创建用户状态管理
js
// src/redux/slices/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';
const USER_KEY = 'news_app_user';
// 异步加载用户信息
export const loadUser = createAsyncThunk(
'user/loadUser',
async (_, { rejectWithValue }) => {
try {
const userStr = await AsyncStorage.getItem(USER_KEY);
return userStr ? JSON.parse(userStr) : null;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// 异步保存用户信息
const saveUserToStorage = async (user) => {
try {
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
} catch (error) {
console.error('保存用户信息失败:', error);
}
};
const userSlice = createSlice({
name: 'user',
initialState: {
isLoggedIn: false,
userInfo: null,
loading: false,
error: null,
},
reducers: {
login: (state, action) => {
state.isLoggedIn = true;
state.userInfo = action.payload;
saveUserToStorage(action.payload);
},
logout: (state) => {
state.isLoggedIn = false;
state.userInfo = null;
AsyncStorage.removeItem(USER_KEY);
},
updateUserInfo: (state, action) => {
state.userInfo = { ...state.userInfo, ...action.payload };
saveUserToStorage(state.userInfo);
},
},
extraReducers: (builder) => {
builder
.addCase(loadUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loadUser.fulfilled, (state, action) => {
state.loading = false;
if (action.payload) {
state.isLoggedIn = true;
state.userInfo = action.payload;
}
})
.addCase(loadUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
export const { login, logout, updateUserInfo } = userSlice.actions;
export default userSlice.reducer;2.3 创建设置状态管理
js
// src/redux/slices/settingsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';
const SETTINGS_KEY = 'news_app_settings';
// 异步加载设置
export const loadSettings = createAsyncThunk(
'settings/loadSettings',
async (_, { rejectWithValue }) => {
try {
const settingsStr = await AsyncStorage.getItem(SETTINGS_KEY);
return settingsStr ? JSON.parse(settingsStr) : {};
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// 异步保存设置
const saveSettingsToStorage = async (settings) => {
try {
await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch (error) {
console.error('保存设置失败:', error);
}
};
const settingsSlice = createSlice({
name: 'settings',
initialState: {
theme: 'light', // light, dark, system
country: 'us',
categories: ['general', 'business', 'technology', 'sports', 'entertainment', 'health', 'science'],
notifications: true,
autoRefresh: true,
refreshInterval: 300000, // 5分钟
loading: false,
error: null,
},
reducers: {
setTheme: (state, action) => {
state.theme = action.payload;
saveSettingsToStorage(state);
},
setCountry: (state, action) => {
state.country = action.payload;
saveSettingsToStorage(state);
},
toggleCategory: (state, action) => {
const category = action.payload;
const index = state.categories.indexOf(category);
if (index > -1) {
state.categories.splice(index, 1);
} else {
state.categories.push(category);
}
saveSettingsToStorage(state);
},
setCategories: (state, action) => {
state.categories = action.payload;
saveSettingsToStorage(state);
},
toggleNotifications: (state) => {
state.notifications = !state.notifications;
saveSettingsToStorage(state);
},
toggleAutoRefresh: (state) => {
state.autoRefresh = !state.autoRefresh;
saveSettingsToStorage(state);
},
setRefreshInterval: (state, action) => {
state.refreshInterval = action.payload;
saveSettingsToStorage(state);
},
updateSettings: (state, action) => {
state = { ...state, ...action.payload };
saveSettingsToStorage(state);
},
},
extraReducers: (builder) => {
builder
.addCase(loadSettings.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loadSettings.fulfilled, (state, action) => {
state.loading = false;
state = { ...state, ...action.payload };
})
.addCase(loadSettings.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
export const {
setTheme,
setCountry,
toggleCategory,
setCategories,
toggleNotifications,
toggleAutoRefresh,
setRefreshInterval,
updateSettings
} = settingsSlice.actions;
export default settingsSlice.reducer;3. 创建设置页面
js
// src/screens/SettingsScreen.js
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, Switch, TouchableOpacity, ScrollView, Alert } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import {
loadSettings,
setTheme,
setCountry,
toggleCategory,
toggleNotifications,
toggleAutoRefresh,
setRefreshInterval
} from '../redux/slices/settingsSlice';
import { logout } from '../redux/slices/userSlice';
const categories = [
{ id: 'general', name: '综合' },
{ id: 'business', name: '商业' },
{ id: 'entertainment', name: '娱乐' },
{ id: 'health', name: '健康' },
{ id: 'science', name: '科技' },
{ id: 'sports', name: '体育' },
{ id: 'technology', name: '技术' },
];
const countries = [
{ code: 'us', name: '美国' },
{ code: 'cn', name: '中国' },
{ code: 'uk', name: '英国' },
{ code: 'jp', name: '日本' },
{ code: 'de', name: '德国' },
{ code: 'fr', name: '法国' },
];
const refreshIntervals = [
{ value: 60000, label: '1分钟' },
{ value: 300000, label: '5分钟' },
{ value: 600000, label: '10分钟' },
{ value: 1800000, label: '30分钟' },
{ value: 3600000, label: '1小时' },
];
const SettingsScreen = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const {
theme,
country,
categories: selectedCategories,
notifications,
autoRefresh,
refreshInterval
} = useSelector(state => state.settings);
const { isLoggedIn } = useSelector(state => state.user);
useEffect(() => {
dispatch(loadSettings());
}, [dispatch]);
const handleLogout = () => {
Alert.alert(
'退出登录',
'确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '退出',
style: 'destructive',
onPress: () => dispatch(logout()),
},
],
{ cancelable: true }
);
};
const handleThemeChange = (newTheme) => {
dispatch(setTheme(newTheme));
};
const handleCountryChange = (newCountry) => {
dispatch(setCountry(newCountry));
};
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>外观</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>主题</Text>
<View style={styles.themeOptions}>
<TouchableOpacity
style={[styles.themeOption, theme === 'light' && styles.selectedThemeOption]}
onPress={() => handleThemeChange('light')}
>
<Text style={[styles.themeText, theme === 'light' && styles.selectedThemeText]}>浅色</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.themeOption, theme === 'dark' && styles.selectedThemeOption]}
onPress={() => handleThemeChange('dark')}
>
<Text style={[styles.themeText, theme === 'dark' && styles.selectedThemeText]}>深色</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.themeOption, theme === 'system' && styles.selectedThemeOption]}
onPress={() => handleThemeChange('system')}
>
<Text style={[styles.themeText, theme === 'system' && styles.selectedThemeText]}>系统</Text>
</TouchableOpacity>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>新闻设置</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>国家/地区</Text>
<View style={styles.countryOptions}>
{countries.map((item) => (
<TouchableOpacity
key={item.code}
style={[styles.countryOption, country === item.code && styles.selectedCountryOption]}
onPress={() => handleCountryChange(item.code)}
>
<Text style={[styles.countryText, country === item.code && styles.selectedCountryText]}>
{item.name}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>新闻分类</Text>
<View style={styles.categoryOptions}>
{categories.map((category) => (
<TouchableOpacity
key={category.id}
style={[
styles.categoryOption,
selectedCategories.includes(category.id) && styles.selectedCategoryOption
]}
onPress={() => dispatch(toggleCategory(category.id))}
>
<Text style={[
styles.categoryText,
selectedCategories.includes(category.id) && styles.selectedCategoryText
]}
numberOfLines={1}
>
{category.name}
</Text>
))}
</View>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>自动刷新</Text>
<Switch
value={autoRefresh}
onValueChange={toggleAutoRefresh}
trackColor={{ false: '#E0E0E0', true: '#007AFF' }}
thumbColor={autoRefresh ? '#FFFFFF' : '#F4F3F4'}
/>
</View>
{autoRefresh && (
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>刷新间隔</Text>
<View style={styles.intervalOptions}>
{refreshIntervals.map((interval) => (
<TouchableOpacity
key={interval.value}
style={[
styles.intervalOption,
refreshInterval === interval.value && styles.selectedIntervalOption
]}
onPress={() => dispatch(setRefreshInterval(interval.value))}
>
<Text style={[
styles.intervalText,
refreshInterval === interval.value && styles.selectedIntervalText
]}
>
{interval.label}
</Text>
))}
</View>
</View>
)}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>通知</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>推送通知</Text>
<Switch
value={notifications}
onValueChange={toggleNotifications}
trackColor={{ false: '#E0E0E0', true: '#007AFF' }}
thumbColor={notifications ? '#FFFFFF' : '#F4F3F4'}
/>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>账户</Text>
{isLoggedIn ? (
<TouchableOpacity style={styles.settingItem} onPress={handleLogout}>
<Text style={styles.settingLabel}>退出登录</Text>
<Ionicons name="log-out-outline" size={20} color="#FF3B30" />
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.settingItem}
onPress={() => navigation.navigate('Login')}
>
<Text style={styles.settingLabel}>登录/注册</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
</TouchableOpacity>
)}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>关于</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>版本</Text>
<Text style={styles.versionText}>1.0.0</Text>
</View>
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingLabel}>隐私政策</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
</TouchableOpacity>
<TouchableOpacity style={styles.settingItem}>
<Text style={styles.settingLabel}>用户协议</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
section: {
marginTop: 20,
backgroundColor: 'white',
paddingHorizontal: 16,
paddingVertical: 12,
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#999',
marginBottom: 12,
textTransform: 'uppercase',
},
settingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
settingLabel: {
fontSize: 16,
color: '#333',
},
themeOptions: {
flexDirection: 'row',
},
themeOption: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: '#F0F0F0',
marginLeft: 8,
},
selectedThemeOption: {
backgroundColor: '#007AFF',
},
themeText: {
fontSize: 14,
color: '#333',
},
selectedThemeText: {
color: 'white',
},
countryOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
maxWidth: 200,
},
countryOption: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: '#F0F0F0',
marginLeft: 8,
marginBottom: 8,
},
selectedCountryOption: {
backgroundColor: '#007AFF',
},
countryText: {
fontSize: 14,
color: '#333',
},
selectedCountryText: {
color: 'white',
},
categoryOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
maxWidth: 200,
},
categoryOption: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: '#F0F0F0',
marginLeft: 8,
marginBottom: 8,
},
selectedCategoryOption: {
backgroundColor: '#007AFF',
},
categoryText: {
fontSize: 14,
color: '#333',
},
selectedCategoryText: {
color: 'white',
},
intervalOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
maxWidth: 200,
},
intervalOption: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
backgroundColor: '#F0F0F0',
marginLeft: 8,
marginBottom: 8,
},
selectedIntervalOption: {
backgroundColor: '#007AFF',
},
intervalText: {
fontSize: 14,
color: '#333',
},
selectedIntervalText: {
color: 'white',
},
versionText: {
fontSize: 14,
color: '#999',
},
});
export default SettingsScreen;4. 更新导航,添加设置页面
js
// src/navigation/AppNavigator.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import HomeScreen from '../screens/HomeScreen';
import NewsDetailScreen from '../screens/NewsDetailScreen';
import FavoritesScreen from '../screens/FavoritesScreen';
import SearchScreen from '../screens/SearchScreen';
import SettingsScreen from '../screens/SettingsScreen';
import LoginScreen from '../screens/LoginScreen';
const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
const HomeStack = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="HomeMain" component={HomeScreen} />
<Stack.Screen name="NewsDetail" component={NewsDetailScreen} />
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
);
};
const AppNavigator = () => {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
headerShown: false,
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Search') {
iconName = focused ? 'search' : 'search-outline';
} else if (route.name === 'Favorites') {
iconName = focused ? 'heart' : 'heart-outline';
} else if (route.name === 'Settings') {
iconName = focused ? 'settings' : 'settings-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeStack} options={{ title: '首页' }} />
<Tab.Screen name="Search" component={SearchScreen} options={{ title: '搜索' }} />
<Tab.Screen name="Favorites" component={FavoritesScreen} options={{ title: '收藏' }} />
<Tab.Screen name="Settings" component={SettingsScreen} options={{ title: '设置' }} />
</Tab.Navigator>
</NavigationContainer>
);
};
export default AppNavigator;5. 创建登录页面
js
// src/screens/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { useDispatch } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { login } from '../redux/slices/userSlice';
const LoginScreen = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = () => {
setLoading(true);
// 模拟登录/注册
setTimeout(() => {
const userInfo = {
id: '1',
name: name || '用户',
email: email,
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
};
dispatch(login(userInfo));
navigation.goBack();
setLoading(false);
}, 1000);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<Text style={styles.title}>{isLogin ? '登录' : '注册'}</Text>
<Text style={styles.subtitle}>
{isLogin ? '欢迎回来' : '创建新账户'}
</Text>
</View>
<View style={styles.form}>
{!isLogin && (
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>姓名</Text>
<TextInput
style={styles.input}
placeholder="请输入姓名"
placeholderTextColor="#999"
value={name}
onChangeText={setName}
/>
</View>
)}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>邮箱</Text>
<TextInput
style={styles.input}
placeholder="请输入邮箱"
placeholderTextColor="#999"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>密码</Text>
<TextInput
style={styles.input}
placeholder="请输入密码"
placeholderTextColor="#999"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
disabled={loading}
>
<Text style={styles.submitButtonText}>
{loading ? '处理中...' : isLogin ? '登录' : '注册'}
</Text>
</TouchableOpacity>
<View style={styles.switchContainer}>
<Text style={styles.switchText}>
{isLogin ? '还没有账户?' : '已有账户?'}
</Text>
<TouchableOpacity onPress={() => setIsLogin(!isLogin)}>
<Text style={styles.switchLink}>
{isLogin ? '立即注册' : '立即登录'}
</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
paddingVertical: 32,
},
header: {
marginTop: 40,
marginBottom: 40,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#999',
},
form: {
flex: 1,
},
inputContainer: {
marginBottom: 20,
},
inputLabel: {
fontSize: 14,
fontWeight: '500',
color: '#333',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 12,
fontSize: 16,
color: '#333',
},
submitButton: {
backgroundColor: '#007AFF',
borderRadius: 8,
paddingVertical: 14,
alignItems: 'center',
marginTop: 24,
},
submitButtonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
switchContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
},
switchText: {
fontSize: 14,
color: '#999',
},
switchLink: {
fontSize: 14,
color: '#007AFF',
marginLeft: 4,
},
});
export default LoginScreen;6. 实现主题切换
6.1 创建主题上下文
js
// src/contexts/ThemeContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { useSelector } from 'react-redux';
const ThemeContext = createContext();
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
export const ThemeProvider = ({ children }) => {
const systemColorScheme = useColorScheme();
const { theme: themeSetting } = useSelector(state => state.settings);
const [isDarkMode, setIsDarkMode] = useState(
themeSetting === 'system' ? systemColorScheme === 'dark' : themeSetting === 'dark'
);
useEffect(() => {
if (themeSetting === 'system') {
setIsDarkMode(systemColorScheme === 'dark');
} else {
setIsDarkMode(themeSetting === 'dark');
}
}, [themeSetting, systemColorScheme]);
const theme = {
colors: {
primary: '#4F46E5',
secondary: '#10B981',
background: isDarkMode ? '#121212' : '#F3F4F6',
surface: isDarkMode ? '#1E1E1E' : '#FFFFFF',
text: isDarkMode ? '#FFFFFF' : '#1F2937',
textSecondary: isDarkMode ? '#A0AEC0' : '#6B7280',
border: isDarkMode ? '#2D3748' : '#E5E7EB',
error: '#EF4444',
success: '#10B981',
warning: '#F59E0B',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
xl: 16,
},
shadows: {
sm: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
md: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
},
};
return (
<ThemeContext.Provider value={{ theme, isDarkMode }}>
{children}
</ThemeContext.Provider>
);
};6.2 更新 App.js,添加主题提供者
js
// App.js
import React, { useEffect } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider, useDispatch } from 'react-redux';
import { StatusBar } from 'expo-status-bar';
import { store } from './src/redux/store';
import AppNavigator from './src/navigation/AppNavigator';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { loadSettings } from './src/redux/slices/settingsSlice';
import { loadFavorites } from './src/redux/slices/favoritesSlice';
import { loadUser } from './src/redux/slices/userSlice';
// 初始化应用数据
const AppInitializer = () => {
const dispatch = useDispatch();
useEffect(() => {
// 加载设置
dispatch(loadSettings());
// 加载收藏
dispatch(loadFavorites());
// 加载用户信息
dispatch(loadUser());
}, [dispatch]);
return null;
};
// 主题化的应用组件
const ThemedApp = () => {
const { isDarkMode } = useTheme();
return (
<>
<StatusBar style={isDarkMode ? 'light' : 'dark'} />
<AppNavigator />
<AppInitializer />
</>
);
};
export default function App() {
return (
<Provider store={store}>
<SafeAreaProvider>
<ThemeProvider>
<ThemedApp />
</ThemeProvider>
</SafeAreaProvider>
</Provider>
);
}7. 优化性能
7.1 使用 useSelector 的缓存功能
js
// src/components/NewsList.js
import { useSelector } from 'react-redux';
// 优化前
const { headlines, loading, error } = useSelector(state => state.news);
// 优化后
const headlines = useSelector(state => state.news.headlines);
const loading = useSelector(state => state.news.loading);
const error = useSelector(state => state.news.error);7.2 使用 useCallback 和 useMemo
js
// src/components/NewsList.js
import { useCallback, useMemo } from 'react';
// 使用 useCallback 缓存回调函数
const handleCategoryChange = useCallback((categoryId) => {
dispatch(setCategory(categoryId));
}, [dispatch]);
// 使用 useMemo 缓存计算结果
const filteredHeadlines = useMemo(() => {
return headlines.filter(item => item.title.includes(searchQuery));
}, [headlines, searchQuery]);7.3 使用 createEntityAdapter 管理规范化数据
js
// src/redux/slices/newsSlice.js
import { createEntityAdapter } from '@reduxjs/toolkit';
const newsAdapter = createEntityAdapter({
selectId: (news) => news.url,
sortComparer: (a, b) => new Date(b.publishedAt) - new Date(a.publishedAt),
});
const initialState = newsAdapter.getInitialState({
loading: false,
error: null,
selectedCategory: 'general',
selectedCountry: 'us',
});
const newsSlice = createSlice({
name: 'news',
initialState,
reducers: {
// 其他 reducers
},
extraReducers: (builder) => {
builder
.addCase(fetchTopHeadlines.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTopHeadlines.fulfilled, (state, action) => {
state.loading = false;
newsAdapter.setAll(state, action.payload);
})
.addCase(fetchTopHeadlines.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
// 导出选择器
export const {
selectAll: selectAllNews,
selectById: selectNewsById,
selectIds: selectNewsIds,
} = newsAdapter.getSelectors((state) => state.news);8. 总结
通过本章节的学习,我们掌握了如何使用 Redux Toolkit 实现全局状态管理,包括:
- 如何优化全局状态结构
- 如何创建用户状态管理
- 如何创建设置状态管理
- 如何实现主题切换
- 如何优化性能
这些技能将帮助我们构建更加健壮、可维护的 React Native 应用。
9. 下一步
接下来,我们将学习如何打包和发布应用到应用商店,完成整个开发流程。
