当前位置: 首页 > article >正文

TypeScript+React+Redux:类型安全的状态管理最佳实践

前言

在现代前端开发中,React 作为最流行的 UI 库之一,以其组件化和声明式编程的优势深受开发者喜爱。然而,随着应用规模的扩大,组件之间的状态管理变得越来越复杂。如何在保证代码可维护性的同时,高效地管理全局状态,成为了每个 React 开发者必须面对的挑战。

Redux 作为 React 生态中最经典的状态管理工具,提供了一种可预测的状态管理方案。但随着 Redux 生态的演进,开发者们逐渐发现,传统的 Redux 开发模式存在大量样板代码,学习曲线陡峭,甚至可能让项目陷入 "过度设计" 的陷阱。

本文将带你从零开始,深入探讨 React + Redux 的状态管理演进之路。我们将从最简单的组件状态管理出发,逐步引入 Redux,并借助 Redux Toolkit 等现代工具,打造一个高效、可维护的状态管理架构。无论你是 Redux 新手,还是希望优化现有项目的开发者,相信本文都能为你带来新的启发。

优势

  • 增强的代码质量与可靠性:使用TypeScript为React组件和Redux状态提供了静态类型检查,可以在开发阶段就捕捉到许多潜在错误,如属性类型不匹配或状态访问错误等。这有助于提高应用程序的整体可靠性和健壮性。
  • 更好的可维护性:TypeScript的强类型系统使得代码更加清晰易懂,特别是对于大型项目或者团队协作时。明确的数据结构定义减少了理解成本,让新加入项目的开发者能更快上手。
  • 简化复杂状态管理: Redux帮助集中和管理应用的状态,而TypeScript确保了这些状态在被操作时类型的正确性。结合两者,可以更轻松地处理复杂的业务逻辑而不牺牲类型安全。
  • 改进的开发体验:通过TypeScript,开发者可以获得智能感知(IntelliSense)支持,包括自动完成和内联文档查看等功能,极大地提升了编码效率。同时,由于类型错误会在编译期就被发现,减少了调试时间。
  • 促进更好的架构设计:在使用TypeScript编写React组件和Redux reducer时,开发者往往会倾向于创建更加模块化、组织良好的代码结构。这种倾向促进了良好软件设计原则的应用,如单一职责原则等。
  • 社区支持与生态系统:TypeScript拥有活跃的社区和丰富的库支持,尤其是在与React和Redux集成方面。这意味着你可以找到大量的教程、指南以及开源解决方案来解决遇到的问题。
  • 未来兼容性:随着JavaScript生态系统的不断发展,TypeScript作为其超集,能够无缝适应新的语言特性和模式。这意味着投资于TypeScript是面向未来的,有助于保持技术栈的现代化。

实现步骤

下载第三方包

安装redux全局状态管理包

npm install @reduxjs/toolkit

 安装持久化存储

npm install redux-persist

这里我测试用的是登录的测试用例,所以我这里创建的是authSlice.tsx,这个根据业务场景的需求创建

// authSlice.tsx

import { createSlice } from '@reduxjs/toolkit'

// 定义一个接口,用于描述认证状态
interface AuthState {
  isLoggedIn: boolean
}

// 定义初始状态,表示用户未登录
const initialState: AuthState = {
  isLoggedIn: false,
}

// 创建一个切片,用于管理用户认证状态
const authSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    // 登录操作,将isLoggedIn状态设置为true
    login: (state) => {
      state.isLoggedIn = true
    },
    // 登出操作,将isLoggedIn状态设置为false
    logout: (state) => {
      state.isLoggedIn = false
    },
  },
})

// 导出登录和登出的action
export const { login, logout } = authSlice.actions

// 导出切片
export default authSlice

然后创建一个文件来处理本地持久化存储数据的index.tsx

// index.tsx

import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // 默认使用 localStorage
import authReducer from './slices/authSlice'
import { combineReducers } from 'redux'

// 配置redux-persist,
// 指定存储方式、存储位置、需要持久化的reducer
const persistConfig = {
  key: 'root', // 存储的键名
  storage, // 指定存储方式,这里使用localStorage
  whitelist: ['authReducer'], // 指定需要持久化的reducer
  blacklist: [], // 写在这块的数据不会存在storage
}

// 创建一个根reducer,将所有的reducer合并在一起
const reducers = combineReducers({
  authReducer: authReducer.reducer,
})

// 创建持久化的reducer
const persistedReducer = persistReducer(persistConfig, reducers)

// 导出一个名为store的常量,该常量是一个配置好的Redux store
export const store = configureStore({
  // 将persistedReducer作为reducer
  reducer: persistedReducer,
  // 配置中间件,getDefaultMiddleware是一个函数,用于获取默认的中间件
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      // 关闭序列化检查
      serializableCheck: false,
    }),
})

