Skip to content

redux-toolkit与redux 核心概念(Day8 2/3) ​

本页对应 React + TypeScript + 医通项目,共 5 段视频,约 1 小时 1 分。

学习目标 ​

  • 能使用 NavLink 的 className 回调,根据 isActive 返回自定义高亮类名,并在 CSS 中定义对应高亮样式。
  • 能将路径与路由组件的映射关系抽离为路由表数组,并使用 useRoutes 生成路由配置。
  • 能完成一级路由页面改造:配置 NavLink 导航、路由展示区域及默认重定向。
  • 能通过 children 配置二级路由,使用 Outlet 渲染子路由,并正确区分完整路径与简写路径的写法规则。
  • 能使用 useNavigate 实现编程式导航,并通过第二个参数 replace 或数字参数控制历史记录的前进、后退与替换行为。
  • 能说明 Redux 的核心概念(store、action、reducer),并使用 redux-toolkit 创建 slice/store,在组件中完成状态读取与更新。

知识点 ​

本节资料边界:转写稿与笔记不是同一主题 ​

本节笔记只有两条核心信息:redux-toolkit 是官方提供的 Redux 工具包,作用是对数据状态进行集中式管理;react-redux 是在 React 中更方便使用 Redux 的绑定层。 但给定转写稿实际讲的是 React Router:NavLink 自定义高亮、路由表与 useRoutes、Outlet 二级路由、useNavigate 编程式导航等。它们属于路由层知识,不能硬说成 redux-toolkit 的推导过程,否则就是编造。因此本文只围绕笔记中的 Redux 两条主线扩写;转写稿仅用于说明“资料边界”和纠正同音字,不把路由 API 混入 Redux 教程。本节笔记未附图片和代码,故不插入图片与代码块,避免虚构。

转写稿中需纠正的同音字/误写包括:nameve L、name link 应为 NavLink;bo strap 应为 Bootstrap;use.Roots、use roose 应为 useRoutes;out light 应为 Outlet;nave user naG、use navigate 应为 useNavigate;exactive、is active 应为 isActive;pass 应为 path;eleement 应为 element;重定项、重命项 应为 重定向;local store reach 应为 localStorage;GS 应为 JS。这些纠错只用于说明转写稿主题,不作为 Redux 知识点。

知识点一:redux-toolkit 是官方 Redux 工具包 ​

为什么需要: 当多个组件需要共享同一份数据,且更新逻辑分散时,只靠组件局部状态容易产生“数据源不一致、更新入口太多、排查困难”的问题。Redux 的思路是把这类状态集中管理,而 redux-toolkit 是官方提供的工具包,用来降低原生 Redux 的使用成本。笔记明确它的作用是“对数据状态进行集中式管理”,所以理解重点不是“多了一个新库”,而是“官方给出的 Redux 使用方案”。

怎么想: 先判断状态是否真的需要集中管理。跨页面、跨层级、多个组件共同读写的数据,才适合放进集中式状态;单个组件内部的临时状态,不应为了“统一”而全局化。redux-toolkit 仍然是 Redux 体系,不是 React 专属,也不等于路由工具。转写稿里的 NavLink、useRoutes、Outlet、useNavigate 解决的是“页面跳转与路由展示”,不是“业务数据集中管理”,两者层级不同。

怎么做: 在概念上把它作为 Redux 的官方工具包接入项目:围绕集中式 store 组织状态,把不同业务状态拆成清晰的状态切片,让组件通过统一入口读取和更新。由于本节笔记没有给出具体 API 与代码,本文不虚构 configureStore、createSlice 等代码细节;实际编码时应以官方文档和课程后续代码为准。

易错点: 一是把 redux-toolkit 当成 React 路由或组件库;二是把所有状态都塞进全局,导致简单问题复杂化;三是只记住“工具包”三个字,却忽略它仍然要求单向数据流和清晰的状态边界;四是把转写稿中的 useNavigate 当成 Redux 的跳转方法,这是路由导航,不是状态管理。

知识点二:react-redux 是 React 与 Redux 的绑定层 ​

