Skip to content

redux-toolkit基本使用与创建仓库-获取仓库数据-(Day8 3/3) ​

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

学习目标 ​

  • 能够配置普通路由并通过 Link 的查询字符串传递 query 参数,使用 useSearchParams 获取指定 key 的值,并调用其设置方法更新 query 参数,通过页面展示验证结果。
  • 能够通过 Link 的 state 属性传递对象等复杂数据,使用 useLocation 接收并按数据结构解构取值,通过页面展示验证传递与接收结果。
  • 能够使用 React.lazy 与 Suspense 对路由组件实现按需加载,设置 fallback 加载提示,并封装可复用的懒加载函数,通过路由切换验证加载效果。
  • 能够解释 Bootstrap 样式因相对路径在多级路由下加载失败的原因,并将引入路径改为以 / 开头的绝对路径,通过刷新页面验证样式不再丢失。
  • 能够区分 BrowserRouter 的 HTML5 history 模式与 HashRouter 的 hash 模式在 URL 表现上的差异,并说明 hash 模式对相对路径资源加载的影响。

知识点 ​

本节范围与转写稿说明 ​

本节笔记的主题是“18-2. redux-toolkit 基本使用”,笔记只给出了两步:安装 @reduxjs/toolkit,以及从包中导入 createSlice、configureStore。需要先说明:你提供的转写稿讲的是路由 query 参数、state 参数、路由懒加载、bootstrap 相对路径、hash/history 模式等内容,和本节 Redux Toolkit 主题并不匹配。为避免编造,我不能把“query 参数怎么取”“Suspense 怎么包懒加载组件”硬套成 Redux Toolkit 的使用步骤;这些内容属于路由章节。转写稿里一些同音字也应纠正,例如“战位服”应为“占位符”、“优质”应为“use”、“哈西”应为“hash”,但它们不属于本节。下面只依据本节笔记,把“安装”和“导入”两个知识点讲清楚。本节笔记没有附带图片,因此不插入图片。

安装 @reduxjs/toolkit ​

为什么需要:createSlice、configureStore 都不是浏览器或 React 自带的能力,它们来自 @reduxjs/toolkit 这个 npm 包。项目里如果不先安装依赖,后续写 import ... from '@reduxjs/toolkit' 时就会找不到模块。
怎么想:基本使用顺序应当是“先有依赖,再有导入,最后才写切片和仓库”。所以第一步不是急着写代码,而是确认项目根目录和包管理器,把 toolkit 装进当前项目。
怎么做:在项目根目录执行笔记中的安装命令:

bash
npm install @reduxjs/toolkit

执行后应检查 package.json 的依赖中是否出现 @reduxjs/toolkit,并确认 node_modules 已安装。
易错点:包名是带作用域的 @reduxjs/toolkit,不要写成 @redux/toolkit 或 redux-toolkit;安装命令要在项目根目录执行,不要跑到桌面或别的目录;如果团队使用 pnpm/yarn,应与项目锁文件保持一致,不要混用包管理器导致依赖状态混乱。

导入 createSlice 与 configureStore ​

为什么需要:安装只是把包放进项目,真正使用前还要在模块中导入函数。笔记明确说明:createSlice 是创建切片的函数,configureStore 是创建仓库的函数。先把这两个函数导入,后续才能分别组织切片和创建 store。
怎么想:这是一个“命名导入”场景,包中导出了多个成员,所以要用花括号按名称取出。导入位置通常是在需要使用它们的模块顶部;不要把 npm install 写进 JavaScript 文件。
怎么做:保留笔记中的导入语句:

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

其中 createSlice 对应“创建切片”,configureStore 对应“创建仓库”。本节只讲到导入,具体 slice 的字段和 store 的配置,笔记没有展开,就不在这里编造示例。
易错点:漏写花括号会变成默认导入,和 toolkit 的命名导出不匹配;函数名大小写要准确,createSlice 的 S 大写,configureStore 的 S 大写;不要把它误写成 React 包或 react-redux 包的导入;也不要把 configureStore 和旧版 Redux 的 createStore 混为一谈。

建立“安装→导入”的基本使用顺序 ​

