Redux必须手册,第3部分:基本Redux数据流

1,368 阅读9分钟

介绍

第1部分:Redux概述和概念中,我们研究了Redux如何通过为我们提供一个放置全局应用程序状态的中央位置来帮助我们构建可维护的应用程序。我们还讨论了Redux的核心概念,例如dispatch action对象,使用返回新state值的reducer函数以及使用thunk编写异步逻辑。第2部分:Redux应用程序结构中,我们看到了Redux Toolkit中的configureStorecreateSlice以及React-Redux中的ProvideruseSelector等API如何协同工作,使我们可以编写Redux逻辑并与React组件中的逻辑进行交互。

现在你对这些部分是什么有了一些了解,是时候将这些知识付诸实践了。我们将构建一个小型的社交媒体供稿应用程序,其中将包含许多功能,这些功能演示了一些实际的用例。这将帮助你了解如何在自己的应用程序中使用Redux。

警告

该示例应用程序并不意味着它是一个完整的可投入生产的项目。目的是帮助你学习Redux API和典型的使用模式,并使用一些有限的示例为你指明正确的方向。另外,我们构建的一些早期作品将在以后进行更新,以展示更好的做事方式。请通读整个教程,以了解所有正在使用的概念。

主要posts流

我们的社交媒体供稿应用程序的主要功能是帖子列表。继续进行时,我们将向此功能添加更多片段,但首先,我们的首要目标是仅在屏幕上显示帖子条目列表。

创建Posts Slice

第一步是创建一个新的Redux “slice”,其中将包含我们发布的数据。一旦我们在Redux store中存储了这些数据,就可以创建React组件以在页面上显示该数据。

src内,创建一个新的features文件夹,在features内放置一个posts文件夹,并添加一个名为postsSlice.js的新文件。

我们将使用Redux Toolkit的createSlice函数制作一个reducer函数,该函数知道如何处理我们的posts数据。 Reducer函数需要包含一些初始数据,以便Redux store在应用程序启动时加载这些值。

现在,我们将创建一个内部包含一些假post对象的数组,以便我们可以开始添加UI。

我们将导入createSlice,定义我们的初始posts数组,将其传递给createSlice,并导出createSlice为我们生成的posts reducer函数:

import { createSlice } from '@reduxjs/toolkit'

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

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {}
})

export default postsSlice.reducer

每次创建新slice时,都需要将其reducer函数添加到Redux store中。我们已经在创建Redux store,但是现在它内部没有任何数据。打开app/store.js,导入postsReducer函数,并更新对configureStore的调用,以使postsReducer作为名为posts的reducer字段传递:

import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '../features/posts/postsSlice'

export default configureStore({
  reducer: {
    posts: postsReducer
  }
})

这告诉Redux我们希望我们的顶级state对象在其中包含一个名为posts的字段,并且在dispatch action时,postReducer函数更新state.posts的所有数据。

我们可以通过打开Redux DevTools Extension并查看当前state内容来确认此方法有效:

image.png

显示posts列表

现在我们的store中有一些posts数据,我们可以创建一个显示posts列表的React组件。与我们的posts流功能有关的所有代码都应该放在posts文件夹中,因此继续在其中创建一个名为PostsList.js的新文件。

如果要呈现posts列表,则需要从某处获取数据。React组件可以使用React-Redux库中的useSelector hook从Redux store中读取数据。你编写的“selector函数”将以整个Redux state对象作为参数来调用,并且应从store中返回该组件所需的特定数据。

我们最初的PostsList组件将从Redux store中读取state.posts值,然后循环遍历所有posts并在屏幕上显示它们:

import React from 'react'
import { useSelector } from 'react-redux'

export const PostsList = () => {
  const posts = useSelector(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.js中的路由,以便显示PostsList组件而不是“welcome”消息。将PostsList组件导入App.js,然后将欢迎文本替换为<PostsList />。我们还将把它包装在React Fragment中,因为我们很快就会在主页上添加其他内容:

import React from 'react'
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect
} from 'react-router-dom'

import { Navbar } from './app/Navbar'

import { PostsList } from './features/posts/PostsList'

function App() {
  return (
    <Router>
      <Navbar />
      <div className="App">
        <Switch>
          <Route
            exact
            path="/"
            render={() => (
              <React.Fragment>
                <PostsList />
              </React.Fragment>
            )}
          />
          <Redirect to="/" />
        </Switch>
      </div>
    </Router>
  )
}

export default App

添加之后,我们应用的主页现在应如下所示:

image.png

更进一步!我们已经向Redux store添加了一些数据,并将其显示在屏幕上的React组件中。

添加新的Posts

很高兴看到别人写的帖子,但是我们希望能够自己写帖子。让我们创建一个“添加新帖子”的表单,让我们编写帖子并保存。

我们将首先创建一个空表单并将其添加到页面中。然后,我们将表单连接到Redux store,以便在单击“保存帖子”按钮时添加新帖子。

添加新帖子表单

在我们的posts文件夹中创建AddPostForm.js。我们将为帖子标题添加文本输入,并为帖子正文添加文本输入区域:

import React, { useState } from 'react'

export const AddPostForm = () => {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  const onTitleChanged = e => setTitle(e.target.value)
  const onContentChanged = e => setContent(e.target.value)

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          value={title}
          onChange={onTitleChanged}
        />
        <label htmlFor="postContent">Content:</label>
        <textarea
          id="postContent"
          name="postContent"
          value={content}
          onChange={onContentChanged}
        />
        <button type="button">Save Post</button>
      </form>
    </section>
  )
}

