Redux 学习8: RTK Query 进阶用法

1,237 阅读2分钟

Redux 学习8: RTK Query 进阶用法

💡 你将要学到什么?

  1. 学会如何使用tags和ids来缓存数据和重新获取数据
  2. 如何在react以外使用RTK Query
  3. 如何在RTK Query 中修改后端返回的数据
  4. 如何处理websocket以及stream 数据

提示:需要先完成Redux 学习7,才学习这部分内容

简介

在redux 学习7的基础上,我们继续学习使用RTK Query的一些其它高阶使用方法

编辑文章

修改EditPostForm组件

如果想要更新文章,我们需要增加一个修改方法,更新文章的时候,需要包括一个文章id,并且需要使用PATCH 方法来跟新

features/api/apiSlice.js
--------------------------
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  tagTypes: ['Post'],
  endpoints: builder => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`
    }),
    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        body: initialPost
      }),
      invalidatesTags: ['Post']
    }),
    // 在这里新增一个更新文章的方法
    editPost: builder.mutation({
      query: post => ({
        url: `/posts/${post.id}`,
        method: 'PATCH',
        body: post
      })
    })
  })
})

export const {
  useGetPostsQuery,
  useGetPostQuery,
  useAddNewPostMutation,
  // useEditPostMutation 这个是我们增加的editPost自动生成的
  useEditPostMutation
} = apiSlice

在 组件里边,需要从store里边读取文章内容,并且修改文章后,发送请求更新文章。

我们可以使用在 中使用的useGetPostQuery 获取所有文章,然后使用我们刚才定义的useEditPostMutation来发送请求到后端,保存修改后的文章。

features/posts/EditPostForm.js
------------------------------
  import React, { useState } from 'react'
// import { useDispatch, useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'

// import { postUpdated, selectPostById } from './postsSlice'

import { Spinner } from '../../components/Spinner'
import { useGetPostQuery, useEditPostMutation } from '../api/apiSlice'

export const EditPostForm = ({ match }) => {
  const { postId } = match.params

  // 这里引入修改文章的方法
  const { data: post } = useGetPostQuery(postId)
  const [updatePost, { isLoading }] = useEditPostMutation()

  const [title, setTitle] = useState(post.title)
  const [content, setContent] = useState(post.content)

  const history = useHistory()

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

  const onSavePostClicked = async () => {
    if (title && content) {
      //
      await updatePost({ id: postId, title, content })
      history.push(`/posts/${postId}`)
    }
  }
  // 这里loading
  const spinner = isLoading ? <Spinner text="Saving..." /> : null

  return (
    <section>
      <h2>Edit Post</h2>
      <form>
        <label htmlFor="postTitle">Post Title:</label>
        <input
          type="text"
          id="postTitle"
          name="postTitle"
          placeholder="What's on your mind?"
          value={title}
          onChange={onTitleChanged}
          disabled={isLoading}
        />
        <label htmlFor="postContent">Content:</label>
        <textarea
          id="postContent"
          name="postContent"
          value={content}
          onChange={onContentChanged}
          disabled={isLoading}
        />
      </form>
      <button type="button" onClick={onSavePostClicked} disabled={isLoading}>
        Save Post
      </button>
      {spinner}
    </section>
  )
}

不同组件,使用缓存数据

让我们来看看发生了什么,打开浏览器开发者工具,来到首页的时候,我们刷新浏览器,会发现发送了两个接口,一个是/posts,当我们点击了"View Post"按钮,你会发现浏览器发送了/posts/:postId接口,来获取文章的详细内容。

RTK Query 允许不同的组件同时绑定store里边的某一项数据,比如这里的posts,RTK Query内部会缓存数据,避免多次重复的请求,比如在一个组件里边,使用了useGetPostQuery(42),RTK Query会去请求数据,这个时候,如果另外一个组件也引用了useGetPostQuery(42),这个时候,RTK Query就不会再去请求数据了,而是使用缓存的数据。

当store里边的某些数据,如果引用的次数变为0的时候,RTK Query会内部开启一个定时器,如果定时器过期,RTK Query会清除数据,如果没有过期,这个时候,有另外一个引用加入,那么定时器就会被取消,同时使用缓存的数据。

在这个例子里边,当我们点击了查看文章的时候,组件被挂载,并且发送接口去获取数据, 组件被销毁,文章的数据的引用变为0,同时RTK Query 启动一个定时器,然后当我们点击了编辑文章的时候,组件被挂载,此时文章的数据的引用又变为1了,所以RTK Query取消定时器,同时使用缓存的数据。

RTK Query对于引用为0的数据,默认缓存时间是60s,但是这个可以通过修改keepUnusedDataFor来改变。

同步更新数据

我们的现在可以保存数据到后端了,但是有一个问题。如果我们点击了保存按钮,当回到的时候,发现数据还是旧数据。同时,如果我们回到首页,也是使用的是旧数据,我们需要一个方法来更新文章的数据,而不是编辑完以后,还是使用缓存的旧文章数据。我们需要找到一个方法,当一篇文章被修改后,重新去获取文章的数据,并且所有文章的列表也需要重新去获取

前面我们已经使用"tags"来同步更新数据,比如当增加了一篇文章的时候,去重新获取所有文章的列表。在getPosts中使用'Post',在addNewPost中使用同样的'Post',当新增一篇文章的时候,我们让RTK Query去重新获取整个文章列表。 我们这里同样可以使用'Post'来同步getPost和editPost,但是这样的话,就会出现一种情况,比如我们点击view post按钮后,查看文章A,第二次进入,就不会发送请求了,但是如果我们使用'Post'来同步getPost和editPost的话,假设我们编辑的是文章B,当我们回到posts列表后,重新点击view post按钮,查看文章A,就会发现又发送了请求接口。 非常幸运的是RTK Query提供了一些方法,来处理这种情况,比如特殊的tags,比如{type: 'Post', id: 123}

我们getPosts定义了一个providesTags,providesTags也可以定义为一个函数,有3个参数,一个result是接口返回的结果,arg是我们传入的参数,我们这里就可以使用这个函数,返回一个数组。

  // 返回特殊的数组,result是接口返回的结果,arg是传入的参数
      providesTags: (result = [], error, arg) => {
        console.log(result, arg)
        return ['Post', ...result.map(({ id }) => ({ type: 'Post', id }))]
      },
  

为了达到我们的目标,我们需要按照以下步骤,进行修改:

  • getPosts: 提供一个通用的'Post' tag,同时也提供一些特殊的tag,比如{type: 'Post', id} tag,来同步更新文章数据。
  • getPost:提供一个特殊的{type: 'Post', id} tag
  • addNewPost:提供一个通用的'Post' tag,当新增一篇文章的时候,重新去获取整个文章列表。
  • editPost:提供一个特殊的{type: 'Post', id} tag

通过这些增加的代码,我们就可以编辑完文章后,同步更新文章数据

  features/api/apiSlice.js
------------------------------------
  import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  tagTypes: ['Post'],
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
      // providesTags: ['Post'],
      // 返回特殊的数组,result是接口返回的结果,arg是传入的参数
      providesTags: (result = [], error, arg) => {
        console.log(result, arg)
        return ['Post', ...result.map(({ id }) => ({ type: 'Post', id }))]
      },
    }),
    getPost: builder.query({
      query: (postId) => `/posts/${postId}`,
      providesTags: ['Post'],
      // providesTags: (result, error, arg) => [{ type: 'Post', id: arg }],
    }),
    addNewPost: builder.mutation({
      query: (initialPost) => ({
        url: '/posts',
        method: 'POST',
        body: initialPost,
      }),
      invalidatesTags: ['Post'],
    }),
    // 在这里新增一个更新文章的方法
    editPost: builder.mutation({
      query: (post) => ({
        url: `/posts/${post.id}`,
        method: 'PATCH',
        body: post,
      }),
      invalidatesTags: ['Post'],
      // invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }],
    }),
  }),
})

export const {
  useGetPostsQuery,
  useGetPostQuery,
  useAddNewPostMutation,
  useEditPostMutation,
} = apiSlice

因为通过接口获取的数据result可能是undefined,所以这里设置result=[], 其它的设置都可以直接返回一个数组,代码比较简单,直接查看代码就可以了。

现在让我们打开Network看看发生了什么?

当我们编辑一篇文章,我们会看到两个请求:

  • 调用 PATCH /posts/:postId来保存文章
  • 调用/posts/:postId 重新获取文章
  • 如果我们点击"Posts",重新回到首页,会发现调用/posts,获取所有文章数据

因为我们使用tags来告诉RTK Query ,让RTK Query知道什么时候,使用缓存数据,什么时候重新去获取数据。 比如如果我们更新了文章数据,就会去重新发送请求,去获取文章数据。

ℹ️ 提示:RTK Query 还有很多其它的配置项,如果感兴趣,可以去查看