// 导出包裹
export const persist = persistStore(store)

// 导出类型
export type RootState = ReturnType<typeof store.getState>

然后在入口文件main.tsx里调用

// main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store, persist } from './store'
import App from './App'
import { PersistGate } from 'redux-persist/integration/react'
import './service/mock'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persist}>
        <App />
      </PersistGate>
    </Provider>
  </StrictMode>
)

在配置路由时获取数据来限制访问路由

// PrivateRoute.tsx

import { Navigate, Outlet } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from '@/store/index'

interface PrivateRouteProps {
  children: JSX.Element
}

const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
  const isLoggedIn = useSelector((state: RootState) => state.authReducer.isLoggedIn)

  return isLoggedIn ? children : <Navigate to="/login" />
}

export default PrivateRoute

登陆时存储本地数据状态

// LoginPage.tsx    

import React, { useState } from 'react'
import { Button, Form, Input, Typography } from 'antd'
import { useDispatch } from 'react-redux'
import { login } from '@/store/slices/authSlice'
import { useNavigate } from 'react-router-dom'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import axios from 'axios'

const { Title } = Typography

const Login: React.FC = () => {
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const dispatch = useDispatch()
  const navigate = useNavigate()

  const doLogin = async (username: string, password: string) => {
    try {
      const response = await axios.post('/api/login', { username, password })
      if (response.data.code == 200) {
        dispatch(login())
        navigate('/')
        return
      }

      alert('用户名或密码错误')
      console.log('登录结果:', response.data)
    } catch (error) {
      console.error('登录失败:', error)
    }
  }

  const onFinish = () => {
    doLogin(username, password)
  }

  return (
    <div style={
  
  { display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
      <Form name="normal_login" className="login-form" initialValues={
  
  { remember: true }} onFinish={onFinish}>
        <Title level={2} style={
  
  { textAlign: 'center' }}>
          登录
        </Title>
        <Form.Item name="username" rules={[{ required: true, message: '请输入用户名!' }]}>
          <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="用户名" value={username} onChange={(e) => setUsername(e.target.value)} />
        </Form.Item>
        <Form.Item name="password" rules={[{ required: true, message: '请输入密码!' }]}>
          <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="密码" value={password} onChange={(e) => setPassword(e.target.value)} />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" className="login-form-button">
            登录
          </Button>
        </Form.Item>
      </Form>
    </div>
  )
}

export default Login

结语

  1. 始终定义明确的类型

    为 State、Action 和 Payload 定义清晰的类型,避免使用 any

  2. 利用 Redux Toolkit 的类型推断

    createSlice 和 createAsyncThunk 可以自动生成类型,减少手动定义的工作量。

  3. 自定义类型化的 Hooks

    封装 useAppSelector 和 useAppDispatch,提升代码复用性。

  4. 使用工具函数简化类型定义

    例如 PayloadAction 和 TypedUseSelectorHook,减少重复代码。

  5. 保持类型与业务逻辑的一致性

    当业务逻辑发生变化时,及时更新类型定义。


http://www.kler.cn/a/535541.html

相关文章:

  • 网络面试题(第一部分)
  • ONLYOFFICE 文档 8.3 已发布:PDF 图章、合并形状、更多格式支持等
  • el-table中的某个字段最多显示两行,超出部分显示“...详情”,怎么办
  • 【教程】docker升级镜像
  • 在线教程丨YOLO系列10年更新11个版本,最新模型在目标检测多项任务中达SOTA
  • CSS关系选择器详解
  • MySQL知识大总结(进阶)
  • 如何开设一个Facebook账户:详细步骤与注意事项
  • 人工智能丨利用人工智能与自动化实现高效运营推广
  • 十. Redis 事务和 “锁机制”——> 并发秒杀处理的详细说明
  • python爬虫常用库
  • 深入浅出 NVM:如何管理 Node 版本?
  • 8.[网鼎杯 2020 青龙组]AreUSerialz
  • DeepSeek技术报告解析:为什么DeepSeek-R1 可以用低成本训练出高效的模型
  • Beans模块之工厂模块注解模块InitDestroyAnnotationBeanPostProcessor
  • PostgreSQL存储过程和执行
  • 【工具篇】深度揭秘 Midjourney:开启 AI 图像创作新时代
  • 备战蓝桥杯-洛谷
  • 使用Python的Tabulate库优雅地格式化表格数据
  • SpringCloud Gateway 动态路由配置全解
  • 如何用大语言模型提高工作效率
  • fio使用手册
  • springboot005-Java沉浸式戏曲文化体验系统
  • Vue基础:计算属性(描述依赖响应式状态的复杂逻辑)
  • 使用Docker安装MongoDB数据库
  • AIoT 未来趋势:机遇与挑战并存