首页
/ Redux Essentials Part 4 详解:在 React 组件中使用 Redux 数据(选择器、prepare 回调与 extraReducers)

Redux Essentials Part 4 详解:在 React 组件中使用 Redux 数据(选择器、prepare 回调与 extraReducers)

2026-09-04 13:22:23作者:戚魁泉Nursing

本篇基于 Redux 官方仓库中的 Essentials 系列教程第四部分,围绕"如何让多个 React 组件正确地读、改 Redux 数据"展开:从单帖页面与编辑表单,到 prepare 回调准备 action payload、可复用选择器封装状态读取、以及用 extraReducers 让多个 slice 响应同一个 action。读完后,你将掌握组织 dispatch 逻辑、封装 selector、在多 slice 应用间共享事件这几类 Redux 核心模式,并能理解它们背后的 store 更新机制。

前置知识与本章目标

本章假设你已经掌握 Part 3 的 Redux 数据流与 React-Redux API——即如何从空项目起步、添加 slice、用 useSelector/useDispatch 读数据和派发 action,以及如何创建带类型的 useAppSelector/useAppDispatch。熟悉 React Router 的 <Link><Route> 页面路由也有帮助。

在 Part 3 里我们完成了社交媒体信息流的基础版。本章用同样的核心步骤继续给应用加功能,使其更实用:

  • 查看单篇帖子(SinglePostPage
  • 编辑已有帖子(EditPostForm + postUpdated reducer)
  • 显示帖子作者(新增 users slice)
  • 帖子时间戳与相对时间显示(date 字段 + TimeAgo
  • emoji 反应按钮(reactionAdded reducer)
  • 用户登录/登出(auth slice + ProtectedRoute + extraReducers

本章代码示例聚焦每部分的关键概念与改动,完整可运行代码见 Redux Essentials 官方示例工程(redux-essentials-example-app)中对应 checkpoint 的分支。

显示单篇帖子

主信息流里帖子正文过长时只显示节选。为了能完整阅读,我们需要一个独立的帖子详情页,URL 形如 /posts/123,其中 123 是要查看的帖子 ID。

创建 SinglePostPage 组件

features/posts 功能文件夹中新增 SinglePostPage 组件。路由配置会告诉 React Router 把 URL 的第二段解析为名为 postId 的变量,组件通过 useParams 钩子读取它:

import { useParams } from 'react-router-dom'

import { useAppSelector } from '@/app/hooks'

export const SinglePostPage = () => {
  const { postId } = useParams()

  const post = useAppSelector(state =>
    state.posts.find(post => post.id === postId)
  )

  if (!post) {
    return (
      <section>
        <h2>Post not found!</h2>
      </section>
    )
  }

  return (
    <section>
      <article className="post">
        <h2>{post.title}</h2>
        <p className="post-content">{post.content}</p>
      </article>
    </section>
  )
}

拿到 postId 后,我们在 selector 函数里用它从 Redux store 中找到对应的帖子对象。state.posts 是全部帖子的数组,用 Array.find() 遍历并返回 ID 匹配的那条即可。

有两个关键点值得注意:

  1. 组件会在 useAppSelector 返回值变成新引用时重新渲染。 组件应始终尽量只选择它渲染所需的最小数据集,这样才能确保只在真正需要时渲染。
  2. 必须处理"找不到"的分支。 用户可能直接手敲 URL,或者数据尚未加载,此时 find() 返回 undefined 而不是帖子对象。组件要检查这种情况并显示 "Post not found!" 提示;若能取到帖子对象,则渲染标题与正文。

这段逻辑和 <PostsList> 里遍历 posts 数组渲染节选的代码比较相似。你可以试着抽出一个 Post 组件在两个地方复用,但帖子节选与完整帖子的展示方式已有差别——通常更好的做法是先各自独立编写,观察一段时间后如果发现两块代码足够相似,再抽取可复用组件。

添加单帖路由与链接

组件就位后,在 App.tsx 中定义路由。顺便把"主页"内容抽成独立的 <PostsMainPage> 组件提升可读性:

import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

import { Navbar } from './components/Navbar'
import { PostsMainPage } from './features/posts/PostsMainPage'
import { SinglePostPage } from './features/posts/SinglePostPage'

function App() {
  return (
    <Router>
      <Navbar />
      <div className="App">
        <Routes>
          <Route path="/" element={<PostsMainPage />}></Route>
          <Route path="/posts/:postId" element={<SinglePostPage />} />
        </Routes>
      </div>
    </Router>
  )
}

export default App

然后在 <PostsList> 的渲染逻辑里给每篇帖子加上指向该帖的 <Link>

import { Link } from 'react-router-dom'
import { useAppSelector } from '@/app/hooks'

export const PostsList = () => {
  const posts = useAppSelector(state => state.posts)

  const renderedPosts = posts.map(post => (
    <article className="post-excerpt" key={post.id}>
      <h3>
        <Link to={`/posts/${post.id}`}>{post.title}</Link>
      </h3>
      <p className="post-content">{post.content.substring(0, 100)}</p>
    </article>
  ))

  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {renderedPosts}
    </section>
  )
}

由于现在可以从列表页点进详情页了,<Navbar> 里也应提供一个返回主帖列表页的链接:

import { Link } from 'react-router-dom'

export const Navbar = () => {
  return (
    <nav>
      <section>
        <h1>Redux Essentials Example</h1>

        <div className="navContent">
          <div className="navLinks">
            <Link to="/">Posts</Link>
          </div>
        </div>
      </section>
    </nav>
  )
}

编辑帖子

写完帖子保存后才发现写错地方是很常见的体验,所以我们需要"编辑已有帖子"的能力。新组件 <EditPostForm> 要能接收一个帖子 ID,从 store 中读出该帖,让用户修改标题和正文,保存后把改动写回 store。

更新帖子条目:postUpdated reducer

首先要更新 postsSlice,新增一个 reducer 函数和对应 action,让 store 知道如何更新帖子。在 createSlice()reducers 对象里添加一个新函数。reducer 的命名应当能清晰描述正在发生的事,因为它会作为 action type 字符串的一部分出现在 Redux DevTools 里。第一个 reducer 叫 postAdded,所以这个叫 postUpdated

Redux 本身不关心你给 reducer 函数起什么名字——叫 postAddedaddPostPOST_ADDED 还是 someRandomName,运行效果完全一样。但官方建议用过去式命名(如 postAdded),因为我们在描述的是"应用中发生过的一个事件"

要更新一个帖子对象,需要知道:

  • 被更新帖子的 ID,以便在 state 中找到正确对象
  • 用户输入的新 titlecontent 字段

Redux action 对象必须有 type 字段(通常是有描述性的字符串),还可以携带其他描述"发生了什么"的字段。约定上把附加信息放在 action.payload 中,而 payload 里放什么由我们自己决定——字符串、数字、对象、数组都行。这里有三块信息(idtitlecontent),所以让 payload 是一个包含这三个字段的对象,action 对象形如 {type: 'posts/postUpdated', payload: {id, title, content}}

createSlice 生成的 action creator 默认期望传入一个参数,该值会成为 action.payload。和 postAdded 一样,这里传的是完整 Post 对象,因此 reducer 参数声明为 action: PayloadAction<Post>

reducer 负责决定 state 实际如何更新:根据 ID 找到帖子对象,只更新其 titlecontent 字段:

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// omit state types

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded(state, action: PayloadAction<Post>) {
      state.push(action.payload)
    },
    postUpdated(state, action: PayloadAction<Post>) {
      const { id, title, content } = action.payload
      const existingPost = state.find(post => post.id === id)
      if (existingPost) {
        existingPost.title = title
        existingPost.content = content
      }
    }
  }
})

