Appearance
全局状态共享实战
第六部分:数据请求与状态管理
在实际开发中,全局状态管理是一个重要的部分。本文将通过一个完整的实战案例,展示如何使用 Redux Toolkit 来实现全局状态共享,包括用户认证、数据管理等功能。
1. 项目结构
src/
app/
store.js
hooks.js
features/
auth/
authSlice.js
AuthScreen.js
ProtectedRoute.js
posts/
postsSlice.js
PostsList.js
PostDetail.js
users/
usersSlice.js
UserProfile.js
components/
LoadingSpinner.js
ErrorMessage.js
navigation/
AppNavigator.js
utils/
api.js
storage.js2. 认证状态管理
2.1 创建 Auth Slice
jsx
// src/features/auth/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { apiService } from '../../utils/api';
import { storageService } from '../../utils/storage';
export const login = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await apiService.post('/auth/login', credentials);
// 保存 token 到本地存储
await storageService.setToken(response.data.token);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '登录失败');
}
}
);
export const register = createAsyncThunk(
'auth/register',
async (userData, { rejectWithValue }) => {
try {
const response = await apiService.post('/auth/register', userData);
// 保存 token 到本地存储
await storageService.setToken(response.data.token);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '注册失败');
}
}
);
export const logout = createAsyncThunk(
'auth/logout',
async () => {
// 从本地存储中删除 token
await storageService.removeToken();
}
);
export const checkAuth = createAsyncThunk(
'auth/checkAuth',
async (_, { rejectWithValue }) => {
try {
const token = await storageService.getToken();
if (!token) {
return rejectWithValue('未登录');
}
const response = await apiService.get('/auth/me');
return response.data;
} catch (error) {
// 如果 token 无效,从本地存储中删除
await storageService.removeToken();
return rejectWithValue('登录已过期');
}
}
);
const initialState = {
user: null,
token: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
isAuthenticated: false,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// 登录
.addCase(login.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
})
.addCase(login.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '登录失败';
})
// 注册
.addCase(register.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(register.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
})
.addCase(register.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '注册失败';
})
// 登出
.addCase(logout.fulfilled, (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
state.status = 'idle';
})
// 检查认证状态
.addCase(checkAuth.pending, (state) => {
state.status = 'loading';
})
.addCase(checkAuth.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload;
state.isAuthenticated = true;
})
.addCase(checkAuth.rejected, (state) => {
state.status = 'idle';
state.user = null;
state.token = null;
state.isAuthenticated = false;
});
},
});
export const { clearError } = authSlice.actions;
export default authSlice.reducer;2.2 登录屏幕
jsx
// src/features/auth/AuthScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { login, register, clearError } from './authSlice';
export default function AuthScreen({ navigation }) {
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const dispatch = useDispatch();
const { status, error, isAuthenticated } = useSelector((state) => state.auth);
React.useEffect(() => {
if (isAuthenticated) {
navigation.replace('Main');
}
}, [isAuthenticated, navigation]);
const handleSubmit = async () => {
if (!email || !password) {
Alert.alert('错误', '请填写邮箱和密码');
return;
}
if (!isLogin && !name) {
Alert.alert('错误', '请填写姓名');
return;
}
dispatch(clearError());
if (isLogin) {
dispatch(login({ email, password }));
} else {
dispatch(register({ name, email, password }));
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>{isLogin ? '登录' : '注册'}</Text>
{!isLogin && (
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="姓名"
marginBottom={10}
/>
)}
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="邮箱"
keyboardType="email-address"
autoCapitalize="none"
marginBottom={10}
/>
<TextInput
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="密码"
secureTextEntry
marginBottom={20}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
<TouchableOpacity
style={[styles.button, status === 'loading' && styles.buttonDisabled]}
onPress={handleSubmit}
disabled={status === 'loading'}
>
<Text style={styles.buttonText}>
{status === 'loading' ? '处理中...' : isLogin ? '登录' : '注册'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.switchButton}
onPress={() => setIsLogin(!isLogin)}
>
<Text style={styles.switchButtonText}>
{isLogin ? '没有账号?注册' : '已有账号?登录'}
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 30,
textAlign: 'center',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
borderRadius: 8,
},
button: {
backgroundColor: '#4CAF50',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 10,
},
buttonDisabled: {
backgroundColor: '#9e9e9e',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
switchButton: {
marginTop: 20,
alignItems: 'center',
},
switchButtonText: {
color: '#2196F3',
fontSize: 16,
},
errorText: {
color: 'red',
fontSize: 14,
marginBottom: 10,
textAlign: 'center',
},
});2.3 受保护的路由
jsx
// src/features/auth/ProtectedRoute.js
import React from 'react';
import { useSelector } from 'react-redux';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import AuthScreen from './AuthScreen';
import MainNavigator from '../../navigation/MainNavigator';
import LoadingSpinner from '../../components/LoadingSpinner';
const Stack = createStackNavigator();
export default function ProtectedRoute() {
const { isAuthenticated, status } = useSelector((state) => state.auth);
if (status === 'loading') {
return <LoadingSpinner />;
}
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="Main" component={MainNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
);
}3. 帖子状态管理
3.1 创建 Posts Slice
jsx
// src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { apiService } from '../../utils/api';
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async (_, { rejectWithValue }) => {
try {
const response = await apiService.get('/posts');
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '获取帖子失败');
}
}
);
export const fetchPostById = createAsyncThunk(
'posts/fetchPostById',
async (postId, { rejectWithValue }) => {
try {
const response = await apiService.get(`/posts/${postId}`);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '获取帖子详情失败');
}
}
);
export const createPost = createAsyncThunk(
'posts/createPost',
async (postData, { rejectWithValue }) => {
try {
const response = await apiService.post('/posts', postData);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '创建帖子失败');
}
}
);
export const updatePost = createAsyncThunk(
'posts/updatePost',
async ({ id, postData }, { rejectWithValue }) => {
try {
const response = await apiService.put(`/posts/${id}`, postData);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '更新帖子失败');
}
}
);
export const deletePost = createAsyncThunk(
'posts/deletePost',
async (postId, { rejectWithValue }) => {
try {
await apiService.delete(`/posts/${postId}`);
return postId;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '删除帖子失败');
}
}
);
const initialState = {
posts: [],
currentPost: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
export const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
clearCurrentPost: (state) => {
state.currentPost = null;
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// 获取帖子列表
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.posts = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '获取帖子失败';
})
// 获取帖子详情
.addCase(fetchPostById.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchPostById.fulfilled, (state, action) => {
state.status = 'succeeded';
state.currentPost = action.payload;
})
.addCase(fetchPostById.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '获取帖子详情失败';
})
// 创建帖子
.addCase(createPost.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(createPost.fulfilled, (state, action) => {
state.status = 'succeeded';
state.posts.unshift(action.payload);
})
.addCase(createPost.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '创建帖子失败';
})
// 更新帖子
.addCase(updatePost.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(updatePost.fulfilled, (state, action) => {
state.status = 'succeeded';
state.posts = state.posts.map(post =>
post.id === action.payload.id ? action.payload : post
);
if (state.currentPost && state.currentPost.id === action.payload.id) {
state.currentPost = action.payload;
}
})
.addCase(updatePost.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '更新帖子失败';
})
// 删除帖子
.addCase(deletePost.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(deletePost.fulfilled, (state, action) => {
state.status = 'succeeded';
state.posts = state.posts.filter(post => post.id !== action.payload);
})
.addCase(deletePost.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '删除帖子失败';
});
},
});
export const { clearCurrentPost, clearError } = postsSlice.actions;
export default postsSlice.reducer;3.2 帖子列表
jsx
// src/features/posts/PostsList.js
import React, { useEffect } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts, deletePost } from './postsSlice';
import LoadingSpinner from '../../components/LoadingSpinner';
import ErrorMessage from '../../components/ErrorMessage';
export default function PostsList({ navigation }) {
const dispatch = useDispatch();
const { posts, status, error } = useSelector((state) => state.posts);
const { user } = useSelector((state) => state.auth);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchPosts());
}
}, [status, dispatch]);
const handleDelete = (postId) => {
Alert.alert(
'确认删除',
'确定要删除这篇帖子吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => dispatch(deletePost(postId)),
},
]
);
};
if (status === 'loading') {
return <LoadingSpinner />;
}
if (status === 'failed') {
return <ErrorMessage message={error} />;
}
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.addButton}
onPress={() => navigation.navigate('CreatePost')}
>
<Text style={styles.addButtonText}>+ 创建帖子</Text>
</TouchableOpacity>
<FlatList
data={posts}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.post}
onPress={() => navigation.navigate('PostDetail', { postId: item.id })}
>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.body} numberOfLines={2}>{item.body}</Text>
<Text style={styles.author}>作者: {item.author.name}</Text>
{user && user.id === item.author.id && (
<View style={styles.actions}>
<TouchableOpacity
style={styles.editButton}
onPress={() => navigation.navigate('EditPost', { postId: item.id })}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleDelete(item.id)}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</View>
)}
</TouchableOpacity>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
addButton: {
backgroundColor: '#4CAF50',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginBottom: 15,
},
addButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
post: {
backgroundColor: '#f9f9f9',
padding: 15,
borderRadius: 8,
marginBottom: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
body: {
fontSize: 14,
color: '#666',
marginBottom: 10,
},
author: {
fontSize: 12,
color: '#999',
marginBottom: 10,
},
actions: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
editButton: {
marginRight: 15,
},
editButtonText: {
color: '#2196F3',
fontSize: 14,
},
deleteButton: {
},
deleteButtonText: {
color: '#f44336',
fontSize: 14,
},
});3.3 帖子详情
jsx
// src/features/posts/PostDetail.js
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPostById, clearCurrentPost } from './postsSlice';
import LoadingSpinner from '../../components/LoadingSpinner';
import ErrorMessage from '../../components/ErrorMessage';
export default function PostDetail({ route, navigation }) {
const { postId } = route.params;
const dispatch = useDispatch();
const { currentPost, status, error } = useSelector((state) => state.posts);
useEffect(() => {
dispatch(fetchPostById(postId));
return () => {
dispatch(clearCurrentPost());
};
}, [postId, dispatch]);
if (status === 'loading') {
return <LoadingSpinner />;
}
if (status === 'failed') {
return <ErrorMessage message={error} />;
}
if (!currentPost) {
return <ErrorMessage message="帖子不存在" />;
}
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>{currentPost.title}</Text>
<Text style={styles.author}>作者: {currentPost.author.name}</Text>
<Text style={styles.date}>{new Date(currentPost.createdAt).toLocaleString()}</Text>
<Text style={styles.body}>{currentPost.body}</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 15,
},
author: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
date: {
fontSize: 12,
color: '#999',
marginBottom: 20,
},
body: {
fontSize: 16,
lineHeight: 24,
},
});4. 用户状态管理
4.1 创建 Users Slice
jsx
// src/features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { apiService } from '../../utils/api';
export const fetchUserById = createAsyncThunk(
'users/fetchUserById',
async (userId, { rejectWithValue }) => {
try {
const response = await apiService.get(`/users/${userId}`);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '获取用户信息失败');
}
}
);
export const updateUser = createAsyncThunk(
'users/updateUser',
async ({ userId, userData }, { rejectWithValue }) => {
try {
const response = await apiService.put(`/users/${userId}`, userData);
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data?.message || '更新用户信息失败');
}
}
);
const initialState = {
users: {},
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// 获取用户信息
.addCase(fetchUserById.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users[action.payload.id] = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '获取用户信息失败';
})
// 更新用户信息
.addCase(updateUser.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(updateUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users[action.payload.id] = action.payload;
})
.addCase(updateUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || '更新用户信息失败';
});
},
});
export const { clearError } = usersSlice.actions;
export default usersSlice.reducer;4.2 用户资料
jsx
// src/features/users/UserProfile.js
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, Alert } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUserById, updateUser, clearError } from './usersSlice';
import { logout } from '../auth/authSlice';
import LoadingSpinner from '../../components/LoadingSpinner';
import ErrorMessage from '../../components/ErrorMessage';
export default function UserProfile({ navigation }) {
const dispatch = useDispatch();
const { user: currentUser } = useSelector((state) => state.auth);
const { users, status, error } = useSelector((state) => state.users);
const user = users[currentUser?.id];
const [name, setName] = React.useState('');
const [email, setEmail] = React.useState('');
const [isEditing, setIsEditing] = React.useState(false);
useEffect(() => {
if (currentUser) {
dispatch(fetchUserById(currentUser.id));
}
}, [currentUser, dispatch]);
useEffect(() => {
if (user) {
setName(user.name);
setEmail(user.email);
}
}, [user]);
const handleUpdate = () => {
if (!name || !email) {
Alert.alert('错误', '请填写所有字段');
return;
}
dispatch(clearError());
dispatch(updateUser({ userId: currentUser.id, userData: { name, email } }))
.unwrap()
.then(() => {
Alert.alert('成功', '个人资料更新成功');
setIsEditing(false);
})
.catch((error) => {
console.error('更新失败:', error);
});
};
const handleLogout = () => {
Alert.alert(
'确认登出',
'确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '登出',
style: 'destructive',
onPress: () => dispatch(logout()),
},
]
);
};
if (status === 'loading' && !user) {
return <LoadingSpinner />;
}
if (status === 'failed') {
return <ErrorMessage message={error} />;
}
if (!user) {
return <ErrorMessage message="用户信息不存在" />;
}
return (
<View style={styles.container}>
<Text style={styles.title}>个人资料</Text>
{isEditing ? (
<View style={styles.form}>
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="姓名"
marginBottom={15}
/>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="邮箱"
keyboardType="email-address"
autoCapitalize="none"
marginBottom={20}
/>
<TouchableOpacity
style={[styles.button, status === 'loading' && styles.buttonDisabled]}
onPress={handleUpdate}
disabled={status === 'loading'}
>
<Text style={styles.buttonText}>
{status === 'loading' ? '更新中...' : '保存'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setIsEditing(false);
setName(user.name);
setEmail(user.email);
}}
>
<Text style={styles.cancelButtonText}>取消</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.profile}>
<View style={styles.profileItem}>
<Text style={styles.label}>姓名:</Text>
<Text style={styles.value}>{user.name}</Text>
</View>
<View style={styles.profileItem}>
<Text style={styles.label}>邮箱:</Text>
<Text style={styles.value}>{user.email}</Text>
</View>
<View style={styles.profileItem}>
<Text style={styles.label}>注册时间:</Text>
<Text style={styles.value}>{new Date(user.createdAt).toLocaleString()}</Text>
</View>
<TouchableOpacity
style={styles.editButton}
onPress={() => setIsEditing(true)}
>
<Text style={styles.editButtonText}>编辑资料</Text>
</TouchableOpacity>
</View>
)}
<TouchableOpacity
style={styles.logoutButton}
onPress={handleLogout}
>
<Text style={styles.logoutButtonText}>退出登录</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 30,
textAlign: 'center',
},
profile: {
marginBottom: 30,
},
profileItem: {
flexDirection: 'row',
marginBottom: 15,
},
label: {
fontSize: 16,
fontWeight: '500',
width: 80,
},
value: {
fontSize: 16,
flex: 1,
},
form: {
marginBottom: 30,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
borderRadius: 8,
},
button: {
backgroundColor: '#4CAF50',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
},
buttonDisabled: {
backgroundColor: '#9e9e9e',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
cancelButton: {
padding: 15,
alignItems: 'center',
},
cancelButtonText: {
color: '#666',
fontSize: 16,
},
editButton: {
backgroundColor: '#2196F3',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 20,
},
editButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
logoutButton: {
backgroundColor: '#f44336',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 'auto',
},
logoutButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
});5. 导航配置
5.1 主导航
jsx
// src/navigation/MainNavigator.js
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createStackNavigator } from '@react-navigation/stack';
import { Text, View } from 'react-native';
import PostsList from '../features/posts/PostsList';
import PostDetail from '../features/posts/PostDetail';
import CreatePost from '../features/posts/CreatePost';
import EditPost from '../features/posts/EditPost';
import UserProfile from '../features/users/UserProfile';
const Tab = createBottomTabNavigator();
const PostStack = createStackNavigator();
function PostStackNavigator() {
return (
<PostStack.Navigator>
<PostStack.Screen
name="PostsList"
component={PostsList}
options={{ title: '帖子' }}
/>
<PostStack.Screen
name="PostDetail"
component={PostDetail}
options={{ title: '帖子详情' }}
/>
<PostStack.Screen
name="CreatePost"
component={CreatePost}
options={{ title: '创建帖子' }}
/>
<PostStack.Screen
name="EditPost"
component={EditPost}
options={{ title: '编辑帖子' }}
/>
</PostStack.Navigator>
);
}
export default function MainNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={PostStackNavigator}
options={{ title: '首页' }}
/>
<Tab.Screen
name="Profile"
component={UserProfile}
options={{ title: '我的' }}
/>
</Tab.Navigator>
);
}6. 工具函数
6.1 API 服务
jsx
// src/utils/api.js
import axios from 'axios';
import { storageService } from './storage';
const API_BASE_URL = 'https://api.example.com';
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
async (config) => {
// 添加认证 token
const token = await storageService.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// 处理错误
if (error.response?.status === 401) {
// 处理认证错误
storageService.removeToken();
// 导航到登录页
}
return Promise.reject(error);
}
);
// API 方法
export const apiService = {
// GET 请求
get: (url, params) => api.get(url, { params }),
// POST 请求
post: (url, data) => api.post(url, data),
// PUT 请求
put: (url, data) => api.put(url, data),
// DELETE 请求
delete: (url) => api.delete(url),
};6.2 存储服务
jsx
// src/utils/storage.js
import AsyncStorage from '@react-native-async-storage/async-storage';
const TOKEN_KEY = 'auth_token';
export const storageService = {
// 保存 token
setToken: async (token) => {
try {
await AsyncStorage.setItem(TOKEN_KEY, token);
} catch (error) {
console.error('保存 token 失败:', error);
}
},
// 获取 token
getToken: async () => {
try {
return await AsyncStorage.getItem(TOKEN_KEY);
} catch (error) {
console.error('获取 token 失败:', error);
return null;
}
},
// 删除 token
removeToken: async () => {
try {
await AsyncStorage.removeItem(TOKEN_KEY);
} catch (error) {
console.error('删除 token 失败:', error);
}
},
// 保存数据
setItem: async (key, value) => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
} catch (error) {
console.error('保存数据失败:', error);
}
},
// 获取数据
getItem: async (key) => {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error('获取数据失败:', error);
return null;
}
},
// 删除数据
removeItem: async (key) => {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('删除数据失败:', error);
}
},
};6.3 通用组件
jsx
// src/components/LoadingSpinner.js
import React from 'react';
import { View, ActivityIndicator, Text, StyleSheet } from 'react-native';
export default function LoadingSpinner({ message = '加载中...' }) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#4CAF50" />
<Text style={styles.message}>{message}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
message: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
});
// src/components/ErrorMessage.js
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
export default function ErrorMessage({ message, onRetry }) {
return (
<View style={styles.container}>
<Text style={styles.message}>{message}</Text>
{onRetry && (
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
<Text style={styles.retryButtonText}>重试</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
padding: 20,
},
message: {
fontSize: 16,
color: 'red',
textAlign: 'center',
marginBottom: 20,
},
retryButton: {
backgroundColor: '#4CAF50',
padding: 10,
borderRadius: 5,
},
retryButtonText: {
color: '#fff',
fontSize: 16,
},
});7. 应用入口
jsx
// App.js
import React, { useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
import { Provider } from 'react-redux';
import { store } from './src/app/store';
import ProtectedRoute from './src/features/auth/ProtectedRoute';
import { checkAuth } from './src/features/auth/authSlice';
export default function App() {
useEffect(() => {
// 检查认证状态
store.dispatch(checkAuth());
}, []);
return (
<Provider store={store}>
<StatusBar style="auto" />
<ProtectedRoute />
</Provider>
);
}
// src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import postsReducer from '../features/posts/postsSlice';
import usersReducer from '../features/users/usersSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
posts: postsReducer,
users: usersReducer,
},
});8. 最佳实践
8.1 状态管理最佳实践
- 模块化:将状态按功能模块分割成不同的 slice
- 单一职责:每个 slice 只负责管理相关的状态
- 异步操作:使用
createAsyncThunk处理异步操作 - 错误处理:在每个 slice 中处理错误状态
- 选择器:使用
createSelector优化状态选择
8.2 性能优化
- 避免不必要的 re-renders:使用
useSelector选择最小的状态片段 - 缓存:使用
createSelector缓存计算结果 - 批量更新:使用
batch批量处理多个 actions - 防抖和节流:对频繁触发的操作使用防抖和节流
8.3 安全
- token 管理:使用安全的方式存储 token
- API 调用:使用拦截器统一处理认证
- 输入验证:在客户端和服务器端都进行输入验证
- 错误处理:不要在生产环境中暴露详细的错误信息
8.4 代码组织
- 目录结构:按功能模块组织代码
- 命名规范:使用一致的命名规范
- 注释:为复杂的逻辑添加注释
- 测试:为关键功能编写测试
9. 总结
通过本文的实战案例,你应该掌握了以下内容:
- 如何使用 Redux Toolkit 管理全局状态
- 如何处理用户认证
- 如何管理帖子数据
- 如何管理用户信息
- 如何实现导航和路由保护
- 如何封装 API 服务和存储服务
- 如何创建通用组件
- 状态管理的最佳实践
在实际开发中,合理使用 Redux Toolkit 可以创建出更加可预测、可测试和可维护的应用状态管理系统,提升应用的整体质量和开发效率。