为什么需要: Redux 本身不依赖 React。React 组件要想方便地接入集中式 store、读取状态、触发更新,就需要一个绑定层。笔记说“react-redux:在 React 中更方便的使用 redux”,关键词是“更方便”,它不是替代 Redux,而是连接 React 与 Redux 的桥梁。

怎么想: 把职责分开:redux-toolkit 负责 Redux 侧的状态组织与工具支持;react-redux 负责让 React 组件树接入 store,并在组件中消费状态、派发更新。这样想可以避免“到底学的是 Redux 还是 React”的混乱。

怎么做: 概念上分三步理解:先有集中式 store;再让 React 组件树能够访问这个 store;最后组件按需读取状态,并通过统一入口触发更新。具体 Provider、selector、dispatch 的写法本节笔记未展开,不能凭空补成“本节代码”。实际项目中应保持“状态归属清晰、更新入口集中、组件只做必要订阅”。

易错点: 一是以为装了 react-redux 就等于有了 Redux 状态;二是把 react-redux 当成路由库;三是组件里到处直接改状态,破坏集中管理;四是订阅范围过大导致不必要的重渲染。转写稿中 useNavigate 的 replace 参数讲的是“替换当前历史记录”,不是 Redux 的状态替换,不能混淆。

知识点三:集中式管理的判断与调试思路 ​

为什么需要: 集中式管理不是为了“看起来高级”,而是为了解决共享状态的一致性和可追踪性。若没有边界,集中式 store 会变成垃圾桶。

怎么想: 先列状态清单:哪些数据跨组件共享?哪些只是表单临时值?哪些由路由决定?路由状态应交给路由层,业务共享状态才考虑 Redux。转写稿的调试过程——如 useRoutes 路径报错、Outlet 展示二级路由、useNavigate 的 replace 影响历史记录——说明排查时要先定位“是路由配置层、组件展示层还是导航行为层”。这个“先分层定位”的思路可以借鉴,但不能把路由调试结论写成 Redux 结论。

怎么做: 按“状态归属 → 更新入口 → 消费组件 → 排查层级”的顺序设计。出现问题时先确认数据是否真的进了集中式 store,再确认组件是否从正确入口读取,最后确认更新是否走了统一流程。

易错点: 重复存储同一份数据;把临时 UI 状态全局化;把路由参数和全局业务状态混在一起;调试时只看组件,不检查状态入口。本节笔记未附图片与代码,因此正文不插入图片、不编造代码块;后续应以课程实际代码为准。

本节说明:转写稿与 Redux 笔记不匹配 ​

先说明一个关键问题:本节笔记标题是“18-1. redux 核心概念”,核心内容是 store、slice、reducer、action、actionCreator、dispatch 六个概念;但提供的转写稿实际讲的是 React Router 相关内容,包括 NavLink 自定义高亮、路由表、useRoutes、Outlet 二级路由、useNavigate 编程式导航等。转写稿中还有大量同音字/误识别,如“nameve L”应为 NavLink,“useRoots”应为 useRoutes,“out light”应为 Outlet,“nave user naG”应为 useNavigate,“exactive”应为 isActive,“处/Foss”应为 true/false,“bo strap”应为 Bootstrap。它们属于路由章节,不能硬套成 Redux 的推导和调试过程。为避免编造,本节只按笔记中已有的 Redux 概念和类比展开;笔记没有给出代码片段,因此不虚构 API 代码。

Redux 核心概念关系图

整体类比:六个概念先对上号 ​

笔记给出一组项目类比:store 是数据仓库,对应项目经理;slice 是数据切片,对应子项目;reducer 是“减速器”,对应程序员;action 是动作行为,对应需求文档;actionCreator 是 action 的创造者,对应产品经理;dispatch 是分发,对应指派任务。
为什么需要这组类比?因为 Redux 概念多、名字抽象,初学时容易把“谁产生变化、谁执行变化、谁保存结果”混在一起。怎么想?把一次状态更新看成一个项目流转:产品经理写需求文档,项目经理指派任务,程序员按文档改代码,子项目是项目的一部分,最终结果进入数据仓库。怎么做?先记住“action 描述发生了什么,dispatch 负责派发,reducer 负责计算新状态,store 负责保存,slice 负责按业务拆分”。易错点是:笔记里的“减速器”不是规范术语,规范叫 reducer;不要按字面理解成“减速”,它不是降低速度,而是根据 action 计算新状态。