为什么需要:很多初学错误不是概念不会,而是顺序乱了:先写导入,发现报 Cannot find module '@reduxjs/toolkit',再回头安装;或者安装后包名写错,仍然导入失败。把顺序固定下来,排查会简单很多。
怎么想:先确认依赖存在,再确认导入语句的包名、花括号、函数名是否正确。若导入报错,先查 package.json 和 node_modules,再查拼写;若提示某个函数不是函数,优先检查命名导入和包版本。
怎么做:按“根目录安装 → 模块顶部导入 → 再写后续切片和仓库”的顺序推进。本节笔记的完整可保留代码就是上面两条:安装命令和导入语句。
易错点:重复安装、在多个文件里重复写安装命令、导入后未使用、把包名写成相对路径 ./@reduxjs/toolkit,都是常见问题。先守住这两步,再进入后续 createSlice 和 configureStore 的具体使用。

本节要解决的问题 ​

Redux Toolkit 把“状态放哪儿、怎么读、怎么改、怎么监听”串成一条线:用 createSlice 定义切片,用 configureStore 组装仓库,用 store.getState() 读数据,用 store.dispatch(actionCreator(payload)) 改数据,用 store.subscribe() 监听变化。为什么需要这样?因为多个切片的数据要集中管理,修改必须走统一入口,否则任意赋值会让状态变化不可追踪。怎么想?先定义切片初始状态和 reducer,再把切片 reducer 交给仓库,最后在仓库外部只通过 API 操作。易错点:不要在组件或普通代码里直接改 store 内部数据,修改应交给 reducer。

说明:转写稿主要内容是路由参数、懒加载、Bootstrap 路径和 hash 模式,与本节“创建仓库”不是同一主题,不能把 query/state 参数推导硬搬进来;只采用其中“先打印看结构,再决定怎么取值”的调试思路。转写稿里“战位服”应纠正为“占位符”,但那是路由内容,本节不展开。

创建仓库:configureStore ​

为什么需要:切片只是“状态模块”,还要有仓库把多个切片组合起来。怎么想:createSlice 负责定义 name、initialState、reducers;configureStore 的 reducer 对象负责把 count、user 两个分支挂到仓库上。怎么做:先创建切片,再从 slice.actions 取 actionCreator,最后配置 store。易错点:configureStore({ reducer: { count: countSlice.reducer } }) 里传的是 slice.reducer,不是整个 slice;仓库 state 的结构由 reducer 对象的 key 决定。

js
// 1. 导入 createSlice包
import {createSlice, configureStore} from '@reduxjs/toolkit'

// 2. 创建切片
const countSlice = createSlice({
    name:'count',// name:指定切片名
    initialState:{// 切片数据
        num:99,
        msg:'atguigu'
    },
    reducers:{
        /**
         * 
         * @param {*} state  切片状态数据
         * @param {*} action {type:'count/addNum',payload:数据}
         * 没创建一个reducer方法,redux会自动帮我们创建一个同名的方法,在切片的 actions属性上,该方法的身份是,actionCreator
         */
        addNum(state, action){ 
            state.num += action.payload
        },
        changeMsg(state, action){
            state.msg += action.payload
        }
    }
})
// 获取actionCreator
let {addNum, changeMsg} = countSlice.actions; 

const userSlice = createSlice({
    name:'user',
    initialState:{
        username:'atguigu',
        age:10
    },
    reducers:{
        addAge(state, {payload}){
            state.age += payload
        }
    }
})
// actionCreator  addAge
const {addAge} = userSlice.actions;

// 创建仓库
const store = configureStore({
    reducer:{
        count: countSlice.reducer,
        user: userSlice.reducer
    }
})
console.log('store: ',store);

// 获取仓库数据  store.getState()
console.log('store.getState(): ', store.getState());

// 如何修改切片数据    只有reducer中的方法【程序员才能修改数据】
// 项目经理分配一个任务给产品经理,产品经理创建一个需求,将需求给程序员开发

store.dispatch(addNum(3));// addNum(3) ==> {type:'count/addNum',payload:3}
// 触发 count切片中的addNum函数的执行,并且将 action对象作为第二个参数传过去

console.log(store.getState());

// 监听store中数据的变化,当仓库数据发生修改时,会触发回调函数的执行。返回值是取消监听的函数,如果调用执行,那么就会取消监听
const unsubscribe = store.subscribe(()=>{
    console.log('subscribe: ',store.getState());
})

store.dispatch(addNum(5))
unsubscribe();// 取消监听
store.dispatch(addNum(7))

获取仓库数据:store.getState() ​