export const { postAdded, postUpdated } = postsSlice.actions

export default postsSlice.reducer

最后把 createSlice 生成的 action creator 导出,这样 UI 在用户保存帖子时才能派发 postUpdated

创建 EditPostForm 组件

<EditPostForm> 看起来类似 <AddPostForm><SinglePostPage>,但逻辑略有不同:需要根据 URL 中的 postId 从 store 取出帖子对象,用它初始化输入框,用户提交时把新的标题和正文写回 store,并用 React Router 的 useNavigate 钩子在保存后跳转到单帖页:

import React from 'react'
import { useNavigate, useParams } from 'react-router-dom'

import { useAppSelector, useAppDispatch } from '@/app/hooks'
import { postUpdated } from './postsSlice'

// omit form element types

export const EditPostForm = () => {
  const { postId } = useParams()

  const post = useAppSelector(state =>
    state.posts.find(post => post.id === postId)
  )

  const dispatch = useAppDispatch()
  const navigate = useNavigate()

  if (!post) {
    return (
      <section>
        <h2>Post not found!</h2>
      </section>
    )
  }

  const onSavePostClicked = (e: React.FormEvent<EditPostFormElements>) => {
    // Prevent server submission
    e.preventDefault()

    const { elements } = e.currentTarget
    const title = elements.postTitle.value
    const content = elements.postContent.value

    if (title && content) {
      dispatch(postUpdated({ id: post.id, title, content }))
      navigate(`/posts/${postId}`)
    }
  }

  return (
    <section>
      <h2>Edit Post</h2>
      <form onSubmit={onSavePostClicked}>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          defaultValue={post.title}
          required
        />
        <label htmlFor="postContent">Content:</label>
        <textarea
          id="postContent"
          name="postContent"
          defaultValue={post.content}
          required
        />

        <button>Save Post</button>
      </form>
    </section>
  )
}

注意这里的 Redux 相关代码非常少:通过 useAppSelector 从 store 读值,在用户与 UI 交互时通过 useAppDispatch 派发 action,仅此而已。

SinglePostPage 一样,需要在 App.tsx 中导入并添加以 postId 为路由参数的路由:

import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

import { Navbar } from './components/Navbar'
import { PostsMainPage } from './features/posts/PostsMainPage'
import { SinglePostPage } from './features/posts/SinglePostPage'
import { EditPostForm } from './features/posts/EditPostForm'

function App() {
  return (
    <Router>
      <Navbar />
      <div className="App">
        <Routes>
          <Route path="/" element={<PostsMainPage />}></Route>
          <Route path="/posts/:postId" element={<SinglePostPage />} />
          <Route path="/editPost/:postId" element={<EditPostForm />} />
        </Routes>
      </div>
    </Router>
  )
}

export default App

再在 SinglePostPage 中加一个路由到 EditPostForm 的链接:

import { Link, useParams } from 'react-router-dom'

export const SinglePostPage = () => {
  // omit other contents

  <p className="post-content">{post.content}</p>

  <Link to={`/editPost/${post.id}`} className="button">
    Edit Post
  </Link>

用 prepare 回调准备 Action Payload

前面看到,createSlice 生成的 action creator 通常接收一个参数并作为 action.payload。这简化了最常见的使用方式,但有时需要更多工作来准备 action 对象的内容。以 postAdded 为例:我们需要为新帖子生成一个唯一 ID,还要保证 payload 形如 {id, title, content}

当前我们在 React 组件里生成 ID、构造 payload 对象再传给 postAdded。但如果有多个组件需要派发同一个 action,或者构造 payload 的逻辑很复杂呢?那样每次派发都要重复这套逻辑,而且组件被迫要知道这个 action 的 payload 长什么样。

如果 action 需要包含唯一 ID 或其他随机值,一定要先生成好再放进 action 对象。Reducer 永远不应计算随机值,那会让结果不可预测。

如果手写 action creator,可以把准备工作逻辑放进去:

// hand-written action creator
function postAdded(title: string, content: string) {
  const id = nanoid()
  return {
    type: 'posts/postAdded',
    payload: { id, title, content }
  }
}

createSlice 已经替我们生成了这些 action creator——代码更短,却需要另一种方式定制 action.payloadcreateSlice 提供了"prepare 回调":reducer 定义可以是一个 {reducer, prepare} 对象,prepare 函数可以接收多个参数、生成唯一 ID 等随机值、执行任意同步逻辑来决定 action 对象的内容,最后返回一个含 payload 字段的对象(返回对象还可以包含 meta 字段用于附加描述性信息,以及表示该 action 是否代表某种错误的布尔 error 字段):

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action: PayloadAction<Post>) {
        state.push(action.payload)
      },
      prepare(title: string, content: string) {
        return {
          payload: { id: nanoid(), title, content }
        }
      }
    }
    // other reducers here
  }
})

