Redxu(RTK) 基础 异步逻辑与数据请求 第5.2.1节 加载帖子第二部分 使用 createAsyncThunk 请求数据( thunk 和extraReducers)

本篇学习如何正确编写和使用thunk,并且学习通过获取thunk状态在页面上显示不同内容(比如提示正在加载、加载失败、或者是显示加载成功后的数据)的范式。
对应文档这个位置

请求过程中的加载状态

当我们进行 API 请求时,我们可以将其进度视为一个小型状态机,它处于下面四种可能的状态之一:

  • 请求尚未开始
  • 请求正在进行中
  • 请求成功,我们现在有了我们需要的数据
  • 请求失败,可能有错误信息

我们 可以 使用一些布尔值来跟踪该信息,例如 isLoading: true,但最好将这些状态作为单个枚举值。好的模式是使用如下所示的状态部分:

{
  // 多个可能的状态枚举值
  status: 'idle' | 'loading' | 'succeeded' | 'failed',
  error: string | null
}

现在,postsSlice 状态是一个单一的 posts 数组。我们需要将其更改为具有 posts 数组以及加载状态字段的对象。
这个加载状态可以帮助我们确定异步状态当前具体处于哪个阶段(pending?succeeded?failed?)

//旧的posts initialState
 [
  {
    id: "1",
    title: "First Post!",
    content: "Hello!",
    user: "0",
    //意思就是这篇文章的创建时间是渲染时间10分钟之前(强制写的,为了示范)
    date: sub(new Date(), { minutes: 10 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      hooray: 0,
      heart: 0,
      rocket: 0,
      eyes: 0,
    },
  },
  {
    id: "2",
    title: "Second Post",
    content: "More text",
    user: "1",
    date: sub(new Date(), { minutes: 5 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      hooray: 0,
      heart: 0,
      rocket: 0,
      eyes: 0,
    },
  },

];

友情提示,别忘记更改对应的selector呢!

是的,这 确实 意味着我们现在有一个看起来像 state.posts.posts 的嵌套对象路径,这有点重复和愚蠢:) , 如果我们想避免这种情况, 可以 将嵌套数组名称更改为 items 或 data 或其他东西,但我们暂时保持原样。

// 更新后的posts initialState

const postsInitialState = [
  {
    id: "1",
    title: "First Post!",
    content: "Hello!",
    user: "0",
    //意思就是这篇文章的创建时间是渲染时间10分钟之前(强制写的,为了示范)
    date: sub(new Date(), { minutes: 10 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      hooray: 0,
      heart: 0,
      rocket: 0,
      eyes: 0,
    },
  },
  {
    id: "2",
    title: "Second Post",
    content: "More text",
    user: "1",
    date: sub(new Date(), { minutes: 5 }).toISOString(),
    reactions: {
      thumbsUp: 0,
      hooray: 0,
      heart: 0,
      rocket: 0,
      eyes: 0,
    },
  },
];
const initialState = {
  posts: postsInitialState,
  status: "idle",
  error: null,
};

上面我们仅仅对initialState进行了变换,在其中加入了 status和 error两个新字段,这么做有点多余,仅仅是为了告诉读者向redux数据中加入 “status”的具体位置。
那么加入这两个字段是还不够的,原因在于我们已经不需要在其中硬编码数据了,也就是说posts字段应该是空数组而不是写好的一些数据,也就是下面这样

const initialState = {
  posts: [],
  status: 'idle',
  error: null
}

因为多“套”了一层 posts,所以现在需要做些改动,我只把postSlice贴出来吧。

//这是注释,显示文件路径捏:/src/features/posts/postsSlice.ts
import { RootState } from "./../../app/store";
import { createSlice, nanoid, PayloadAction } from "@reduxjs/toolkit";

const initialState: {
  posts: IPost[];
  status: "idle" | "loading" | "failed"|"success";
  error: null | any;
} = {
  posts: [],
  status: "idle",
  error: null,
};

export type Treactions = {
  thumbsUp: number;
  hooray: number;
  heart: number;
  rocket: number;
  eyes: number;
};
export type IPost = {
  id: string;
  content: string;
  title: string;
  user: string;
  date: string;
  reactions: Treactions;
};

注意,我只贴出了必要的变更,该变更引起的其他变化,请自己手动调整。。。

createAsyncThunk请求数据 (动手编写一个thunk,并为它设计异步拉取数据的功能)

之前我们已经讲明白thunk,也给posts加入了一个新的状态值,下一步可以着手发送HTTP请求来获取后端数据并更新到redux中了。

Redux Toolkit 的 createAsyncThunk API 生成 thunk,为你自动 dispatch 那些 表示(promise)异步请求动作"start/success/failure" (注意不是pending/fulfiled/rejected) 的action。
让我们从添加一个 thunk 开始,该 thunk 将进行 AJAX 调用以检索帖子列表。我们将从 src/api 文件夹中引入 client 工具库,并使用它向 '/fakeApi/posts' 发出请求。

注意上面讲到的 "start/success/failure" action ,之前我们在编写reducer的时候,rtk自动生成了actionCreator,
thunk类似,当使用createAsyncThunk函数中,第一个参数就是action的前缀名字,而后缀名字,则就是"pending/fulfilled/rejected"(这对应Promise的三个状态),
createAsyncThunk自动添加的"pending/fulfilled/rejected"后缀自动拼接出action,这样的后缀信息方便我们知晓thunk异步操作具体处于哪个状态,方便供程序使用(比如,正在请求就是pending,页面显示个转圈圈的提示组件)。
另外,我们坚持使用createAsyncThunk这个RTK提供的工具函数,而不是自己手动编写thunk,相对来说,前者效率高,而且不易出错。
createAsyncThunk 接收 2 个参数:一个前面说过了,第二个是一个函数,在这个函数内部编写异步逻辑,获取异步操作的数据并返回这个数据,注意返回的数据形式会是 Promise,或者一个被拒绝的带有错误的 Promise,所以官网称这个函数为一个 “payload creator” 回调函数
这个返回的数据会被包装成action的payload。
在这个payload creator中,文档建议使用try/catch形式配合async/await来编写异步逻辑。
注意下面的代码片段中的fetchPost,他就是一个使用了createAsyncThunk编写的thunk函数

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/fakeApi/posts')
  return response.data
})