为什么需要:派发前后都要确认状态是否按预期变化。怎么想:先 console.log(store) 看仓库结构,再 console.log(store.getState()) 看当前快照;本例应看到 count.num、count.msg、user.username、user.age。怎么做:直接调用 store.getState(),不要把它当成会自动更新的变量。易错点:getState() 返回的是当前时刻快照;要拿最新值,应在 dispatch 后或 subscribe 回调里重新调用。

修改切片状态:store.dispatch(actionCreator(payload)) ​

为什么需要:统一入口才能触发 reducer、更新 state、通知订阅者。怎么想:addNum(3) 不是直接改 num,而是生成类似 {type:'count/addNum',payload:3} 的 action;dispatch 把它交给仓库,匹配到 count 切片的 addNum。怎么做:从 slice.actions 解构 actionCreator,调用后传给 dispatch。易错点:不要写 store.getState().count.num = 100;actionCreator 要从 slice.actions 取;payload 要和 reducer 中的使用一致。RTK 允许在 reducer 里写 state.num += action.payload,是因为内部使用 Immer,但这仍然只应发生在 reducer 中。

监听和取消监听仓库数据变化 ​

为什么需要:状态变化后可能要同步日志、本地存储或更新外部系统。怎么想:subscribe 注册回调,返回 unsubscribe;调用 unsubscribe() 后不再监听。怎么做:先订阅,再派发;取消后再派发,观察回调是否还执行。易错点:unsubscribe 是函数,要加括号调用;订阅回调里应重新 getState() 取最新值;取消监听不会撤销已经发生的状态修改。

js
// 监听
const unsubscribe = store.subscribe(()=>{
              
})
// 取消监听
unsubscribe();

调试顺序与易错点 ​

建议按“打印 store → 打印 getState() → dispatch → 再打印 getState() → 订阅 → 派发 → 取消订阅 → 再派发”的顺序验证。易错点集中在:切片 reducer 挂载 key 写错、actionCreator 没从 slice.actions 取、把 getState() 当响应式数据、忘记调用 unsubscribe()。本节代码里的 console.log 就是最直接的调试证据。

先说明内容对应关系 ​

本节笔记标题写的是“store 模块化使用”,但笔记正文为空,转写稿实际讲解的是路由参数传递、路由懒加载、bootstrap 路径和 hash/history 模式。为避免编造未出现的内容,下面按转写稿实际内容整理;本节没有出现图片,代码按转写稿中的片段保留并纠正同音字。

query 参数:传参简单,取值要经 get ​

为什么需要:有些数据只是查询条件,比如用户名、年龄,放在 URL 查询字符串里最直观。怎么想:query 参数不需要在路由配置里写占位符,因为 ?key=value&key2=value2 已经把键值明确写出来了。怎么做:把普通 a 标签换成 Link,href 改成 to,路径后拼查询字符串:

jsx
<Link to="/home/message/query?username=@硅谷&age=20">query 参数</Link>

路由配置仍是普通路径,不需要额外占位符:

jsx
{
  path: "query",
  element: <QueryTest />
}

组件里用 useSearchParams 接收。注意它返回的是数组,第一项是对象,要通过原型上的 get 方法取值:

jsx
let [searchQuery, setSearchQuery] = useSearchParams();
let username = searchQuery.get("username");
let age = searchQuery.get("age");

设置参数时调用解构出来的函数,例如:

jsx
setSearchQuery("username=尚硅谷&age=100");

易错点:不要以为 useSearchParams 第一项能直接点出 username;转写稿中打印后能看到它是对象,但值要用 get("username") 取。query 的好处是传参简单、路由无需特殊配置,缺点是取值比 params 麻烦。

state 参数:可传复杂类型,但不体现在地址栏 ​

为什么需要:有时要传对象、数组等复杂数据,query 只能拼基本类型,state 更合适。怎么想:state 参数通过 Link 的 state 属性传,不写在 URL 里。怎么做:

jsx
<Link to="/home/message/state" state={{ id: "1", title: "吃饭", isDone: false }}>
  state 参数
</Link>

路由配置同样不需要特殊配置。组件里用 useLocation 接收:

jsx
const x = useLocation();
const { id, title, isDone } = x.state;

转写稿强调,解构时要看清结构:数据在 location.state 里,所以也可以写成与结构一致的形式。渲染布尔值时用三元表达式,例如 isDone ? "已完成" : "未完成"。

易错点:state 参数刷新后地址栏没有任何体现,用户不知道数据从哪来;转写稿也明确说这种方式用得相对少,query 和 params 更常用。

