| [a762898] | 1 | import type {
|
|---|
| 2 | QueryStateSelector,
|
|---|
| 3 | UseMutation,
|
|---|
| 4 | UseQuery,
|
|---|
| 5 | } from '@internal/query/react/buildHooks'
|
|---|
| 6 | import { ANY } from '@internal/tests/utils/helpers'
|
|---|
| 7 | import type { SerializedError } from '@reduxjs/toolkit'
|
|---|
| 8 | import type {
|
|---|
| 9 | QueryDefinition,
|
|---|
| 10 | SubscriptionOptions,
|
|---|
| 11 | TypedQueryStateSelector,
|
|---|
| 12 | } from '@reduxjs/toolkit/query/react'
|
|---|
| 13 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
|---|
| 14 | import { useState } from 'react'
|
|---|
| 15 |
|
|---|
| 16 | let amount = 0
|
|---|
| 17 | let nextItemId = 0
|
|---|
| 18 |
|
|---|
| 19 | interface Item {
|
|---|
| 20 | id: number
|
|---|
| 21 | }
|
|---|
| 22 |
|
|---|
| 23 | const api = createApi({
|
|---|
| 24 | baseQuery: (arg: any) => {
|
|---|
| 25 | if (arg?.body && 'amount' in arg.body) {
|
|---|
| 26 | amount += 1
|
|---|
| 27 | }
|
|---|
| 28 |
|
|---|
| 29 | if (arg?.body && 'forceError' in arg.body) {
|
|---|
| 30 | return {
|
|---|
| 31 | error: {
|
|---|
| 32 | status: 500,
|
|---|
| 33 | data: null,
|
|---|
| 34 | },
|
|---|
| 35 | }
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | if (arg?.body && 'listItems' in arg.body) {
|
|---|
| 39 | const items: Item[] = []
|
|---|
| 40 | for (let i = 0; i < 3; i++) {
|
|---|
| 41 | const item = { id: nextItemId++ }
|
|---|
| 42 | items.push(item)
|
|---|
| 43 | }
|
|---|
| 44 | return { data: items }
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | return {
|
|---|
| 48 | data: arg?.body ? { ...arg.body, ...(amount ? { amount } : {}) } : {},
|
|---|
| 49 | }
|
|---|
| 50 | },
|
|---|
| 51 | endpoints: (build) => ({
|
|---|
| 52 | getUser: build.query<{ name: string }, number>({
|
|---|
| 53 | query: () => ({
|
|---|
| 54 | body: { name: 'Timmy' },
|
|---|
| 55 | }),
|
|---|
| 56 | }),
|
|---|
| 57 | getUserAndForceError: build.query<{ name: string }, number>({
|
|---|
| 58 | query: () => ({
|
|---|
| 59 | body: {
|
|---|
| 60 | forceError: true,
|
|---|
| 61 | },
|
|---|
| 62 | }),
|
|---|
| 63 | }),
|
|---|
| 64 | getIncrementedAmount: build.query<{ amount: number }, void>({
|
|---|
| 65 | query: () => ({
|
|---|
| 66 | url: '',
|
|---|
| 67 | body: {
|
|---|
| 68 | amount,
|
|---|
| 69 | },
|
|---|
| 70 | }),
|
|---|
| 71 | }),
|
|---|
| 72 | updateUser: build.mutation<{ name: string }, { name: string }>({
|
|---|
| 73 | query: (update) => ({ body: update }),
|
|---|
| 74 | }),
|
|---|
| 75 | getError: build.query({
|
|---|
| 76 | query: () => '/error',
|
|---|
| 77 | }),
|
|---|
| 78 | listItems: build.query<Item[], { pageNumber: number }>({
|
|---|
| 79 | serializeQueryArgs: ({ endpointName }) => {
|
|---|
| 80 | return endpointName
|
|---|
| 81 | },
|
|---|
| 82 | query: ({ pageNumber }) => ({
|
|---|
| 83 | url: `items?limit=1&offset=${pageNumber}`,
|
|---|
| 84 | body: {
|
|---|
| 85 | listItems: true,
|
|---|
| 86 | },
|
|---|
| 87 | }),
|
|---|
| 88 | merge: (currentCache, newItems) => {
|
|---|
| 89 | currentCache.push(...newItems)
|
|---|
| 90 | },
|
|---|
| 91 | forceRefetch: () => {
|
|---|
| 92 | return true
|
|---|
| 93 | },
|
|---|
| 94 | }),
|
|---|
| 95 | }),
|
|---|
| 96 | })
|
|---|
| 97 |
|
|---|
| 98 | describe('type tests', () => {
|
|---|
| 99 | test('useLazyQuery hook callback returns various properties to handle the result', () => {
|
|---|
| 100 | function User() {
|
|---|
| 101 | const [getUser] = api.endpoints.getUser.useLazyQuery()
|
|---|
| 102 | const [{ successMsg, errMsg, isAborted }, setValues] = useState({
|
|---|
| 103 | successMsg: '',
|
|---|
| 104 | errMsg: '',
|
|---|
| 105 | isAborted: false,
|
|---|
| 106 | })
|
|---|
| 107 |
|
|---|
| 108 | const handleClick = (abort: boolean) => async () => {
|
|---|
| 109 | const res = getUser(1)
|
|---|
| 110 |
|
|---|
| 111 | // no-op simply for clearer type assertions
|
|---|
| 112 | res.then((result) => {
|
|---|
| 113 | if (result.isSuccess) {
|
|---|
| 114 | expectTypeOf(result).toMatchTypeOf<{
|
|---|
| 115 | data: {
|
|---|
| 116 | name: string
|
|---|
| 117 | }
|
|---|
| 118 | }>()
|
|---|
| 119 | }
|
|---|
| 120 |
|
|---|
| 121 | if (result.isError) {
|
|---|
| 122 | expectTypeOf(result).toMatchTypeOf<{
|
|---|
| 123 | error: { status: number; data: unknown } | SerializedError
|
|---|
| 124 | }>()
|
|---|
| 125 | }
|
|---|
| 126 | })
|
|---|
| 127 |
|
|---|
| 128 | expectTypeOf(res.arg).toBeNumber()
|
|---|
| 129 |
|
|---|
| 130 | expectTypeOf(res.requestId).toBeString()
|
|---|
| 131 |
|
|---|
| 132 | expectTypeOf(res.abort).toEqualTypeOf<() => void>()
|
|---|
| 133 |
|
|---|
| 134 | expectTypeOf(res.unsubscribe).toEqualTypeOf<() => void>()
|
|---|
| 135 |
|
|---|
| 136 | expectTypeOf(res.updateSubscriptionOptions).toEqualTypeOf<
|
|---|
| 137 | (options: SubscriptionOptions) => void
|
|---|
| 138 | >()
|
|---|
| 139 |
|
|---|
| 140 | expectTypeOf(res.refetch).toMatchTypeOf<() => void>()
|
|---|
| 141 |
|
|---|
| 142 | expectTypeOf(res.unwrap()).resolves.toEqualTypeOf<{ name: string }>()
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | return (
|
|---|
| 146 | <div>
|
|---|
| 147 | <button onClick={handleClick(false)}>Fetch User successfully</button>
|
|---|
| 148 | <button onClick={handleClick(true)}>Fetch User and abort</button>
|
|---|
| 149 | <div>{successMsg}</div>
|
|---|
| 150 | <div>{errMsg}</div>
|
|---|
| 151 | <div>{isAborted ? 'Request was aborted' : ''}</div>
|
|---|
| 152 | </div>
|
|---|
| 153 | )
|
|---|
| 154 | }
|
|---|
| 155 | })
|
|---|
| 156 |
|
|---|
| 157 | test('useMutation hook callback returns various properties to handle the result', async () => {
|
|---|
| 158 | function User() {
|
|---|
| 159 | const [updateUser] = api.endpoints.updateUser.useMutation()
|
|---|
| 160 | const [successMsg, setSuccessMsg] = useState('')
|
|---|
| 161 | const [errMsg, setErrMsg] = useState('')
|
|---|
| 162 | const [isAborted, setIsAborted] = useState(false)
|
|---|
| 163 |
|
|---|
| 164 | const handleClick = async () => {
|
|---|
| 165 | const res = updateUser({ name: 'Banana' })
|
|---|
| 166 |
|
|---|
| 167 | expectTypeOf(res).resolves.toMatchTypeOf<
|
|---|
| 168 | | {
|
|---|
| 169 | error: { status: number; data: unknown } | SerializedError
|
|---|
| 170 | }
|
|---|
| 171 | | {
|
|---|
| 172 | data: {
|
|---|
| 173 | name: string
|
|---|
| 174 | }
|
|---|
| 175 | }
|
|---|
| 176 | >()
|
|---|
| 177 |
|
|---|
| 178 | expectTypeOf(res.arg).toMatchTypeOf<{
|
|---|
| 179 | endpointName: string
|
|---|
| 180 | originalArgs: { name: string }
|
|---|
| 181 | track?: boolean
|
|---|
| 182 | }>()
|
|---|
| 183 |
|
|---|
| 184 | expectTypeOf(res.requestId).toBeString()
|
|---|
| 185 |
|
|---|
| 186 | expectTypeOf(res.abort).toEqualTypeOf<() => void>()
|
|---|
| 187 |
|
|---|
| 188 | expectTypeOf(res.unwrap()).resolves.toEqualTypeOf<{ name: string }>()
|
|---|
| 189 |
|
|---|
| 190 | expectTypeOf(res.reset).toEqualTypeOf<() => void>()
|
|---|
| 191 | }
|
|---|
| 192 |
|
|---|
| 193 | return (
|
|---|
| 194 | <div>
|
|---|
| 195 | <button onClick={handleClick}>Update User and abort</button>
|
|---|
| 196 | <div>{successMsg}</div>
|
|---|
| 197 | <div>{errMsg}</div>
|
|---|
| 198 | <div>{isAborted ? 'Request was aborted' : ''}</div>
|
|---|
| 199 | </div>
|
|---|
| 200 | )
|
|---|
| 201 | }
|
|---|
| 202 | })
|
|---|
| 203 |
|
|---|
| 204 | test('top level named hooks', () => {
|
|---|
| 205 | interface Post {
|
|---|
| 206 | id: number
|
|---|
| 207 | name: string
|
|---|
| 208 | fetched_at: string
|
|---|
| 209 | }
|
|---|
| 210 |
|
|---|
| 211 | type PostsResponse = Post[]
|
|---|
| 212 |
|
|---|
| 213 | const api = createApi({
|
|---|
| 214 | baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/' }),
|
|---|
| 215 | tagTypes: ['Posts'],
|
|---|
| 216 | endpoints: (build) => ({
|
|---|
| 217 | getPosts: build.query<PostsResponse, void>({
|
|---|
| 218 | query: () => ({ url: 'posts' }),
|
|---|
| 219 | providesTags: (result) =>
|
|---|
| 220 | result ? result.map(({ id }) => ({ type: 'Posts', id })) : [],
|
|---|
| 221 | }),
|
|---|
| 222 | updatePost: build.mutation<Post, Partial<Post>>({
|
|---|
| 223 | query: ({ id, ...body }) => ({
|
|---|
| 224 | url: `post/${id}`,
|
|---|
| 225 | method: 'PUT',
|
|---|
| 226 | body,
|
|---|
| 227 | }),
|
|---|
| 228 | invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
|
|---|
| 229 | }),
|
|---|
| 230 | addPost: build.mutation<Post, Partial<Post>>({
|
|---|
| 231 | query: (body) => ({
|
|---|
| 232 | url: `post`,
|
|---|
| 233 | method: 'POST',
|
|---|
| 234 | body,
|
|---|
| 235 | }),
|
|---|
| 236 | invalidatesTags: ['Posts'],
|
|---|
| 237 | }),
|
|---|
| 238 | }),
|
|---|
| 239 | })
|
|---|
| 240 |
|
|---|
| 241 | expectTypeOf(api.useGetPostsQuery).toEqualTypeOf(
|
|---|
| 242 | api.endpoints.getPosts.useQuery,
|
|---|
| 243 | )
|
|---|
| 244 |
|
|---|
| 245 | expectTypeOf(api.useUpdatePostMutation).toEqualTypeOf(
|
|---|
| 246 | api.endpoints.updatePost.useMutation,
|
|---|
| 247 | )
|
|---|
| 248 |
|
|---|
| 249 | expectTypeOf(api.useAddPostMutation).toEqualTypeOf(
|
|---|
| 250 | api.endpoints.addPost.useMutation,
|
|---|
| 251 | )
|
|---|
| 252 | })
|
|---|
| 253 |
|
|---|
| 254 | test('UseQuery type can be used to recreate the hook type', () => {
|
|---|
| 255 | const fakeQuery = ANY as UseQuery<
|
|---|
| 256 | typeof api.endpoints.getUser.Types.QueryDefinition
|
|---|
| 257 | >
|
|---|
| 258 |
|
|---|
| 259 | expectTypeOf(fakeQuery).toEqualTypeOf(api.endpoints.getUser.useQuery)
|
|---|
| 260 | })
|
|---|
| 261 |
|
|---|
| 262 | test('UseMutation type can be used to recreate the hook type', () => {
|
|---|
| 263 | const fakeMutation = ANY as UseMutation<
|
|---|
| 264 | typeof api.endpoints.updateUser.Types.MutationDefinition
|
|---|
| 265 | >
|
|---|
| 266 |
|
|---|
| 267 | expectTypeOf(fakeMutation).toEqualTypeOf(
|
|---|
| 268 | api.endpoints.updateUser.useMutation,
|
|---|
| 269 | )
|
|---|
| 270 | })
|
|---|
| 271 |
|
|---|
| 272 | test('TypedQueryStateSelector creates a pre-typed version of QueryStateSelector', () => {
|
|---|
| 273 | type Post = {
|
|---|
| 274 | id: number
|
|---|
| 275 | title: string
|
|---|
| 276 | }
|
|---|
| 277 |
|
|---|
| 278 | type PostsApiResponse = {
|
|---|
| 279 | posts: Post[]
|
|---|
| 280 | total: number
|
|---|
| 281 | skip: number
|
|---|
| 282 | limit: number
|
|---|
| 283 | }
|
|---|
| 284 |
|
|---|
| 285 | type QueryArgument = number | undefined
|
|---|
| 286 |
|
|---|
| 287 | type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
|
|---|
| 288 |
|
|---|
| 289 | type SelectedResult = Pick<PostsApiResponse, 'posts'>
|
|---|
| 290 |
|
|---|
| 291 | const postsApiSlice = createApi({
|
|---|
| 292 | baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
|
|---|
| 293 | reducerPath: 'postsApi',
|
|---|
| 294 | tagTypes: ['Posts'],
|
|---|
| 295 | endpoints: (build) => ({
|
|---|
| 296 | getPosts: build.query<PostsApiResponse, QueryArgument>({
|
|---|
| 297 | query: (limit = 5) => `?limit=${limit}&select=title`,
|
|---|
| 298 | }),
|
|---|
| 299 | }),
|
|---|
| 300 | })
|
|---|
| 301 |
|
|---|
| 302 | const { useGetPostsQuery } = postsApiSlice
|
|---|
| 303 |
|
|---|
| 304 | function PostById({ id }: { id: number }) {
|
|---|
| 305 | const { post } = useGetPostsQuery(undefined, {
|
|---|
| 306 | selectFromResult: (state) => ({
|
|---|
| 307 | post: state.data?.posts.find((post) => post.id === id),
|
|---|
| 308 | }),
|
|---|
| 309 | })
|
|---|
| 310 |
|
|---|
| 311 | expectTypeOf(post).toEqualTypeOf<Post | undefined>()
|
|---|
| 312 |
|
|---|
| 313 | return <li>{post?.title}</li>
|
|---|
| 314 | }
|
|---|
| 315 |
|
|---|
| 316 | const EMPTY_ARRAY: Post[] = []
|
|---|
| 317 |
|
|---|
| 318 | const typedSelectFromResult: TypedQueryStateSelector<
|
|---|
| 319 | PostsApiResponse,
|
|---|
| 320 | QueryArgument,
|
|---|
| 321 | BaseQueryFunction,
|
|---|
| 322 | SelectedResult
|
|---|
| 323 | > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
|
|---|
| 324 |
|
|---|
| 325 | function PostsList() {
|
|---|
| 326 | const { posts } = useGetPostsQuery(undefined, {
|
|---|
| 327 | selectFromResult: typedSelectFromResult,
|
|---|
| 328 | })
|
|---|
| 329 |
|
|---|
| 330 | expectTypeOf(posts).toEqualTypeOf<Post[]>()
|
|---|
| 331 |
|
|---|
| 332 | return (
|
|---|
| 333 | <div>
|
|---|
| 334 | <ul>
|
|---|
| 335 | {posts.map((post) => (
|
|---|
| 336 | <PostById key={post.id} id={post.id} />
|
|---|
| 337 | ))}
|
|---|
| 338 | </ul>
|
|---|
| 339 | </div>
|
|---|
| 340 | )
|
|---|
| 341 | }
|
|---|
| 342 | })
|
|---|
| 343 | })
|
|---|