store:为什么需要数据仓库 ​

为什么需要?当多个组件要共享同一份状态时,如果只靠组件间传参,层级一深就会很乱,所以需要一个集中存放状态的地方。怎么想?把 store 想成项目经理:所有任务和信息在这里汇总,别人不直接改仓库,而是按流程提交变化。怎么做?在概念上,把应用级共享状态放进 store,组件读取 store,更新时通过 dispatch 走统一入口。易错点:store 不是用来替代所有组件局部状态的;只把需要跨组件共享、需要可追踪变化的状态放进去。也不要在组件里直接修改 store 中的数据。

slice:为什么要把状态切片 ​

为什么需要?一个应用的状态往往很大,如果全部混在一起,阅读和维护都困难。怎么想?把 slice 想成“子项目”:一个项目可以拆成多个子项目,每个子项目有自己的边界。怎么做?按业务模块划分 slice,例如用户、订单、消息各成一片;每片通常包含自己的初始状态和 reducer 逻辑。易错点:slice 不是多个 store,它仍是整体状态树的一部分;拆分要按业务域,不要按“每个组件一片”机械拆,否则边界会碎。

reducer:为什么需要纯计算 ​

为什么需要?状态变化必须可预测、可追踪,不能谁想改就随便改。怎么想?reducer 像程序员:拿到“需求文档”action 后,根据当前状态计算出新状态。怎么做?概念上,reducer 接收旧状态和 action,返回新状态;它应保持纯计算,不直接改旧状态,不写请求、随机数等副作用。易错点:第一,规范名称是 reducer,不是“减速器”;第二,reducer 不负责发请求;第三,不要直接修改原状态对象,应返回新的状态结果。这样调试时才能看清“哪个 action 导致了哪次变化”。

action 与 actionCreator:为什么要把“发生了什么”说清楚 ​

为什么需要?如果直接改状态,很难知道变化原因。action 用来描述“发生了什么”。怎么想?action 像需求文档:它说明要做什么,但不亲自改代码。actionCreator 像产品经理:专门创建 action。怎么做?概念上,action 是一个描述对象,至少应能表达动作类型和必要数据;actionCreator 是创建 action 的函数,让创建过程集中、复用。易错点:action 不是状态本身,也不是直接修改 store 的指令;actionCreator 不执行 reducer,它只负责产生 action。不要把“写需求”和“改代码”混成一步。

dispatch:为什么需要统一分发 ​

为什么需要?如果每个组件都绕过流程直接改 store,状态变化就不可控。dispatch 是统一入口。怎么想?它像项目经理指派任务:把 action 这份“需求文档”派发给 store,再由 reducer 处理。怎么做?组件触发变化时,通过 dispatch 派发 action;store 收到后交给 reducer 计算新状态。易错点:dispatch 不是 reducer,它不负责计算;不要绕过 dispatch 直接改 store;也不要把异步请求本身当成 dispatch 的职责,异步逻辑应另行组织。

串起来:一次更新的推导顺序 ​

把六个概念串成一条线:用户操作后,由 actionCreator 创建 action;组件通过 dispatch 派发 action;store 收到 action 后交给 reducer;reducer 根据旧状态和 action 返回新状态;store 保存新状态,视图再根据新状态刷新。这个顺序是本节最该记住的“推导链”。易错点是把顺序颠倒:actionCreator 不直接改 store,dispatch 不直接算状态,reducer 不创建 action。转写稿中的路由调试过程不能套到这里,否则会把 NavLink、Outlet、useNavigate 误当成 Redux 概念。

本节小结与检查清单 ​

检查自己是否能说清:store 存什么;slice 为什么拆;reducer 为什么必须是纯计算;action 描述什么;actionCreator 创建什么;dispatch 派发什么。再用项目类比复述一遍:项目经理、子项目、程序员、需求文档、产品经理、指派任务。最后提醒:本节笔记没有代码,不要为了“补代码”虚构实现;转写稿属于路由章节,不能作为 Redux 调试记录使用。

学习边界说明 ​