路由懒加载:lazy 与 Suspense 配合 ​

为什么需要:如果 routes/index.js 一开始就 import 所有路由组件,用户只看首页,也会把全部页面加载一遍。怎么想:首页必须加载,其他页面点开再加载,即按需加载/懒加载。怎么做:从 react 引入 lazy 和 Suspense,用 lazy(() => import("路径")) 加载组件:

jsx
const About = lazy(() => import("./About"));

懒加载组件必须用 Suspense 包裹,fallback 是加载未完成时显示的内容:

jsx
<Suspense fallback={<div>组件正在加载中</div>}>
  <About />
</Suspense>

转写稿中把所有懒加载路由都套上 Suspense,并进一步封装 load 函数,减少重复代码。易错点:转写稿调试时出现报错,原因就是某个懒加载组件忘了包 Suspense;只要用了 lazy,就要检查是否都有 Suspense。

bootstrap 样式丢失:不要写相对路径 ​

为什么需要:刷新页面后样式飞了,检查发现 bootstrap 没加载成功。怎么想:./css/bootstrap.css 是相对路径,多级路由下会把前面的路由也当目录,去错误位置找文件。怎么做:改成 /css/bootstrap.css,斜杠表示网站根目录,也就是 public/打包后的目录。易错点:路由层级越深,相对路径越容易出错;转写稿明确要求“千万不要加点”。

hash 与 history 模式的区别 ​

为什么需要:BrowserRouter 对应 HTML5 history 模式,路径是 /home/news;HashRouter 对应 hash 模式,路径带 #。怎么做:把包裹 APP 的 BrowserRouter 换成 HashRouter 即可。易错点:hash 模式路径不干净,但相对路径下刷新样式不易丢;实际网站多用 history 模式,因此仍要养成写绝对路径的习惯。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例 1:redux-toolkit 基本使用(创建切片 → 创建仓库 → 获取数据 → 修改状态) ​

js
// package.json 中需添加 "type": "module",否则 node 直接运行会报 import 语法错误
// 安装:npm install @reduxjs/toolkit

// 1. 导入 createSlice、configureStore
import { createSlice, configureStore } from '@reduxjs/toolkit'

// 2. 创建切片
const countSlice = createSlice({
  name: 'count', // 切片名,会作为 action type 的前缀
  initialState: {
    // 切片数据
    num: 99,
    msg: 'atguigu'
  },
  reducers: {
    /**
     * @param {*} state  切片状态数据
     * @param {*} action {type:'count/addNum', payload:数据}
     * 每创建一个 reducer 方法,redux 会自动帮我们创建一个同名方法挂在切片的 actions 属性上,
     * 该方法的身份是 actionCreator
     */
    addNum(state, action) {
      state.num += action.payload
    },
    changeMsg(state, action) {
      state.msg += action.payload
    }
  }
})

// 获取 actionCreator
const { addNum, changeMsg } = countSlice.actions

// 3. 创建仓库
const store = configureStore({
  reducer: {
    count: countSlice.reducer
  }
})
console.log('store: ', store)

// 4. 获取仓库数据 store.getState()
console.log('store.getState(): ', store.getState())

// 5. 如何修改切片数据:只有 reducer 中的方法才能修改数据
console.log(addNum(3)) // { type: 'count/addNum', payload: 3 }

store.dispatch(addNum(3)) // 触发 count 切片中 addNum 的执行,并把 action 对象作为第二个参数传进去
console.log(store.getState())

store.dispatch(changeMsg('!'))
console.log(store.getState())

要点解释与易错点

  • createSlice 的三个参数缺一不可:name、initialState、reducers;name 决定了自动生成的 action type 为 切片名/方法名(如 count/addNum)。
  • reducers 里写的每个方法,redux-toolkit 会自动生成同名的 actionCreator 放在 slice.actions 上,必须解构出来再 dispatch,不能直接 store.dispatch(addNum) 传函数本身。
  • 修改状态写 state.num += action.payload(直接“改”state),这是 Immer 的能力,不要写成 state = {...state, num: ...},那样不会生效。
  • 取参数统一用 action.payload(也可解构 { payload }),名字是固定的,写成 action.data 拿不到值。
  • configureStore 的参数是对象 { reducer: { count: countSlice.reducer } },注意这里是 countSlice.reducer(单数),不是 reducers。
  • 易错点:直接 store.getState().count.num = 100 改数据——绕过了 reducer,页面/监听都不会有正确响应,必须 dispatch。