将该组件导入App.js,并将其添加到<PostsList />组件上方:

<Route
  exact
  path="/"
  render={() => (
    <React.Fragment>
      <AddPostForm />
      <PostsList />
    </React.Fragment>
  )}
/>

你应该看到表格显示在标题下方的页面中。

保存帖子的输入

现在,让我们更新posts slice,以将新的posts条目添加到Redux store。

我们的posts slice负责处理posts数据的所有更新。在createSlice调用内部,有一个称为reducers的对象。现在,它是空的。我们需要在其中添加一个reducer函数来处理添加posts的情况。

reducers内部,添加一个名为postAdded的函数,该函数将接收两个参数:当前state值和已dispatch的action对象。由于posts slice仅知道其负责的数据,因此state参数将是其本身的数组,而不是整个Redux state对象。

action对象将把我们的新post对象作为action.payload字段,并将该新post对象放入state数组中。

当我们编写postAdded reducer函数时,createSlice将自动生成一个具有相同名称的“action creator”函数。当用户单击“保存帖子”时,我们可以导出该action creator并在UI组件中使用它来dispatch action。

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    postAdded(state, action) {
      state.push(action.payload)
    }
  }
})

export const { postAdded } = postsSlice.actions

export default postsSlice.reducer

警告

切记:reducer函数必须始终通过复制来不变地创建新的state值!调用Array.push()之类的变更函数或修改createSlice()中的state.someField = someValue之类的对象字段是安全的,因为它使用Immer库在内部将这些变更转换为安全的不可变更新,但不要尝试进行变更createSlice之外的任何数据!

Dispatch "Post Added" Action

我们的AddPostForm具有文本输入和“保存帖子”按钮,但是该按钮尚未执行任何操作。我们需要添加一个单击处理程序,该处理程序将dispatch postAdded action creator,并传递一个新的post对象,其中包含用户编写的标题和内容。

我们的post对象还需要有一个id字段。现在,我们的初始测试帖子使用了一些假数字作为ID。我们可以编写一些代码来弄清楚下一个递增的ID号应该是什么,但是如果我们生成一个随机的唯一ID会更好。Redux Toolkit具有可用于此的nanoid函数。

为了从组件中dispatch action,我们需要访问store的dispatch函数。我们通过调用React-Redux的useDispatch hook来实现这一点。我们还需要将postAdded action creator导入到该文件中。

一旦我们的组件中有了dispatch函数,我们就可以在点击处理程序中调用dispatch(postAdded())。我们可以从React组件的useState hook中获取标题和内容值,生成一个新的ID,然后将它们放到一个新的post对象中,然后传递给postAdded()

import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'

import { postAdded } from './postsSlice'

export const AddPostForm = () => {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')

  const dispatch = useDispatch()

  const onTitleChanged = e => setTitle(e.target.value)
  const onContentChanged = e => setContent(e.target.value)

  const onSavePostClicked = () => {
    if (title && content) {
      dispatch(
        postAdded({
          id: nanoid(),
          title,
          content
        })
      )

      setTitle('')
      setContent('')
    }
  }

  return (
    <section>
      <h2>Add a New Post</h2>
      <form>
        {/* omit form inputs */}
        <button type="button" onClick={onSavePostClicked}>
          Save Post
        </button>
      </form>
    </section>
  )
}

现在,尝试输入标题和一些文本,然后单击“保存帖子”。你应该在帖子列表中看到该帖子的新项目。

恭喜你!你刚刚构建了第一个可正常运行的React + Redux应用程序!

这显示了完整的Redux数据流周期:

  • 我们的帖子列表使用useSelector从store中读取了初始的帖子集,并呈现了初始UI
  • 我们dispatch了postAdded action,其中包含新帖子条目的数据
  • posts reducer看到了postAdded action,并用新条目更新了posts数组
  • Redux store告诉UI,某些数据已更改
  • 帖子列表读取更新后的帖子数组,并重新呈现以显示新帖子

此后我们将添加的所有新功能将遵循你在此处看到的相同基本模式:添加state slice,编写reducer函数,dispatch action以及基于Redux store中的数据呈现UI。

我们可以检查Redux DevTools Extension来查看我们dispatch的action,并查看响应该操作如何更新Redux state。如果单击动作列表中的"posts/postAdded"条目,则“action”选项卡应如下所示:

image.png

“Diff”标签还应向我们显示state.posts添加了一个新项目,该新项目位于索引2。

请注意,我们的AddPostForm组件内部有一些React useState hook,以跟踪用户输入的标题和内容值。记住,Redux store应该只包含被认为是应用程序“全局”的数据! 在这种情况下,只有AddPostForm才需要知道输入字段的最新值,因此我们希望将该数据保持在React组件state,而不是尝试将临时数据保留在Redux store中。 当用户完成表单操作后,我们将dispatch Redux action,以根据用户输入的最终值来更新store。