本节笔记的主题是 redux-toolkit 基本使用,明确给出的内容只有两步:安装 @reduxjs/toolkit,以及从包中导入 createSlice、configureStore。但提供的转写稿实际讲的是 React Router 相关内容,例如自定义高亮样式类名、路由表、useRoutes、Outlet 二级路由、useNavigate 编程式导航等,其中还有“bo strap(Bootstrap)”“useRoots(useRoutes)”“out light(Outlet)”“nave gate(useNavigate)”等同音转写问题。由于这些内容与本节 redux-toolkit 笔记不是同一主题,若强行混入就会编造 Redux Toolkit 的用法。因此,下面只依据本节笔记扩写安装与导入两个知识点,不把转写稿中的路由调试过程误写成 Redux Toolkit 内容。

安装依赖:为什么需要先安装 ​

为什么需要:本节要使用 createSlice 和 configureStore,它们来自 @reduxjs/toolkit 这个包。项目如果没有安装该依赖,后续导入时就会找不到模块。所以第一步不是写切片或仓库,而是先把包安装到项目依赖中。

怎么想:安装是“准备工具”,导入是“把工具取出来用”。先确认当前终端位于项目根目录,再执行安装命令。不要在任意目录随手安装,否则包可能被装到别处,项目代码仍然无法导入。

怎么做:在项目根目录执行笔记中的命令:

bash
npm install @reduxjs/toolkit

安装完成后,再进入代码文件写导入语句。本节不展开切片和仓库的具体配置,先把依赖入口跑通。

易错点:包名要写完整,是 @reduxjs/toolkit,不要漏掉 @reduxjs/,也不要随意改大小写;安装目录应是当前项目根目录;安装后如果编辑器仍提示找不到模块,先检查终端所在目录和项目依赖是否安装成功,而不是立刻改导入代码。

导入函数:为什么只导入这两个 ​

为什么需要:笔记明确要用两个函数:createSlice 创建切片,configureStore 创建仓库。它们不是全局变量,必须先从 @reduxjs/toolkit 导入,才能在当前模块中使用。

怎么想:先看职责分工,再看导入方式。createSlice 面向“切片”的创建;configureStore 面向“仓库”的创建。本节只要求认识这两个入口,不要求展开完整配置。因此导入时使用命名导入,把两个函数从包中取出来。

怎么做:保留笔记中的导入代码:

js
import {createSlice, configureStore} from '@reduxjs/toolkit'

导入后,当前模块中就拥有 createSlice 和 configureStore 两个标识符。后续写切片时用前者,写仓库时用后者。若暂时不写具体逻辑,也应先确认导入路径、函数名大小写与笔记一致。

易错点:第一,导入来源必须是 @reduxjs/toolkit,不要凭感觉改成其他路径;第二,createSlice 中的 S 是大写,configureStore 中的 S 也是大写,写成 createslice、configurestore 会报错;第三,这是命名导入,花括号不能丢;第四,不要把 configureStore 和 createSlice 的职责说反——笔记写得很清楚:前者创建仓库,后者创建切片。

本节小结与调试思路 ​

本节的知识点很少,但它是后续使用 Redux Toolkit 的入口。调试时按顺序排查:先看终端是否在项目根目录执行了 npm install @reduxjs/toolkit;再看导入语句是否原样使用 import {createSlice, configureStore} from '@reduxjs/toolkit';最后看函数名大小写和花括号是否正确。只要安装与导入这两步成立,本节目标就完成了。转写稿中的路由表、Outlet、useNavigate 等内容不属于本节 redux-toolkit 笔记范围,本文不将其作为 Redux Toolkit 的推导依据。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

jsx
/* src/main.jsx */
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)
jsx
/* src/App.jsx */
import { NavLink } from 'react-router-dom'
import './App.css'

export default function App() {
  return (
    <div className="list-group">
      <NavLink className={({ isActive }) => (isActive ? 'myActive' : '')} to="/home">
        Home
      </NavLink>
      <NavLink className={({ isActive }) => (isActive ? 'myActive' : '')} to="/login">
        Login
      </NavLink>
      <NavLink className={({ isActive }) => (isActive ? 'myActive' : '')} to="/user">
        User
      </NavLink>
    </div>
  )
}
css
/* src/App.css */
.myActive {
  color: #fff;
  background-color: green;
}