组件因此不再关心 payload 对象长什么样——action creator 会把它组装好。于是可以把组件改成直接传入 titlecontent 两个参数,ID 自动生成:

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

  // Now we can pass these in as separate arguments,
  // and the ID will be generated automatically
  dispatch(postAdded(title, content))

  e.currentTarget.reset()
}

源码佐证:本章教程使用的是 @reduxjs/toolkitconfigureStore。在本仓库核心包的 src/createStore.ts 中可以看到 dispatch 的基础校验:action 必须是普通对象(isPlainObject 检查)、type 必须是字符串且不能为 undefined,并且 reducer 执行期间禁止再次 dispatch("Reducers may not dispatch actions.")。这正是"action 是纯数据事件、随机值必须在 dispatch 前生成进 payload"这条规则在核心层的依据——校验逻辑见 src/utils/isAction.ts。同时,src/createStore.ts 的 JSDoc 也明确标注 createStore 已弃用,推荐使用 @reduxjs/toolkitconfigureStore——教程示例工程正是遵循这一官方建议。

用选择器(Selectors)读取数据

此时已经有好几个组件在按 ID 查找帖子,重复着 state.posts.find() 调用。这既是重复代码,又是脆弱的——后续章节我们会改变 posts slice 的 state 结构(规范化),届时必须找到每处引用 state.posts 的代码逐一修改。TypeScript 能在编译期帮我们抓住与 state 类型不匹配的代码,但更理想的是:改数据格式时不必反复重写组件,也不必在组件里重复逻辑。

一个办法是在 slice 文件中定义可复用的 selector 函数,组件用这些 selector 提取数据。这样以后 state 结构变化时,只需要更新 slice 文件里的代码。

定义 Selector 函数

每次调用 useAppSelector(state => state.posts) 时其实都在写 selector 函数,只是定义为内联形式。它就是一个普通函数,也可以写成独立形式:

const selectPosts = (state: RootState) => state.posts
const posts = useAppSelector(selectPosts)

Selector 通常作为独立函数写在 slice 文件里。它们一般以整个 Redux RootState 作为第一个参数,还可以接受其他参数。

抽取 Posts Selectors

<PostsList> 需要读取全部帖子列表,<SinglePostPage><EditPostForm> 需要按 ID 查找单帖。我们从 postsSlice.ts 导出两个小 selector:

import type { RootState } from '@/app/store'

const postsSlice = createSlice(/* omit slice code*/)

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

export default postsSlice.reducer

export const selectAllPosts = (state: RootState) => state.posts

export const selectPostById = (state: RootState, postId: string) =>
  state.posts.find(post => post.id === postId)

注意这些 selector 的 state 参数就是根 state 对象,与直接写在 useAppSelector 里的内联匿名 selector 一样。

组件中使用方式:

// omit imports
import { selectAllPosts } from './postsSlice'

export const PostsList = () => {
  const posts = useAppSelector(selectAllPosts)
  // omit component contents
}
// omit imports
import { selectPostById } from './postsSlice'

export const SinglePostPage = () => {
  const { postId } = useParams()

  const post = useAppSelector(state => selectPostById(state, postId!))
  // omit component logic
}
// omit imports
import { postUpdated, selectPostById } from './postsSlice'

export const EditPostForm = () => {
  const { postId } = useParams()

  const post = useAppSelector(state => selectPostById(state, postId!))
  // omit component logic
}

useParams() 得到的 postId 类型是 string | undefined,而 selectPostById 期望一个有效的 string。可以用 TS 的 ! 非空断言告诉编译器此处不会是 undefined。(这有一定危险性,但这里可以这样假设,因为我们知道路由配置只在 URL 中有帖子 ID 时才渲染 <EditPostForm>。)

后续我们延续这个模式:selector 写在 slice 中,而不是内联在组件的 useAppSelector 里。记住这并非强制要求,但是一个值得遵循的好模式!

有效使用选择器

封装数据查找、写可复用 selector 通常是好主意。理想情况下组件甚至不需要知道数据存放在 Redux state 的哪个位置——直接用 slice 提供的 selector 访问即可。

之后教程还会介绍"记忆化(memoized)selector",通过优化重渲染、跳过不必要的重算来提升性能。

但和任何抽象一样,它不应该是无时无刻、无处不做的事。写 selector 意味着更多需要理解和维护的代码。不要觉得每个 state 字段都必须写 selector。可以先不写,等发现自己在应用的很多地方查找同一批值时,再补上。

可选:在 createSlice 内定义 Selector

前面我们看到 selector 可以写为 slice 文件中的独立函数。有时也可以直接在 createSlice 内部定义来精简代码:createSlice 除了必需的 nameinitialStatereducers 字段和可选的 extraReducers 字段外,还接受一个可选的 selectors 字段。它的形状类似 reducers:键是 selector 函数名,值是待生成的 selector 函数。

与独立 selector 函数的关键区别:这些 selector 的 state 参数只是该 slice 的 slice state,而不再是整个 RootState