示例 2:多个切片合并进同一个仓库(store 模块化结构) ​

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

// ---- count 切片 ----
const countSlice = createSlice({
  name: 'count',
  initialState: {
    num: 99,
    msg: 'atguigu'
  },
  reducers: {
    addNum(state, action) {
      state.num += action.payload
    },
    changeMsg(state, action) {
      state.msg += action.payload
    }
  }
})
const { addNum, changeMsg } = countSlice.actions

// ---- user 切片 ----
const userSlice = createSlice({
  name: 'user',
  initialState: {
    username: 'atguigu',
    age: 10
  },
  reducers: {
    addAge(state, { payload }) {
      state.age += payload
    }
  }
})
// actionCreator  addAge
const { addAge } = userSlice.actions

// 创建仓库:reducer 用对象,key 就是 state 里的分支名
const store = configureStore({
  reducer: {
    count: countSlice.reducer,
    user: userSlice.reducer
  }
})

console.log(store.getState())
// { count: { num: 99, msg: 'atguigu' }, user: { username: 'atguigu', age: 10 } }

store.dispatch(addNum(3))
store.dispatch(addAge(5))

// 读取时按分支名取值
console.log(store.getState().count.num) // 102
console.log(store.getState().user.age) // 15

要点解释与易错点

  • configureStore({ reducer: { count: ..., user: ... } }) 中的 key 就是最终 state 的分支名,getState() 得到的是 { count: {...}, user: {...} } 这种嵌套结构,取值必须带分支名(getState().user.age)。
  • 每个切片只管理自己那份 initialState,切片之间互不干扰;把切片拆到不同文件、再在 store/index.js 里统一 configureStore,就是 store 模块化的做法。
  • 易错点:多个切片用了相同的 name,会导致 action type 冲突(如两个 count/addNum),一定要保证切片名唯一。
  • 易错点:addAge(5) 中 addAge 是从 userSlice.actions 解构出来的,如果误从 countSlice.actions 解构会得到 undefined,dispatch 时报错。

示例 3:监听与取消监听仓库数据变化(store.subscribe) ​

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

const countSlice = createSlice({
  name: 'count',
  initialState: { num: 99, msg: 'atguigu' },
  reducers: {
    addNum(state, action) {
      state.num += action.payload
    }
  }
})
const { addNum } = countSlice.actions

const store = configureStore({
  reducer: { count: countSlice.reducer }
})

// 监听 store 中数据的变化,数据被修改时触发回调;返回值是取消监听的函数
const unsubscribe = store.subscribe(() => {
  console.log('subscribe: ', store.getState())
})

store.dispatch(addNum(5)) // 触发回调,打印 { count: { num: 104, msg: 'atguigu' } }

unsubscribe() // 取消监听

store.dispatch(addNum(7)) // 不再触发回调,但数据依然被修改
console.log(store.getState()) // { count: { num: 111, msg: 'atguigu' } }

要点解释与易错点

  • store.subscribe(fn) 的返回值 unsubscribe 是取消监听函数,必须接收并保存,否则无法取消监听(比如组件卸载时不取消就会造成泄漏)。
  • 回调里拿最新数据要用 store.getState() 重新获取,不要依赖外部闭包里的旧变量。
  • 易错点:以为 unsubscribe() 会阻止数据修改——它只是停止回调,dispatch 依然会改数据。
  • 易错点:每次 dispatch 都会触发一次回调,一次操作里连续 dispatch 多次就会打印多次,需要合并时应在业务层合并 action。