要点解释:

  • className 的值可以是一个回调函数,路由解析模板时会自动调用它,回调函数的返回值会直接成为最终写进标签的类名。
  • 回调函数的参数是一个对象,其中包含 isActive 属性(布尔值),用来表示“当前路由是否处于激活状态”;直接解构 { isActive } 即可使用。
  • 这样就不会使用默认的 active 类名;如果项目中引入了 Bootstrap 之类的第三方库,它们也有 .active,全局覆盖后会影响其他地方,所以需要自定义类名来避免冲突。
  • 易错点:忘记在回调里 return(用了大括号却没有写 return),或者写成 className="myActive" 这种固定字符串,导致三条链接同时高亮。

示例 2:路由表 + useRoutes + Outlet 二级路由 ​

jsx
/* src/routes/index.jsx —— 路由表:一个装 path 与 element 映射关系的普通数组 */
import { Navigate } from 'react-router-dom'
import Home from '../pages/Home'
import About from '../pages/About'
import News from '../pages/News'
import Message from '../pages/Message'

const routes = [
  { path: '/', element: <Navigate to="/home" /> },
  {
    path: '/home',
    element: <Home />,
    children: [
      { path: '', element: <Navigate to="/home/news" /> }, // 访问 /home 时默认展示 news
      { path: 'news', element: <News /> },                 // 简写,等价于 /home/news
      { path: 'message', element: <Message /> }            // 简写,等价于 /home/message
    ]
  },
  { path: '/about', element: <About /> },
  { path: '*', element: <div>404 页面</div> }
]

export default routes
jsx
/* src/pages/Home.jsx —— 一级路由组件内部用 Outlet 展示二级路由 */
import { NavLink, Outlet } from 'react-router-dom'

export default function Home() {
  return (
    <div>
      <div className="list-group">
        <NavLink className="list-group-item" to="news">News</NavLink>
        <NavLink className="list-group-item" to="message">Message</NavLink>
      </div>
      <div className="panel-body">
        <Outlet />
      </div>
    </div>
  )
}
jsx
/* src/App.jsx —— 用 useRoutes 根据路由表生成路由配置 */
import { useRoutes } from 'react-router-dom'
import routes from './routes'

export default function App() {
  const element = useRoutes(routes)
  return <div className="panel-body">{element}</div>
}

要点解释:

  • 路由表本质上就是普通数组,写 path 和 element 的映射关系,比手写一堆 <Route> 标签简洁得多;再交给 useRoutes(routes) 生成真正的路由配置。
  • 重定向有两种写法:{ index: true, element: <Navigate to="..." /> } 或 { path: '/', element: <Navigate to="..." /> },两者都可以。
  • 二级路由用 children 属性配置,children 中每个子路由同样写 path + element。
  • 二级路由的 path 使用简写时前面不能加斜杠:写成 path: 'news' 会被自动拼成 /home/news;写成 path: '/news' 则被当成一级路由,会找不到。
  • 易错点:路由表文件在 src/routes 下,import Home from '../pages/Home' 的路径要带 ../(视频里出现的 “cant resolve” 报错就是少了一个点);另外,所有路由组件都要在路由表文件里 import,而不要继续留在 App.jsx 里。

示例 3:编程式导航 useNavigate ​

jsx
/* src/App.jsx 中的按钮区 */
import { useNavigate } from 'react-router-dom'

export default function App() {
  const navigate = useNavigate()

  // 编程式导航可以加逻辑判断:有 token 才进用户中心
  const toUser = () => {
    const token = localStorage.getItem('token')
    if (token) {
      navigate('/user')
    } else {
      navigate('/login')
    }
  }

  return (
    <div>
      <button onClick={() => navigate('/about')}>去 About</button>
      <button onClick={() => navigate('/home')}>去 Home</button>
      {/* 跳二级路由必须写完整路径,简写 news 不生效 */}
      <button onClick={() => navigate('/home/news')}>去 News</button>
      <button onClick={toUser}>去用户中心</button>

      {/* replace: true —— 替换当前历史记录,而不是新增一条 */}
      <button onClick={() => navigate('/about', { replace: true })}>replace 跳 About</button>

      {/* 传数字相当于 history.go() */}
      <button onClick={() => navigate(-1)}>后退一步</button>
      <button onClick={() => navigate(1)}>前进一步</button>
      <button onClick={() => navigate(2)}>前进两步</button>
    </div>
  )
}

