Skip to content

全局状态共享实战

第六部分:数据请求与状态管理

在实际开发中,全局状态管理是一个重要的部分。本文将通过一个完整的实战案例,展示如何使用 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.js

2. 认证状态管理

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 状态管理最佳实践

  1. 模块化:将状态按功能模块分割成不同的 slice
  2. 单一职责:每个 slice 只负责管理相关的状态
  3. 异步操作:使用 createAsyncThunk 处理异步操作
  4. 错误处理:在每个 slice 中处理错误状态
  5. 选择器:使用 createSelector 优化状态选择

8.2 性能优化

  1. 避免不必要的 re-renders:使用 useSelector 选择最小的状态片段
  2. 缓存:使用 createSelector 缓存计算结果
  3. 批量更新:使用 batch 批量处理多个 actions
  4. 防抖和节流:对频繁触发的操作使用防抖和节流

8.3 安全

  1. token 管理:使用安全的方式存储 token
  2. API 调用:使用拦截器统一处理认证
  3. 输入验证:在客户端和服务器端都进行输入验证
  4. 错误处理:不要在生产环境中暴露详细的错误信息

8.4 代码组织

  1. 目录结构:按功能模块组织代码
  2. 命名规范:使用一致的命名规范
  3. 注释:为复杂的逻辑添加注释
  4. 测试:为关键功能编写测试

9. 总结

通过本文的实战案例,你应该掌握了以下内容:

  1. 如何使用 Redux Toolkit 管理全局状态
  2. 如何处理用户认证
  3. 如何管理帖子数据
  4. 如何管理用户信息
  5. 如何实现导航和路由保护
  6. 如何封装 API 服务和存储服务
  7. 如何创建通用组件
  8. 状态管理的最佳实践

在实际开发中,合理使用 Redux Toolkit 可以创建出更加可预测、可测试和可维护的应用状态管理系统,提升应用的整体质量和开发效率。

© 2026 编程马·菜鸟教程 版权所有