常见错误 ​

  1. 现象:调用 addNum(3) 后 store.getState() 里的 num 没有变化,或报 “Actions must be plain objects”。 原因:把 actionCreator 当成普通函数直接调用,没有交给 dispatch;或者试图在组件里直接改 state。 修法:写成 store.dispatch(addNum(3)),actionCreator 只负责产出 {type, payload},修改状态一律走 dispatch。

  2. 现象:在 reducer 里写 state = { ...state, num: state.num + 1 } 后数据不更新,或后续 mutate 报错。 原因:RTK 内部用 Immer 的 draft,重新给 state 变量赋值会丢掉 draft 引用;同时“既重新赋值又直接改”是混用两种写法。 修法:reducers 内二选一——要么直接 state.num += action.payload,要么 return { ...state, num: ... },不要给 state 重新赋值。

  3. 现象:store.getState() 拿到 { count: {...}, user: {...} },组件里却按 state.num 取值,得到 undefined。 原因:configureStore({ reducer: { count, user } }) 中每个 key 就是一层命名空间,state 结构由这些 key 决定。 修法:按命名空间取 store.getState().count.num;组件里用 useSelector(state => state.count.num)。

  4. 现象:调用了 unsubscribe() 之后回调还在打印;或页面切换多次后控制台日志成倍增加。 原因:subscribe 返回的取消函数没有保存/没有在组件卸载时调用;或者每个组件各订阅一次却没人清理。 修法:const unsub = store.subscribe(fn) 保存返回值,在确定不再需要时(如 useEffect 的 cleanup)调用 unsub()。

  5. 现象:store 模块化拆分后启动报 Cannot access 'xxx' before initialization,或某个 slice 的 actions 是 undefined。 原因:slice 文件里 import store,store 文件里又 import slice,形成循环依赖。 修法:slice 文件只导出 actions 和 reducer,不引用 store;store 只在 store/index.js 里汇总组装,组件通过 hooks 拿数据。

  6. 现象:路由懒加载的组件一切换就白屏,控制台报 Suspense 相关错误。 原因:lazy() 加载的组件没有用 <Suspense fallback={...}> 包裹,加载期间没有兜底 UI。 修法:所有 lazy 组件统一用 Suspense 包裹(可封装成 load(Component) 高阶函数),fallback 给 loading 提示。

  7. 现象:多级路由下刷新页面,bootstrap 样式丢失,Network 里 CSS 请求 404。 原因:样式用相对路径 ./css/bootstrap.css,浏览器把当前多级路由当成目录去拼接。 修法:改成以 / 开头的绝对路径 /css/bootstrap.css,永远从站点根目录(public)找。

练习 ​

  1. 新建 todoSlice,initialState 为 { list: [] },实现 addTodo(payload 为文本)、removeTodo(payload 为 id)、toggleTodo(payload 为 id)三个 reducer,并把 slice 注册进 configureStore,用 dispatch 连续添加两条再删除一条,打印每一步的 getState()。

  2. 写一段验证代码:先 subscribe 打印状态,然后 dispatch 三次,调用 unsubscribe(),再 dispatch 一次。说明控制台一共打印几次、为什么。

  3. 把课堂示例中的 count 和 user 拆成 store/countSlice.js、store/userSlice.js、store/index.js 三个文件,要求 slice 文件里不出现 import store,并验证 getState() 的结构与拆分前完全一致。

  4. 在 countSlice 的 addNum 里分别用“直接 mutate”和“return 新对象”两种写法实现同一逻辑,各 dispatch 一次,对比结果;再故意写一次 state = { ...state, num: 100 },观察并解释现象。

  5. 用 lazy + Suspense 封装一个 load(Component) 函数,把 About、Message 两个路由组件改成懒加载,并解释 fallback 里应该放什么内容、为什么不能省略。

原视频对照 ​

#视频时长
1路由参数传递-query参数10:13
2路由参数传递-state参数07:34
3路由懒加载15:26
4bootstrap引入使用相对目录导致文件加载失败的原因04:59
5html5-history模式-hash路由模式区别04:54

小结 ​

  • slice.actions 上的方法与 reducers 里的方法同名,这是 RTK 自动生成的 actionCreator,不要手写 {type:'count/addNum'}。
  • 仓库的 state 形状由 configureStore 中 reducer 对象的 key 决定,拆几个 slice 就有几层命名空间。
  • Immer 让“直接改 state”在 reducers 内成为合法写法,但这条特权只存在于 reducers 内部,出了 reducers 一律 dispatch。
  • subscribe 是全局的、粗粒度的,适合调试和少量副作用;组件取数应优先用 useSelector。
  • 模块化的正确方向是:slice 只依赖自身,store 汇总 slice,组件依赖 store 的 hooks,禁止反向 import。
  • 懒加载是“用的时候才加载”,必须配 Suspense 兜底,否则加载时间差会表现为白屏。
  • 静态资源路径一律用 / 开头,不要用 ./,这样无论路由多深、用 history 还是 hash 模式都不会丢样式。
  • history 模式路径干净、是实际项目的主流选择;hash 模式路径带 #,但天然规避多级路由下的相对路径问题。