把 posts slice 的 selector 移到 createSlice 内部大致是这样:

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    /* omit reducer logic */
  },
  selectors: {
    // Note that these selectors are given just the `PostsState`
    // as an argument, not the entire `RootState`
    selectAllPosts: postsState => postsState,
    selectPostById: (postsState, postId: string) => {
      return postsState.find(post => post.id === postId)
    }
  }
})

export const { selectAllPosts, selectPostById } = postsSlice.selectors

export default postsSlice.reducer

// We've replaced these standalone selectors:
// export const selectAllPosts = (state: RootState) => state.posts
//
// export const selectPostById = (state: RootState, postId: string) =>
//   state.posts.find(post => post.id === postId)

仍然有些场合必须在 createSlice 外写独立 selector 函数——尤其是当你要调用其他需要完整 RootState 作为参数的 selector 时,独立函数才能让类型正确对齐。

用户与帖子

目前应用只有一个 state slice:逻辑在 postsSlice.ts,数据在 state.posts,所有组件都与 posts 功能相关。真实应用通常有很多不同的 state slice 和若干"功能文件夹"。

社交媒体应用总得有"别人"参与!我们给应用加上维护用户列表的能力,并让帖子功能利用这些数据。

添加 Users Slice

"users"和"posts"是不同概念,所以要把它们的代码和数据分开:新增 features/users 文件夹并放入 usersSlice 文件。和 posts slice 一样,先加几条初始数据以便操作:

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import type { RootState } from '@/app/store'

interface User {
  id: string
  name: string
}

const initialState: User[] = [
  { id: '0', name: 'Tianna Jenkins' },
  { id: '1', name: 'Kevin Grant' },
  { id: '2', name: 'Madison Price' }
]

const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {}
})

export default usersSlice.reducer

export const selectAllUsers = (state: RootState) => state.users

export const selectUserById = (state: RootState, userId: string | null) =>
  state.users.find(user => user.id === userId)

暂时不需要更新数据,所以 reducers 字段留空对象(后面章节会补上)。和之前一样,把 usersReducer 导入 store 文件并加入配置:

import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '@/features/posts/postsSlice'
import usersReducer from '@/features/users/usersSlice'

export default configureStore({
  reducer: {
    posts: postsReducer,
    users: usersReducer
  }
})

现在根 state 的形状是 {posts, users},与传给 reducer 参数的对象一致。

为帖子添加作者字段

应用里每篇帖子都由某个用户撰写,每次新增帖子都应记录是哪位用户写的。这需要改动 Redux state 和 <AddPostForm> 组件两部分。

首先更新 Post 数据类型,加入 user: string 字段(创建该帖的用户 ID),并给 initialState 中的已有帖子补上 user 字段。然后更新现有 reducer:postAdded 的 prepare 回调要接收用户 ID 参数并放入 action;而更新帖子时想携带 user 字段——更新只需要被改帖子的 id 以及新的 titlecontent。为此定义一个只含这三个字段的 PostUpdate 类型,作为 postUpdated 的 payload:

export interface Post {
  id: string
  title: string
  content: string
  user: string
}

type PostUpdate = Pick<Post, 'id' | 'title' | 'content'>

const initialState: Post[] = [
  { id: '1', title: 'First Post!', content: 'Hello!', user: '0' },
  { id: '2', title: 'Second Post', content: 'More text', user: '2' }
]

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action: PayloadAction<Post>) {
        state.push(action.payload)
      },
      prepare(title: string, content: string, userId: string) {
        return {
          payload: {
            id: nanoid(),
            title,
            content,
            user: userId
          }
        }
      }
    },
    postUpdated(state, action: PayloadAction<PostUpdate>) {
      const { id, title, content } = action.payload
      const existingPost = state.find(post => post.id === id)
      if (existingPost) {
        existingPost.title = title
        existingPost.content = content
      }
    }
  }
})

现在 <AddPostForm> 可以用 useSelector 从 store 读取用户列表并展示为下拉框,取选中用户的 ID 传给 postAdded action creator。顺带给表单加一点校验:只有标题和正文都填了内容才允许点 "Save Post":

import { selectAllUsers } from '@/features/users/usersSlice'

// omit other imports and form types

const AddPostForm = () => {
  const dispatch = useAppDispatch()
  const users = useAppSelector(selectAllUsers)

  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
    const userId = elements.postAuthor.value

    dispatch(postAdded(title, content, userId))

    e.currentTarget.reset()
  }

  const usersOptions = users.map(user => (
    <option key={user.id} value={user.id}>
      {user.name}
    </option>
  ))

  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="postAuthor">Author:</label>
        <select id="postAuthor" name="postAuthor" required>
          <option value=""></option>
          {usersOptions}
        </select>
        <label htmlFor="postContent">Content:</label>
        <textarea
          id="postContent"
          name="postContent"
          defaultValue=""
          required
        />
        <button>Save Post</button>
      </form>
    </section>
  )
}

接下来需要在帖子列表项和 <SinglePostPage> 里显示作者名。由于这种信息要在多处显示,做一个 PostAuthor 组件:接收用户 ID 作为 prop,查出对应的用户对象并格式化用户名:

import { useAppSelector } from '@/app/hooks'

import { selectUserById } from '@/features/users/usersSlice'

interface PostAuthorProps {
  userId: string
}

export const PostAuthor = ({ userId }: PostAuthorProps) => {
  const author = useAppSelector(state => selectUserById(state, userId))

  return <span>by {author?.name ?? 'Unknown author'}</span>
}

注意每个组件都在遵循同一个模式:任何需要从 Redux store 读数据的组件都可以用 useAppSelector 钩子提取它需要的具体数据;同时,许多组件可以同时访问 Redux store 中的同一份数据。

PostAuthor 导入 PostsList.tsxSinglePostPage.tsx,以 <PostAuthor userId={post.user} /> 渲染即可。每新增一条帖子,选中用户的名字都会显示在渲染的帖子里。

更多帖子功能:时间戳与列表排序

为帖子存储日期

