diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index 12a3b76761..20cfe610dd 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -938,6 +938,46 @@ _(optional, not applicable with `queryFn`)_ [summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema) +A [Standard Schema](https://standardschema.dev/) compliant schema that validates the error response returned by your `baseQuery` **before** it is passed to [`transformErrorResponse`](#transformerrorresponse). This lets you catch unexpected error shapes from the server early, before any transformation logic runs. + +This is the error-side counterpart to [`rawResponseSchema`](#rawresponseschema), which does the same for successful responses. + +:::info How `rawErrorResponseSchema` differs from `errorResponseSchema` + +- **`rawErrorResponseSchema`** — validates the _raw_ error value straight from `baseQuery`, _before_ `transformErrorResponse` runs. Use this when you want to ensure the server's error response matches the shape your `transformErrorResponse` function expects as input. +- **`errorResponseSchema`** — validates the error value _after_ `transformErrorResponse` has run (or the raw value if no transform is provided). Use this when you want to guarantee the final error shape that your UI components will consume. + +You can use both on the same endpoint to validate at each stage of the pipeline. + +::: + +```ts title="rawErrorResponseSchema usage" no-transpile +import { createApi } from '@reduxjs/toolkit/query/react' +import * as v from 'valibot' + +// Schema matching the raw error shape returned by your baseQuery +const rawApiErrorSchema = v.object({ + status: v.number(), + data: v.object({ + code: v.string(), + message: v.string(), + }), +}) + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (build) => ({ + getPost: build.query({ + query: (id) => `posts/${id}`, + // Validates the raw error from baseQuery BEFORE transformErrorResponse + rawErrorResponseSchema: rawApiErrorSchema, + // Extracts just the nested error data + transformErrorResponse: (raw) => raw.data, + }), + }), +}) +``` + [examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema) #### `metaSchema` diff --git a/docs/usage/nextjs.mdx b/docs/usage/nextjs.mdx index e6a38c64b9..d899dc4c3d 100644 --- a/docs/usage/nextjs.mdx +++ b/docs/usage/nextjs.mdx @@ -169,7 +169,7 @@ export type AppDispatch = AppStore['dispatch'] // file: app/StoreProvider.tsx 'use client' -import { useRef } from 'react' +import { useState } from 'react' import { Provider } from 'react-redux' // highlight-start import { makeStore, AppStore } from '../lib/store' @@ -181,18 +181,33 @@ export default function StoreProvider({ children: React.ReactNode }) { // highlight-start - const storeRef = useRef(undefined) - if (!storeRef.current) { - // Create the store instance the first time this renders - storeRef.current = makeStore() - } + // Create the store instance once using a lazy initializer + const [store] = useState(() => makeStore()) // highlight-end - return {children} + return {children} } ``` -In this example code we are ensuring that this client component is re-render safe by checking the value of the reference to ensure that the store is only created once. This component will only be rendered once per request on the server, but might be re-rendered multiple times on the client if there are stateful client components located above this component in the tree, or if this component also contains other mutable state that causes a re-render. +In this example code we are ensuring that this client component is re-render safe by using `useState` with a lazy initializer function. React guarantees the initializer only runs once, so the store is created exactly one time. This component will only be rendered once per request on the server, but might be re-rendered multiple times on the client if there are stateful client components located above this component in the tree, or if this component also contains other mutable state that causes a re-render. + +:::warning React 19 Breaking Change + +Previous versions of this guide used a `useRef`-based pattern to create the store during render: + +```ts no-transpile +// ❌ This pattern breaks in React 19 +const storeRef = useRef(undefined) +if (!storeRef.current) { + storeRef.current = makeStore() +} +``` + +**React 19 no longer allows accessing `ref.current` during render.** Doing so will throw: `"Cannot access refs during render"`. This applies to both React 19 Strict Mode and concurrent features. + +The `useState` lazy initializer pattern shown above is the recommended replacement. It is safe for concurrent rendering and works with all versions of React. + +::: :::tip Why Client Components? @@ -248,7 +263,7 @@ export type AppDispatch = AppStore['dispatch'] // file: app/StoreProvider.tsx 'use client' -import { useRef } from 'react' +import { useState } from 'react' import { Provider } from 'react-redux' import { makeStore, AppStore } from '../lib/store' // highlight-start @@ -262,15 +277,15 @@ export default function StoreProvider({ count: number children: React.ReactNode }) { - const storeRef = useRef(null) - if (!storeRef.current) { - storeRef.current = makeStore() - // highlight-start - storeRef.current.dispatch(initializeCount(count)) - // highlight-end - } - - return {children} + // highlight-start + const [store] = useState(() => { + const store = makeStore() + store.dispatch(initializeCount(count)) + return store + }) + // highlight-end + + return {children} } ``` @@ -342,7 +357,7 @@ export const useAppStore = useStore.withTypes() // file: app/ProductName.tsx 'use client' -import { useRef } from 'react' +import { useState } from 'react' import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks' import { initializeProduct, @@ -354,11 +369,10 @@ export default function ProductName({ product }: { product: Product }) { // highlight-start // Initialize the store with the product information const store = useAppStore() - const initialized = useRef(false) - if (!initialized.current) { + const [initialized] = useState(() => { store.dispatch(initializeProduct(product)) - initialized.current = true - } + return true + }) const name = useAppSelector((state) => state.product.name) const dispatch = useAppDispatch() // highlight-end @@ -372,7 +386,7 @@ export default function ProductName({ product }: { product: Product }) { } ``` -Here we are using the same initialization pattern as before, of dispatching actions to the store, to set the route-specific data. The `initialized` ref is used to ensure that the store is only initialized once per route change. +Here we are using the same initialization pattern as before, of dispatching actions to the store, to set the route-specific data. The `useState` lazy initializer is used to ensure that the store is only initialized once per route change. It is worth noting that initializing the store with a `useEffect` would not work because `useEffect` only runs on the client. This would result in hydration errors or flicker because the result from a server side render would not match the result from the client side render.