Redux Essentials Part 3:React + Redux 基础数据流实战——从 configureStore 到 createSlice、useSelector 与 useDispatch
本文基于 Redux 官方教程 Redux Essentials, Part 3: Basic Redux Data Flow 展开,带你从零搭建一个社交帖子流应用(social media feed app),完整走通 Redux 的核心数据流:用 configureStore 创建唯一的全局 store,用 createSlice 编写 reducer 逻辑,用 useSelector 从 store 中读取数据,用 useDispatch 派发 action 更新状态。读完后,你将掌握一套可直接复制到实际项目中的 store 配置、slice 编写、TypeScript 类型导出与组件交互的完整模式,并能对照 redux 核心仓库源码理解每一次 dispatch 背后发生了什么。
学习目标与前置知识
按照原文档,本篇教程覆盖四个核心能力:
- 如何在 React 应用中设置一个 Redux store;
- 如何用
createSlice向 Redux store 添加若干"slice"(reducer 逻辑切片); - 如何用
useSelector钩子让组件读取 Redux 数据; - 如何用
useDispatch钩子在组件中派发 action。
前置知识方面,原文档要求熟悉 Redux 的关键术语——"actions"、"reducers"、"store"、"dispatching"(可参考 Part 1: Redux Overview and Concepts),并对 TypeScript 语法有基本了解。教程全程使用 TypeScript 编写代码:Redux 本身可以用纯 JavaScript 编写,但使用 TypeScript 能防止许多常见错误、为代码提供内置文档,并让编辑器在 React 组件和 Redux reducer 等位置提示所需的变量类型。官方强烈建议所有 Redux 应用都使用 TypeScript。
原文档同时提醒:示例应用并非一个完整的生产级项目,其目的是帮助学习 Redux API 与典型使用模式;教程早期搭建的部分内容,后续章节会用更好的方式重新组织。建议完整读完整个教程系列以了解全部概念。
项目准备:一个预配置好的起点
教程基于一个预配置好的 starter 项目:它已经装好 React 和 Redux,带有默认样式,并内置了一个假 REST API(fake REST API),让应用可以编写真实的 API 请求代码。安装依赖后,使用 yarn dev 命令启动本地开发服务器即可(项目默认使用 Yarn 4 作为包管理器,NPM、PNPM、Bun 也可以)。
完成本教程后,如果要开始自己的项目,推荐使用 Redux 官方的 Vite 和 Next.js 模板(见 Installation 文档 的 "Create a React + Redux app" 一节)作为创建新项目最快的方式——模板已预装 Redux Toolkit 和 React-Redux,并附带与 Part 1 相同的 "counter" 示例应用,可以跳过添加 Redux 包和配置 store 的步骤,直接写业务代码。
初始项目的目录结构如下:
/public:基础 CSS 样式及图标等静态文件/srcmain.tsx:应用入口文件,渲染<App>组件;示例中还在页面加载时设置假 REST APIApp.tsx:主应用组件,渲染顶部导航栏,并负责其他内容的客户端路由index.css:整个应用的样式/apiclient.ts:一个小型fetch封装客户端,用于发起 HTTP GET 和 POST 请求server.ts:为数据提供假 REST API,应用后续会从这些假端点获取数据
/appNavbar.tsx:渲染顶部页头和导航内容
此时加载应用,只能看到页头和一条欢迎消息,没有任何功能。
设置 Redux Store
添加 Redux 包
项目的 package.json 中已安装好使用 Redux 所需的两个包:
@reduxjs/toolkit:现代 Redux 包,包含构建应用所需的全部 Redux 函数react-redux:让 React 组件与 Redux store 交互所需的函数
如果从零开始搭建项目,需要先把这两个包加进项目。
创建 Store:configureStore 与 reducer 选项
Redux 的一个核心原则是:整个应用只能有一个(one)store 实例。
惯例是把 store 实例放在独立文件中创建并导出。目录结构由开发者自己决定,但把应用级的设置和配置放在 src/app/ 文件夹是标准做法。先新建 src/app/store.ts 文件并创建 store:
Redux Toolkit 提供了一个 configureStore 方法。这个函数创建一个全新的 Redux store 实例,可以传入多个选项来改变 store 行为;它还会自动应用最常见、最实用的配置,包括检查典型错误,以及启用 Redux DevTools 扩展(可查看状态内容和 action 历史)。
import { configureStore } from '@reduxjs/toolkit'
import type { Action } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
// An example slice reducer function that shows how a Redux reducer works inside.
// We'll replace this soon with real app logic.
function counterReducer(state: CounterState = { value: 0 }, action: Action) {
switch (action.type) {
// Handle actions here
default: {
return state
}
}
}
export const store = configureStore({
// Pass in the root reducer setup as the `reducer` argument
reducer: {
// Declare that `state.counter` will be updated by the `counterReducer` function
counter: counterReducer
}
})
configureStore 必须传入一个 reducer 选项。这通常是一个对象,包含应用不同部分的各个 "slice reducer"(必要时也可以单独创建根 reducer 函数,直接作为 reducer 参数传入)。
教程第一步先传入了一个临时的 counter slice reducer 用于演示结构,稍后会替换成真实应用的 slice reducer。
仓库中的现成示例可以印证这种写法:counter-ts 示例的 store 同样是 configureStore({ reducer: { counter: counterReducer } }),并在同一文件中导出 AppDispatch / RootState 类型:
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
如果你使用 Next.js,设置过程会多出几步,详见 Setup with Next.js 页面。
从源码看 reducer 选项如何变成根 reducer
configureStore 内部会把 reducer 对象的各 slice reducer 合并为一个根 reducer。redux 核心仓库中的 combineReducers 正是这种"对象 → 单一 reducer 函数"的转换实现,其中包含几个值得注意的行为(Redux Toolkit 的 configureStore 沿用了同样的校验思路):
- 初始化探测(见 src/combineReducers.ts 的
assertReducerShape):store 创建时会用INIT类型的 action 调用每个 slice reducer。如果某个 slice 在state === undefined时返回undefined,会直接抛出 "returned undefined during initialization" 错误——这就是为什么 slice 必须显式返回初始状态,而不能是undefined; - 状态形状校验:在开发环境下,
combineReducers会检查传入的 state 是否包含未定义的 key,并对多余的 key 发出 "Unexpected key(s)" 警告(见 src/combineReducers.ts 的getUnexpectedStateShapeWarningMessage),帮助尽早发现 preloaded state 与 reducer 不匹配的问题; - 按需更新:只有当某个 slice 的返回值引用发生变化(
nextStateForKey !== previousStateForKey)时,组合 reducer 才会返回新的 state 对象(见 src/combineReducers.ts),否则原样返回旧引用——这正是"未处理该 action 的 slice 不触发重渲染"的底层机制。
此外,store 创建时会主动 dispatch 一个 INIT action 来填充初始状态树(见 src/createStore.ts):
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)
也就是说,你在 DevTools 里看到的第一份 state(例如 { counter: { value: 0 } }),正是各 slice reducer 对 INIT action 的返回值组合出来的。
提供 Store:通过 Provider 注入
Redux 本身是一个纯 JS 库,可以和任意 UI 层配合。本应用使用 React,因此需要让 React 组件能访问 Redux store——使用 React-Redux 库,把 store 传入 <Provider> 组件。它利用 React 的 Context API 让应用内所有组件都能访问到这个 store。
原文档特别强调:不应该在其他应用代码文件里直接 import 这个 store! 原因有二:
- 由于只有一个 store 文件,直接导入 store 可能意外造成循环引用(file A imports B imports C imports A),导致难以追踪的 bug;
- 我们需要为组件和 Redux 逻辑编写测试,而这些测试需要创建各自独立的 store 实例。通过 Context 提供 store 保持了灵活性并避免导入问题。
在入口文件 main.tsx 中导入 store,用带 store 的 <Provider> 包裹 <App>:
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
// skip mock API setup
const root = createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
检查 Redux 状态
有了 store 之后,就可以用 Redux DevTools 扩展查看当前状态:打开浏览器 DevTools(例如在页面任意位置右键选择 "Inspect"),点击 "Redux" 标签页,即可看到已派发 action 的历史和当前 state 值。
当前 state 应该长这样:
{
counter: {
value: 0
}
}
这个形状正是传给 configureStore 的 reducer 选项定义的:一个对象,带有名为 counter 的字段,而 counter 字段对应的 slice reducer 返回 {value} 形状的状态。
导出 Store 类型
既然使用 TypeScript,会经常引用"Redux state 的类型"和"Redux store dispatch 函数的类型"。这些类型需要从 store.ts 文件导出。定义方式是使用 TS 的 typeof 操作符,让 TS 基于 store 定义自动推断类型:
import { configureStore } from '@reduxjs/toolkit'
// omit counter slice setup
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
// Infer the type of `store`
export type AppStore = typeof store
// Infer the `AppDispatch` type from the store itself
export type AppDispatch = typeof store.dispatch
// Same for the `RootState` type
export type RootState = ReturnType<typeof store.getState>
在编辑器中悬停在 RootState 类型上,应该看到 type RootState = { counter: CounterState; }。由于该类型是从 store 定义自动派生的,将来对 reducer 配置的任何修改都会自动反映到 RootState 类型上——只需定义一次,始终精确。
仓库中 counter-ts 示例 使用的就是同一套类型推导模式(AppDispatch = typeof store.dispatch、RootState = ReturnType<typeof store.getState>),并额外定义了一个 AppThunk 类型供异步逻辑使用。
导出带类型的 Hooks(pre-typed hooks)
组件中会大量使用 React-Redux 的 useSelector 和 useDispatch,每次使用时它们都需要引用 RootState 和 AppDispatch 类型。可以预先配置好"带类型"的钩子版本,把类型内置进去,避免重复标注。
React-Redux 9.1 提供了 .withTypes() 方法,可为这些钩子应用正确类型。导出这些预置类型钩子后,在应用其余部分直接使用:
// This file serves as a central hub for re-exporting pre-typed Redux hooks.
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
仓库中的 counter-ts 示例 hooks 文件 与本教程完全一致,是这套模式的现成参考。
到此,setup 阶段结束。
主帖子信息流(Main Posts Feed)
社交流应用的主功能是一个帖子列表。这个功能会逐步扩展,但第一个目标只是把帖子条目显示在屏幕上。
创建 Posts Slice
"slice"是应用中单个功能的一组 Redux reducer 逻辑和 action,通常一起定义在单个文件中。名字来源于把根 Redux state 对象拆分成多个"切片"。
在 src 下新建 features 文件夹,在 features 下建 posts 文件夹,并添加文件 postsSlice.ts。
将使用 Redux Toolkit 的 createSlice 函数生成一个能处理 posts 数据的 reducer 函数。Reducer 函数需要包含一些初始数据,这样应用启动时 store 里就有这些值。先用一个假帖子对象数组开始搭建 UI:
import { createSlice } from '@reduxjs/toolkit'
// Define a TS type for the data we'll be using
export interface Post {
id: string
title: string
content: string
}
// Create an initial state value for the reducer, with that type
const initialState: Post[] = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
// Create the slice and pass in the initial state
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})
// Export the generated reducer function
export default postsSlice.reducer
每创建一个新 slice,都要把它的 reducer 函数加进 Redux store。打开 app/store.ts,导入 postsReducer,移除所有 counter 相关代码,并更新 configureStore 调用,让 postsReducer 以字段名 posts 传入:
import { configureStore } from '@reduxjs/toolkit'
// Removed the `counterReducer` function, `CounterState` type, and `Action` import
import postsReducer from '@/features/posts/postsSlice'
export const store = configureStore({
reducer: {
posts: postsReducer
}
})
这告诉 Redux:顶层 state 对象中要有一个名为 posts 的字段,state.posts 的所有数据在派发 action 时都由 postsReducer 更新。可以打开 Redux DevTools 扩展确认当前 state 内容——此时应显示 posts 数组里的那两条假帖子。
显示帖子列表:useSelector 读取数据
现在 store 里有帖子数据了,可以创建一个 React 组件来展示。所有与 feed posts 功能相关的代码都应放在 posts 文件夹内,新建 PostsList.tsx(注意:这是一个使用 JSX 语法、用 TypeScript 编写的 React 组件,必须使用 .tsx 扩展名,TypeScript 才能正确编译)。
React 组件可以通过 React-Redux 库的 useSelector 钩子从 store 读取数据。自己写的"selector 函数"会接收整个 Redux state 对象作为参数,应返回该组件需要的特定数据。由于使用 TypeScript,所有组件都应使用 src/app/hooks.ts 中定义好的预置类型 useAppSelector 钩子。
初始的 PostsList 组件从 store 读取 state.posts,遍历帖子数组并在屏幕上逐条显示:
import { useAppSelector } from '@/app/hooks'
export const PostsList = () => {
// Select the `state.posts` value from the store into the component
const posts = useAppSelector(state => state.posts)
const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))
return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
然后更新 App.tsx 的路由:导入 PostsList,用 <PostsList /> 替换欢迎文本,并用 React Fragment 包一层(因为很快要在主页添加别的内容):
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import { Navbar } from './components/Navbar'
import { PostsList } from './features/posts/PostsList'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Routes>
<Route
path="/"
element={
<>
<PostsList />
</>
}
></Route>
</Routes>
</div>
</Router>
)
}
export default App
添加完成后,主页应呈现一个帖子列表("First Post!" 与 "Second Post" 两条帖子):
有了进展:数据已加入 Redux store,并已在 React 组件中显示出来。
添加新帖子
添加新帖表单
在 posts 文件夹中创建 AddPostForm.tsx,加入帖子标题文本输入和正文文本域:
import React from 'react'
// TS types for the input fields
interface AddPostFormFields extends HTMLFormControlsCollection {
postTitle: HTMLInputElement
postContent: HTMLTextAreaElement
}
interface AddPostFormElements extends HTMLFormElement {
readonly elements: AddPostFormFields
}
export const AddPostForm = () => {
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()
const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value
console.log('Values: ', { title, content })
e.currentTarget.reset()
}
return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
defaultValue=""
required
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}
注意此时还没有任何 Redux 相关逻辑。示例中使用"uncontrolled"输入和 HTML5 表单校验来防止提交空字段——如何读取表单值属于 React 使用习惯问题,与 Redux 无关。
把该组件导入 App.tsx,放在 <PostsList /> 上方:
// omit outer `<App>` definition
<Route
path="/"
element={
<>
<AddPostForm />
<PostsList />
</>
}
></Route>
表单应显示在页头下方。
保存帖子条目:case reducer 与 PayloadAction
更新 posts slice 来新增帖子。posts slice 负责处理所有 posts 数据的更新。createSlice 调用中有一个 reducers 对象(当前为空),需要在里面添加一个处理"新增帖子"情况的 reducer 函数。
在 reducers 内添加名为 postAdded 的函数,接收两个参数:当前 state 值和被派发的 action 对象。由于 posts slice 只知道它负责的数据,state 参数是帖子数组本身,而不是整个 Redux state 对象。
action 对象会把新帖子放在 action.payload 字段中。声明 reducer 函数时还需要告诉 TypeScript action.payload 的具体类型,以便它在传参和访问 action.payload 内容时做正确检查:从 Redux Toolkit 导入 PayloadAction 类型,把 action 参数声明为 action: PayloadAction<Post>。
实际的状态更新就是把新帖子对象加入 state 数组,可以直接用 state.push():
警告:Redux reducer 函数必须始终以不可变方式创建新 state(做拷贝)! 在 createSlice() 内部调用 Array.push() 这类"突变"函数、或直接修改字段(如 state.someField = someValue)是安全的,因为 createSlice 内部使用 Immer 库把这些突变转换为安全的不可变更新;但不要在 createSlice 之外尝试突变任何数据!
这个警告在仓库示例中同样成立:counterSlice.ts 的注释明确写道——"Redux Toolkit allows us to write 'mutating' logic in reducers. It doesn't actually mutate the state because it uses the Immer library, which detects changes to a 'draft state' and produces a brand new immutable state based off those changes"。
写好 postAdded reducer 后,createSlice 会自动生成同名的action creator 函数。可以导出这个 action creator,在 UI 组件中于用户点击 "Save Post" 时派发该 action:
// Import the `PayloadAction` TS type
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
// omit initial state
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// Declare a "case reducer" named `postAdded`.
// The type of `action.payload` will be a `Post` object.
postAdded(state, action: PayloadAction<Post>) {
// "Mutate" the existing state array, which is
// safe to do here because `createSlice` uses Immer inside.
state.push(action.payload)
}
}
})
// Export the auto-generated action creator with the same name
export const { postAdded } = postsSlice.actions
export default postsSlice.reducer
从术语上说,这里的 postAdded 是一个 "case reducer" 的例子:它是 slice 内部处理某一个特定 action 类型的 reducer 函数。概念上等同于在一个 switch 里写了一个 case——"当看到这个确切的 action 类型时,执行这段逻辑":
function sliceReducer(state = initialState, action) {
switch (action.type) {
case 'posts/postAdded': {
// update logic here
}
}
}
注意 action 类型字符串的格式:createSlice 的 name 字段('posts')与 reducer 名(postAdded)拼成 'posts/postAdded'——这就是后面在 DevTools 的 action 列表中看到的名字。
派发 "Post Added" action
AddPostForm 有文本输入和触发 submit handler 的 "Save Post" 按钮,但按钮目前什么都不做。需要更新 submit handler,派发 postAdded action creator 并传入包含用户所写标题和正文的新帖子对象。
帖子对象还需要 id 字段。初始测试帖子用的是假编号,与其手写代码推算下一个自增 ID,不如生成一个随机唯一 ID——Redux Toolkit 提供了一个 nanoid 函数(Part 4 会进一步讨论 ID 生成与派发 action)。
要从组件派发 action,需要拿到 store 的 dispatch 函数——通过调用 React-Redux 的 useDispatch 钩子获得。既然使用 TypeScript,应该导入带正确类型的 useAppDispatch 钩子。同时还需要把 postAdded action creator 导入该文件。
有了 dispatch 函数后,就可以在提交处理中调用 dispatch(postAdded()):从表单取出 title 和 content,生成新 ID,组合成新帖子对象传给 postAdded():
import React from 'react'
import { nanoid } from '@reduxjs/toolkit'
import { useAppDispatch } from '@/app/hooks'
import { type Post, postAdded } from './postsSlice'
// omit form types
export const AddPostForm = () => {
// Get the `dispatch` method from the store
const dispatch = useAppDispatch()
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()
const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value
// Create the post object and dispatch the `postAdded` action
const newPost: Post = {
id: nanoid(),
title,
content
}
dispatch(postAdded(newPost))
e.currentTarget.reset()
}
return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
defaultValue=""
required
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}
现在输入标题和正文、点击 "Save Post",帖子列表里应该出现这条新帖子。
恭喜——你刚刚构建了第一个能跑的 React + Redux 应用!
完整的数据流闭环(以及它在源码中的样子)
以上步骤演示了完整的 Redux 数据流循环:
- 帖子列表通过
useSelector从 store 读取初始帖子集,渲染了初始 UI - 我们派发了
postAddedaction,其中包含新帖子的数据 - posts reducer 看到
postAddedaction,用新条目更新了帖子数组 - Redux store 通知 UI 有数据发生了变化
- 帖子列表读取到更新后的帖子数组,重新渲染以显示新帖子
后续要添加的所有新功能都会遵循同样的基本模式:添加 state slice、编写 reducer 函数、派发 action、根据 store 数据渲染 UI。
可以打开 Redux DevTools 扩展,查看已派发的 action,以及 state 如何因该 action 更新:点击 action 列表中的 "posts/postAdded" 条目,"Action" 标签页会显示该 action 对象(type: 'posts/postAdded' 与 payload 中的新帖子);"Diff" 标签页则会显示 state.posts 在索引 2 处新增了一个条目。
从源码层面看,这一次 dispatch(postAdded(newPost)) 在 redux 核心中的执行路径是(见 src/createStore.ts):
- action 合法性校验:base
dispatch要求 action 必须是普通对象(isPlainObject检查,否则报错提示可能需要redux-thunk等中间件),且type必须存在、必须为字符串; - 禁止 reducer 再派发:如果当前正处于 dispatch 过程中(
isDispatching === true),调用dispatch会直接抛出 "Reducers may not dispatch actions." 错误,保证 reducer 是纯函数式的单向执行; - 同步执行 reducer:
currentState = currentReducer(currentState, action)——整个 state 树由根 reducer(由combineReducers式的合并逻辑生成)基于旧 state 和 action 计算出来,postAdded的 case reducer 通过 Immer 对state.posts的push被转成了不可变更新; - 通知所有订阅者:dispatch 完成后遍历
currentListeners(订阅列表在每次 dispatch 前做快照拷贝,防止 dispatch 期间 subscribe/unsubscribe 造成 bug),逐个调用 listener——React-Redux 的useSelector就是通过这些订阅感知到 state 变化、重跑 selector 并按需重渲染组件的。
store 对外暴露的完整接口定义在 src/types/store.ts 的 Store 接口中:dispatch、getState、subscribe、replaceReducer 以及 [Symbol.observable]——DevTools、React-Redux 都建立在这几个基础能力之上。
设计原则:store 只放"全局"数据
原文档强调:Redux store 只应包含被认为是应用"全局"(global)的数据! 本例中,只有 AddPostForm 需要知道输入框的最新值。即使表单改用"受控"(controlled)输入,临时数据也应该放在 React 组件 state 中,而不是塞进 Redux store。当用户完成表单操作后,再派发一个 Redux action,用用户的最终输入去更新 store。
本篇总结
到这里,Redux 应用的基础已经搭好——store、带 reducer 的 slice、以及派发 action 的 UI。回顾本部分学到的要点:
- 一个 Redux 应用只有一个
store,通过<Provider>组件传递给 React 组件 - Redux state 由 "reducer 函数" 更新:
- reducer 总是以不可变方式计算新 state:拷贝现有 state 值,并在拷贝上应用新数据
- Redux Toolkit 的
createSlice会为你生成 "slice reducer" 函数,让你可以写"突变"式代码,由内部转化为安全的不可变更新 - 这些 slice reducer 被加入
configureStore的reducer字段,从而定义了 store 内的数据和 state 字段名
- React 组件用
useSelector钩子从 store 读取数据:- selector 函数接收整个
state对象,应返回一个值 - 每次 Redux store 更新时 selector 都会重跑,如果其返回的数据发生变化,组件会重渲染
- selector 函数接收整个
- React 组件用
useDispatch钩子派发 action 更新 store:createSlice会为 slice 中每个 reducer 生成对应的 action creator 函数- 在组件中调用
dispatch(someActionCreator())派发 action - reducer 运行后检查该 action 是否与己相关,必要时返回新 state
- 表单输入值等临时数据应保留为 React 组件 state 或纯 HTML 输入字段;用户完成表单后再派发 Redux action 更新 store
- 如果使用 TypeScript,初始 setup 应基于 store 定义
RootState和AppDispatch类型,并导出 React-ReduxuseSelector和useDispatch钩子的预置类型版本
下一步
掌握了基础 Redux 数据流之后,可以继续 Part 4: Using Redux Data,为应用添加更多功能,学习如何操作已经在 store 中的数据;更完整的数据流全景可以结合 Part 1 与 Part 2: App Structure 一起阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