社交媒体信息流通常按创建时间排序,并以"5 hours ago"这类相对描述显示创建时间。为此需要给帖子条目跟踪 date 字段。

post.user 字段一样,要更新 postAdded 的 prepare 回调保证派发时总带上 post.date。但它不是传入的参数——我们总是想用派发那一刻的时间戳,所以让 prepare 回调自己生成:

Redux 的 action 和 state 应只包含纯 JS 值:对象、数组和基本类型。不要把类实例、函数、Date/Map/Set 实例或其他不可序列化值放进 Redux!

既然不能直接把 Date 类实例放进 store,post.date 就以时间戳字符串形式保存。初始 state 值里加上 date(用 date-fnssub 从当前时间往前减几分钟),prepare 回调里也加:

import { createSlice, nanoid } from '@reduxjs/toolkit'
import { sub } from 'date-fns'

const initialState: Post[] = [
  {
    // omitted fields
    content: 'Hello!',
    date: sub(new Date(), { minutes: 10 }).toISOString()
  },
  {
    // omitted fields
    content: 'More text',
    date: sub(new Date(), { minutes: 5 }).toISOString()
  }
]

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded: {
      reducer(state, action: PayloadAction<Post>) {
        state.push(action.payload)
      },
      prepare(title: string, content: string, userId: string) {
        return {
          payload: {
            id: nanoid(),
            date: new Date().toISOString(),
            title,
            content,
            user: userId
          }
        }
      }
    }
    // omit `postUpdated
  }
})

相对时间显示组件 TimeAgo

和作者一样,相对时间戳描述需要在 <PostsList><SinglePostPage> 中都显示。新增 <TimeAgo> 组件,用 date-fnsparseISOformatDistanceToNow 把时间戳字符串格式化为相对描述:

import { parseISO, formatDistanceToNow } from 'date-fns'

interface TimeAgoProps {
  timestamp: string
}

export const TimeAgo = ({ timestamp }: TimeAgoProps) => {
  let timeAgo = ''
  if (timestamp) {
    const date = parseISO(timestamp)
    const timePeriod = formatDistanceToNow(date)
    timeAgo = `${timePeriod} ago`
  }

  return (
    <time dateTime={timestamp} title={timestamp}>
      &nbsp; <i>{timeAgo}</i>
    </time>
  )
}

排序帖子列表

<PostsList> 当前按帖子在 store 中的存储顺序显示:示例里最老的帖子在前,新帖追加到数组末尾——新帖总是出现在页面底部。社交信息流通常是最新在前。虽然我们知道 state.posts 数组已经是"最老在前"排好的,理论上反转列表即可,但为了保险还是自己显式排序。

由于 array.sort() 会原地修改现有数组,需要先复制 state.posts 再对副本排序。post.date 保存的是 ISO 时间戳字符串,可以直接字符串比较来排序:

// Sort posts in reverse chronological order by datetime string
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date))

const renderedPosts = orderedPosts.map(post => {
  // omit rendering logic
})

帖子反应按钮

我们的帖子现在还有点无聊——让朋友们能给帖子加 emoji 反应吧!在 <PostsList><SinglePostPage> 的每篇帖子底部加一排 emoji 反应按钮。用户每点一次,就要更新 Redux store 中该帖子对应的计数器字段。由于反应计数数据在 Redux store 里,在应用不同部分间切换时,任何使用该数据的组件都会一致地显示相同的值。

在帖子中跟踪反应数据

数据里还没有 post.reactions 字段,所以要更新 initialState 帖子对象和 postAdded 的 prepare 回调,保证每篇帖子都带有如 reactions: {thumbsUp: 0, tada: 0, heart: 0, rocket: 0, eyes: 0} 的数据。

然后定义处理"用户点击反应按钮"的新 reducer。和编辑帖子类似,需要知道帖子 ID 和用户点了哪个反应。action.payload 是形如 {id, reaction} 的对象,reducer 找到对应帖子并更新正确的 reactions 字段:

import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
import { sub } from 'date-fns'

export interface Reactions {
  thumbsUp: number
  tada: number
  heart: number
  rocket: number
  eyes: number
}

export type ReactionName = keyof Reactions

export interface Post {
  id: string
  title: string
  content: string
  user: string
  date: string
  reactions: Reactions
}

type PostUpdate = Pick<Post, 'id' | 'title' | 'content'>

const initialReactions: Reactions = {
  thumbsUp: 0,
  tada: 0,
  heart: 0,
  rocket: 0,
  eyes: 0
}

const initialState: Post[] = [
  // omit initial state
]

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit other reducers
    reactionAdded(
      state,
      action: PayloadAction<{ postId: string; reaction: ReactionName }>
    ) {
      const { postId, reaction } = action.payload
      const existingPost = state.find(post => post.id === postId)
      if (existingPost) {
        existingPost.reactions[reaction]++
      }
    }
  }
})

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

几个重要的 Redux 理念在这里集中体现:

  • createSlice 允许在 reducer 里写"看起来像修改(mutating)"的逻辑。 如果不用 createSlice 和 Immer,existingPost.reactions[reaction]++ 这一行会真的修改已有 post.reactions 对象,违反 reducer 规则并很可能在应用其他地方引发 bug。而因为用了 createSlice,这种更复杂的更新逻辑可以写得更简单,由 Immer 负责把它变成安全的不可变更新。
  • action 对象只包含描述"发生了什么"所需的最少信息:哪个帖子、哪个反应名被点击。可以把算好的新计数值放进 action,但action 对象越小越好,状态更新计算应放在 reducer 里
  • 由此推出:reducer 可以包含计算新 state 所需的任意多逻辑,状态更新逻辑应该写在 reducer 里。这避免了在不同组件里重复逻辑、或 UI 层拿不到最新数据的问题。

使用 Immer 时,你可以"修改"现有 state 对象,或者自己返回一个新 state 值,但不能两者同时做。

显示 ReactionButtons 组件