当dispatch(fecthPost)的时候,fetchPost这个thunk会先dispatch 一个 类型为"posts/fetchPosts/pending",
类似的,如果thunk内部的promise成功返回一个数据,一个 类型为"posts/fetchPosts/fulfilled"的action就会被dispatch,注意,这个action内部肯定还有promise返回的数据作为payload

在组件中 dispatch thunk(如何实际使用thunk)

我们已经编写好一个thunk了,下面找一个实际的组件去使用它,这里要配合useDispatch钩子和useEffect钩子,注意下面示例内容。

features/posts/PostsList.js
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
// omit other imports
import { selectAllPosts, fetchPosts } from './postsSlice'

export const PostsList = () => {
  const dispatch = useDispatch()
  const posts = useSelector(selectAllPosts)

  const postStatus = useSelector(state => state.posts.status)

  useEffect(() => {
    if (postStatus === 'idle') {
      dispatch(fetchPosts())
    }
  }, [postStatus, dispatch])

只要页面能正确显示post,就说明thunk是能正常使用了!
注意我们一定要判断posts.status,而不是每次渲染页面都要拉取数据。

Reducer 与 Loading Action

前面我们已经介绍过thunk可能会触发三种状态的action,
我们编写reducer的时候,reducer的名字加上slice的name就是该reducer要处理的action的"type",
而对thunk,他对应的action 的"type"是thunk的第一个参数+三种状态,这些action对应的reducer要通过extraReducers 编写,extraReducers编写范式十分明确,我把模板实例复制在下面:

 // The `extraReducers` field lets the slice handle actions defined elsewhere,
  // including actions generated by createAsyncThunk or in other slices.
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state: any) => {
        state.status = "loading";
      })
      .addCase(incrementAsync.fulfilled, (state: any, action: any) => {
        state.status = "idle";
        state.value += action.payload;
      })
      .addCase(incrementAsync.rejected, (state: any) => {
        state.status = "failed";
      });
  },

