Redux 学习8: RTK Query 进阶用法
💡 你将要学到什么?
- 学会如何使用tags和ids来缓存数据和重新获取数据
- 如何在react以外使用RTK Query
- 如何在RTK Query 中修改后端返回的数据
- 如何处理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 还有很多其它的配置项,如果感兴趣,可以去查看