和作者、时间戳一样,反应要在所有显示帖子的地方使用,所以创建 <ReactionButtons> 组件,接收 post 作为 prop。用户点击按钮时,用该反应的 emoji 名派发 reactionAdded

import { useAppDispatch } from '@/app/hooks'

import type { Post, ReactionName } from './postsSlice'
import { reactionAdded } from './postsSlice'

const reactionEmoji: Record<ReactionName, string> = {
  thumbsUp: '👍',
  tada: '🎉',
  heart: '❤️',
  rocket: '🚀',
  eyes: '👀'
}

interface ReactionButtonsProps {
  post: Post
}

export const ReactionButtons = ({ post }: ReactionButtonsProps) => {
  const dispatch = useAppDispatch()

  const reactionButtons = Object.entries(reactionEmoji).map(
    ([stringName, emoji]) => {
      // Ensure TS knows this is a _specific_ string type
      const reaction = stringName as ReactionName
      return (
        <button
          key={reaction}
          type="button"
          className="muted-button reaction-button"
          onClick={() => dispatch(reactionAdded({ postId: post.id, reaction }))}
        >
          {emoji} {post.reactions[reaction]}
        </button>
      )
    }
  )

  return <div>{reactionButtons}</div>
}

现在每点一次反应按钮,该反应的计数器就加一。在应用不同部分间浏览时,每次查看这篇帖子都会看到正确的计数——即使在 <PostsList> 里点了反应按钮、再到 <SinglePostPage> 单独查看该帖子也一样。这是因为每个组件读的都是 Redux store 中同一份帖子数据。

添加用户登录

添加 Auth Slice

目前是在 <AddPostForm> 里手动选择"谁在写帖子"。为了更真实,应让用户先登录,这样就知道帖子是谁写的(也为后续功能打基础)。作为小型示例应用,这里不实现真正的认证校验(重点是学习如何使用 Redux 功能,而非实现真正的 auth),只显示用户名列表让用户选一个。

新增一个 auth slice 跟踪 state.auth.username,之后用户添加帖子时就能自动带上正确的用户 ID。认证 state 就是当前登录用户名,登出时重置为 null

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface AuthState {
  username: string | null
}

const initialState: AuthState = {
  // Note: a real app would probably have more complex auth state,
  // but for this example we'll keep things simple
  username: null
}

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    userLoggedIn(state, action: PayloadAction<string>) {
      state.username = action.payload
    },
    userLoggedOut(state) {
      state.username = null
    }
  }
})

export const { userLoggedIn, userLoggedOut } = authSlice.actions

export const selectCurrentUsername = (state: RootState) => state.auth.username

export default authSlice.reducer
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
  }
})

添加登录页与 ProtectedRoute

改变应用入口行为:用户首先看到登录界面,登录后才能看到帖子页。创建 <LoginPage> 组件:从 store 读取用户列表、展示为下拉框,表单提交时派发 userLoggedIn,然后导航到 /posts 路由以显示 <PostsMainPage>

import React from 'react'
import { useNavigate } from 'react-router-dom'

import { useAppDispatch, useAppSelector } from '@/app/hooks'
import { selectAllUsers } from '@/features/users/usersSlice'

import { userLoggedIn } from './authSlice'

interface LoginPageFormFields extends HTMLFormControlsCollection {
  username: HTMLSelectElement
}
interface LoginPageFormElements extends HTMLFormElement {
  readonly elements: LoginPageFormFields
}

export const LoginPage = () => {
  const dispatch = useAppDispatch()
  const users = useAppSelector(selectAllUsers)
  const navigate = useNavigate()

  const handleSubmit = (e: React.FormEvent<LoginPageFormElements>) => {
    e.preventDefault()

    const username = e.currentTarget.elements.username.value
    dispatch(userLoggedIn(username))
    navigate('/posts')
  }

  const usersOptions = users.map(user => (
    <option key={user.id} value={user.id}>
      {user.name}
    </option>
  ))

  return (
    <section>
      <h2>Welcome to Tweeter!</h2>
      <h3>Please log in:</h3>
      <form onSubmit={handleSubmit}>
        <label htmlFor="username">User:</label>
        <select id="username" name="username" required>
          <option value=""></option>
          {usersOptions}
        </select>
        <button>Log In</button>
      </form>
    </section>
  )
}

接下来更新 <App> 的路由:根路由 / 显示 <LoginPage>,未授权访问其他页面时重定向回登录页。常见做法是加一个"受保护路由"组件:接收若干 React 组件作为 children,做授权检查,只有用户已授权才显示子组件。<ProtectedRoute> 读取 state.auth.username 做检查,把帖子相关的路由区块整个包起来:

import {
  BrowserRouter as Router,
  Route,
  Routes,
  Navigate
} from 'react-router-dom'

import { useAppSelector } from './app/hooks'
import { Navbar } from './components/Navbar'
import { LoginPage } from './features/auth/LoginPage'
import { PostsMainPage } from './features/posts/PostsMainPage'
import { SinglePostPage } from './features/posts/SinglePostPage'
import { EditPostForm } from './features/posts/EditPostForm'

import { selectCurrentUsername } from './features/auth/authSlice'

const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
  const username = useAppSelector(selectCurrentUsername)

  if (!username) {
    return <Navigate to="/" replace />
  }

  return children
}

function App() {
  return (
    <Router>
      <Navbar />
      <div className="App">
        <Routes>
          <Route path="/" element={<LoginPage />} />
          <Route
            path="/*"
            element={
              <ProtectedRoute>
                <Routes>
                  <Route path="/posts" element={<PostsMainPage />} />
                  <Route path="/posts/:postId" element={<SinglePostPage />} />
                  <Route path="/editPost/:postId" element={<EditPostForm />} />
                </Routes>
              </ProtectedRoute>
            }
          />
        </Routes>
      </div>
    </Router>
  )
}

export default App