注意,extraReducers还有其他写法,推荐上面的,下面我再贴上文档写法:

但是,有时 slice 的 reducer 需要响应 没有 定义到该 slice 的 reducers 字段中的 action。这个时候就需要使用 slice 中的 extraReducers 字段。
extraReducers 选项是一个接收名为 builder 的参数的函数。builder 对象提供了一些方法,让我们可以定义额外的 case reducer,这些 reducer 将响应在 slice 之外定义的 action。我们将使用 builder.addCase(actionCreator, reducer) 来处理异步 thunk dispatch 的每个 action。

注意下方的addCase,既可以接受action type ,也可以接受action creator。(还可以接受createAsyncThunk 生成的thunk.xxx)

import { increment } from '../features/counter/counterSlice'

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // slice-specific reducers here
  },
  extraReducers: builder => {
    builder
      .addCase('counter/decrement', (state, action) => {})
      .addCase(increment, (state, action) => {})
  }
})

下面是文档上关于fetchPost这个thunk对应的 extraReducers的编写方法。

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/fakeApi/posts')
  return response.data
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit existing reducers here
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        // Add any fetched posts to the array
        state.posts = state.posts.concat(action.payload)
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

根据promise的不同结果,分别对应标记status和更新state的posts字段或者是error字段。

显示加载状态 (在thunk请求的不同阶段渲染不同内容)

在异步请求的loading阶段,我们想要在页面上显示一些提示性信息,比如“正在加载数据”,请看下面代码:

//这是注释,显示文件路径捏:/src/features/posts/PostList.tsx
import { PostAuthor } from "./PostAuthor";
import { TimeAgo } from "./TimeAgo";
import { ReactionButtons } from "./ReactionButtons";
import { selectAllPosts, fetchPosts, IPost } from "./postsSlice";
import { Link } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { useEffect } from "react";

const PostExcerpt = ({ post }: { post: IPost }) => {
  return (
    <article className="post-excerpt" key={post.id}>
      <h3>{post.title}</h3>
      <div>
        <PostAuthor userId={post.user} />
        <TimeAgo timestamp={post.date} />
      </div>
      <p className="post-content">{post.content.substring(0, 100)}</p>

      <ReactionButtons post={post} />
      <Link to={`/posts/${post.id}`} className="button muted-button">
        View Post
      </Link>
    </article>
  );
};

export const PostList = () => {
  const dispatch = useAppDispatch();
  const posts = useAppSelector(selectAllPosts);

  const postStatus = useAppSelector((state) => state.posts.status);
  const error = useAppSelector((state) => state.posts.error);

  useEffect(() => {
    if (postStatus === "idle") {
      dispatch(fetchPosts());
    }
  }, [postStatus, dispatch]);

  let content;

  if (postStatus === "loading") {
    content = <div>正在加载捏!</div>;
  } else if (postStatus === "success") {
    // Sort posts in reverse chronological order by datetime string
    const orderedPosts = posts
      .slice()
      .sort((a, b) => b.date.localeCompare(a.date));

    content = orderedPosts.map((post) => (
      <PostExcerpt key={post.id} post={post} />
    ));
  } else if (postStatus === "failed") {
    content = <div>{error}</div>;
  }

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

export default PostList;

上面已经封装了一个新的Excerpt组件,他是在redux中已经正确拉取到post数据的时候把post的内容渲染到页面上的逻辑,把这段逻辑抽离到Excerpt中,意图让代码逻辑更清晰,而在另外一边,如果redux还没有顺利拉取到post数据,我们转而在页面上渲染Spinner组件(是文档提供的一个东东,顾名思义,可以一直转圈圈)。

到现在为止,我们可以正确编写和使用thunk,并且知道通过获取thunk状态在页面上显示不同内容(比如提示正在加载、加载失败、或者是显示加载成功后的数据)的范式。

posted @ 2023-03-08 16:30  刘老六  阅读(339)  评论(0编辑  收藏  举报