要点解释:

  • useNavigate() 返回一个 navigate 函数,填上路由表里配置的 path 就能跳转,本质是用 JS 做路由跳转,不用 A 标签。
  • 与 NavLink / Link 最大的差别:编程式导航是 JS 代码,可以加 if 判断(如判断 localStorage 里有没有 token)实现有条件跳转;A 标签一点就跳,没法插入逻辑。
  • 第二个参数 { replace: true }:不生成新的历史记录,而是替换掉当前这条,点浏览器后退会跳过被替换的页面;不写时默认是 false(新增一条)。
  • navigate(-1) / navigate(1) / navigate(2) 传数字,表示在历史记录中后退/前进几步,效果类似 history.go()。
  • 易错点:跳二级路由时 navigate('news') 这种简写不生效,要写完整的 /home/news;另外,跳到 /home 时因为配了重定向,实际会落到 /home/news。

常见错误 ​

  • 现象:NavLink 高亮样式不生效,或所有链接都带上自定义类名。
    原因:className 回调参数未正确解构 isActive,或返回值写成了固定字符串。
    修法:回调参数中取出 isActive,按布尔值返回目标类名或空字符串。

  • 现象:改用路由表后页面报模块找不到。
    原因:路由表文件位置变化后,组件导入路径仍沿用原文件相对路径。
    修法:以路由表文件所在目录为基准重写 import 路径,重点检查 ./、../。

  • 现象:二级路由配置后无法匹配,或跳到错误的一级路由。
    原因:子路由 path 前多写了 /,被当成一级路由处理。
    修法:子路由简写时不要以 / 开头;需要完整路径时再写完整二级路径。

  • 现象:二级路由组件不显示,但地址栏路径已变化。
    原因:父级路由组件中没有放置 Outlet,或放置位置不在目标内容区。
    修法:在父路由组件负责展示子路由的位置放入 Outlet,并确认其未被条件渲染屏蔽。

  • 现象:使用 useNavigate 跳转二级页面失败。
    原因:把二级路径当成一级路径简写,例如只写子路径。
    修法:编程式导航跳二级路由时写完整目标路径,或确保当前匹配上下文能正确拼接。

  • 现象:点击跳转后浏览器后退丢失上一页。
    原因:navigate 第二参数误用了 replace: true。
    修法:需要保留历史记录时不要传 replace: true;只有明确要替换当前记录时才使用。

练习 ​

  1. 将现有 NavLink 默认高亮改为自定义类名,并在样式文件中分别验证激活态与非激活态。
  2. 把当前分散写的路由配置改造成路由表数组,再用 useRoutes 生成配置,检查所有组件导入路径。
  3. 为 home 增加 news、message 两个二级路由,并设置访问 home 时默认重定向到其中一个子路由。
  4. 在页面中增加三个按钮:跳转 about、跳转 home/news、后退一步;验证普通跳转与 replace 跳转的历史记录差异。
  5. 故意把某个二级路由 path 改成以 / 开头,观察匹配结果并修正配置。

原视频对照 ​

#视频时长
1自定义高亮样式类名06:58
2路由表09:24
3一级路由页面练习11:06
4Outlet二级路由配置14:55
5编程式导航-useNavigate19:17

小结 ​

  • 路由改造要先保证路径、组件、展示位置三者一致,再处理高亮和跳转体验。
  • NavLink 的高亮逻辑应围绕 isActive 判断,不要覆盖第三方库通用类名。
  • 路由表适合集中维护映射关系,但文件迁移后最容易暴露相对路径问题。
  • 二级路由能否显示,不只看配置,还要检查父组件中的 Outlet 位置。
  • 子路由路径简写和完整写法要统一策略,尤其注意开头 / 的语义差异。
  • 编程式导航适合带条件跳转;历史记录行为要结合 replace 和前进后退参数验证。
  • 页面练习应先做布局和内容区定位,再接入路由配置,排错成本更低。