现在认证行为的两面都工作正常:

  • 用户未登录时访问 /posts<ProtectedRoute> 会把用户重定向回 / 显示 <LoginPage>
  • 用户登录后派发 userLoggedIn() 更新 Redux state,并强制导航到 /posts,此时 <ProtectedRoute> 会放行显示帖子页。

用当前用户更新 UI

既然应用运行期间已经知道登录者是谁,就可以在导航栏显示真实用户名,并提供"Log Out"按钮。

需要先从 store 拿到当前用户对象以读取 user.name。做法是先用 auth slice 拿到当前用户名,再据此查找用户对象。这个操作可能在多处需要,适合写成一个可复用的 selectCurrentUser selector。把它放在 usersSlice.ts 中,内部导入并依赖 authSlice.tsselectCurrentUsername

import { selectCurrentUsername } from '@/features/auth/authSlice'

// omit the rest of the slice and selectors

export const selectCurrentUser = (state: RootState) => {
  const currentUsername = selectCurrentUsername(state)
  return selectUserById(state, currentUsername)
}

把一个 selector 组合在另一个 selector 内部使用常常很有用——这里同时组合了 selectCurrentUsernameselectUserById

其余模式与前文一致:从 store 选中相关 state(当前用户对象)、显示其值,点击 "Log Out" 按钮时派发 userLoggedOut()

import { Link } from 'react-router-dom'

import { useAppDispatch, useAppSelector } from '@/app/hooks'

import { userLoggedOut } from '@/features/auth/authSlice'
import { selectCurrentUser } from '@/features/users/usersSlice'

import { UserIcon } from './UserIcon'

export const Navbar = () => {
  const dispatch = useAppDispatch()
  const user = useAppSelector(selectCurrentUser)

  const isLoggedIn = !!user

  let navContent: React.ReactNode = null

  if (isLoggedIn) {
    const onLogoutClicked = () => {
      dispatch(userLoggedOut())
    }

    navContent = (
      <div className="navContent">
        <div className="navLinks">
          <Link to="/posts">Posts</Link>
        </div>
        <div className="userDetails">
          <UserIcon size={32} />
          {user.name}
          <button className="button small" onClick={onLogoutClicked}>
            Log Out
          </button>
        </div>
      </div>
    )
  }

  return (
    <nav>
      <section>
        <h1>Redux Essentials Example</h1>
        {navContent}
      </section>
    </nav>
  )
}

顺手把 <AddPostForm> 也改为使用 state 中的登录用户名,去掉用户下拉框:移除所有 postAuthor 输入字段的引用,加一个 useAppSelectorauthSlice 读取用户 ID:

export const AddPostForm = () => {
  const dispatch = useAppDispatch()
  const userId = useAppSelector(selectCurrentUsername)!

  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
    // Removed the `postAuthor` field everywhere in the component

    dispatch(postAdded(title, content, userId))

    e.currentTarget.reset()
  }

最后,允许当前用户编辑其他用户的帖子并不合理。更新 <SinglePostPage>,只有当帖子作者 ID 等于当前用户 ID 时才显示 "Edit Post" 按钮:

import { selectCurrentUsername } from '@/features/auth/authSlice'

export const SinglePostPage = () => {
  const { postId } = useParams()

  const post = useAppSelector(state => selectPostById(state, postId!))
  const currentUsername = useAppSelector(selectCurrentUsername)!

  if (!post) {
    return (
      <section>
        <h2>Post not found!</h2>
      </section>
    )
  }

  const canEdit = currentUsername === post.user

  return (
    <section>
      <article className="post">
        <h2>{post.title}</h2>
        <div>
          <PostAuthor userId={post.user} />
          <TimeAgo timestamp={post.date} />
        </div>
        <p className="post-content">{post.content}</p>
        <ReactionButtons post={post} />
        {canEdit && (
          <Link to={`/editPost/${post.id}`} className="button">
            Edit Post
          </Link>
        )}
      </article>
    </section>
  )
}

登出时清空其他 Slice 的 State

还差最后一块认证拼图。目前如果以用户 A 登录、新建帖子、登出,再以用户 B 登录,会同时看到初始示例帖子和用户 A 的新帖。这从代码角度是"正确"的——Redux 按我们写的逻辑工作:posts 列表 state 被更新了,页面没刷新,同一份 JS 数据仍在内存中。但从应用行为看这令人困惑,甚至可能是隐私泄露:如果用户 B 和 A 互不相识,或多人共用一台电脑,登录时不应看到彼此的数据。

因此应该在当前用户登出时清空已有的 posts state。

让多个 Slice 处理同一个 Action

以往每次想更新 state,我们都定义一个新的 case reducer、导出生成的 action creator、再从组件派发。这里可以照做,但会变成连续派发两个 action:

dispatch(userLoggedOut())
// This seems like it's duplicate behavior
dispatch(clearUserData())

每派发一次 action,整个 Redux store 更新流程都要走一遍——运行 reducer、通知订阅的 UI 组件、重渲染更新的组件。这没问题,是 Redux 和 React 的工作方式,但连续派发两个 action 通常是个信号,提示我们需要重新思考逻辑的组织方式

我们已经在派发 userLoggedOut() 了,只是它是从 auth slice 导出的 action。如果能让 posts slice 也监听它就更好了。

回想之前的建议:把 action 理解为**"应用中发生的一个事件",而不是"设置某个值的命令"**。这里是这个理念的实际体现——我们不需要单独的 clearUserData action,因为只发生了一个事件:"用户登出了"。我们只需要一种方式让这一个 userLoggedOut action 在多处被处理,从而一次性应用所有相关的 state 更新。

使用 extraReducers 处理外部 Action

createSlice 接受一个名为 extraReducers 的选项,让 slice 监听应用中其他地方定义的 action。每当这些外部 action 被派发,该 slice 也可以更新自己的 state。也就是说:许多不同的 slice reducer 都可以响应同一个被派发的 action,每个 slice 按需更新自己的 state!

