From 93e5baaadcb4fefce9e439bb4ffecd569d4078f5 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 20:57:49 +0100 Subject: [PATCH 01/11] extract common props to separate def and add missing phantom property --- .../toolkit/src/query/endpointDefinitions.ts | 177 +++++++++++++++--- 1 file changed, 153 insertions(+), 24 deletions(-) diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index c19488c1a2..a0d6751dc7 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -40,6 +40,7 @@ import type { import { isNotNullish } from './utils' import type { NamedSchemaError } from './standardSchema' +const rawResultType = /* @__PURE__ */ Symbol() const resultType = /* @__PURE__ */ Symbol() const baseQuery = /* @__PURE__ */ Symbol() @@ -118,10 +119,52 @@ type EndpointDefinitionWithQuery< arg: QueryArg, ): unknown - /** A schema for the result *before* it's passed to `transformResponse` */ + /** + * A schema for the result *before* it's passed to `transformResponse` + * + * @example + * ```ts + * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const postSchema = v.object({ id: v.number(), name: v.string() }) + * type Post = v.InferOutput + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPostName: build.query({ + * query: ({ id }) => `/post/${id}`, + * rawResponseSchema: postSchema, + * transformResponse: (post) => post.name, + * }), + * }) + * }) + * ``` + */ rawResponseSchema?: StandardSchemaV1 - /** A schema for the error object returned by the `query` or `queryFn`, *before* it's passed to `transformErrorResponse` */ + /** + * A schema for the error object returned by the `query` or `queryFn`, *before* it's passed to `transformErrorResponse` + * + * @example + * ```ts + * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * import {customBaseQuery, baseQueryErrorSchema} from "./customBaseQuery" + * + * const api = createApi({ + * baseQuery: customBaseQuery, + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * rawErrorResponseSchema: baseQueryErrorSchema, + * transformErrorResponse: (error) => error.data, + * }), + * }) + * }) + * ``` + */ rawErrorResponseSchema?: StandardSchemaV1> } @@ -193,32 +236,98 @@ type BaseEndpointTypes = { ResultType: ResultType } -export type BaseEndpointDefinition< +interface CommonEndpointDefinition< QueryArg, BaseQuery extends BaseQueryFn, ResultType, - RawResultType extends BaseQueryResult = BaseQueryResult, -> = ( - | ([CastAny, {}>] extends [NEVER] - ? never - : EndpointDefinitionWithQuery< - QueryArg, - BaseQuery, - ResultType, - RawResultType - >) - | EndpointDefinitionWithQueryFn -) & { - /** A schema for the arguments to be passed to the `query` or `queryFn` */ +> { + /** + * A schema for the arguments to be passed to the `query` or `queryFn`. + * + * @example + * ```ts + * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * argSchema: v.object({ id: v.number() }), + * }), + * }) + * }) + * ``` + */ argSchema?: StandardSchemaV1 - /** A schema for the result (including `transformResponse` if provided) */ + /** + * A schema for the result (including `transformResponse` if provided) + * + * @example + * ```ts + * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const postSchema = v.object({ id: v.number(), name: v.string() }) + * type Post = v.InferOutput + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * responseSchema: postSchema, + * }), + * }) + * }) + * ``` + */ responseSchema?: StandardSchemaV1 - /** A schema for the error object returned by the `query` or `queryFn` (including `transformErrorResponse` if provided) */ + /** + * A schema for the error object returned by the `query` or `queryFn` (including `transformErrorResponse` if provided) + * + * @example + * ```ts + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * import { customBaseQuery, baseQueryErrorSchema } from "./customBaseQuery" + * + * const api = createApi({ + * baseQuery: customBaseQuery, + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * errorResponseSchema: baseQueryErrorSchema, + * }), + * }) + * }) + * ``` + */ errorResponseSchema?: StandardSchemaV1> - /** A schema for the `meta` property returned by the `query` or `queryFn` */ + /** + * A schema for the `meta` property returned by the `query` or `queryFn` + * + * @example + * ```ts + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * import { customBaseQuery, baseQueryMetaSchema } from "./customBaseQuery" + * + * const api = createApi({ + * baseQuery: customBaseQuery, + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * metaSchema: baseQueryMetaSchema, + * }), + * }) + * }) + * ``` + */ metaSchema?: StandardSchemaV1> /** @@ -237,12 +346,32 @@ export type BaseEndpointDefinition< onSchemaFailure?: SchemaFailureHandler skipSchemaValidation?: boolean +} - /* phantom type */ - [resultType]?: ResultType - /* phantom type */ - [baseQuery]?: BaseQuery -} & HasRequiredProps< +export type BaseEndpointDefinition< + QueryArg, + BaseQuery extends BaseQueryFn, + ResultType, + RawResultType extends BaseQueryResult = BaseQueryResult, +> = ( + | ([CastAny, {}>] extends [NEVER] + ? never + : EndpointDefinitionWithQuery< + QueryArg, + BaseQuery, + ResultType, + RawResultType + >) + | EndpointDefinitionWithQueryFn +) & + CommonEndpointDefinition & { + /* phantom type */ + [rawResultType]?: RawResultType + /* phantom type */ + [resultType]?: ResultType + /* phantom type */ + [baseQuery]?: BaseQuery + } & HasRequiredProps< BaseQueryExtraOptions, { extraOptions: BaseQueryExtraOptions }, { extraOptions?: BaseQueryExtraOptions } From 6a99786c84dac9ed2016cde1db5cc4e8a196b23f Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 21:00:22 +0100 Subject: [PATCH 02/11] try docblocks --- docs/rtk-query/api/createApi.mdx | 64 ++++++++++++++++++++++++++++++++ website/docusaurus.config.ts | 1 + 2 files changed, 65 insertions(+) diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index d881735cfb..3eb74d38cd 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -221,6 +221,20 @@ export type QueryDefinition< updateCachedData, // available for query endpoints only }: QueryCacheLifecycleApi, ): Promise + + argSchema?: StandardSchemaV1 + + /* only available with `query`, not `queryFn` */ + rawResponseSchema?: StandardSchemaV1> + + responseSchema?: StandardSchemaV1 + + /* only available with `query`, not `queryFn` */ + rawErrorResponseSchema?: StandardSchemaV1> + + errorResponseSchema?: StandardSchemaV1> + + metaSchema?: StandardSchemaV1> } ``` @@ -792,6 +806,56 @@ async function onCacheEntryAdded( ): Promise ``` +### Schema Validation + +#### `argSchema` + +_(optional)_ + +[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema) + +#### `responseSchema` + +_(optional)_ + +[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema) + +#### `rawResponseSchema` + +_(optional, only for query endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema) + +#### `errorResponseSchema` + +_(optional)_ + +[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema) + +#### `rawErrorResponseSchema` + +_(optional, only for query endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema) + +#### `metaSchema` + +_(optional)_ + +[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema) + +[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema) + ## Return value See [the "created Api" API reference](./created-api/overview) diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index b042530c54..a850b8eaa9 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -33,6 +33,7 @@ const config: Config = { 'index.ts', 'query/index.ts', 'query/createApi.ts', + 'query/endpointDefinitions.ts', 'query/react/index.ts', 'query/react/ApiProvider.tsx', ], From 4e7616f75fe2c3522d085e74ccd365b08b8f79fd Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 21:09:27 +0100 Subject: [PATCH 03/11] add no-transpile to codeblocks --- packages/toolkit/src/query/endpointDefinitions.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index a0d6751dc7..5cd0661449 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -124,6 +124,8 @@ type EndpointDefinitionWithQuery< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -149,6 +151,8 @@ type EndpointDefinitionWithQuery< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import {customBaseQuery, baseQueryErrorSchema} from "./customBaseQuery" @@ -246,6 +250,8 @@ interface CommonEndpointDefinition< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -267,6 +273,8 @@ interface CommonEndpointDefinition< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -291,6 +299,8 @@ interface CommonEndpointDefinition< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import { customBaseQuery, baseQueryErrorSchema } from "./customBaseQuery" @@ -313,6 +323,8 @@ interface CommonEndpointDefinition< * * @example * ```ts + * // codeblock-meta no-transpile + * * import { createApi } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import { customBaseQuery, baseQueryMetaSchema } from "./customBaseQuery" From f7ed3621ec18fb10bf7887b3361b8cb256aebbf2 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 21:21:36 +0100 Subject: [PATCH 04/11] add onSchemaFailure and skipSchemaValidation docs --- docs/rtk-query/api/createApi.mdx | 20 ++++++ packages/toolkit/src/query/createApi.ts | 52 ++++++++++++++ .../toolkit/src/query/endpointDefinitions.ts | 70 ++++++++++++++++--- 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index 3eb74d38cd..ec42bc6834 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -483,6 +483,26 @@ You can set this globally in `createApi`, but you can also override the default If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you. ::: +### `onSchemaFailure` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `onSchemaFailure` to each individual endpoint definition. +::: + +### `skipSchemaValidation` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `skipSchemaValidation` to each individual endpoint definition. +::: + ## Endpoint Definition Parameters ### `query` diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index 31fde60be8..4b5cf2886b 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -214,7 +214,59 @@ export interface CreateApiOptions< NoInfer > + /** + * A function that is called when a schema validation fails. + * + * Gets called with a `NamedSchemaError` and an object containing the endpoint name, the type of the endpoint, the argument passed to the endpoint, and the query cache key (if applicable). + * + * `NamedSchemaError` has the following properties: + * - `issues`: an array of issues that caused the validation to fail + * - `value`: the value that was passed to the schema + * - `schemaName`: the name of the schema that was used to validate the value (e.g. `argSchema`) + * + * @example + * ```ts + * // codeblock-meta no-transpile + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * }), + * }), + * onSchemaFailure: (error, info) => { + * console.error(error, info) + * }, + * }) + * ``` + */ onSchemaFailure?: SchemaFailureHandler + /** + * Defaults to `false`. + * + * If set to `true`, will skip schema validation for all endpoints, unless overridden by the endpoint. + * + * @example + * ```ts + * // codeblock-meta no-transpile + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * skipSchemaValidation: process.env.NODE_ENV === "test", // skip schema validation in tests, since we'll be mocking the response + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * responseSchema: v.object({ id: v.number(), name: v.string() }), + * }), + * }) + * }) + * ``` + */ skipSchemaValidation?: boolean } diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 5cd0661449..f03a3d7aa9 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -120,12 +120,11 @@ type EndpointDefinitionWithQuery< ): unknown /** - * A schema for the result *before* it's passed to `transformResponse` + * A schema for the result *before* it's passed to `transformResponse`. * * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -147,12 +146,11 @@ type EndpointDefinitionWithQuery< rawResponseSchema?: StandardSchemaV1 /** - * A schema for the error object returned by the `query` or `queryFn`, *before* it's passed to `transformErrorResponse` + * A schema for the error object returned by the `query` or `queryFn`, *before* it's passed to `transformErrorResponse`. * * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import {customBaseQuery, baseQueryErrorSchema} from "./customBaseQuery" @@ -251,7 +249,6 @@ interface CommonEndpointDefinition< * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -269,12 +266,11 @@ interface CommonEndpointDefinition< argSchema?: StandardSchemaV1 /** - * A schema for the result (including `transformResponse` if provided) + * A schema for the result (including `transformResponse` if provided). * * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * @@ -295,12 +291,11 @@ interface CommonEndpointDefinition< responseSchema?: StandardSchemaV1 /** - * A schema for the error object returned by the `query` or `queryFn` (including `transformErrorResponse` if provided) + * A schema for the error object returned by the `query` or `queryFn` (including `transformErrorResponse` if provided). * * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import { customBaseQuery, baseQueryErrorSchema } from "./customBaseQuery" @@ -319,12 +314,11 @@ interface CommonEndpointDefinition< errorResponseSchema?: StandardSchemaV1> /** - * A schema for the `meta` property returned by the `query` or `queryFn` + * A schema for the `meta` property returned by the `query` or `queryFn`. * * @example * ```ts * // codeblock-meta no-transpile - * * import { createApi } from '@reduxjs/toolkit/query/react' * import * as v from "valibot" * import { customBaseQuery, baseQueryMetaSchema } from "./customBaseQuery" @@ -356,7 +350,61 @@ interface CommonEndpointDefinition< */ structuralSharing?: boolean + /** + * A function that is called when a schema validation fails. + * + * Gets called with a `NamedSchemaError` and an object containing the endpoint name, the type of the endpoint, the argument passed to the endpoint, and the query cache key (if applicable). + * + * `NamedSchemaError` has the following properties: + * - `issues`: an array of issues that caused the validation to fail + * - `value`: the value that was passed to the schema + * - `schemaName`: the name of the schema that was used to validate the value (e.g. `argSchema`) + * + * @example + * ```ts + * // codeblock-meta no-transpile + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * onSchemaFailure: (error, info) => { + * console.error(error, info) + * }, + * }), + * }) + * }) + * ``` + */ onSchemaFailure?: SchemaFailureHandler + + /** + * Defaults to `false`. + * + * If set to `true`, will skip schema validation for this endpoint. + * Overrides the global setting. + * + * @example + * ```ts + * // codeblock-meta no-transpile + * import { createApi } from '@reduxjs/toolkit/query/react' + * import * as v from "valibot" + * + * const api = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: '/' }), + * endpoints: (build) => ({ + * getPost: build.query({ + * query: ({ id }) => `/post/${id}`, + * responseSchema: v.object({ id: v.number(), name: v.string() }), + * skipSchemaValidation: process.env.NODE_ENV === "test", // skip schema validation in tests, since we'll be mocking the response + * }), + * }) + * }) + * ``` + */ skipSchemaValidation?: boolean } From d74a3ce3fc0221b8ae9effe2e6cf5192f4b349da Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 22:10:02 +0100 Subject: [PATCH 05/11] blurb --- docs/rtk-query/api/createApi.mdx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index ec42bc6834..f2a1f592ac 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -828,6 +828,14 @@ async function onCacheEntryAdded( ### Schema Validation +Endpoints can have schemas for runtime validation of various values. Any [Standard Schema](https://standardschema.dev/) compliant library can be used. + +:::warning + +Schema failures are treated as _fatal_, meaning that normal error handling such as tag invalidation will not be executed. + +::: + #### `argSchema` _(optional)_ @@ -846,7 +854,7 @@ _(optional)_ #### `rawResponseSchema` -_(optional, only for query endpoints)_ +_(optional, not applicable with `queryFn`)_ [summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema) @@ -862,7 +870,7 @@ _(optional)_ #### `rawErrorResponseSchema` -_(optional, only for query endpoints)_ +_(optional, not applicable with `queryFn`)_ [summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema) From f9e3ad98fbc66c6b3279397d6af61104e9acb856 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 22:43:27 +0100 Subject: [PATCH 06/11] usage with typescript --- docs/rtk-query/usage-with-typescript.mdx | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/docs/rtk-query/usage-with-typescript.mdx b/docs/rtk-query/usage-with-typescript.mdx index a9f67bbbd9..061682c191 100644 --- a/docs/rtk-query/usage-with-typescript.mdx +++ b/docs/rtk-query/usage-with-typescript.mdx @@ -703,3 +703,135 @@ function AddPost() { ) } ``` + +## Schema Validation + +Endpoints can have schemas for runtime validation of various values. Any [Standard Schema](https://standardschema.dev/) compliant library can be used. See [API reference](./api/createApi.mdx#schema-validation) for full list of available schemas. + +When following the default approach of explicitly specifying type parameters for queries and mutations, the schemas will be required to match the types provided. + +```ts title="Explicitly typed endpoint" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const postSchema = v.object({ + id: v.number(), + name: v.string(), +}) +type Post = v.InferOutput + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getPost: build.query({ + query: ({ id }) => `/post/${id}`, + responseSchema: postSchema, // errors if type mismatch + }), + }), +}) +``` + +Schemas can also be used as a source of inference, meaning that the type parameters can be omitted. + +```ts title="Implicitly typed endpoint" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const postSchema = v.object({ + id: v.number(), + name: v.string(), +}) +type Post = v.InferOutput + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getPost: build.query({ + // infer arg from here + query: ({ id }: { id: number }) => `/post/${id}`, + // infer result from here + responseSchema: postSchema, + }), + getTransformedPost: build.query({ + // infer arg from here + query: ({ id }: { id: number }) => `/post/${id}`, + // infer untransformed result from here + rawResponseSchema: postSchema, + // infer transformed result from here + transformResponse: (response) => ({ + ...response, + published_at: new Date(response.published_at), + }), + }), + }), +}) +``` + +:::warning + +Schemas should _not_ perform any transformation that would change the type of the value. + +```ts title="Incorrect usage" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' +import { titleCase } from 'lodash' + +const postSchema = v.object({ + id: v.number(), + name: v.pipe( + v.string(), + v.transform(titleCase), // fine - string -> string + ), + published_at: v.pipe( + v.string(), + // highlight-next-line + v.transform((s) => new Date(s)), // not allowed! + v.date(), + ), +}) +type Post = v.InferOutput + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getPost: build.query({ + query: ({ id }) => `/post/${id}`, + responseSchema: postSchema, + }), + }), +}) +``` + +Instead, transformation should be done with `transformResponse` and `transformErrorResponse` (when using `query`) or inside `queryFn` (when using `queryFn`). + +```ts title="Correct usage" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const postSchema = v.object({ + id: v.number(), + name: v.string(), + published_at: v.string(), +}) +type RawPost = v.InferOutput +type Post = Omit & { published_at: Date } + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getPost: build.query({ + query: ({ id }) => `/post/${id}`, + // use rawResponseSchema to validate *before* transformation + rawResponseSchema: postSchema, + // highlight-start + transformResponse: (response) => ({ + ...response, + published_at: new Date(response.published_at), + }), + // highlight-end + }), + }), +}) +``` + +::: From 048df389dcec2a7f831aa641e2a8a9642863207e Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 6 Apr 2025 23:26:26 +0100 Subject: [PATCH 07/11] queries and mutations pages --- docs/rtk-query/usage/mutations.mdx | 56 ++++++++++++++++++++++++++++++ docs/rtk-query/usage/queries.mdx | 42 ++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/docs/rtk-query/usage/mutations.mdx b/docs/rtk-query/usage/mutations.mdx index 15a3b2e8f4..3c8dc80243 100644 --- a/docs/rtk-query/usage/mutations.mdx +++ b/docs/rtk-query/usage/mutations.mdx @@ -326,3 +326,59 @@ export const { allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" > + +## Runtime Validation using Schemas + +Endpoints can use any [Standard Schema](https://standardschema.dev/) compliant library for runtime validation of various values. See [API reference](../api/createApi.mdx#schema-validation) for full list of available schemas. + +Most commonly, you'll want to use `responseSchema` to validate the response from the server (or `rawResponseSchema` when using `transformResponse`). + +```ts title="Using responseSchema" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const postSchema = v.object({ + id: v.number(), + name: v.string(), +}) +type Post = v.InferOutput +type TransformedPost = Omit & { published_at: Date } + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + updatePost: build.mutation>({ + query(data) { + const { id, ...body } = data + return { + url: `post/${id}`, + method: 'PUT', + body, + } + }, + responseSchema: postSchema, + }), + updatePostWithTransform: build.mutation>({ + query(data) { + const { id, ...body } = data + return { + url: `post/${id}`, + method: 'PUT', + body, + } + }, + rawResponseSchema: postSchema, + transformResponse: (response) => ({ + ...response, + published_at: new Date(response.published_at), + }), + // responseSchema can still be provided, to validate the transformed response + responseSchema: v.object({ + id: v.number(), + name: v.string(), + published_at: v.date(), + }), + }), + }), +}) +``` diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index 6f185aedf9..755e0af83c 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -350,6 +350,48 @@ const { status, data, error, refetch } = dispatch( ::: +### Runtime Validation using Schemas + +Endpoints can use any [Standard Schema](https://standardschema.dev/) compliant library for runtime validation of various values. See [API reference](../api/createApi.mdx#schema-validation) for full list of available schemas. + +Most commonly, you'll want to use `responseSchema` to validate the response from the server (or `rawResponseSchema` when using `transformResponse`). + +```ts title="Using responseSchema" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const postSchema = v.object({ + id: v.number(), + name: v.string(), +}) +type Post = v.InferOutput +type TransformedPost = Omit & { published_at: Date } + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getPost: build.query({ + query: ({ id }) => `/post/${id}`, + responseSchema: postSchema, + }), + getTransformedPost: build.query({ + query: ({ id }) => `/post/${id}`, + rawResponseSchema: postSchema, + transformResponse: (response) => ({ + ...response, + published_at: new Date(response.published_at), + }), + // responseSchema can still be provided, to validate the transformed response + responseSchema: v.object({ + id: v.number(), + name: v.string(), + published_at: v.date(), + }), + }), + }), +}) +``` + ## Example: Observing caching behavior This example demonstrates request deduplication and caching behavior: From 38a40aebacf09c0f9d08f7ff4cecad5203544b76 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Mon, 7 Apr 2025 00:19:37 +0100 Subject: [PATCH 08/11] infinite queries --- docs/rtk-query/usage/infinite-queries.mdx | 56 +++++++++++++++++++++++ docs/rtk-query/usage/mutations.mdx | 13 +++--- docs/rtk-query/usage/queries.mdx | 12 ++--- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/docs/rtk-query/usage/infinite-queries.mdx b/docs/rtk-query/usage/infinite-queries.mdx index c43dd02572..bbd53e3e33 100644 --- a/docs/rtk-query/usage/infinite-queries.mdx +++ b/docs/rtk-query/usage/infinite-queries.mdx @@ -531,3 +531,59 @@ const projectsApi = createApi({ }), }) ``` + +## Runtime Validation using Schemas + +Endpoints can use any [Standard Schema](https://standardschema.dev/) compliant library for runtime validation of various values. See [API reference](../api/createApi.mdx#schema-validation) for full list of available schemas. + +Most commonly, you'll want to use `responseSchema` to validate the response from the server (or `rawResponseSchema` when using `transformResponse`). + +```ts title="Using responseSchema" no-transpile +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +const pokemonSchema = v.object({ + id: v.number(), + name: v.string(), +}) +type Pokemon = v.InferOutput +const transformedPokemonSchema = v.object({ + ...pokemonSchema.entries, + id: v.string(), +}) +type TransformedPokemon = v.InferOutput + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }), + endpoints: (build) => ({ + getInfinitePokemon: build.infiniteQuery({ + query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`, + // argSchema for infinite queries must have both queryArg and pageParam + argSchema: v.object({ + queryArg: v.string(), + pageParam: v.number(), + }), + responseSchema: v.array(pokemonSchema), + }), + getTransformedPokemon: build.infiniteQuery< + TransformedPokemon[], + string, + number + >({ + query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`, + argSchema: v.object({ + queryArg: v.string(), + pageParam: v.number(), + }), + rawResponseSchema: v.array(pokemonSchema), + transformResponse: (response) => + response.map((pokemon) => ({ + ...pokemon, + id: String(pokemon.id), + })), + // responseSchema can still be provided, to validate the transformed response + responseSchema: v.array(transformedPokemonSchema), + }), + }), +}) +``` diff --git a/docs/rtk-query/usage/mutations.mdx b/docs/rtk-query/usage/mutations.mdx index 3c8dc80243..d1c38300e1 100644 --- a/docs/rtk-query/usage/mutations.mdx +++ b/docs/rtk-query/usage/mutations.mdx @@ -340,9 +340,14 @@ import * as v from 'valibot' const postSchema = v.object({ id: v.number(), name: v.string(), + published_at: v.string(), }) type Post = v.InferOutput -type TransformedPost = Omit & { published_at: Date } +const transformedPost = v.object({ + ...postSchema.entries, + published_at: v.date(), +}) +type TransformedPost = v.InferOutput const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/' }), @@ -373,11 +378,7 @@ const api = createApi({ published_at: new Date(response.published_at), }), // responseSchema can still be provided, to validate the transformed response - responseSchema: v.object({ - id: v.number(), - name: v.string(), - published_at: v.date(), - }), + responseSchema: transformedPost, }), }), }) diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index 755e0af83c..827e8ec026 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -365,7 +365,11 @@ const postSchema = v.object({ name: v.string(), }) type Post = v.InferOutput -type TransformedPost = Omit & { published_at: Date } +const transformedPost = v.object({ + ...postSchema.entries, + published_at: v.date(), +}) +type TransformedPost = v.InferOutput const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/' }), @@ -382,11 +386,7 @@ const api = createApi({ published_at: new Date(response.published_at), }), // responseSchema can still be provided, to validate the transformed response - responseSchema: v.object({ - id: v.number(), - name: v.string(), - published_at: v.date(), - }), + responseSchema: transformedPost, }), }), }) From f2d067b6cf3bbb515af18add986c175a586b1b34 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Mon, 7 Apr 2025 00:33:23 +0100 Subject: [PATCH 09/11] promote heading --- docs/rtk-query/usage/queries.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index 827e8ec026..ccd98eb74c 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -350,7 +350,7 @@ const { status, data, error, refetch } = dispatch( ::: -### Runtime Validation using Schemas +## Runtime Validation using Schemas Endpoints can use any [Standard Schema](https://standardschema.dev/) compliant library for runtime validation of various values. See [API reference](../api/createApi.mdx#schema-validation) for full list of available schemas. From 4a6bc84ff864c59258557eeed84ad0ed51ea28fb Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Mon, 7 Apr 2025 00:37:13 +0100 Subject: [PATCH 10/11] try exporting type for docs --- packages/toolkit/src/query/endpointDefinitions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index f03a3d7aa9..496dbbedb1 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -56,7 +56,7 @@ export type SchemaFailureHandler = ( info: SchemaFailureInfo, ) => void -type EndpointDefinitionWithQuery< +export type EndpointDefinitionWithQuery< QueryArg, BaseQuery extends BaseQueryFn, ResultType, @@ -170,7 +170,7 @@ type EndpointDefinitionWithQuery< rawErrorResponseSchema?: StandardSchemaV1> } -type EndpointDefinitionWithQueryFn< +export type EndpointDefinitionWithQueryFn< QueryArg, BaseQuery extends BaseQueryFn, ResultType, From 566ea4506f2fc74f67e3b5db53be151154d16bf9 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Mon, 7 Apr 2025 00:42:07 +0100 Subject: [PATCH 11/11] export other type used in docblocks --- packages/toolkit/src/query/react/buildHooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 86ee665d82..ae78077a6d 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -161,7 +161,7 @@ export type TypedUseQueryHookResult< > = TypedUseQueryStateResult & TypedUseQuerySubscriptionResult -type UseQuerySubscriptionOptions = SubscriptionOptions & { +export type UseQuerySubscriptionOptions = SubscriptionOptions & { /** * Prevents a query from automatically running. *