extraReducers 字段是一个接收 builder 参数的函数。builder 对象上有三个方法,各让 slice 以不同方式监听其他 action 并做 state 更新:

  • builder.addCase(actionCreator, caseReducer):监听一个特定的 action type;
  • builder.addMatcher(matcherFunction, caseReducer):用 Redux Toolkit 的 "matcher" 函数比较 action 对象,监听多种 action type 中的一种;
  • builder.addDefaultCase(caseReducer):添加一个 case reducer,当本 slice 中其他都没匹配到该 action 时运行(等价于 switch 中的 default 分支)。

这些方法可以链式调用,如 builder.addCase().addCase().addMatcher().addDefaultCase()。若多个 matcher 都匹配,它们按定义顺序执行。

据此,把 userLoggedOutauthSlice.ts 导入 postsSlice.ts,在 postsSlice.extraReducers 中监听该 action,返回空数组以在登出时重置帖子列表:

import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
import { sub } from 'date-fns'

import { userLoggedOut } from '@/features/auth/authSlice'

// omit initial state and types

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded: {
      // omit postAdded and other case reducers
    }
  },
  extraReducers: (builder) => {
    // Pass the action creator to `builder.addCase()`
    builder.addCase(userLoggedOut, (state) => {
      // Clear out the list of posts whenever the user logs out
      return []
    })
  }
})

调用 builder.addCase(userLoggedOut, caseReducer)。在这个 reducer 里可以写和其他 case reducer 一样的"修改式"状态更新;但既然我们要整体替换现有 state,最简单的做法就是直接返回一个空数组作为新的 posts state。

现在点击 "Log Out" 按钮再以另一用户登录,"Posts" 页面就是空的——我们成功在登出时清空了 posts state。

reducersextraReducers 的区别:

  • reducers 字段通常是对象。对 reducers 对象中定义的每个 case reducer,createSlice 会自动生成同名的 action creator,以及用于在 Redux DevTools 中显示的 action type 字符串。reducers 定义 slice 的一部分新 action
  • extraReducers 接收一个带 builder 参数的函数,builder.addCase()builder.addMatcher() 用于处理其他 action type,定义新 action。extraReducers 处理在 slice 外部定义的 action

源码佐证:为什么"多个 slice 响应同一 action"在底层必然成立? 在本仓库核心包的 src/combineReducers.ts 中,combineReducers 返回的 combination 函数对每一个传入的 action 都会遍历所有子 reducer 逐一调用 reducer(previousStateForKey, action),并把各自返回值收集为新的 state 对象,只有当某个子 state 的引用发生变化时(hasChanged)才返回新对象,否则返回原 state 引用。这正是教程中"posts slice 监听 auth slice 的 userLoggedOut"能工作的机制:configureStore 底层就是 combineReducers 的产物,每个 slice reducer 都会收到同一 action,自己决定是否返回新 state。而 createStoredispatchsrc/createStore.ts)在 reducer 计算出 currentState 后立即遍历所有订阅者 listener(),触发 React-Redux 的选中值比对与重渲染——所以"连续派发两个 action 会走两遍完整流程"的说法在源码层面可以直接验证。combineReducers 还会对子 reducer 做形状断言(assertReducerShapesrc/combineReducers.ts):任何对未知 action 返回 undefined 的 reducer 都会抛错,这也解释了为什么 case reducer 不处理某 action 时必须返回原 state。

本章要点回顾

到这里,应用已经能查看和编辑单帖、显示每篇帖子的作者、添加 emoji 反应、跟踪登录/登出的当前用户了。总结本章需要记住的核心要点:

  • 任何 React 组件都可以按需使用 Redux store 中的数据
    • 任何组件都能读取 store 中的任意数据
    • 多个组件可以同时读取同一份数据
    • 组件应提取渲染自身所需的最小数据集
    • 组件可以把 props、state 和 Redux store 的值组合起来决定渲染什么 UI,可以从 store 读多份数据并按显示需要重塑数据
    • 任何组件都能派发 action 引发 state 更新
  • Redux action creator 可以用 prepare 回调准备内容正确的 action 对象
    • createSlicecreateAction 都接受返回 action payload 的 "prepare 回调"
    • 唯一 ID 和随机值应放进 action,而不是在 reducer 里计算
  • Reducer 应包含真正的状态更新逻辑
    • Reducer 可以包含计算下一个 state 所需的任意逻辑
    • Action 对象只包含描述"发生了什么"所需的信息
  • 可以写可复用的 "selector" 函数封装从 Redux state 中读取值
    • Selector 是以 Redux state 为参数、返回某些数据的函数
  • Action 应被理解为描述"已发生的事件",许多 reducer 可以响应同一个被派发的 action
    • 应用通常一次只派发一个 action
    • Case reducer 名(和 action)通常用过去式命名,如 postAdded
    • 许多 slice reducer 可以对同一个 action 各自做 state 更新
    • createSlice.extraReducers 让 slice 监听在 slice 外部定义的 action
    • 可以从 case reducer 返回一个新值作为替换来重置 state 值,而不是修改现有 state

下一步

到这里,你应该已经熟悉如何在 Redux store 和 React 组件之间处理数据。目前我们只使用了初始 state 里的数据或用户添加的数据。在 Part 5: Async Logic and Data Fetching 中,我们将学习如何处理来自服务器 API 的数据。

(本文基于 docs/tutorials/essentials/part-4-using-data.md 原文完整内容撰写,源码级佐证来自 src/createStore.tssrc/combineReducers.tssrc/utils/isAction.ts。注意:本仓库是 redux 核心包,教程示例使用 @reduxjs/toolkitconfigureStorecreateSlice,仓库核心包的 createStore 已标记弃用并推荐迁移到 Redux Toolkit,两者行为机制在 dispatch 校验与 reducer 组合层面保持一致。)

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384