diff --git a/docs/reference/redux-toolkit/actionCreatorMiddleware.mdx b/docs/reference/redux-toolkit/actionCreatorMiddleware.mdx new file mode 100644 index 00000000..fede5df8 --- /dev/null +++ b/docs/reference/redux-toolkit/actionCreatorMiddleware.mdx @@ -0,0 +1,68 @@ +--- +id: actionCreatorMiddleware +title: Action Creator Middleware +sidebar_label: Action Creator Middleware +hide_title: true +--- + +  + +# Action Creator Middleware + +A custom middleware that detects if an action creator has been mistakenly dispatched, instead of being called before dispatching. + +A common mistake is to call `dispatch(actionCreator)` instead of `dispatch(actionCreator())`. +This tends to "work" as the action creator has the static `type` property, but can lead to unexpected behavior. + +## Options + +```ts no-transpile +export interface ActionCreatorInvariantMiddlewareOptions { + /** + * The function to identify whether a value is an action creator. + * The default checks for a function with a static type property and match method. + */ + isActionCreator?: (action: unknown) => action is Function & { type?: unknown } +} +``` + +## Exports + +### `createActionCreatorInvariantMiddleware` + +Creates an instance of the action creator check middleware, with the given options. + +You will most likely not need to call this yourself, as `getDefaultMiddleware` already does so. +Example: + +```ts +// file: reducer.ts noEmit + +export default function (state = {}, action: any) { + return state +} + +// file: store.ts + +import { + configureStore, + createActionCreatorInvariantMiddleware, + Tuple, +} from '@reduxjs/toolkit' +import reducer from './reducer' + +// Augment middleware to consider all functions with a static type property to be action creators +const isActionCreator = ( + action: unknown, +): action is Function & { type: unknown } => + typeof action === 'function' && 'type' in action + +const actionCreatorMiddleware = createActionCreatorInvariantMiddleware({ + isActionCreator, +}) + +const store = configureStore({ + reducer, + middleware: () => new Tuple(actionCreatorMiddleware), +}) +``` diff --git a/docs/reference/redux-toolkit/autoBatchEnhancer.mdx b/docs/reference/redux-toolkit/autoBatchEnhancer.mdx new file mode 100644 index 00000000..6ddf9f48 --- /dev/null +++ b/docs/reference/redux-toolkit/autoBatchEnhancer.mdx @@ -0,0 +1,157 @@ +--- +id: autoBatchEnhancer +title: autoBatchEnhancer +sidebar_label: autoBatchEnhancer +hide_title: true +--- + +  + +# `autoBatchEnhancer` + +A Redux store enhancer that looks for one or more "low-priority" dispatched actions in a row, and queues a callback to run subscriber notifications on a delay. It then notifies subscribers either when the queued callback runs, or when the next "normal-priority" action is dispatched, whichever is first. + +## Basic Usage + +```ts +import { + createSlice, + configureStore, + autoBatchEnhancer, + prepareAutoBatched, +} from '@reduxjs/toolkit' + +interface CounterState { + value: number +} + +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 } satisfies CounterState as CounterState, + reducers: { + incrementBatched: { + // Batched, low-priority + reducer(state) { + state.value += 1 + }, + // highlight-start + // Use the `prepareAutoBatched` utility to automatically + // add the `action.meta[SHOULD_AUTOBATCH]` field the enhancer needs + prepare: prepareAutoBatched(), + // highlight-end + }, + // Not batched, normal priority + decrementUnbatched(state) { + state.value -= 1 + }, + }, +}) +const { incrementBatched, decrementUnbatched } = counterSlice.actions + +// includes batch enhancer by default, as of RTK 2.0 +const store = configureStore({ + reducer: counterSlice.reducer, +}) +``` + +## API + +### `autoBatchEnhancer` + +```ts title="autoBatchEnhancer signature" no-transpile +export type SHOULD_AUTOBATCH = string +type AutoBatchOptions = + | { type: 'tick' } + | { type: 'timer'; timeout: number } + | { type: 'raf' } + | { type: 'callback'; queueNotification: (notify: () => void) => void } + +export type autoBatchEnhancer = (options?: AutoBatchOptions) => StoreEnhancer +``` + +:::tip +As of RTK 2.0, the `autoBatchEnhancer` is included by default when calling `configureStore`. + +This means to configure it, you should instead pass an callback that receives `getDefaultEnhancers` and calls it with your desired settings. + +```ts title="Configuring autoBatchEnhancer with getDefaultEnhancers" +import { configureStore } from '@reduxjs/toolkit' + +const store = configureStore({ + reducer: () => 0, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ + autoBatch: { type: 'tick' }, + }), +}) +``` + +::: + +Creates a new instance of the autobatch store enhancer. + +Any action that is tagged with `action.meta[SHOULD_AUTOBATCH] = true` will be treated as "low-priority", and a notification callback will be queued. The enhancer will delay notifying subscribers until either: + +- The queued callback runs and triggers the notifications +- A "normal-priority" action (any action _without_ `action.meta[SHOULD_AUTOBATCH] = true`) is dispatched in the same tick + +`autoBatchEnhancer` accepts options to configure how the notification callback is queued: + +- `{type: 'raf'}`: queues using `requestAnimationFrame` (default) +- `{type: 'tick'}`: queues using `queueMicrotask` +- `{type: 'timer', timeout: number}`: queues using `setTimeout` +- `{type: 'callback', queueNotification: (notify: () => void) => void}`: lets you provide your own callback, such as a debounced or throttled function + +The default behavior is to queue the notifications using `requestAnimationFrame`. + +The `SHOULD_AUTOBATCH` value is meant to be opaque - it's currently a string for simplicity, but could be a `Symbol` in the future. + +### `prepareAutoBatched` + +```ts title="prepareAutoBatched signature" no-transpile +type prepareAutoBatched = () => (payload: T) => { payload: T; meta: unknown } +``` + +Creates a function that accepts a `payload` value, and returns an object with `{payload, meta: {[SHOULD_AUTOBATCH]: true}}`. This is meant to be used with RTK's `createSlice` and its "`prepare` callback" syntax: + +```ts no-transpile +createSlice({ + name: 'todos', + initialState, + reducers: { + todoAdded: { + reducer(state, action: PayloadAction) { + state.push(action.payload) + }, + // highlight-start + prepare: prepareAutoBatched(), + // highlight-end + }, + }, +}) +``` + +## Batching Approach and Background + +The post [A Comparison of Redux Batching Techniques](https://blog.isquaredsoftware.com/2020/01/blogged-answers-redux-batching-techniques/) describes four different approaches for "batching Redux actions/dispatches" + +- a higher-order reducer that accepts multiple actions nested inside one real action, and iterates over them together +- an enhancer that wraps `dispatch` and debounces the notification callback +- an enhancer that wraps `dispatch` to accept an array of actions +- React's `unstable_batchedUpdates()`, which just combines multiple queued renders into one but doesn't affect subscriber notifications + +This enhancer is a variation of the "debounce" approach, but with a twist. + +Instead of _just_ debouncing _all_ subscriber notifications, it watches for any actions with a specific `action.meta[SHOULD_AUTOBATCH]: true` field attached. + +When it sees an action with that field, it queues a callback. The reducer is updated immediately, but the enhancer does _not_ notify subscribers right way. If other actions with the same field are dispatched in succession, the enhancer will continue to _not_ notify subscribers. Then, when the queued callback runs, it finally notifies all subscribers, similar to how React batches re-renders. + +The additional twist is also inspired by React's separation of updates into "low-priority" and "immediate" behavior (such as a render queued by an AJAX request vs a render queued by a user input that should be handled synchronously). + +If some low-pri actions have been dispatched and a notification microtask is queued, then a _normal_ priority action (without the field) is dispatched, the enhancer will go ahead and notify all subscribers synchronously as usual, and _not_ notify them at the end of the tick. + +This allows Redux users to selectively tag certain actions for effective batching behavior, making this purely opt-in on a per-action basis, while retaining normal notification behavior for all other actions. + +### RTK Query and Batching + +RTK Query already marks several of its key internal action types as batchable. By adding the `autoBatchEnhancer` to the store setup, it improves the overall UI performance, especially when rendering large lists of components that use the RTKQ query hooks. diff --git a/docs/reference/redux-toolkit/codemods.mdx b/docs/reference/redux-toolkit/codemods.mdx new file mode 100644 index 00000000..912bda22 --- /dev/null +++ b/docs/reference/redux-toolkit/codemods.mdx @@ -0,0 +1,81 @@ +--- +id: codemods +title: Codemods +sidebar_label: Codemods +hide_title: true +--- + +  + +# Codemods + +Per [the description in `1.9.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0), we have removed the "object" argument from `createReducer` and `createSlice.extraReducers` in the RTK 2.0 major version. We've also added a new optional form of `createSlice.reducers` that uses a callback instead of an object. + +To simplify upgrading codebases, we've published a set of codemods that will automatically transform the deprecated "object" syntax into the equivalent "builder" syntax. + +The codemods package is available on NPM as [**`@reduxjs/rtk-codemods`**](https://www.npmjs.com/package/@reduxjs/rtk-codemods). It currently contains these codemods: + +- `createReducerBuilder`: migrates `createReducer` calls that use the removed object syntax to the builder callback syntax +- `createSliceBuilder`: migrates `createSlice` calls that use the removed object syntax for `extraReducers` to the builder callback syntax +- `createSliceReducerBuilder`: migrates `createSlice` calls that use the still-standard object syntax for `reducers` to the optional new builder callback syntax, including uses of prepared reducers + +To run the codemods against your codebase, run `npx @reduxjs/rtk-codemods path/of/files/ or/some**/*glob.js`. + +Examples: + +```bash +npx @reduxjs/rtk-codemods createReducerBuilder ./src + +npx @reduxjs/rtk-codemods createSliceBuilder ./packages/my-app/**/*.ts +``` + +We also recommend re-running Prettier on the codebase before committing the changes. + +**These codemods _should_ work, but we would greatly appreciate testing and feedback on more real-world codebases!** + +Before: + +```js +createReducer(initialState, { + [todoAdded1a]: (state, action) => { + // stuff + }, + [todoAdded1b]: (state, action) => action.payload, +}) + +const slice1 = createSlice({ + name: 'a', + initialState: {}, + extraReducers: { + [todoAdded1a]: (state, action) => { + // stuff + }, + [todoAdded1b]: (state, action) => action.payload, + }, +}) +``` + +After: + +```js +createReducer(initialState, (builder) => { + builder.addCase(todoAdded1a, (state, action) => { + // stuff + }) + + builder.addCase(todoAdded1b, (state, action) => action.payload) +}) + +const slice1 = createSlice({ + name: 'a', + initialState: {}, + + extraReducers: (builder) => { + builder.addCase(todoAdded1a, (state, action) => { + // stuff + }) + + builder.addCase(todoAdded1b, (state, action) => action.payload) + }, +}) +``` diff --git a/docs/reference/redux-toolkit/combineSlices.mdx b/docs/reference/redux-toolkit/combineSlices.mdx new file mode 100644 index 00000000..71f2c16a --- /dev/null +++ b/docs/reference/redux-toolkit/combineSlices.mdx @@ -0,0 +1,359 @@ +--- +id: combineSlices +title: combineSlices +sidebar_label: combineSlices +hide_title: true +--- + +  + +# `combineSlices` + +## Overview + +A function that combines slices into a single reducer, and enables injection of more reducers after initialisation. + +```ts +// file: slices/api.ts noEmit +import type { Api } from '@reduxjs/toolkit/query' + +export declare const api: Api<() => any, {}, 'api', never> + +// file: slices/users.ts noEmit +import type { Slice } from '@reduxjs/toolkit' + +export declare const userSlice: Slice + +// file: slices/index.ts +import { combineSlices } from '@reduxjs/toolkit' +import { api } from './api' +import { userSlice } from './users' + +export const rootReducer = combineSlices(api, userSlice) + +// file: store.ts +import { configureStore } from '@reduxjs/toolkit' +import { rootReducer } from './slices' + +export const store = configureStore({ + reducer: rootReducer, +}) +``` + +:::note + +A "slice" for `combineSlices` is typically created with [`createSlice`](./createSlice.mdx), +but can be any "slice-like" object with `reducerPath` and `reducer` properties (meaning RTK Query [API instances](/rtk-query/api/created-api/overview.mdx) are also compatible). + +```ts no-transpile +const withUserReducer = rootReducer.inject({ + reducerPath: 'user', + reducer: userReducer, +}) + +const withApiReducer = rootReducer.inject(fooApi) +``` + +For simplicity, this `{ reducerPath, reducer }` shape will be described in these docs as a "slice". + +::: + +## Parameters + +`combineSlices` accepts a set of slices and/or reducer map objects, and combines them into a single reducer. + +Slices will be mounted at their `reducerPath`, and items from reducer map objects will be mounted under their respective key. + +```ts no-transpile +const rootReducer = combineSlices(counterSlice, baseApi, { + user: userSlice.reducer, + auth: authSlice.reducer, +}) +// is like +const rootReducer = combineReducers({ + [counterSlice.reducerPath]: counterSlice.reducer, + [baseApi.reducerPath]: baseApi.reducer, + user: userSlice.reducer, + auth: authSlice.reducer, +}) +``` + +:::caution + +If multiple slices/map objects have the same reducer path, the reducer provided later in the arguments will override the previous. + +However, typing will not be able to account for this. It's best to ensure that all of your reducers will aim for a unique location. + +::: + +## Return Value + +`combineSlices` returns a reducer function, with attached methods. + +```ts no-transpile +interface CombinedSliceReducer + extends Reducer> { + withLazyLoadedSlices(): CombinedSliceReducer< + InitialState, + DeclaredState & Partial + > + inject( + slice: Slice, + config?: InjectConfig + ): CombinedSliceReducer> + selector: { + (selectorFn: Selector, selectState?: SelectFromRootState) => WrappedSelector + original(state: DeclaredState) => InitialState & Partial + } +} +``` + +### `withLazyLoadedSlices` + +It's recommended to [infer your RootState type from your store](https://redux.js.org/usage/usage-with-typescript#define-root-state-and-dispatch-types), which is inferred from the reducer. However, this can present issues if slices are lazy loaded, and thus not able to be inferred from. + +`withLazyLoadedSlices` allows you to declare slices that will be added to state later, which will be included in the final state type. + +One possible pattern of managing this would be with declaration merging: + +```ts no-transpile title="Using declaration merging to declare injected slices" +// file: slices/index.ts +import { combineSlices } from '@reduxjs/toolkit' +import { staticSlice } from './static' + +export interface LazyLoadedSlices {} + +export const rootReducer = + combineSlices(staticSlice).withLazyLoadedSlices() + +// keys in LazyLoadedSlices are marked as optional +export type RootState = ReturnType + +// file: slices/lazySlice.ts +import type { WithSlice } from '@reduxjs/toolkit' +import { rootReducer } from '.' + +const lazySlice = createSlice({ + /* ... */ +}) + +declare module '.' { + export interface LazyLoadedSlices extends WithSlice {} +} + +const injectedReducer = rootReducer.inject(lazySlice) + +// and/or + +const injectedSlice = lazySlice.injectInto(rootReducer) +``` + +:::tip + +The above example uses the `WithSlice` utility type for a slice mounted under its `reducerPath`. If the slice is mounted under a different key, you can declare it as a regular key instead. + +```ts no-transpile title="Declaring a slice mounted outside its reducerPath" +// file: slices/lazySlice.ts +import { rootReducer } from '.' + +const lazySlice = createSlice({ + /* ... */ +}) + +declare module '.' { + export interface LazyLoadedSlices { + customKey: LazyState + } +} + +const injectedReducer = rootReducer.inject({ + reducerPath: 'customKey', + reducer: lazySlice.reducer, +}) + +// and/or + +const injectedSlice = lazySlice.injectInto(rootReducer, { + reducerPath: 'customKey', +}) +``` + +::: + +### `inject` + +`inject` allows you to add a slice to your set of reducers after initialisation. +It expects to be passed a slice and an optional config, and returns an updated version of the reducer with the slice included. + +This is mainly useful for lazy loading reducers. + +```ts no-transpile +const reducerWithUser = rootReducer.inject(userSlice) +``` + +:::note + +`inject` adds the slice to the map of reducers in your original reducer, but doesn't dispatch an action. + +This means that the added reducer state will not show up in your store until the next action is dispatched. + +::: + +#### Reducer replacement + +By default, replacing a reducer is not allowed. +In development mode, a warning will be logged to console if a new reducer instance is attempted to inject into a `reducerPath` that's already injected. (It won't warn if the same reducer instance is injected into the same place twice.) + +If you wish to allow replacing a reducer with a new instance, you must explicitly pass `overrideExisting: true` as part of your configuration object. + +```ts no-transpile +const reducerWithUser = rootReducer.inject(userSlice, { + overrideExisting: true, +}) +``` + +This may be useful for hot reload, or "removing" a reducer by replacing it with a function that always returns `null`. +Note that for predictable behavior, your types should account for all of the possible reducers you intend to occupy a path. + +```ts no-transpile title="'Removing' a reducer, by replacing it with a no-op function" +declare module '.' { + export interface LazyLoadedSlices { + removable: RemovableState | null + } +} + +const withInjected = rootReducer.inject( + { reducerPath: 'removable', reducer: removableReducer }, + { overrideExisting: true }, +) + +const emptyReducer = () => null + +const removeReducer = () => + rootReducer.inject( + { reducerPath: 'removable', reducer: emptyReducer }, + { overrideExisting: true }, + ) +``` + +### `selector` + +As noted previously, an injected reducer can still be undefined in state if no action has been dispatched. + +Dealing with this possibly-optional state can be inconvient when writing selectors, as you may end up with a lot of results being possibly undefined or relying on explicit defaults. + +`selector` allows you to get around this, by wrapping the reducer state in a `Proxy` that ensures that any currently injected reducers evaluate to their initial state if they're currently `undefined` in state. + +```ts no-transpile +declare module '.' { + export interface LazyLoadedSlices extends WithSlice {} +} + +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 }, + reducers: { + /* ... */ + }, +}) + +const withCounter = rootReducer.inject(counterSlice) + +const selectCounterValue = (rootState: RootState) => rootState.counter?.value // number | undefined + +const wrappedSelectCounterValue = withCounter.selector( + (rootState) => rootState.counter.value, // number +) + +console.log( + selectCounterValue({}), // undefined + selectCounterValue({ counter: { value: 2 } }), // 2 + wrappedSelectCounterValue({}), // 0 + wrappedSelectCounterValue({ counter: { value: 2 } }), // 2 +) +``` + +:::caution + +The `Proxy` retrieves a reducer's initial state by calling it with a randomly generated action type - don't try to handle this as a special case inside your reducer. + +::: + +#### Nested combined reducer + +The wrapped selector expects to use the state returned by the combined reducer as its first argument. + +If the combined reducer is nested further inside the store state, pass a `selectState` callback as the second argument to `selector`: + +```ts no-transpile +interface RootState { + innerCombined: ReturnType +} + +const selectCounterValue = withCounter.selector( + (combinedState) => combinedState.counter.value, + (rootState: RootState) => rootState.innerCombined, +) + +console.log( + selectCounterValue({ + innerCombined: {}, + }), // 0 + selectCounterValue({ + innerCombined: { + counter: { + value: 2, + }, + }, + }), // 2 +) +``` + +#### `original` + +Similar to [Immer usage](/usage/immer-reducers.md#debugging-and-inspecting-drafted-state), an `original` function is provided to retrieve the original state value provided to the `Proxy`. + +This is mainly useful for debugging/inspecting, as `Proxy` instances tend to be displayed in a format that's hard to read. + +The function is attached as a method on the `selector` function: + +```ts no-transpile +const wrappedSelectCounterValue = withCounter.selector((rootState) => { + console.log(withCounter.selector.original(rootState)) + return rootState.counter.value +}) +``` + +## Slice integration + +### `injectInto` + +Slice instances returned by [`createSlice`](./createSlice) have an attached `injectInto` method, which receive an injectable reducer from `combineSlices` and returns an "injected" version of that slice. + +```ts no-transpile +const injectedCounterSlice = counterSlice.injectInto(rootReducer) +``` + +An optional configuration object can be passed. This follows [`inject`](#inject)'s options with an additional `reducerPath` field, for injecting the slice under a path other than its current `reducerPath` property. + +```ts no-transpile +const aCounterSlice = counterSlice.injectInto(rootReducer, { + reducerPath: 'aCounter', +}) +``` + +### `selectors` / `getSelectors` + +Similar to [`selector`](#selector), the selectors from an "injected" slice instance behave slightly differently. + +If the slice state is undefined in the store state passed, the selector will instead be called with the slice's initial state. + +`selectors` will also reflect the change in `reducerPath` if one was made during injection. + +```ts no-transpile +console.log( + injectedCounterSlice.selectors.selectValue({}), // 0 + injectedCounterSlice.selectors.selectValue({ counter: { value: 2 } }), // 2 + aCounterSlice.selectors.selectValue({ aCounter: { value: 2 } }), // 2 +) +``` diff --git a/docs/reference/redux-toolkit/configureStore.mdx b/docs/reference/redux-toolkit/configureStore.mdx new file mode 100644 index 00000000..fe02e3eb --- /dev/null +++ b/docs/reference/redux-toolkit/configureStore.mdx @@ -0,0 +1,316 @@ +--- +id: configureStore +title: configureStore +sidebar_label: configureStore +hide_title: true +--- + +  + +# `configureStore` + +The standard method for creating a Redux store. It uses the low-level Redux core `createStore` method internally, but wraps that to provide good defaults to the store setup for a better development experience. + +## Purpose and Behavior + +A standard Redux store setup typically requires multiple pieces of configuration: + +- Combining the slice reducers into the root reducer +- Creating the middleware enhancer, usually with the thunk middleware or other side effects middleware, as well as middleware that might be used for development checks +- Adding the Redux DevTools enhancer, and composing the enhancers together +- Calling `createStore` + +Legacy Redux usage patterns typically required several dozen lines of copy-pasted boilerplate to achieve this. + +Redux Toolkit's `configureStore` simplifies that setup process, by doing all that work for you. One call to `configureStore` will: + +- Call `combineReducers` to combine your slices reducers into the root reducer function +- Add the thunk middleware and called `applyMiddleware` +- In development, automatically add more middleware to check for common mistakes like accidentally mutating the state +- Automatically set up the Redux DevTools Extension connection +- Call `createStore` to create a Redux store using that root reducer and those configuration options + +`configureStore` also offers an improved API and usage patterns compared to the original `createStore` by accepting named fields for `reducer`, `preloadedState`, `middleware`, `enhancers`, and `devtools`, as well as much better TS type inference. + +## Parameters + +`configureStore` accepts a single configuration object parameter, with the following options: + +```ts no-transpile + +interface ConfigureStoreOptions< + S = any, + A extends Action = UnknownAction, + M extends Tuple> = Tuple> + E extends Tuple = Tuple, + P = S +> { + /** + * A single reducer function that will be used as the root reducer, or an + * object of slice reducers that will be passed to `combineReducers()`. + */ + reducer: Reducer | ReducersMapObject + + /** + * An array of Redux middleware to install. If not supplied, defaults to + * the set of middleware returned by `getDefaultMiddleware()`. + */ + middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware) => M) | M + + /** + * Whether to enable Redux DevTools integration. Defaults to `true`. + * + * Additional configuration can be done by passing Redux DevTools options + */ + devTools?: boolean | DevToolsOptions + + /** + * Whether to check for duplicate middleware instances. Defaults to `true`. + */ + duplicateMiddlewareCheck?: boolean + + /** + * The initial state, same as Redux's createStore. + * You may optionally specify it to hydrate the state + * from the server in universal apps, or to restore a previously serialized + * user session. If you use `combineReducers()` to produce the root reducer + * function (either directly or indirectly by passing an object as `reducer`), + * this must be an object with the same shape as the reducer map keys. + */ + preloadedState?: P + + /** + * The store enhancers to apply. See Redux's `createStore()`. + * All enhancers will be included before the DevTools Extension enhancer. + * If you need to customize the order of enhancers, supply a callback + * function that will receive the getDefaultEnhancers, + * and should return a new array (such as `getDefaultEnhancers().concat(offline)`). + * If you only need to add middleware, you can use the `middleware` parameter instead. + */ + enhancers?: (getDefaultEnhancers: GetDefaultEnhancers) => E | E +} + +function configureStore< + S = any, + A extends Action = UnknownAction, + M extends Tuple> = Tuple> + E extends Tuple = Tuple, + P = S +>(options: ConfigureStoreOptions): EnhancedStore +``` + +### `reducer` + +If this is a single function, it will be directly used as the root reducer for the store. + +If it is an object of slice reducers, like `{users : usersReducer, posts : postsReducer}`, +`configureStore` will automatically create the root reducer by passing this object to the +[Redux `combineReducers` utility](https://redux.js.org/api/combinereducers). + +### `middleware` + +A callback which will receive `getDefaultMiddleware` as its argument, +and should return a middleware array. + +If this option is provided, it should return all the middleware functions you +want added to the store. `configureStore` will automatically pass those to `applyMiddleware`. + +If not provided, `configureStore` will call `getDefaultMiddleware` and use the +array of middleware functions it returns. + +For more details on how the `middleware` parameter works and the list of middleware that are added by default, see the +[`getDefaultMiddleware` docs page](./getDefaultMiddleware.mdx). + +:::note Tuple +Typescript users are required to use a `Tuple` instance (if not using a `getDefaultMiddleware` result, which is already a `Tuple`), for better inference. + +```ts no-transpile +import { configureStore, Tuple } from '@reduxjs/toolkit' + +configureStore({ + reducer: rootReducer, + middleware: () => new Tuple(additionalMiddleware, logger), +}) +``` + +Javascript-only users are free to use a plain array if preferred. + +::: + +### `devTools` + +If this is a boolean, it will be used to indicate whether `configureStore` should automatically enable support for [the Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools). + +If it is an object, then the DevTools Extension will be enabled, and the options object will be passed to `composeWithDevtools()`. See +the DevTools Extension docs for [`EnhancerOptions`](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md) for +a list of the specific options that are available. + +Defaults to `true`. + +### `duplicateMiddlewareCheck` + +If enabled, the store will check the final middleware array to see if there are any duplicate middleware references. This will catch issues like accidentally adding the same RTK Query API middleware twice (such as adding both the base API middleware and an injected API middleware, which are actually the exact same function reference). + +Defaults to `true`. + +#### `trace` + +The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/Features/Trace.md) that show exactly where each action was dispatched. +Capturing the traces can add a bit of overhead, so the DevTools Extension allows users to configure whether action stack traces are captured by [setting the 'trace' argument](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace). +If the DevTools are enabled by passing `true` or an object, then `configureStore` will default to enabling capturing action stack traces in development mode only. + +### `preloadedState` + +An optional initial state value to be passed to the Redux `createStore` function. + +### `enhancers` + +A callback function to customize the array of enhancers. + +Enhancers returned by this callback will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`. + +:::tip Dev Tools +This should _not_ include the Redux DevTools Extension `composeWithDevTools`, as this is already handled by `configureStore`. + +Example: `enhancers: () => new Tuple(offline)` will result in a final setup of `[offline, devToolsExtension]`. +::: + +If not provided, `configureStore` will call `getDefaultEnhancers` and use the array of enhancers it returns (including `applyMiddleware` with specified middleware). + +Where you wish to add onto or customize the default enhancers, you may pass a callback function that will receive `getDefaultEnhancers` as its argument, and should return an enhancer array. + +Example: `enhancers: (defaultEnhancers) => defaultEnhancers.prepend(offline)` will result in a final setup +of `[offline, applyMiddleware, devToolsExtension]`. + +For more details on how the `enhancer` parameter works and the list of enhancers that are added by default, see the [`getDefaultEnhancers` docs page](./getDefaultEnhancers). + +:::caution Middleware + +If you don't use `getDefaultEnhancers` and instead return an array, the `applyMiddleware` enhancer will _not_ be used. + +`configureStore` will warn in console if any middleware are provided (or left as default) but not included in the final list of enhancers. + +```ts no-transpile +// warns - middleware customised but not included in final enhancers +configureStore({ + reducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) + enhancers: [offline(offlineConfig)], +}) + +// fine - default enhancers included +configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => getDefaultEnhancers().concat(offline(offlineConfig)), +}) + +// also allowed +configureStore({ + reducer, + middleware: () => [], + enhancers: () => [offline(offlineConfig)], +}) +``` + +Note that if using Typescript, the `middleware` option is required to be provided _before_ the enhancer option, as the type of `getDefaultEnhancers` depends on its result. + +::: + +:::note Tuple +Typescript users are required to use a `Tuple` instance (if not using a `getDefaultEnhancer` result, which is already a `Tuple`), for better inference. + +```ts no-transpile +import { configureStore, Tuple } from '@reduxjs/toolkit' + +configureStore({ + reducer: rootReducer, + enhancers: () => new Tuple(offline), +}) +``` + +Javascript-only users are free to use a plain array if preferred. + +::: + +## Usage + +### Basic Example + +```ts +// file: reducers.ts noEmit +import type { Reducer } from '@reduxjs/toolkit' +declare const rootReducer: Reducer<{}> +export default rootReducer + +// file: store.ts +import { configureStore } from '@reduxjs/toolkit' + +import rootReducer from './reducers' + +const store = configureStore({ reducer: rootReducer }) +// The store now has redux-thunk added and the Redux DevTools Extension is turned on +``` + +### Full Example + +```ts no-transpile +// file: todos/todosReducer.ts noEmit +import type { Reducer } from '@reduxjs/toolkit' +declare const reducer: Reducer<{}> +export default reducer + +// file: visibility/visibilityReducer.ts noEmit +import type { Reducer } from '@reduxjs/toolkit' +declare const reducer: Reducer<{}> +export default reducer + +// file: store.ts +import { configureStore } from '@reduxjs/toolkit' + +// We'll use redux-logger just as an example of adding another middleware +import logger from 'redux-logger' + +// And use redux-batched-subscribe as an example of adding enhancers +import { batchedSubscribe } from 'redux-batched-subscribe' + +import todosReducer from './todos/todosReducer' +import visibilityReducer from './visibility/visibilityReducer' + +const reducer = { + todos: todosReducer, + visibility: visibilityReducer, +} + +const preloadedState = { + todos: [ + { + text: 'Eat food', + completed: true, + }, + { + text: 'Exercise', + completed: false, + }, + ], + visibilityFilter: 'SHOW_COMPLETED', +} + +const debounceNotify = _.debounce((notify) => notify()) + +const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), + devTools: process.env.NODE_ENV !== 'production', + preloadedState, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ + autoBatch: false, + }).concat(batchedSubscribe(debounceNotify)), +}) + +// The store has been created with these options: +// - The slice reducers were automatically passed to combineReducers() +// - redux-thunk and redux-logger were added as middleware +// - The Redux DevTools Extension is disabled for production +// - The middleware, batched subscribe, and devtools enhancers were composed together +``` diff --git a/docs/reference/redux-toolkit/createAction.mdx b/docs/reference/redux-toolkit/createAction.mdx new file mode 100644 index 00000000..6fbec5d9 --- /dev/null +++ b/docs/reference/redux-toolkit/createAction.mdx @@ -0,0 +1,153 @@ +--- +id: createAction +title: createAction +sidebar_label: createAction +hide_title: true +--- + +  + +# `createAction` + +A helper function for defining a Redux [action](https://redux.js.org/basics/actions) type and creator. + +```js +function createAction(type, prepareAction?) +``` + +The usual way to define an action in Redux is to separately declare an _action type_ constant and an _action creator_ function for constructing actions of that type. + +```ts +const INCREMENT = 'counter/increment' + +function increment(amount: number) { + return { + type: INCREMENT, + payload: amount, + } +} + +const action = increment(3) +// { type: 'counter/increment', payload: 3 } +``` + +The `createAction` helper combines these two declarations into one. It takes an action type and returns an action creator for that type. The action creator can be called either without arguments or with a `payload` to be attached to the action. + +```ts +import { createAction } from '@reduxjs/toolkit' + +const increment = createAction('counter/increment') + +let action = increment() +// { type: 'counter/increment' } + +action = increment(3) +// returns { type: 'counter/increment', payload: 3 } + +console.log(`The action type is: ${increment.type}`) +// 'The action type is: counter/increment' +``` + +## Using Prepare Callbacks to Customize Action Contents + +By default, the generated action creators accept a single argument, which becomes `action.payload`. This requires the caller to construct the entire payload correctly and pass it in. + +In many cases, you may want to write additional logic to customize the creation of the `payload` value, such as accepting multiple parameters for the action creator, generating a random ID, or getting the current timestamp. To do this, `createAction` accepts an optional second argument: a "prepare callback" that will be used to construct the payload value. + +```ts +import { createAction, nanoid } from '@reduxjs/toolkit' + +const addTodo = createAction('todos/add', function prepare(text: string) { + return { + payload: { + text, + id: nanoid(), + createdAt: new Date().toISOString(), + }, + } +}) + +console.log(addTodo('Write more docs')) +/** + * { + * type: 'todos/add', + * payload: { + * text: 'Write more docs', + * id: '4AJvwMSWEHCchcWYga3dj', + * createdAt: '2019-10-03T07:53:36.581Z' + * } + * } + **/ +``` + +If provided, all arguments from the action creator will be passed to the prepare callback, and it should return an object with the `payload` field (otherwise the payload of created actions will be `undefined`). Additionally, the object can have a `meta` and/or an `error` field that will also be added to created actions. `meta` may contain extra information about the action, `error` may contain details about the action failure. These three fields (`payload`, `meta` and `error`) adhere to the specification of [Flux Standard Actions](https://github.com/redux-utilities/flux-standard-action#actions). + +**Note:** The type field will be added automatically. + +## Usage with createReducer() + +Action creators can be passed directly to `addCase` in a [createReducer()](createReducer.mdx) build callback. + +```ts +import { createAction, createReducer } from '@reduxjs/toolkit' + +const increment = createAction('counter/increment') +const decrement = createAction('counter/decrement') + +const counterReducer = createReducer(0, (builder) => { + builder.addCase(increment, (state, action) => state + action.payload) + builder.addCase(decrement, (state, action) => state - action.payload) +}) +``` + +:::warning Non-String Action Types +As of Redux 5.0, action types are _required_ to be strings. An error will be thrown by the store if a non-string action type reaches the original store dispatch. +::: + +## actionCreator.match + +Every generated actionCreator has a `.match(action)` method that can be used to determine if the passed action is of the same type as an action that would be created by the action creator. + +This has different uses: + +### As a TypeScript Type Guard + +This `match` method is a [TypeScript type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) and can be used to discriminate the `payload` type of an action. + +This behavior can be particularly useful when used in custom middlewares, where manual casts might be neccessary otherwise. + +```ts +import { createAction } from '@reduxjs/toolkit' +import type { Action } from '@reduxjs/toolkit' + +const increment = createAction('INCREMENT') + +function someFunction(action: Action) { + // accessing action.payload would result in an error here + if (increment.match(action)) { + // action.payload can be used as `number` here + } +} +``` + +### With redux-observable + +The `match` method can also be used as a filter method, which makes it powerful when used with redux-observable: + +```ts +import { createAction } from '@reduxjs/toolkit' +import type { Action } from '@reduxjs/toolkit' +import type { Observable } from 'rxjs' +import { map, filter } from 'rxjs/operators' + +const increment = createAction('INCREMENT') + +export const epic = (actions$: Observable) => + actions$.pipe( + filter(increment.match), + map((action) => { + // action.payload can be safely used as number here (and will also be correctly inferred by TypeScript) + // ... + }), + ) +``` diff --git a/docs/reference/redux-toolkit/createAsyncThunk.mdx b/docs/reference/redux-toolkit/createAsyncThunk.mdx new file mode 100644 index 00000000..872d1466 --- /dev/null +++ b/docs/reference/redux-toolkit/createAsyncThunk.mdx @@ -0,0 +1,783 @@ +--- +id: createAsyncThunk +title: createAsyncThunk +sidebar_label: createAsyncThunk +hide_title: true +--- + +  + +# `createAsyncThunk` + +## Overview + +A function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise. + +This abstracts the standard recommended approach for handling async request lifecycles. + +It does not generate any reducer functions, since it does not know what data you're fetching, how you want to track loading state, or how the data you return needs to be processed. You should write your own reducer logic that handles these actions, with whatever loading state and processing logic is appropriate for your own app. + +:::tip + +Redux Toolkit's [**RTK Query data fetching API**](../rtk-query/overview.md) is a purpose built data fetching and caching solution for Redux apps, and can **eliminate the need to write _any_ thunks or reducers to manage data fetching**. We encourage you to try it out and see if it can help simplify the data fetching code in your own apps! + +::: + +Sample usage: + +```ts no-transpile {5-11,22-25,30} +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' +import { userAPI } from './userAPI' + +// First, create the thunk +const fetchUserById = createAsyncThunk( + 'users/fetchByIdStatus', + async (userId: number, thunkAPI) => { + const response = await userAPI.fetchById(userId) + return response.data + }, +) + +interface UsersState { + entities: User[] + loading: 'idle' | 'pending' | 'succeeded' | 'failed' +} + +const initialState = { + entities: [], + loading: 'idle', +} satisfies UserState as UsersState + +// Then, handle actions in your reducers: +const usersSlice = createSlice({ + name: 'users', + initialState, + reducers: { + // standard reducer logic, with auto-generated action types per reducer + }, + extraReducers: (builder) => { + // Add reducers for additional action types here, and handle loading state as needed + builder.addCase(fetchUserById.fulfilled, (state, action) => { + // Add user to the state array + state.entities.push(action.payload) + }) + }, +}) + +// Later, dispatch the thunk as needed in the app +dispatch(fetchUserById(123)) +``` + +## Parameters + +`createAsyncThunk` accepts three parameters: a string action `type` value, a `payloadCreator` callback, and an `options` object. + +### `type` + +A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request: + +For example, a `type` argument of `'users/requestStatus'` will generate these action types: + +- `pending`: `'users/requestStatus/pending'` +- `fulfilled`: `'users/requestStatus/fulfilled'` +- `rejected`: `'users/requestStatus/rejected'` + +### `payloadCreator` + +A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkAPI.rejectWithValue` function. + +The `payloadCreator` function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native `AsyncStorage`, and so on. + +The `payloadCreator` function will be called with two arguments: + +- `arg`: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. This is useful for passing in values like item IDs that may be needed as part of the request. If you need to pass in multiple values, pass them together in an object when you dispatch the thunk, like `dispatch(fetchUsers({status: 'active', sortBy: 'name'}))`. +- `thunkAPI`: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: + - `dispatch`: the Redux store `dispatch` method + - `getState`: the Redux store `getState` method + - `extra`: the "extra argument" given to the thunk middleware on setup, if available + - `requestId`: a unique string ID value that was automatically generated to identify this request sequence + - `signal`: an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal) that may be used to see if another part of the app logic has marked this request as needing cancelation. + - `rejectWithValue(value, [meta])`: rejectWithValue is a utility function that you can `return` (or `throw`) in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action. If you also pass in a `meta`, it will be merged with the existing `rejectedAction.meta`. + - `fulfillWithValue(value, meta)`: fulfillWithValue is a utility function that you can `return` in your action creator to `fulfill` with a value while having the ability of adding to `fulfilledAction.meta`. + +The logic in the `payloadCreator` function may use any of these values as needed to calculate the result. + +### Options + +An object with the following optional fields: + +- `condition(arg, { getState, extra } ): boolean | Promise`: a callback that can be used to skip execution of the payload creator and all action dispatches, if desired. See [Canceling Before Execution](#canceling-before-execution) for a complete description. +- `dispatchConditionRejection`: if `condition()` returns `false`, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, set this flag to `true`. +- `idGenerator(arg): string`: a function to use when generating the `requestId` for the request sequence. Defaults to use [nanoid](./otherExports.mdx#nanoid), but you can implement your own ID generation logic. +- `serializeError(error: unknown) => any` to replace the internal `miniSerializeError` method with your own serialization logic. +- `getPendingMeta({ arg, requestId }, { getState, extra }): any`: a function to create an object that will be merged into the `pendingAction.meta` field. + +## Return Value + +`createAsyncThunk` returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the `pending`, `fulfilled`, and `rejected` cases attached as nested fields. + +Using the `fetchUserById` example above, `createAsyncThunk` will generate four functions: + +- `fetchUserById`, the thunk action creator that kicks off the async payload callback you wrote + - `fetchUserById.pending`, an action creator that dispatches an `'users/fetchByIdStatus/pending'` action + - `fetchUserById.fulfilled`, an action creator that dispatches an `'users/fetchByIdStatus/fulfilled'` action + - `fetchUserById.rejected`, an action creator that dispatches an `'users/fetchByIdStatus/rejected'` action + +When dispatched, the thunk will: + +- dispatch the `pending` action +- call the `payloadCreator` callback and wait for the returned promise to settle +- when the promise settles: + - if the promise resolved successfully, dispatch the `fulfilled` action with the promise value as `action.payload` + - if the promise resolved with a `rejectWithValue(value)` return value, dispatch the `rejected` action with the value passed into `action.payload` and 'Rejected' as `action.error.message` + - if the promise failed and was not handled with `rejectWithValue`, dispatch the `rejected` action with a serialized version of the error value as `action.error` +- Return a fulfilled promise containing the final dispatched action (either the `fulfilled` or `rejected` action object) + +## Thunk Dispatch Options + +The returned thunk action creator accepts an optional second argument with the following options: + +- `signal`: an optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that will be tracked by the internal abort signal (see [Canceling While Running](#canceling-while-running)) + +```ts no-transpile +const externalController = new AbortController() +dispatch(fetchUserById(123, { signal: externalController.signal })) +externalController.abort() +``` + +## Promise Lifecycle Actions + +`createAsyncThunk` will generate three Redux action creators using [`createAction`](./createAction.mdx): `pending`, `fulfilled`, and `rejected`. Each lifecycle action creator will be attached to the returned thunk action creator so that your reducer logic can reference the action types and respond to the actions when dispatched. Each action object will contain the current unique `requestId` and `arg` values under `action.meta`. + +The action creators will have these signatures: + +```ts no-transpile +interface SerializedError { + name?: string + message?: string + code?: string + stack?: string +} + +interface PendingAction { + type: string + payload: undefined + meta: { + requestId: string + arg: ThunkArg + } +} + +interface FulfilledAction { + type: string + payload: PromiseResult + meta: { + requestId: string + arg: ThunkArg + } +} + +interface RejectedAction { + type: string + payload: undefined + error: SerializedError | any + meta: { + requestId: string + arg: ThunkArg + aborted: boolean + condition: boolean + } +} + +interface RejectedWithValueAction { + type: string + payload: RejectedValue + error: { message: 'Rejected' } + meta: { + requestId: string + arg: ThunkArg + aborted: boolean + } +} + +type Pending = ( + requestId: string, + arg: ThunkArg, +) => PendingAction + +type Fulfilled = ( + payload: PromiseResult, + requestId: string, + arg: ThunkArg, +) => FulfilledAction + +type Rejected = ( + requestId: string, + arg: ThunkArg, +) => RejectedAction + +type RejectedWithValue = ( + requestId: string, + arg: ThunkArg, +) => RejectedWithValueAction +``` + +To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using the "builder callback" notation. + +```ts no-transpile {2,10} +const reducer1 = createReducer(initialState, (builder) => { + builder.addCase(fetchUserById.fulfilled, (state, action) => {}) +}) + +const reducer2 = createSlice({ + name: 'users', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(fetchUserById.fulfilled, (state, action) => {}) + }, +}) +``` + +Additionally, a `settled` matcher is attached, for matching against both fulfilled and rejected actions. Conceptually this is similar to a `finally` block. + +Make sure you use `addMatcher` instead of `addCase`, since `settled` is a matcher rather than an action creator. + +```ts no-transpile {2,10} +const reducer1 = createReducer(initialState, (builder) => { + builder.addMatcher(fetchUserById.settled, (state, action) => {}) +}) + +const reducer2 = createSlice({ + name: 'users', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addMatcher(fetchUserById.settled, (state, action) => {}) + }, +}) +``` + +## Handling Thunk Results + +### Unwrapping Result Actions + +Thunks may return a value when dispatched. A common use case is to return a promise from the thunk, dispatch the thunk from a component, and then wait for the promise to resolve before doing additional work: + +```ts no-transpile +const onClick = () => { + dispatch(fetchUserById(userId)).then(() => { + // do additional work + }) +} +``` + +The thunks generated by `createAsyncThunk` **will always return a resolved promise** with either the `fulfilled` action object or `rejected` action object inside, as appropriate. + +The calling logic may wish to treat these actions as if they were the original promise contents. The promise returned by the dispatched thunk has an `unwrap` property which can be called to extract the `payload` of a `fulfilled` action or to throw either the `error` or, if available, `payload` created by `rejectWithValue` from a `rejected` action: + +```ts no-transpile +// in the component + +const onClick = () => { + dispatch(fetchUserById(userId)) + .unwrap() + .then((originalPromiseResult) => { + // handle result here + }) + .catch((rejectedValueOrSerializedError) => { + // handle error here + }) +} +``` + +Or with async/await syntax: + +```ts no-transpile +// in the component + +const onClick = async () => { + try { + const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap() + // handle result here + } catch (rejectedValueOrSerializedError) { + // handle error here + } +} +``` + +Using the attached `.unwrap()` property is preferred in most cases, however Redux Toolkit also exports an `unwrapResult` function that can be used for a similar purpose: + +```ts no-transpile +import { unwrapResult } from '@reduxjs/toolkit' + +// in the component +const onClick = () => { + dispatch(fetchUserById(userId)) + .then(unwrapResult) + .then((originalPromiseResult) => { + // handle result here + }) + .catch((rejectedValueOrSerializedError) => { + // handle result here + }) +} +``` + +Or with async/await syntax: + +```ts no-transpile +import { unwrapResult } from '@reduxjs/toolkit' + +// in the component +const onClick = async () => { + try { + const resultAction = await dispatch(fetchUserById(userId)) + const originalPromiseResult = unwrapResult(resultAction) + // handle result here + } catch (rejectedValueOrSerializedError) { + // handle error here + } +} +``` + +### Checking Errors After Dispatching + +Note that this means **a failed request or error in a thunk will _never_ return a _rejected_ promise**. We assume that any failure is more of a handled error than an unhandled exception at this point. This is due to the fact that we want to prevent uncaught promise rejections for those who do not use the result of `dispatch`. + +If your component needs to know if the request failed, use `.unwrap` or `unwrapResult` and handle the re-thrown error accordingly. + +## Handling Thunk Errors + +When your `payloadCreator` returns a rejected promise (such as a thrown error in an `async` function), the thunk will dispatch a `rejected` action containing an automatically-serialized version of the error as `action.error`. However, to ensure serializability, everything that does not match the `SerializedError` interface will have been removed from it: + +```ts no-transpile +export interface SerializedError { + name?: string + message?: string + stack?: string + code?: string +} +``` + +If you need to customize the contents of the `rejected` action, you should catch any errors yourself, and then **return** a new value using the `thunkAPI.rejectWithValue` utility. Doing `return rejectWithValue(errorPayload)` will cause the `rejected` action to use that value as `action.payload`. + +The `rejectWithValue` approach should also be used if your API response "succeeds", but contains some kind of additional error details that the reducer should know about. This is particularly common when expecting field-level validation errors from an API. + +```ts no-transpile +const updateUser = createAsyncThunk( + 'users/update', + async (userData, { rejectWithValue }) => { + const { id, ...fields } = userData + try { + const response = await userAPI.updateById(id, fields) + return response.data.user + } catch (err) { + // Use `err.response.data` as `action.payload` for a `rejected` action, + // by explicitly returning it using the `rejectWithValue()` utility + return rejectWithValue(err.response.data) + } + }, +) +``` + +## Cancellation + +### Canceling Before Execution + +If you need to cancel a thunk before the payload creator is called, you may provide a `condition` callback as an option after the payload creator. The callback will receive the thunk argument and an object with `{getState, extra}` as parameters, and use those to decide whether to continue or not. If the execution should be canceled, the `condition` callback should return a literal `false` value or a promise that should resolve to `false`. If a promise is returned, the thunk waits for it to get fulfilled before dispatching the `pending` action, otherwise it proceeds with dispatching synchronously. + +```ts no-transpile +const fetchUserById = createAsyncThunk( + 'users/fetchByIdStatus', + async (userId: number, thunkAPI) => { + const response = await userAPI.fetchById(userId) + return response.data + }, + { + condition: (userId, { getState, extra }) => { + const { users } = getState() + const fetchStatus = users.requests[userId] + if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') { + // Already fetched or in progress, don't need to re-fetch + return false + } + }, + }, +) +``` + +If `condition()` returns `false`, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, pass in `{condition, dispatchConditionRejection: true}`. + +### Canceling While Running + +If you want to cancel your running thunk before it has finished, you can use the `abort` method of the promise returned by `dispatch(fetchUserById(userId))`. + +A real-life example of that would look like this: + +```ts no-transpile +// file: store.ts noEmit +import { configureStore } from '@reduxjs/toolkit' +import type { Reducer } from '@reduxjs/toolkit' +import { useDispatch } from 'react-redux' + +declare const reducer: Reducer<{}> +const store = configureStore({ reducer }) +export const useAppDispatch = () => useDispatch() + +// file: slice.ts noEmit +import { createAsyncThunk } from '@reduxjs/toolkit' +export const fetchUserById = createAsyncThunk( + 'fetchUserById', + (userId: string) => { + /* ... */ + }, +) + +// file: MyComponent.ts +import { fetchUserById } from './slice' +import { useAppDispatch } from './store' +import React from 'react' + +function MyComponent(props: { userId: string }) { + const dispatch = useAppDispatch() + React.useEffect(() => { + // Dispatching the thunk returns a promise + const promise = dispatch(fetchUserById(props.userId)) + return () => { + // `createAsyncThunk` attaches an `abort()` method to the promise + promise.abort() + } + }, [props.userId]) +} +``` + +After a thunk has been cancelled this way, it will dispatch (and return) a `"thunkName/rejected"` action with an `AbortError` on the `error` property. The thunk will not dispatch any further actions. + +Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkAPI.signal` to actually cancel a costly asynchronous action. + +The `fetch` api of modern browsers already comes with support for an `AbortSignal`: + +```ts no-transpile +import { createAsyncThunk } from '@reduxjs/toolkit' + +const fetchUserById = createAsyncThunk( + 'users/fetchById', + async (userId: string, thunkAPI) => { + const response = await fetch(`https://reqres.in/api/users/${userId}`, { + signal: thunkAPI.signal, + }) + return await response.json() + }, +) +``` + +### Checking Cancellation Status + +### Reading the Signal Value + +You can use the `signal.aborted` property to regularly check if the thunk has been aborted and in that case stop costly long-running work: + +```ts no-transpile +import { createAsyncThunk } from '@reduxjs/toolkit' + +const readStream = createAsyncThunk( + 'readStream', + async (stream: ReadableStream, { signal }) => { + const reader = stream.getReader() + + let done = false + let result = '' + + while (!done) { + if (signal.aborted) { + throw new Error('stop the work, this has been aborted!') + } + const read = await reader.read() + result += read.value + done = read.done + } + return result + }, +) +``` + +#### Listening for Abort Events + +You can also call `signal.addEventListener('abort', callback)` to have logic inside the thunk be notified when `promise.abort()` was called. +This can for example be used in conjunction with an axios `CancelToken`: + +```ts no-transpile +import { createAsyncThunk } from '@reduxjs/toolkit' +import axios from 'axios' + +const fetchUserById = createAsyncThunk( + 'users/fetchById', + async (userId: string, { signal }) => { + const source = axios.CancelToken.source() + signal.addEventListener('abort', () => { + source.cancel() + }) + const response = await axios.get(`https://reqres.in/api/users/${userId}`, { + cancelToken: source.token, + }) + return response.data + }, +) +``` + +### Checking if a Promise Rejection was from an Error or Cancellation + +To investigate behavior around thunk cancellation, you can inspect various properties on the `meta` object of the dispatched action. +If a thunk was cancelled, the result of the promise will be a `rejected` action (regardless of whether that action was actually dispatched to the store). + +- If it was cancelled before execution, `meta.condition` will be true. +- If it was aborted while running, `meta.aborted` will be true. +- If neither of those is true, the thunk was not cancelled, it was simply rejected, either by a Promise rejection or `rejectWithValue`. +- If the thunk was not rejected, both `meta.aborted` and `meta.condition` will be `undefined`. + +So if you wanted to test that a thunk was cancelled before executing, you can do the following: + +```ts no-transpile +import { createAsyncThunk } from '@reduxjs/toolkit' + +test('this thunk should always be skipped', async () => { + const thunk = createAsyncThunk( + 'users/fetchById', + async () => throw new Error('This promise should never be entered'), + { + condition: () => false, + } + ) + const result = await thunk()(dispatch, getState, null) + + expect(result.meta.condition).toBe(true) + expect(result.meta.aborted).toBe(false) +}) +``` + +## Examples + +- Requesting a user by ID, with loading state, and only one request at a time: + +```ts no-transpile +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' +import { userAPI, User } from './userAPI' + +const fetchUserById = createAsyncThunk< + User, + string, + { + state: { users: { loading: string; currentRequestId: string } } + } +>('users/fetchByIdStatus', async (userId: string, { getState, requestId }) => { + const { currentRequestId, loading } = getState().users + if (loading !== 'pending' || requestId !== currentRequestId) { + return + } + const response = await userAPI.fetchById(userId) + return response.data +}) + +const usersSlice = createSlice({ + name: 'users', + initialState: { + entities: [], + loading: 'idle', + currentRequestId: undefined, + error: null, + }, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchUserById.pending, (state, action) => { + if (state.loading === 'idle') { + state.loading = 'pending' + state.currentRequestId = action.meta.requestId + } + }) + .addCase(fetchUserById.fulfilled, (state, action) => { + const { requestId } = action.meta + if ( + state.loading === 'pending' && + state.currentRequestId === requestId + ) { + state.loading = 'idle' + state.entities.push(action.payload) + state.currentRequestId = undefined + } + }) + .addCase(fetchUserById.rejected, (state, action) => { + const { requestId } = action.meta + if ( + state.loading === 'pending' && + state.currentRequestId === requestId + ) { + state.loading = 'idle' + state.error = action.error + state.currentRequestId = undefined + } + }) + }, +}) + +const UsersComponent = () => { + const { entities, loading, error } = useSelector((state) => state.users) + const dispatch = useDispatch() + + const fetchOneUser = async (userId) => { + try { + const user = await dispatch(fetchUserById(userId)).unwrap() + showToast('success', `Fetched ${user.name}`) + } catch (err) { + showToast('error', `Fetch failed: ${err.message}`) + } + } + + // render UI here +} +``` + +- Using rejectWithValue to access a custom rejected payload in a component + + _Note: this is a contrived example assuming our userAPI only ever throws validation-specific errors_ + +```ts no-transpile +// file: store.ts noEmit +import { configureStore } from '@reduxjs/toolkit' +import type { Reducer } from '@reduxjs/toolkit' +import { useDispatch } from 'react-redux' +import usersReducer from './user/slice' + +const store = configureStore({ reducer: { users: usersReducer } }) +export const useAppDispatch = () => useDispatch() +export type RootState = ReturnType + +// file: user/userAPI.ts noEmit + +export declare const userAPI: { + updateById(id: string, fields: {}): { data: Response } +} + +// file: user/slice.ts +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' +import { userAPI } from './userAPI' +import type { AxiosError } from 'axios' + +// Sample types that will be used +export interface User { + id: string + first_name: string + last_name: string + email: string +} + +interface ValidationErrors { + errorMessage: string + field_errors: Record +} + +interface UpdateUserResponse { + user: User + success: boolean +} + +export const updateUser = createAsyncThunk< + User, + { id: string } & Partial, + { + rejectValue: ValidationErrors + } +>('users/update', async (userData, { rejectWithValue }) => { + try { + const { id, ...fields } = userData + const response = await userAPI.updateById(id, fields) + return response.data.user + } catch (err) { + let error: AxiosError = err // cast the error for access + if (!error.response) { + throw err + } + // We got validation errors, let's return those so we can reference in our component and set form errors + return rejectWithValue(error.response.data) + } +}) + +interface UsersState { + error: string | null | undefined + entities: Record +} + +const initialState = { + entities: {}, + error: null, +} satisfies UsersState as UsersState + +const usersSlice = createSlice({ + name: 'users', + initialState, + reducers: {}, + extraReducers: (builder) => { + // The `builder` callback form is used here because it provides correctly typed reducers from the action creators + builder.addCase(updateUser.fulfilled, (state, { payload }) => { + state.entities[payload.id] = payload + }) + builder.addCase(updateUser.rejected, (state, action) => { + if (action.payload) { + // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here. + state.error = action.payload.errorMessage + } else { + state.error = action.error.message + } + }) + }, +}) + +export default usersSlice.reducer + +// file: externalModules.d.ts noEmit + +declare module 'some-toast-library' { + export function showToast(type: string, message: string) +} + +// file: user/UsersComponent.ts + +import React from 'react' +import { useAppDispatch } from '../store' +import type { RootState } from '../store' +import { useSelector } from 'react-redux' +import { updateUser } from './slice' +import type { User } from './slice' +import type { FormikHelpers } from 'formik' +import { showToast } from 'some-toast-library' + +interface FormValues extends Omit {} + +const UsersComponent = (props: { id: string }) => { + const { entities, error } = useSelector((state: RootState) => state.users) + const dispatch = useAppDispatch() + + // This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action + const handleUpdateUser = async ( + values: FormValues, + formikHelpers: FormikHelpers, + ) => { + const resultAction = await dispatch(updateUser({ id: props.id, ...values })) + if (updateUser.fulfilled.match(resultAction)) { + // user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk + const user = resultAction.payload + showToast('success', `Updated ${user.first_name} ${user.last_name}`) + } else { + if (resultAction.payload) { + // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here. + formikHelpers.setErrors(resultAction.payload.field_errors) + } else { + showToast('error', `Update failed: ${resultAction.error}`) + } + } + } + + // render UI here +} +``` diff --git a/docs/reference/redux-toolkit/createDynamicMiddleware.mdx b/docs/reference/redux-toolkit/createDynamicMiddleware.mdx new file mode 100644 index 00000000..199e8418 --- /dev/null +++ b/docs/reference/redux-toolkit/createDynamicMiddleware.mdx @@ -0,0 +1,181 @@ +--- +id: createDynamicMiddleware +title: createDynamicMiddleware +sidebar_label: createDynamicMiddleware +hide_title: true +--- + +  + +# `createDynamicMiddleware` + +## Overview + +A "meta-middleware" that allows adding middleware to the dispatch chain after store initialisation. + +## Instance Creation + +```ts no-transpile +import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit' + +const dynamicMiddleware = createDynamicMiddleware() + +const store = configureStore({ + reducer: { + todos: todosReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(dynamicMiddleware.middleware), +}) +``` + +:::tip + +It's possible to pass two type parameters to `createDynamicMiddleware`, `State` and `Dispatch`. + +These are used by methods that receive middleware to ensure that the provided middleware are compatible with the types provided. + +```ts no-transpile +const dynamicMiddleware = createDynamicMiddleware() +``` + +However, if these values are derived from the store (as they should be), a circular type dependency is formed. + +As a result, it's better to use the `withTypes` helper attached to `addMiddleware`, `withMiddleware` and `createDispatchWithMiddlewareHook`. + +```ts no-transpile +import { createDynamicMiddleware } from '@reduxjs/toolkit/react' +import type { RootState, AppDispatch } from './store' + +const dynamicMiddleware = createDynamicMiddleware() + +const { + middleware, + addMiddleware, + withMiddleware, + createDispatchWithMiddlewareHook, +} = dynamicMiddleware + +interface MiddlewareApiConfig { + state: RootState + dispatch: AppDispatch +} + +export const addAppMiddleware = addMiddleware.withTypes() + +export const withAppMiddleware = withMiddleware.withTypes() + +export const createAppDispatchWithMiddlewareHook = + createDispatchWithMiddlewareHook.withTypes() + +export default middleware +``` + +::: + +## Dynamic Middleware Instance + +The "dynamic middleware instance" returned from `createDynamicMiddleware` is an object similar to the object generated by `createListenerMiddleware`. The instance object is _not_ the actual Redux middleware itself. Rather, it contains the middleware and some instance methods used to add middleware to the chain. + +```ts no-transpile +export type DynamicMiddlewareInstance< + State = unknown, + Dispatch extends ReduxDispatch = ReduxDispatch, +> = { + middleware: DynamicMiddleware + addMiddleware: AddMiddleware + withMiddleware: WithMiddleware +} +``` + +### `middleware` + +The wrapper middleware instance, to add to the Redux store. + +You can place this anywhere in the middleware chain, but note that all the middleware you inject into this instance will be contained within this position. + +### `addMiddleware` + +Injects a set of middleware into the instance. + +```ts no-transpile +addMiddleware(logger, listenerMiddleware.instance) +``` + +:::note + +- Middleware are compared by function reference, and each is only added to the chain once. + +- Middleware are stored in an ES6 map, and are thus called in insertion order during dispatch. + +::: + +### `withMiddleware` + +Accepts a set of middleware, and creates an action. When dispatched, it injects the middleware and returns a version of `dispatch` typed to be aware of any extensions added. + +```ts no-transpile +const listenerDispatch = store.dispatch( + withMiddleware(listenerMiddleware.middleware), +) + +const unsubscribe = listenerDispatch(addListener({ type, effect })) +``` + +## React Integration + +When imported from the React-specific entry point (`@reduxjs/toolkit/react`), the result of calling `createDynamicMiddleware` will have extra methods attached. + +_These depend on having `react-redux` installed._ + +```ts no-transpile +interface ReactDynamicMiddlewareInstance< + State = any, + Dispatch extends ReduxDispatch = ReduxDispatch, +> extends DynamicMiddlewareInstance { + createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook< + State, + Dispatch + > + createDispatchWithMiddlewareHookFactory: ( + context?: Context< + ReactReduxContextValue> + >, + ) => CreateDispatchWithMiddlewareHook +} +``` + +### `createDispatchWithMiddlewareHook` + +Accepts a set of middleware, and returns a [`useDispatch`](https://react-redux.js.org/api/hooks#usedispatch) hook returning a `dispatch` typed to include extensions from provided middleware. + +```ts no-transpile +const useListenerDispatch = createDispatchWithMiddlewareHook( + listenerInstance.middleware, +) + +const Component = () => { + const listenerDispatch = useListenerDispatch() + useEffect(() => { + const unsubscribe = listenerDispatch(addListener({ type, effect })) + return () => unsubscribe() + }, [dispatch]) +} +``` + +:::caution + +Middleware is injected when `createDispatchWithMiddlewareHook` is called, not when the `useDispatch` hook is used. + +::: + +### `createDispatchWithMiddlewareHookFactory` + +Accepts a React context instance, and returns a `createDispatchWithMiddlewareHook` built to use that context. + +```ts no-transpile +const createDispatchWithMiddlewareHook = + createDispatchWithMiddlewareHookFactory(context) +``` + +Useful if you're using a [custom context](https://react-redux.js.org/using-react-redux/accessing-store#providing-custom-context) for React Redux. diff --git a/docs/reference/redux-toolkit/createEntityAdapter.mdx b/docs/reference/redux-toolkit/createEntityAdapter.mdx new file mode 100644 index 00000000..1148fabc --- /dev/null +++ b/docs/reference/redux-toolkit/createEntityAdapter.mdx @@ -0,0 +1,463 @@ +--- +id: createEntityAdapter +title: createEntityAdapter +sidebar_label: createEntityAdapter +hide_title: true +--- + +  + +# `createEntityAdapter` + +## Overview + +A function that generates a set of prebuilt reducers and selectors for performing CRUD operations on a [normalized state structure](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape) containing instances of a particular type of data object. These reducer functions may be passed as case reducers to `createReducer` and `createSlice`. They may also be used as "mutating" helper functions inside of `createReducer` and `createSlice`. + +This API was ported from [the `@ngrx/entity` library](https://ngrx.io/guide/entity) created by the NgRx maintainers, but has been significantly modified for use with Redux Toolkit. We'd like to thank the NgRx team for originally creating this API and allowing us to port and adapt it for our needs. + +:::note +The term "Entity" is used to refer to a unique type of data object in an application. For example, in a blogging application, you might have `User`, `Post`, and `Comment` data objects, with many instances of each being stored in the client and persisted on the server. `User` is an "entity" - a unique type of data object that the application uses. Each unique instance of an entity is assumed to have a unique ID value in a specific field. + +As with all Redux logic, [_only_ plain JS objects and arrays should be passed in to the store - **no class instances!**](https://redux.js.org/style-guide/style-guide#do-not-put-non-serializable-values-in-state-or-actions) + +For purposes of this reference, we will use `Entity` to refer to the specific data type that is being managed by a copy of the reducer logic in a specific portion of the Redux state tree, and `entity` to refer to a single instance of that type. Example: in `state.users`, `Entity` would refer to the `User` type, and `state.users.entities[123]` would be a single `entity`. +::: + +The methods generated by `createEntityAdapter` will all manipulate an "entity state" structure that looks like: + +```js +{ + // The unique IDs of each item. Must be strings or numbers + ids: [] + // A lookup table mapping entity IDs to the corresponding entity objects + entities: { + } +} +``` + +`createEntityAdapter` may be called multiple times in an application. If you are using it with plain JavaScript, you may be able to reuse a single adapter definition with multiple entity types if they're similar enough (such as all having an `entity.id` field). For [TypeScript usage](../usage/usage-with-typescript.md#createentityadapter), you will need to call `createEntityAdapter` a separate time for each distinct `Entity` type, so that the type definitions are inferred correctly. + +Sample usage: + +```ts +import { + createEntityAdapter, + createSlice, + configureStore, +} from '@reduxjs/toolkit' + +type Book = { bookId: string; title: string } + +const booksAdapter = createEntityAdapter({ + // Assume IDs are stored in a field other than `book.id` + selectId: (book: Book) => book.bookId, + // Keep the "all IDs" array sorted based on book titles + sortComparer: (a, b) => a.title.localeCompare(b.title), +}) + +const booksSlice = createSlice({ + name: 'books', + initialState: booksAdapter.getInitialState(), + reducers: { + // Can pass adapter functions directly as case reducers. Because we're passing this + // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator + bookAdded: booksAdapter.addOne, + booksReceived(state, action) { + // Or, call them as "mutating" helpers in a case reducer + booksAdapter.setAll(state, action.payload.books) + }, + }, +}) + +const store = configureStore({ + reducer: { + books: booksSlice.reducer, + }, +}) + +type RootState = ReturnType + +console.log(store.getState().books) +// { ids: [], entities: {} } + +// Can create a set of memoized selectors based on the location of this entity state +const booksSelectors = booksAdapter.getSelectors( + (state) => state.books, +) + +// And then use the selectors to retrieve values +const allBooks = booksSelectors.selectAll(store.getState()) +``` + +## Parameters + +`createEntityAdapter` accepts a single options object parameter, with two optional fields inside. + +### `selectId` + +A function that accepts a single `Entity` instance, and returns the value of whatever unique ID field is inside. If not provided, the default implementation is `entity => entity.id`. If your `Entity` type keeps its unique ID values in a field other than `entity.id`, you **must** provide a `selectId` function. + +### `sortComparer` + +A callback function that accepts two `Entity` instances, and should return a standard `Array.sort()` numeric result (1, 0, -1) to indicate their relative order for sorting. + +If provided, the `state.ids` array will be kept in sorted order based on comparisons of the entity objects, so that mapping over the IDs array to retrieve entities by ID should result in a sorted array of entities. + +If not provided, the `state.ids` array will not be sorted, and no guarantees are made about the ordering. In other words, `state.ids` can be expected to behave like a standard Javascript array. + +Note that sorting only kicks in when state is changed via one of the CRUD functions below (for example, `addOne()`, `updateMany()`). + +## Return Value + +A "entity adapter" instance. An entity adapter is a plain JS object (not a class) containing the generated reducer functions, the original provided `selectId` and `sortComparer` callbacks, a method to generate an initial "entity state" value, and functions to generate a set of globalized and non-globalized memoized selector functions for this entity type. + +The adapter instance will include the following methods (additional referenced TypeScript types included): + +```ts no-transpile +export type EntityId = number | string + +export type Comparer = (a: T, b: T) => number + +export type IdSelector = (model: T) => EntityId + +export type Update = { id: EntityId; changes: Partial } + +export interface EntityState { + ids: EntityId[] + entities: Record +} + +export interface EntityDefinition { + selectId: IdSelector + sortComparer: false | Comparer +} + +export interface EntityStateAdapter { + addOne>(state: S, entity: T): S + addOne>(state: S, action: PayloadAction): S + + addMany>(state: S, entities: T[]): S + addMany>(state: S, entities: PayloadAction): S + + setOne>(state: S, entity: T): S + setOne>(state: S, action: PayloadAction): S + + setMany>(state: S, entities: T[]): S + setMany>(state: S, entities: PayloadAction): S + + setAll>(state: S, entities: T[]): S + setAll>(state: S, entities: PayloadAction): S + + removeOne>(state: S, key: EntityId): S + removeOne>(state: S, key: PayloadAction): S + + removeMany>(state: S, keys: EntityId[]): S + removeMany>( + state: S, + keys: PayloadAction, + ): S + + removeAll>(state: S): S + + updateOne>(state: S, update: Update): S + updateOne>( + state: S, + update: PayloadAction>, + ): S + + updateMany>(state: S, updates: Update[]): S + updateMany>( + state: S, + updates: PayloadAction[]>, + ): S + + upsertOne>(state: S, entity: T): S + upsertOne>(state: S, entity: PayloadAction): S + + upsertMany>(state: S, entities: T[]): S + upsertMany>( + state: S, + entities: PayloadAction, + ): S +} + +export interface EntitySelectors { + selectIds: (state: V) => EntityId[] + selectEntities: (state: V) => Record + selectAll: (state: V) => T[] + selectTotal: (state: V) => number + selectById: (state: V, id: EntityId) => T | undefined +} + +export interface EntityAdapter extends EntityStateAdapter { + selectId: IdSelector + sortComparer: false | Comparer + getInitialState(): EntityState + getInitialState(state: S): EntityState & S + getSelectors(): EntitySelectors> + getSelectors( + selectState: (state: V) => EntityState, + ): EntitySelectors +} +``` + +### CRUD Functions + +The primary content of an entity adapter is a set of generated reducer functions for adding, updating, and removing entity instances from an entity state object: + +- `addOne`: accepts a single entity, and adds it if it's not already present. +- `addMany`: accepts an array of entities or an object in the shape of `Record`, and adds them if not already present. +- `setOne`: accepts a single entity and adds or replaces it +- `setMany`: accepts an array of entities or an object in the shape of `Record`, and adds or replaces them. +- `setAll`: accepts an array of entities or an object in the shape of `Record`, and replaces all existing entities with the values in the array. +- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists. +- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist. +- `removeAll`: removes all entities from the entity state object. +- `updateOne`: accepts an "update object" containing an entity ID and an object containing one or more new field values to update inside a `changes` field, and performs a shallow update on the corresponding entity. +- `updateMany`: accepts an array of update objects, and performs shallow updates on all corresponding entities. +- `upsertOne`: accepts a single entity. If an entity with that ID exists, it will perform a shallow update and the specified fields will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added. +- `upsertMany`: accepts an array of entities or an object in the shape of `Record` that will be shallowly upserted. + +:::info Should I add, set or upsert my entity? + +All three options will insert _new_ entities into the list. However they differ in how they handle entities that already exist. If an entity **already exists**: + +- `addOne` and `addMany` will do nothing with the new entity +- `setOne` and `setMany` will completely replace the old entity with the new one. This will also get rid of any properties on the entity that are not present in the new version of said entity. +- `upsertOne` and `upsertMany` will do a shallow copy to merge the old and new entities overwriting existing values, adding any that were not there and not touching properties not provided in the new entity. + +::: + +Each method has a signature that looks like: + +```ts no-transpile +;(state: EntityState, argument: TypeOrPayloadAction>) => + EntityState +``` + +In other words, they accept a state that looks like `{ids: [], entities: {}}`, and calculate and return a new state. + +These CRUD methods may be used in multiple ways: + +- They may be passed as case reducers directly to `createReducer` and `createSlice`. +- They may be used as "mutating" helper methods when called manually, such as a separate hand-written call to `addOne()` inside of an existing case reducer, if the `state` argument is actually an Immer `Draft` value. +- They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array. + +:::note +These methods do _not_ have corresponding Redux actions created - they are just standalone reducers / update logic. **It is entirely up to you to decide where and how to use these methods!** Most of the time, you will want to pass them to `createSlice` or use them inside another reducer. +::: + +Each method will check to see if the `state` argument is an Immer `Draft` or not. If it is a draft, the method will assume that it's safe to continue mutating that draft further. If it is not a draft, the method will pass the plain JS value to Immer's `createNextState()`, and return the immutably updated result value. + +The `argument` may be either a plain value (such as a single `Entity` object for `addOne()` or an `Entity[]` array for `addMany()`, or a `PayloadAction` action object with that same value as `action.payload`. This enables using them as both helper functions and reducers. + +> **Note on shallow updates:** `updateOne`, `updateMany`, `upsertOne`, and `upsertMany` only perform shallow updates in a mutable manner. This means that if your update/upsert consists of an object that includes nested properties, the value of the incoming change will overwrite the **entire** existing nested object. This may be unintended behavior for your application. As a general rule, these methods are best used with [normalized data](../usage/usage-guide.md#managing-normalized-data) that _do not_ have nested properties. + +### `getInitialState` + +Returns a new entity state object like `{ids: [], entities: {}}`. + +It accepts an optional object as an argument. The fields in that object will be merged into the returned initial state value. For example, perhaps you want your slice to also track some loading state: + +```js +const booksSlice = createSlice({ + name: 'books', + initialState: booksAdapter.getInitialState({ + loading: 'idle', + }), + reducers: { + booksLoadingStarted(state, action) { + // Can update the additional state field + state.loading = 'pending' + }, + }, +}) +``` + +You can also pass in an array of entities or a `Record` object to pre-populate the initial state with some entities: + +```js +const booksSlice = createSlice({ + name: 'books', + initialState: booksAdapter.getInitialState( + { + loading: 'idle', + }, + [ + { id: 'a', title: 'First' }, + { id: 'b', title: 'Second' }, + ], + ), + reducers: {}, +}) +``` + +This is equivalent to calling: + +```js +const initialState = booksAdapter.getInitialState({ + loading: 'idle', +}) + +const prePopulatedState = booksAdapter.setAll(initialState, [ + { id: 'a', title: 'First' }, + { id: 'b', title: 'Second' }, +]) +``` + +The first parameter can be `undefined` if no additional properties are needed. + +### Selector Functions + +The entity adapter will contain a `getSelectors()` function that returns a set of selectors that know how to read the contents of an entity state object: + +- `selectIds`: returns the `state.ids` array. +- `selectEntities`: returns the `state.entities` lookup table. +- `selectAll`: maps over the `state.ids` array, and returns an array of entities in the same order. +- `selectTotal`: returns the total number of entities being stored in this state. +- `selectById`: given the state and an entity ID, returns the entity with that ID or `undefined`. + +Each selector function will be created using the `createSelector` function from Reselect, to enable memoizing calculation of the results. + +:::tip + +The `createSelector` instance used can be replaced, by passing it as part of the options object (second parameter): + +```js +import { + createDraftSafeSelectorCreator, + weakMapMemoize, +} from '@reduxjs/toolkit' + +const createWeakMapDraftSafeSelector = + createDraftSafeSelectorCreator(weakMapMemoize) + +const simpleSelectors = booksAdapter.getSelectors(undefined, { + createSelector: createWeakMapDraftSafeSelector, +}) + +const globalizedSelectors = booksAdapter.getSelectors((state) => state.books, { + createSelector: createWeakMapDraftSafeSelector, +}) +``` + +If no instance is passed, it will default to [`createDraftSafeSelector`](./createSelector.mdx#createdraftsafeselector). + +::: + +Because selector functions are dependent on knowing where in the state tree this specific entity state object is kept, `getSelectors()` can be called in two ways: + +- If called without any arguments (or with undefined as the first parameter), it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from. +- It may also be called with a selector function that accepts the entire Redux state tree and returns the correct entity state object. + +For example, the entity state for a `Book` type might be kept in the Redux state tree as `state.books`. You can use `getSelectors()` to read from that state in two ways: + +```js +const store = configureStore({ + reducer: { + books: booksReducer, + }, +}) + +const simpleSelectors = booksAdapter.getSelectors() +const globalizedSelectors = booksAdapter.getSelectors((state) => state.books) + +// Need to manually pass the correct entity state object in to this selector +const bookIds = simpleSelectors.selectIds(store.getState().books) + +// This selector already knows how to find the books entity state +const allBooks = globalizedSelectors.selectAll(store.getState()) +``` + +## Notes + +### Applying Multiple Updates + +If `updateMany()` is called with multiple updates targeted to the same ID, they will be merged into a single update, with later updates overwriting the earlier ones. + +For both `updateOne()` and `updateMany()`, changing the ID of one existing entity to match the ID of a second existing entity will cause the first to replace the second completely. + +Additionally, if there is no item for that ID, the update will be silently ignored. + +## Examples + +Exercising several of the CRUD methods and selectors: + +```js +import { + createEntityAdapter, + createSlice, + configureStore, +} from '@reduxjs/toolkit' + +// Since we don't provide `selectId`, it defaults to assuming `entity.id` is the right field +const booksAdapter = createEntityAdapter({ + // Keep the "all IDs" array sorted based on book titles + sortComparer: (a, b) => a.title.localeCompare(b.title), +}) + +const booksSlice = createSlice({ + name: 'books', + initialState: booksAdapter.getInitialState({ + loading: 'idle', + }), + reducers: { + // Can pass adapter functions directly as case reducers. Because we're passing this + // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator + bookAdded: booksAdapter.addOne, + booksLoading(state, action) { + if (state.loading === 'idle') { + state.loading = 'pending' + } + }, + booksReceived(state, action) { + if (state.loading === 'pending') { + // Or, call them as "mutating" helpers in a case reducer + booksAdapter.setAll(state, action.payload) + state.loading = 'idle' + } + }, + bookUpdated: booksAdapter.updateOne, + }, +}) + +const { bookAdded, booksLoading, booksReceived, bookUpdated } = + booksSlice.actions + +const store = configureStore({ + reducer: { + books: booksSlice.reducer, + }, +}) + +// Check the initial state: +console.log(store.getState().books) +// {ids: [], entities: {}, loading: 'idle' } + +const booksSelectors = booksAdapter.getSelectors((state) => state.books) + +store.dispatch(bookAdded({ id: 'a', title: 'First' })) +console.log(store.getState().books) +// {ids: ["a"], entities: {a: {id: "a", title: "First"}}, loading: 'idle' } + +store.dispatch(bookUpdated({ id: 'a', changes: { title: 'First (altered)' } })) +store.dispatch(booksLoading()) +console.log(store.getState().books) +// {ids: ["a"], entities: {a: {id: "a", title: "First (altered)"}}, loading: 'pending' } + +store.dispatch( + booksReceived([ + { id: 'b', title: 'Book 3' }, + { id: 'c', title: 'Book 2' }, + ]), +) + +console.log(booksSelectors.selectIds(store.getState())) +// "a" was removed due to the `setAll()` call +// Since they're sorted by title, "Book 2" comes before "Book 3" +// ["c", "b"] + +console.log(booksSelectors.selectAll(store.getState())) +// All book entries in sorted order +// [{id: "c", title: "Book 2"}, {id: "b", title: "Book 3"}] +``` diff --git a/docs/reference/redux-toolkit/createListenerMiddleware.mdx b/docs/reference/redux-toolkit/createListenerMiddleware.mdx new file mode 100644 index 00000000..a805e2eb --- /dev/null +++ b/docs/reference/redux-toolkit/createListenerMiddleware.mdx @@ -0,0 +1,896 @@ +--- +id: createListenerMiddleware +title: createListenerMiddleware +sidebar_label: createListenerMiddleware +hide_title: true +--- + +  + +# `createListenerMiddleware` + +## Overview + +A Redux middleware that lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. + +It's intended to be a lightweight alternative to more widely used Redux async middleware like sagas and observables. While similar to thunks in level of complexity and concept, it can be used to replicate some common saga usage patterns. + +Conceptually, you can think of this as being similar to React's `useEffect` hook, except that it runs logic in response to Redux store updates instead of component props/state updates. + +Listener effect callbacks have access to `dispatch` and `getState`, similar to thunks. The listener also receives a set of async workflow functions like `take`, `condition`, `pause`, `fork`, and `unsubscribe`, which allow writing more complex async logic. + +Listeners can be defined statically by calling `listenerMiddleware.startListening()` during setup, or added and removed dynamically at runtime with special `dispatch(addListener())` and `dispatch(removeListener())` actions. + +### Basic Usage + +```js +import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit' + +import todosReducer, { + todoAdded, + todoToggled, + todoDeleted, +} from '../features/todos/todosSlice' + +// Create the middleware instance and methods +const listenerMiddleware = createListenerMiddleware() + +// Add one or more listener entries that look for specific actions. +// They may contain any sync or async logic, similar to thunks. +listenerMiddleware.startListening({ + actionCreator: todoAdded, + effect: async (action, listenerApi) => { + // Run whatever additional side-effect-y logic you want here + console.log('Todo added: ', action.payload.text) + + // Can cancel other running instances + listenerApi.cancelActiveListeners() + + // Run async logic + const data = await fetchData() + + // Pause until action dispatched or state changed + if (await listenerApi.condition(matchSomeAction)) { + // Use the listener API methods to dispatch, get state, + // unsubscribe the listener, start child tasks, and more + listenerApi.dispatch(todoAdded('Buy pet food')) + + // Spawn "child tasks" that can do more work and return results + const task = listenerApi.fork(async (forkApi) => { + // Can pause execution + await forkApi.delay(5) + // Complete the child by returning a value + return 42 + }) + + const result = await task.result + // Unwrap the child result in the listener + if (result.status === 'ok') { + // Logs the `42` result value that was returned + console.log('Child succeeded: ', result.value) + } + } + }, +}) + +const store = configureStore({ + reducer: { + todos: todosReducer, + }, + // Add the listener middleware to the store. + // NOTE: Since this can receive actions with functions inside, + // it should go before the serializability check middleware + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(listenerMiddleware.middleware), +}) +``` + +## `createListenerMiddleware` + +Creates an instance of the middleware, which should then be added to the store via `configureStore`'s `middleware` parameter. + +```ts no-transpile +const createListenerMiddleware = (options?: CreateMiddlewareOptions) => + ListenerMiddlewareInstance + +interface CreateListenerMiddlewareOptions { + extra?: ExtraArgument + onError?: ListenerErrorHandler +} + +type ListenerErrorHandler = ( + error: unknown, + errorInfo: ListenerErrorInfo, +) => void + +interface ListenerErrorInfo { + raisedBy: 'effect' | 'predicate' +} +``` + +### Middleware Options + +- `extra`: an optional "extra argument" that will be injected into the `listenerApi` parameter of each listener. Equivalent to [the "extra argument" in the Redux Thunk middleware](https://redux.js.org/usage/writing-logic-thunks#injecting-config-values-into-thunks) +- `onError`: an optional error handler that gets called with synchronous and async errors raised by `listener` and synchronous errors thrown by `predicate`. + +## Listener Middleware Instance + +The "listener middleware instance" returned from `createListenerMiddleware` is an object similar to the "slice" objects generated by `createSlice`. The instance object is _not_ the actual Redux middleware itself. Rather, it contains the middleware and some instance methods used to add and remove listener entries within the middleware. + +```ts no-transpile +interface ListenerMiddlewareInstance< + State = unknown, + Dispatch extends ThunkDispatch = ThunkDispatch< + State, + unknown, + UnknownAction + >, + ExtraArgument = unknown, +> { + middleware: ListenerMiddleware + startListening: (options: AddListenerOptions) => Unsubscribe + stopListening: ( + options: AddListenerOptions & UnsubscribeListenerOptions, + ) => boolean + clearListeners: () => void +} +``` + +### `middleware` + +The actual Redux middleware. Add this to the Redux store via [the `configureStore.middleware` option](./configureStore.mdx#middleware). + +Since the listener middleware can receive "add" and "remove" actions containing functions, this should normally be added as the first middleware in the chain so that it is before the serializability check middleware. + +```js +const store = configureStore({ + reducer: { + todos: todosReducer, + }, + // Add the listener middleware to the store. + // NOTE: Since this can receive actions with functions inside, + // it should go before the serializability check middleware + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(listenerMiddleware.middleware), +}) +``` + +### `startListening` + +Adds a new listener entry to the middleware. Typically used to "statically" add new listeners during application setup. + +```ts no-transpile +const startListening = (options: AddListenerOptions) => UnsubscribeListener + +interface AddListenerOptions { + // Four options for deciding when the listener will run: + + // 1) Exact action type string match + type?: string + + // 2) Exact action type match based on the RTK action creator + actionCreator?: ActionCreator + + // 3) Match one of many actions using an RTK matcher + matcher?: Matcher + + // 4) Return true based on a combination of action + state + predicate?: ListenerPredicate + + // The actual callback to run when the action is matched + effect: (action: Action, listenerApi: ListenerApi) => void | Promise +} + +type ListenerPredicate = ( + action: Action, + currentState?: State, + originalState?: State, +) => boolean + +type UnsubscribeListener = ( + unsubscribeOptions?: UnsubscribeListenerOptions, +) => void + +interface UnsubscribeListenerOptions { + cancelActive?: true +} +``` + +**You must provide exactly _one_ of the four options for deciding when the listener will run: `type`, `actionCreator`, `matcher`, or `predicate`**. Every time an action is dispatched, each listener will be checked to see if it should run based on the current action vs the comparison option provided. + +These are all acceptable: + +```js +// 1) Action type string +listenerMiddleware.startListening({ type: 'todos/todoAdded', effect }) +// 2) RTK action creator +listenerMiddleware.startListening({ actionCreator: todoAdded, effect }) +// 3) RTK matcher function +listenerMiddleware.startListening({ + matcher: isAnyOf(todoAdded, todoToggled), + effect, +}) +// 4) Listener predicate +listenerMiddleware.startListening({ + predicate: (action, currentState, previousState) => { + // return true when the listener should run + }, + effect, +}) +``` + +Note that the `predicate` option actually allows matching solely against state-related checks, such as "did `state.x` change" or "the current value of `state.x` matches some criteria", regardless of the actual action. + +The ["matcher" utility functions included in RTK](./matching-utilities.mdx) are acceptable as either the `matcher` or `predicate` option. + +The return value is an `unsubscribe()` callback that will remove this listener. By default, unsubscribing will _not_ cancel any active instances of the listener. However, you may also pass in `{cancelActive: true}` to cancel running instances. + +If you try to add a listener entry but another entry with this exact function reference already exists, no new entry will be added, and the existing `unsubscribe` method will be returned. + +The `effect` callback will receive the current action as its first argument, as well as a "listener API" object similar to the "thunk API" object in `createAsyncThunk`. + +All listener predicates and callbacks are checked _after_ the root reducer has already processed the action and updated the state. The `listenerApi.getOriginalState()` method can be used to get the state value that existed before the action that triggered this listener was processed. + +### `stopListening` + +Removes a given listener entry. + +It accepts the same arguments as `startListening()`. It checks for an existing listener entry by comparing the function references of `listener` and the provided `actionCreator/matcher/predicate` function or `type` string. + +By default, this does _not_ cancel any active running instances. However, you may also pass in `{cancelActive: true}` to cancel running instances. + +```ts no-transpile +const stopListening = ( + options: AddListenerOptions & UnsubscribeListenerOptions, +) => boolean + +interface UnsubscribeListenerOptions { + cancelActive?: true +} +``` + +Returns `true` if the listener entry has been removed, or `false` if no subscription matching the input provided has been found. + +```js +// Examples: +// 1) Action type string +listenerMiddleware.stopListening({ + type: 'todos/todoAdded', + listener, + cancelActive: true, +}) +// 2) RTK action creator +listenerMiddleware.stopListening({ actionCreator: todoAdded, effect }) +// 3) RTK matcher function +listenerMiddleware.stopListening({ matcher, effect, cancelActive: true }) +// 4) Listener predicate +listenerMiddleware.stopListening({ predicate, effect }) +``` + +### `clearListeners` + +Removes all current listener entries. It also cancels all active running instances of those listeners as well. + +This is most likely useful for test scenarios where a single middleware or store instance might be used in multiple tests, as well as some app cleanup situations. + +```ts no-transpile +const clearListeners = () => void; +``` + +## Action Creators + +In addition to adding and removing listeners by directly calling methods on the listener instance, you can dynamically add and remove listeners at runtime by dispatching special "add" and "remove" actions. These are exported from the main RTK package as standard RTK-generated action creators. + +### `addListener` + +A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to dynamically add a new listener at runtime. It accepts exactly the same options as `startListening()` + +Dispatching this action returns an `unsubscribe()` callback from `dispatch`. + +```js +// Per above, provide `predicate` or any of the other comparison options +const unsubscribe = store.dispatch(addListener({ predicate, effect })) +``` + +### `removeListener` + +A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to dynamically remove a listener at runtime. Accepts the same arguments as `stopListening()`. + +By default, this does _not_ cancel any active running instances. However, you may also pass in `{cancelActive: true}` to cancel running instances. + +Returns `true` if the listener entry has been removed, `false` if no subscription matching the input provided has been found. + +```js +const wasRemoved = store.dispatch( + removeListener({ predicate, effect, cancelActive: true }), +) +``` + +### `clearAllListeners` + +A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to remove all current listener entries. It also cancels all active running instances of those listeners as well. + +```js +store.dispatch(clearAllListeners()) +``` + +## Listener API + +The `listenerApi` object is the second argument to each listener callback. It contains several utility functions that may be called anywhere inside the listener's logic. + +```ts no-transpile +export interface ListenerEffectAPI< + State, + Dispatch extends ReduxDispatch, + ExtraArgument = unknown, +> extends MiddlewareAPI { + // NOTE: MiddlewareAPI contains `dispatch` and `getState` already + + /** + * Returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran. + * This function can **only** be invoked **synchronously**, it throws error otherwise. + */ + getOriginalState: () => State + /** + * Removes the listener entry from the middleware and prevent future instances of the listener from running. + * It does **not** cancel any active instances. + */ + unsubscribe(): void + /** + * It will subscribe a listener if it was previously removed, noop otherwise. + */ + subscribe(): void + /** + * Returns a promise that resolves when the input predicate returns `true` or + * rejects if the listener has been cancelled or is completed. + * + * The return value is `true` if the predicate succeeds or `false` if a timeout is provided and expires first. + */ + condition: ConditionFunction + /** + * Returns a promise that resolves when the input predicate returns `true` or + * rejects if the listener has been cancelled or is completed. + * + * The return value is the `[action, currentState, previousState]` combination that the predicate saw as arguments. + * + * The promise resolves to null if a timeout is provided and expires first. + */ + take: TakePattern + /** + * Cancels all other running instances of this same listener except for the one that made this call. + */ + cancelActiveListeners: () => void + /** + * Cancels the listener instance that made this call. + */ + cancel: () => void + /** + * Throws a `TaskAbortError` if this listener has been cancelled + */ + throwIfCancelled: () => void + /** + * An abort signal whose `aborted` property is set to `true` + * if the listener execution is either aborted or completed. + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + signal: AbortSignal + /** + * Returns a promise that resolves after `timeoutMs` or + * rejects if the listener has been cancelled or is completed. + */ + delay(timeoutMs: number): Promise + /** + * Queues in the next microtask the execution of a task. + */ + fork(executor: ForkedTaskExecutor): ForkedTask + /** + * Returns a promise that resolves when `waitFor` resolves or + * rejects if the listener has been cancelled or is completed. + * @param promise + */ + pause(promise: Promise): Promise + extra: ExtraArgument +} +``` + +These can be divided into several categories. + +### Store Interaction Methods + +- `dispatch: Dispatch`: the standard `store.dispatch` method +- `getState: () => State`: the standard `store.getState` method +- `getOriginalState: () => State`: returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran. (**Note**: this method can only be called synchronously, during the initial dispatch call stack, to avoid memory leaks. Calling it asynchronously will throw an error.) +- `extra: unknown`: the "extra argument" that was provided as part of the middleware setup, if any + +`dispatch` and `getState` are exactly the same as in a thunk. `getOriginalState` can be used to compare the original state before the listener was started. + +`extra` can be used to inject a value such as an API service layer into the middleware at creation time, and is accessible here. + +### Listener Subscription Management + +- `unsubscribe: () => void`: removes the listener entry from the middleware, and prevent future instances of the listener from running. (This does _not_ cancel any active instances.) +- `subscribe: () => void`: will re-subscribe the listener entry if it was previously removed, or no-op if currently subscribed +- `cancelActiveListeners: () => void`: cancels all other running instances of this same listener _except_ for the one that made this call. (The cancellation will only have a meaningful effect if the other instances are paused using one of the cancellation-aware APIs like `take/cancel/pause/delay` - see "Cancelation and Task Management" in the "Usage" section for more details) +- `cancel: () => void`: cancels the instance of this listener that made this call. +- `throwIfCancelled: () => void`: throws a `TaskAbortError` if the current listener instance was cancelled. +- `signal: AbortSignal`: An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) whose `aborted` property will be set to `true` if the listener execution is aborted or completed. + +Dynamically unsubscribing and re-subscribing this listener allows for more complex async workflows, such as avoiding duplicate running instances by calling `listenerApi.unsubscribe()` at the start of a listener, or calling `listenerApi.cancelActiveListeners()` to ensure that only the most recent instance is allowed to complete. + +### Conditional Workflow Execution + +- `take: (predicate: ListenerPredicate, timeout?: number) => Promise<[Action, State, State] | null>`: returns a promise that will resolve when the `predicate` returns `true`. The return value is the `[action, currentState, previousState]` combination that the predicate saw as arguments. If a `timeout` is provided and expires first, the promise resolves to `null`. +- `condition: (predicate: ListenerPredicate, timeout?: number) => Promise`: Similar to `take`, but resolves to `true` if the predicate succeeds, and `false` if a `timeout` is provided and expires first. This allows async logic to pause and wait for some condition to occur before continuing. See "Writing Async Workflows" below for details on usage. +- `delay: (timeoutMs: number) => Promise`: returns a cancellation-aware promise that resolves after the timeout, or rejects if cancelled before the expiration +- `pause: (promise: Promise) => Promise`: accepts any promise, and returns a cancellation-aware promise that either resolves with the argument promise or rejects if cancelled before the resolution + +These methods provide the ability to write conditional logic based on future dispatched actions and state changes. Both also accept an optional `timeout` in milliseconds. + +`take` resolves to a `[action, currentState, previousState]` tuple or `null` if it timed out, whereas `condition` resolves to `true` if it succeeded or `false` if timed out. + +`take` is meant for "wait for an action and get its contents", while `condition` is meant for checks like `if (await condition(predicate))`. + +Both these methods are cancellation-aware, and will throw a `TaskAbortError` if the listener instance is cancelled while paused. + +Note that both `take` and `condition` will only resolve **after the next action** has been dispatched. They do not resolve immediately even if their predicate would return true for the current state. + +### Child Tasks + +- `fork: (executor: (forkApi: ForkApi) => T | Promise) => ForkedTask`: Launches a "child task" that may be used to accomplish additional work. Accepts any sync or async function as its argument, and returns a `{result, cancel}` object that can be used to check the final status and return value of the child task, or cancel it while in-progress. + +Child tasks can be launched, and waited on to collect their return values. The provided `executor` function will be called asynchronously with a `forkApi` object containing `{pause, delay, signal}`, allowing it to pause or check cancellation status. It can also make use of the `listenerApi` from the listener's scope. + +An example of this might be a listener that forks a child task containing an infinite loop that listens for events from a server. The parent then uses `listenerApi.condition()` to wait for a "stop" action, and cancels the child task. + +The task and result types are: + +```ts no-transpile +interface ForkedTaskAPI { + pause(waitFor: Promise): Promise + delay(timeoutMs: number): Promise + signal: AbortSignal +} + +export type TaskResolved = { + readonly status: 'ok' + readonly value: T +} + +export type TaskRejected = { + readonly status: 'rejected' + readonly error: unknown +} + +export type TaskCancelled = { + readonly status: 'cancelled' + readonly error: TaskAbortError +} + +export type TaskResult = + | TaskResolved + | TaskRejected + | TaskCancelled + +export interface ForkedTask { + result: Promise> + cancel(): void +} +``` + +## TypeScript Usage + +The middleware code is fully TS-typed. However, the `startListening` and `addListener` functions do not know what the store's `RootState` type looks like by default, so `getState()` will return `unknown`. + +To fix this, the middleware provides types for defining "pre-typed" versions of those methods, similar to the pattern used for defing pre-typed React-Redux hooks. We specifically recommend creating the middleware instance in a separate file from the actual `configureStore()` call: + +```ts no-transpile +// listenerMiddleware.ts +import { createListenerMiddleware, addListener } from '@reduxjs/toolkit' +import type { RootState, AppDispatch } from './store' + +declare type ExtraArgument = {foo: string}; + +export const listenerMiddleware = createListenerMiddleware() + +export const startAppListening = listenerMiddleware.startListening.withTypes< + RootState, + AppDispatch, + ExtraArgument +>() + +export const addAppListener = addListener.withTypes() +``` + +Then import and use those pre-typed methods in your components. + +## Usage Guide + +### Overall Purpose + +This middleware lets you run additional logic when some action is dispatched, as a lighter-weight alternative to middleware like sagas and observables that have both a heavy runtime bundle cost and a large conceptual overhead. + +This middleware is not intended to handle all possible use cases. Like thunks, it provides you with a basic set of primitives (including access to `dispatch` and `getState`), and gives you freedom to write any sync or async logic you want. This is both a strength (you can do anything!) and a weakness (you can do anything, with no guard rails!). + +The middleware includes several async workflow primitives that are sufficient to write equivalents to many Redux-Saga effects operators like `takeLatest`, `takeLeading`, and `debounce`, although none of those methods are directly included. (See [the listener middleware tests file for examples of how to write code equivalent to those effects](https://github.com/reduxjs/redux-toolkit/blob/03eafd5236f16574935cdf1c5958e32ee8cf3fbe/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts#L74-L363).) + +### Standard Usage Patterns + +The most common expected usage is "run some logic after a given action was dispatched". For example, you could set up a simple analytics tracker by looking for certain actions and sending extracted data to the server, including pulling user details from the store: + +```js +listenerMiddleware.startListening({ + matcher: isAnyOf(action1, action2, action3), + effect: (action, listenerApi) => { + const user = selectUserDetails(listenerApi.getState()) + + const { specialData } = action.meta + + analyticsApi.trackUsage(action.type, user, specialData) + }, +}) +``` + +However, the `predicate` option also allows triggering logic when some state value has changed, or when the state matches a particular condition: + +```js +listenerMiddleware.startListening({ + predicate: (action, currentState, previousState) => { + // Trigger logic whenever this field changes + return currentState.counter.value !== previousState.counter.value + }, + effect, +}) + +listenerMiddleware.startListening({ + predicate: (action, currentState, previousState) => { + // Trigger logic after every action if this condition is true + return currentState.counter.value > 3 + }, + effect, +}) +``` + +You could also implement a generic API fetching capability, where the UI dispatches a plain action describing the type of resource to be requested, and the middleware automatically fetches it and dispatches a result action: + +```js +listenerMiddleware.startListening({ + actionCreator: resourceRequested, + effect: async (action, listenerApi) => { + const { name, args } = action.payload + listenerApi.dispatch(resourceLoading()) + + const res = await serverApi.fetch(`/api/${name}`, ...args) + listenerApi.dispatch(resourceLoaded(res.data)) + }, +}) +``` + +(That said, we would recommend use of RTK Query for any meaningful data fetching behavior - this is primarily an example of what you _could_ do in a listener.) + +The `listenerApi.unsubscribe` method may be used at any time, and will remove the listener from handling any future actions. As an example, you could create a one-shot listener by unconditionally calling `unsubscribe()` in the body - the effect callback would run the first time the relevant action is seen, then immediately unsubscribe and never run again. (The middleware actually uses this technique internally for the `take/condition` methods) + +### Writing Async Workflows with Conditions + +One of the great strengths of both sagas and observables is their support for complex async workflows, including stopping and starting behavior based on specific dispatched actions. However, the weakness is that both require mastering a complex API with many unique operators (effects methods like `call()` and `fork()` for sagas, RxJS operators for observables), and both add a significant amount to application bundle size. + +While the listener middleware is _not_ meant to fully replace sagas or observables, it does provide a carefully chosen set of APIs to implement long-running async workflows as well. + +Listeners can use the `condition` and `take` methods in `listenerApi` to wait until some action is dispatched or state check is met. The `condition` method is directly inspired by [the `condition` function in Temporal.io's workflow API](https://docs.temporal.io/docs/typescript/workflows/#condition) (credit to [@swyx](https://twitter.com/swyx) for the suggestion!), and `take` is inspired by [the `take` effect from Redux-Saga](https://redux-saga.js.org/docs/api#takepattern). + +The signatures are: + +```ts no-transpile +type ConditionFunction = ( + predicate: ListenerPredicate | (() => boolean), + timeout?: number, +) => Promise + +type TakeFunction = ( + predicate: ListenerPredicate | (() => boolean), + timeout?: number, +) => Promise<[Action, State, State] | null> +``` + +You can use `await condition(somePredicate)` as a way to pause execution of your listener callback until some criteria is met. + +The `predicate` will be called after every action is processed by the reducers, and should return `true` when the condition should resolve. (It is effectively a one-shot listener itself.) If a `timeout` number (in ms) is provided, the promise will resolve `true` if the `predicate` returns first, or `false` if the timeout expires. This allows you to write comparisons like `if (await condition(predicate, timeout))`. + +This should enable writing longer-running workflows with more complex async logic, such as [the "cancellable counter" example from Redux-Saga](https://github.com/redux-saga/redux-saga/blob/1ecb1bed867eeafc69757df8acf1024b438a79e0/examples/cancellable-counter/src/sagas/index.js). + +An example of `condition` usage, from the test suite: + +```ts no-transpile +test('condition method resolves promise when there is a timeout', async () => { + let finalCount = 0 + let listenerStarted = false + + listenerMiddleware.startListening({ + predicate: (action, currentState: CounterState) => { + return increment.match(action) && currentState.value === 0 + }, + effect: async (action, listenerApi) => { + listenerStarted = true + // Wait for either the counter to hit 3, or 50ms to elapse + const result = await listenerApi.condition( + (action, currentState: CounterState) => { + return currentState.value === 3 + }, + 50, + ) + + // In this test, we expect the timeout to happen first + expect(result).toBe(false) + // Save the state for comparison outside the listener + const latestState = listenerApi.getState() + finalCount = latestState.value + }, + }) + + store.dispatch(increment()) + // The listener should have started right away + expect(listenerStarted).toBe(true) + + store.dispatch(increment()) + + // If we wait 150ms, the condition timeout will expire first + await delay(150) + // Update the state one more time to confirm the listener isn't checking it + store.dispatch(increment()) + + // Handled the state update before the delay, but not after + expect(finalCount).toBe(2) +}) +``` + +### Cancellation and Task Management + +The listener middleware supports cancellation of running listener instances, `take/condition/pause/delay` functions, and "child tasks", with an implementation based on [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + +The `listenerApi.pause/delay()` functions provide a cancellation-aware way to have the current listener sleep. `pause()` accepts a promise, while `delay` accepts a timeout value. If the listener is cancelled while waiting, a `TaskAbortError` will be thrown. In addition, both `take` and `condition` support cancellation interruption as well. + +`listenerApi.cancelActiveListeners()` will cancel _other_ existing instances that are running, while `listenerApi.cancel()` can be used to cancel the _current_ instance (which may be useful from a fork, which could be deeply nested and not able to directly throw a promise to break out of the effect execution). `listenerAPi.throwIfCancelled()` can also be useful to bail out of workflows in case cancellation happened while the effect was doing other work. + +`listenerApi.fork()` can used to launch "child tasks" that can do additional work. These can be waited on to collect their results. An example of this might look like: + +```ts no-transpile +listenerMiddleware.startListening({ + actionCreator: increment, + effect: async (action, listenerApi) => { + // Spawn a child task and start it immediately + const task = listenerApi.fork(async (forkApi) => { + // Artificially wait a bit inside the child + await forkApi.delay(5) + // Complete the child by returning a value + return 42 + }) + + const result = await task.result + // Unwrap the child result in the listener + if (result.status === 'ok') { + // Logs the `42` result value that was returned + console.log('Child succeeded: ', result.value) + } + }, +}) +``` + +### Complex Async Workflows + +The provided async workflow primitives (`cancelActiveListeners`, `cancel`, `unsubscribe`, `subscribe`, `take`, `condition`, `pause`, `delay`) can be used to implement behavior that is equivalent to many of the more complex async workflow capabilities found in the Redux-Saga library. This includes effects such as `throttle`, `debounce`, `takeLatest`, `takeLeading`, and `fork/join`. Some examples from the test suite: + +```js +test('debounce / takeLatest', async () => { + // Repeated calls cancel previous ones, no work performed + // until the specified delay elapses without another call + // NOTE: This is also basically identical to `takeLatest`. + // Ref: https://redux-saga.js.org/docs/api#debouncems-pattern-saga-args + // Ref: https://redux-saga.js.org/docs/api#takelatestpattern-saga-args + + listenerMiddleware.startListening({ + actionCreator: increment, + effect: async (action, listenerApi) => { + // Cancel any in-progress instances of this listener + listenerApi.cancelActiveListeners() + + // Delay before starting actual work + await listenerApi.delay(15) + + // do work here + }, + }) +} + +test('takeLeading', async () => { + // Starts listener on first action, ignores others until task completes + // Ref: https://redux-saga.js.org/docs/api#takeleadingpattern-saga-args + + listenerMiddleware.startListening({ + actionCreator: increment, + effect: async (action, listenerApi) => { + listenerCalls++ + + // Stop listening for this action + listenerApi.unsubscribe() + + // Pretend we're doing expensive work + + // Re-enable the listener + listenerApi.subscribe() + }, + }) +}) + +test('cancelled', async () => { + // cancelled allows checking if the current task was cancelled + // Ref: https://redux-saga.js.org/docs/api#cancelled + + let canceledAndCaught = false + let canceledCheck = false + + // Example of canceling prior instances conditionally and checking cancellation + listenerMiddleware.startListening({ + matcher: isAnyOf(increment, decrement, incrementByAmount), + effect: async (action, listenerApi) => { + if (increment.match(action)) { + // Have this branch wait around to be cancelled by the other + try { + await listenerApi.delay(10) + } catch (err) { + // Can check cancellation based on the exception and its reason + if (err instanceof TaskAbortError) { + canceledAndCaught = true + } + } + } else if (incrementByAmount.match(action)) { + // do a non-cancellation-aware wait + await delay(15) + if (listenerApi.signal.aborted) { + canceledCheck = true + } + } else if (decrement.match(action)) { + listenerApi.cancelActiveListeners() + } + }, + }) +}) +``` + +As a more practical example: [this saga-based "long polling" loop](https://gist.github.com/markerikson/5203e71a69fa9dff203c9e27c3d84154) repeatedly asks the server for a message and then processes each response. The child loop is started on demand when a "start polling" action is dispatched, and the loop is cancelled when a "stop polling" action is dispatched. + +That approach can be implemented via the listener middleware: + +```ts no-transpile +// Track how many times each message was processed by the loop +const receivedMessages = { + a: 0, + b: 0, + c: 0, +} + +const eventPollingStarted = createAction('serverPolling/started') +const eventPollingStopped = createAction('serverPolling/stopped') + +listenerMiddleware.startListening({ + actionCreator: eventPollingStarted, + effect: async (action, listenerApi) => { + // Only allow one instance of this listener to run at a time + listenerApi.unsubscribe() + + // Start a child job that will infinitely loop receiving messages + const pollingTask = listenerApi.fork(async (forkApi) => { + try { + while (true) { + // Cancellation-aware pause for a new server message + const serverEvent = await forkApi.pause(pollForEvent()) + // Process the message. In this case, just count the times we've seen this message. + if (serverEvent.type in receivedMessages) { + receivedMessages[ + serverEvent.type as keyof typeof receivedMessages + ]++ + } + } + } catch (err) { + if (err instanceof TaskAbortError) { + // could do something here to track that the task was cancelled + } + } + }) + + // Wait for the "stop polling" action + await listenerApi.condition(eventPollingStopped.match) + pollingTask.cancel() + }, +}) +``` + +### Adding Listeners Inside Components + +Listeners can be added at runtime via `dispatch(addListener())`. This means that you can add listeners anywhere you have access to `dispatch`, and that includes React components. + +Since dispatching `addListener` returns an `unsubscribe` callback, this naturally maps to the behavior of React `useEffect` hooks, which let you return a cleanup function. You can add a listener in an effect, and remove the listener when the hook is cleaned up. + +The basic pattern might look like: + +```js +useEffect(() => { + // Could also just `return dispatch(addListener())` directly, but showing this + // as a separate variable to be clear on what's happening + const unsubscribe = dispatch( + addListener({ + actionCreator: todoAdded, + effect: (action, listenerApi) => { + // do some useful logic here + }, + }), + ) + return unsubscribe +}, []) +``` + +While this pattern is _possible_, **we do not necessarily _recommend_ doing this!** The React and Redux communities have always tried to emphasize basing behavior on _state_ as much as possible. Having React components directly tie into the Redux action dispatch pipeline could potentialy lead to codebases that are more difficult to maintain. + +At the same time, this _is_ a valid technique, both in terms of API behavior and potential use cases. It's been common to lazy-load sagas as part of a code-split app, and that has often required some complex additional setup work to "inject" sagas. In contrast, `dispatch(addListener())` fits naturally into a React component's lifecycle. + +So, while we're not specifically encouraging use of this pattern, it's worth documenting here so that users are aware of it as a possibility. + +### Organizing Listeners in Files + +As a starting point, **it's best to create the listener middleware in a separate file, such as `app/listenerMiddleware.ts`, rather than in the same file as the store**. This avoids any potential circular import problems from other files trying to import `middleware.addListener`. + +From there, so far we've come up with three different ways to organize listener functions and setup. + +First, you can import effect callbacks from slice files into the middleware file, and add the listeners: + +```ts no-transpile title="app/listenerMiddleware.ts" +import { action1, listener1 } from '../features/feature1/feature1Slice' +import { action2, listener2 } from '../features/feature2/feature2Slice' + +listenerMiddleware.startListening({ actionCreator: action1, effect: listener1 }) +listenerMiddleware.startListening({ actionCreator: action2, effect: listener2 }) +``` + +This is probably the simplest option, and mirrors how the store setup pulls together all the slice reducers to create the app. + +The second option is the opposite: have the slice files import the middleware and directly add their listeners: + +```ts no-transpile title="features/feature1/feature1Slice.ts" +import { listenerMiddleware } from '../../app/listenerMiddleware' + +const feature1Slice = createSlice(/* */) +const { action1 } = feature1Slice.actions + +export default feature1Slice.reducer + +listenerMiddleware.startListening({ + actionCreator: action1, + effect: () => {}, +}) +``` + +This keeps all the logic in the slice, although it does lock the setup into a single middleware instance. + +The third option is to create a setup function in the slice, but let the listener file call that on startup: + +```ts no-transpile title="features/feature1/feature1Slice.ts" +import type { AppStartListening } from '../../app/listenerMiddleware' + +const feature1Slice = createSlice(/* */) +const { action1 } = feature1Slice.actions + +export default feature1Slice.reducer + +export const addFeature1Listeners = (startListening: AppStartListening) => { + startListening({ + actionCreator: action1, + effect: () => {}, + }) +} +``` + +```ts no-transpile title="app/listenerMiddleware.ts" +import { addFeature1Listeners } from '../features/feature1/feature1Slice' + +addFeature1Listeners(listenerMiddleware.startListening) +``` + +Feel free to use whichever of these approaches works best in your app. diff --git a/docs/reference/redux-toolkit/createReducer.mdx b/docs/reference/redux-toolkit/createReducer.mdx new file mode 100644 index 00000000..38ff9024 --- /dev/null +++ b/docs/reference/redux-toolkit/createReducer.mdx @@ -0,0 +1,289 @@ +--- +id: createReducer +title: createReducer +sidebar_label: createReducer +hide_title: true +--- + +  + +# `createReducer()` + +## Overview + +A utility that simplifies creating Redux reducer functions. It uses Immer internally to drastically simplify immutable update logic +by writing "mutative" code in your reducers, and supports directly mapping specific action types to case reducer functions +that will update the state when that action is dispatched. + +Redux [reducers](https://redux.js.org/basics/reducers) are often implemented using a `switch` statement, with one `case` for every handled action type. + +```js +const initialState = { value: 0 } + +function counterReducer(state = initialState, action) { + switch (action.type) { + case 'increment': + return { ...state, value: state.value + 1 } + case 'decrement': + return { ...state, value: state.value - 1 } + case 'incrementByAmount': + return { ...state, value: state.value + action.payload } + default: + return state + } +} +``` + +This approach works well, but is a bit boilerplate-y and error-prone. For instance, it is easy to forget the `default` case or +setting the initial state. + +The `createReducer` helper streamlines the implementation of such reducers. It uses a "builder callback" notation to define handlers for specific action types, matching against a range of actions, or handling a default case. This is conceptually similar to a switch statement, but with better TS support. + +With `createReducer`, your reducers instead look like: + +```ts +import { createAction, createReducer } from '@reduxjs/toolkit' + +interface CounterState { + value: number +} + +const increment = createAction('counter/increment') +const decrement = createAction('counter/decrement') +const incrementByAmount = createAction('counter/incrementByAmount') + +const initialState = { value: 0 } satisfies CounterState as CounterState + +const counterReducer = createReducer(initialState, (builder) => { + builder + .addCase(increment, (state, action) => { + state.value++ + }) + .addCase(decrement, (state, action) => { + state.value-- + }) + .addCase(incrementByAmount, (state, action) => { + state.value += action.payload + }) +}) +``` + +## Usage with the "Builder Callback" Notation + +[overloadSummary](docblock://createReducer.ts?token=createReducer) + +### Parameters + +[params](docblock://createReducer.ts?token=createReducer) + +### Example Usage + +[examples](docblock://createReducer.ts?token=createReducer) + +### Builder Methods + +### `builder.addCase` + +[summary,remarks](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addCase) + +#### Parameters + +[params,examples](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addCase) + +### `builder.addAsyncThunk` + +[summary,remarks](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addAsyncThunk) + +#### Parameters + +[params,examples](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addAsyncThunk) + +### `builder.addMatcher` + +[summary,remarks](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addMatcher) + +#### Parameters + +[params,examples](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addMatcher) + +### `builder.addDefaultCase` + +[summary,remarks](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addDefaultCase) + +#### Parameters + +[params,examples](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addDefaultCase) + +### Returns + +The generated reducer function. + +The reducer will have a `getInitialState` function attached that will return the initial state when called. This may be useful for tests or usage with React's `useReducer` hook: + +```js +const counterReducer = createReducer(0, (builder) => { + builder + .addCase('increment', (state, action) => state + action.payload) + .addCase('decrement', (state, action) => state - action.payload) +}) + +console.log(counterReducer.getInitialState()) // 0 +``` + +### Example Usage + +[examples](docblock://createReducer.ts?token=createReducer) + +## Direct State Mutation + +Redux requires reducer functions to be pure and treat state values as immutable. While this is essential for making state updates predictable and observable, it can sometimes make the implementation of such updates awkward. Consider the following example: + +```ts +import { createAction, createReducer } from '@reduxjs/toolkit' + +interface Todo { + text: string + completed: boolean +} + +const addTodo = createAction('todos/add') +const toggleTodo = createAction('todos/toggle') + +const todosReducer = createReducer([] as Todo[], (builder) => { + builder + .addCase(addTodo, (state, action) => { + const todo = action.payload + return [...state, todo] + }) + .addCase(toggleTodo, (state, action) => { + const index = action.payload + const todo = state[index] + return [ + ...state.slice(0, index), + { ...todo, completed: !todo.completed }, + ...state.slice(index + 1), + ] + }) +}) +``` + +The `addTodo` reducer is straightforward if you know the [ES6 spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). However, the code for `toggleTodo` is much less straightforward, especially considering that it only sets a single flag. + +To make things easier, `createReducer` uses [immer](https://github.com/mweststrate/immer) to let you write reducers as if they were mutating the state directly. In reality, the reducer receives a proxy state that translates all mutations into equivalent copy operations. + +```ts +import { createAction, createReducer } from '@reduxjs/toolkit' + +interface Todo { + text: string + completed: boolean +} + +const addTodo = createAction('todos/add') +const toggleTodo = createAction('todos/toggle') + +const todosReducer = createReducer([] as Todo[], (builder) => { + builder + .addCase(addTodo, (state, action) => { + // This push() operation gets translated into the same + // extended-array creation as in the previous example. + const todo = action.payload + state.push(todo) + }) + .addCase(toggleTodo, (state, action) => { + // The "mutating" version of this case reducer is much + // more direct than the explicitly pure one. + const index = action.payload + const todo = state[index] + todo.completed = !todo.completed + }) +}) +``` + +Writing "mutating" reducers simplifies the code. It's shorter, there's less indirection, and it eliminates common mistakes made while spreading nested state. However, the use of Immer does add some "magic", and Immer has its own nuances in behavior. You should read through [pitfalls mentioned in the immer docs](https://immerjs.github.io/immer/pitfalls) . Most importantly, **you need to ensure that you either mutate the `state` argument or return a new state, _but not both_**. For example, the following reducer would throw an exception if a `toggleTodo` action is passed: + +```ts +import { createAction, createReducer } from '@reduxjs/toolkit' + +interface Todo { + text: string + completed: boolean +} + +const toggleTodo = createAction('todos/toggle') + +const todosReducer = createReducer([] as Todo[], (builder) => { + builder.addCase(toggleTodo, (state, action) => { + const index = action.payload + const todo = state[index] + + // This case reducer both mutates the passed-in state... + todo.completed = !todo.completed + + // ... and returns a new value. This will throw an + // exception. In this example, the easiest fix is + // to remove the `return` statement. + return [...state.slice(0, index), todo, ...state.slice(index + 1)] + }) +}) +``` + +## Multiple Case Reducer Execution + +Originally, `createReducer` always matched a given action type to a single case reducer, and only that one case reducer would execute for a given action. + +Using action matchers changes that behavior, as multiple matchers may handle a single action. + +For any dispatched action, the behavior is: + +- If there is an exact match for the action type, the corresponding case reducer will execute first +- Any matchers that return `true` will execute in the order they were defined +- If a default case reducer is provided, and _no_ case or matcher reducers ran, the default case reducer will execute +- If no case or matcher reducers ran, the original existing state value will be returned unchanged + +The executing reducers form a pipeline, and each of them will receive the output of the previous reducer: + +```ts +import { createReducer } from '@reduxjs/toolkit' + +const reducer = createReducer(0, (builder) => { + builder + .addCase('increment', (state) => state + 1) + .addMatcher( + (action) => action.type.startsWith('i'), + (state) => state * 5, + ) + .addMatcher( + (action) => action.type.endsWith('t'), + (state) => state + 2, + ) +}) + +console.log(reducer(0, { type: 'increment' })) +// Returns 7, as the 'increment' case and both matchers all ran in sequence: +// - case 'increment": 0 => 1 +// - matcher starts with 'i': 1 => 5 +// - matcher ends with 't': 5 => 7 +``` + +## Logging Draft State Values + +It's very common for a developer to call `console.log(state)` during the development process. However, browsers display Proxies in a format that is hard to read, which can make console logging of Immer-based state difficult. + +When using either `createSlice` or `createReducer`, you may use the [`current`](./otherExports.mdx#current) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer/current). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal. + +```ts +import { createSlice, current } from '@reduxjs/toolkit' + +const slice = createSlice({ + name: 'todos', + initialState: [{ id: 1, title: 'Example todo' }], + reducers: { + addTodo: (state, action) => { + console.log('before', current(state)) + state.push(action.payload) + console.log('after', current(state)) + }, + }, +}) +``` diff --git a/docs/reference/redux-toolkit/createSelector.mdx b/docs/reference/redux-toolkit/createSelector.mdx new file mode 100644 index 00000000..8697eb84 --- /dev/null +++ b/docs/reference/redux-toolkit/createSelector.mdx @@ -0,0 +1,139 @@ +--- +id: createSelector +title: createSelector +sidebar_label: createSelector +hide_title: true +--- + +  + +# `createSelector` + +## Overview + +The `createSelector` utility from the [Reselect library](https://github.com/reduxjs/reselect), re-exported for ease of use. + +For more details on using `createSelector`, see: + +- The [Reselect API documentation](https://github.com/reduxjs/reselect) +- [React-Redux docs: Hooks API - Using memoizing selectors](https://react-redux.js.org/next/api/hooks#using-memoizing-selectors) +- [Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance](https://blog.isquaredsoftware.com/2017/12/idiomatic-redux-using-reselect-selectors/) +- [React/Redux Links: Reducers and Selectors](https://github.com/markerikson/react-redux-links/blob/master/redux-reducers-selectors.md) + +:::note +Prior to v0.7, RTK re-exported `createSelector` from [`selectorator`](https://github.com/planttheidea/selectorator), which +allowed using string keypaths as input selectors. This was removed, as it ultimately did not provide enough benefits, and +the string keypaths made static typing for selectors difficult. +::: + +## `createDraftSafeSelector` + +In general, we recommend against using selectors inside of reducers: + +- Selectors typically expect the entire Redux state object as an argument, while slice reducers only have access to a specific subset of the entire Redux state +- Reselect's `createSelector` relies on reference comparisons to determine if inputs have changed, and if an Immer Proxy-wrapped draft value is passed in to a selector, the selector may see the same reference and think nothing has changed. + +However, some users have requested the ability to create selectors that will work correctly inside of Immer-powered reducers. One use case for this might be collecting an ordered set of items when using `createEntityAdapter`, such as `const orderedTodos = todosSelectors.selectAll(todosState)`, and then using `orderedTodos` in the rest of the reducer logic. + +Besides re-exporting `createSelector`, RTK also exports a wrapped version of `createSelector` named `createDraftSafeSelector` that allows you to create selectors that can safely be used inside of `createReducer` and `createSlice` reducers with Immer-powered mutable logic. When used with plain state values, the selector will still memoize normally based on the inputs. But, when used with Immer draft values, the selector will err on the side of recalculating the results, just to be safe. + +All selectors created by `entityAdapter.getSelectors` are "draft safe" selectors by default. + +Example: + +```ts no-transpile +const selectSelf = (state: State) => state +const unsafeSelector = createSelector(selectSelf, (state) => state.value) +const draftSafeSelector = createDraftSafeSelector( + selectSelf, + (state) => state.value, +) + +// in your reducer: + +state.value = 1 + +const unsafe1 = unsafeSelector(state) +const safe1 = draftSafeSelector(state) + +state.value = 2 + +const unsafe2 = unsafeSelector(state) +const safe2 = draftSafeSelector(state) +``` + +After executing that, `unsafe1` and `unsafe2` will be of the same value, because the memoized selector was +executed on the same object - but `safe2` will actually be different from `safe1` (with the updated value of `2`), +because the safe selector detected that it was executed on a Immer draft object and recalculated using the current +value instead of returning a cached value. + +:::tip `createDraftSafeSelectorCreator` + +RTK also exports a `createDraftSafeSelectorCreator` function, the "draft safe" equivalent of [`createSelectorCreator`](https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions). + +```ts no-transpile +import { + createDraftSafeSelectorCreator, + weakMapMemoize, +} from '@reduxjs/toolkit' + +const createWeakMapDraftSafeSelector = + createDraftSafeSelectorCreator(weakMapMemoize) + +const selectSelf = (state: State) => state +const draftSafeSelector = createWeakMapDraftSafeSelector( + selectSelf, + (state) => state.value, +) +``` + +::: + +### Defining a Pre-Typed `createDraftSelector` + +As of RTK 2.1, you can define a "pre-typed" version of `createDraftSafeSelector` that can have the type for `state` built in. This lets you set up those types once, so you don't have to repeat them each time you call `createDraftSafeSelector`. + +```ts no-transpile +const createTypedDraftSafeSelector = + createDraftSafeSelector.withTypes() +``` + +Import and use the pre-typed `createTypedDraftSafeSelector` function, and it will automatically know that the `state` argument is of type `RootState`. + +:::warning Known Limitations +Currently this approach only works if input selectors are provided as a single array. + +If you pass the input selectors as separate inline arguments, the parameter types of the result function will not be inferred. As a workaround you can either + +1. Wrap your input selectors in a single array +2. You can annotate the parameter types of the result function: + +```ts no-transpile +import { createSelector } from 'reselect' + +interface Todo { + id: number + completed: boolean +} + +interface Alert { + id: number + read: boolean +} + +export interface RootState { + todos: Todo[] + alerts: Alert[] +} + +export const createTypedDraftSafeSelector = + createDraftSafeSelector.withTypes() + +const selectTodoIds = createTypedDraftSafeSelector( + // Type of `state` is set to `RootState`, no need to manually set the type + (state) => state.todos, + // ❌ Known limitation: Parameter types are not inferred in this scenario + // so you will have to manually annotate them. + (todos: Todo[]) => todos.map(({ id }) => id), +) +``` diff --git a/docs/reference/redux-toolkit/createSlice.mdx b/docs/reference/redux-toolkit/createSlice.mdx new file mode 100644 index 00000000..71c43bf3 --- /dev/null +++ b/docs/reference/redux-toolkit/createSlice.mdx @@ -0,0 +1,664 @@ +--- +id: createSlice +title: createSlice +sidebar_label: createSlice +hide_title: true +--- + +  + +# `createSlice` + +A function that accepts an initial state, an object of reducer functions, and a "slice name", +and automatically generates action creators and action types that correspond to the reducers and state. + +This API is the standard approach for writing Redux logic. + +Internally, it uses [`createAction`](./createAction.mdx) and [`createReducer`](./createReducer.mdx), so +you may also use [Immer](../usage/immer-reducers.md) to write "mutating" immutable updates: + +```ts +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +interface CounterState { + value: number +} + +const initialState = { value: 0 } satisfies CounterState as CounterState + +const counterSlice = createSlice({ + name: 'counter', + initialState, + reducers: { + increment(state) { + state.value++ + }, + decrement(state) { + state.value-- + }, + incrementByAmount(state, action: PayloadAction) { + state.value += action.payload + }, + }, +}) + +export const { increment, decrement, incrementByAmount } = counterSlice.actions +export default counterSlice.reducer +``` + +## Parameters + +`createSlice` accepts a single configuration object parameter, with the following options: + +```ts no-transpile +function createSlice({ + // A name, used in action types + name: string, + // The initial state for the reducer + initialState: State, + // An object of "case reducers". Key names will be used to generate actions. + reducers: Record, + // A "builder callback" function used to add more reducers + extraReducers?: (builder: ActionReducerMapBuilder) => void, + // A preference for the slice reducer's location, used by `combineSlices` and `slice.selectors`. Defaults to `name`. + reducerPath?: string, + // An object of selectors, which receive the slice's state as their first parameter. + selectors?: Record any>, +}) +``` + +### `initialState` + +The initial state value for this slice of state. + +This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`. + +### `name` + +A string name for this slice of state. Generated action type constants will use this as a prefix. + +### `reducers` + +An object containing Redux "case reducer" functions (functions intended to handle a specific action type, equivalent +to a single case statement in a switch). + +The keys in the object will be used to generate string action type constants, and these will show up in the Redux +DevTools Extension when they are dispatched. Also, if any other part of the application happens to dispatch an action +with the exact same type string, the corresponding reducer will be run. Therefore, you should give the functions +descriptive names. + +This object will be passed to [`createReducer`](./createReducer.mdx), so the reducers may safely "mutate" the +state they are given. + +```ts +import { createSlice } from '@reduxjs/toolkit' + +const counterSlice = createSlice({ + name: 'counter', + initialState: 0, + reducers: { + increment: (state) => state + 1, + }, +}) +// Will handle the action type `'counter/increment'` +``` + +#### Customizing Generated Action Creators + +If you need to customize the creation of the payload value of an action creator by means of a [`prepare callback`](./createAction.mdx#using-prepare-callbacks-to-customize-action-contents), the value of the appropriate field of the `reducers` argument object should be an object instead of a function. This object must contain two properties: `reducer` and `prepare`. The value of the `reducer` field should be the case reducer function while the value of the `prepare` field should be the prepare callback function: + +```ts +import { createSlice, nanoid } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +interface Item { + id: string + text: string +} + +const todosSlice = createSlice({ + name: 'todos', + initialState: [] as Item[], + reducers: { + addTodo: { + reducer: (state, action: PayloadAction) => { + state.push(action.payload) + }, + prepare: (text: string) => { + const id = nanoid() + return { payload: { id, text } } + }, + }, + }, +}) +``` + +### The `reducers` "creator callback" notation + +Alternatively, the `reducers` field can be a callback which receives a "create" object. + +The main benefit of this is that you can create [async thunks](./createAsyncThunk) as part of your slice (though for bundle size reasons, you [need a bit of setup for this](#createasyncthunk)). Types are also slightly simplified for prepared reducers. + +```ts title="Creator callback for reducers" +import { createSlice, nanoid } from '@reduxjs/toolkit' + +interface Item { + id: string + text: string +} + +interface TodoState { + loading: boolean + todos: Item[] +} + +const todosSlice = createSlice({ + name: 'todos', + initialState: { + loading: false, + todos: [], + } satisfies TodoState as TodoState, + reducers: (create) => ({ + deleteTodo: create.reducer((state, action) => { + state.todos.splice(action.payload, 1) + }), + addTodo: create.preparedReducer( + (text: string) => { + const id = nanoid() + return { payload: { id, text } } + }, + // action type is inferred from prepare callback + (state, action) => { + state.todos.push(action.payload) + }, + ), + fetchTodo: create.asyncThunk( + async (id: string, thunkApi) => { + const res = await fetch(`myApi/todos?id=${id}`) + return (await res.json()) as Item + }, + { + pending: (state) => { + state.loading = true + }, + rejected: (state, action) => { + state.loading = false + }, + fulfilled: (state, action) => { + state.loading = false + state.todos.push(action.payload) + }, + }, + ), + }), +}) + +export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions +``` + +#### Create Methods + +#### `create.reducer` + +A standard slice case reducer. + +**Parameters** + +- **reducer** The slice case reducer to use. + +```ts no-transpile +create.reducer((state, action) => { + state.todos.push(action.payload) +}) +``` + +#### `create.preparedReducer` + +A [prepared](#customizing-generated-action-creators) reducer, to customize the action creator. + +**Parameters** + +- **prepareAction** The [`prepare callback`](./createAction#using-prepare-callbacks-to-customize-action-contents). +- **reducer** The slice case reducer to use. + +The action passed to the case reducer will be inferred from the prepare callback's return. + +```ts no-transpile +create.preparedReducer( + (text: string) => { + const id = nanoid() + return { payload: { id, text } } + }, + (state, action) => { + state.todos.push(action.payload) + }, +) +``` + +#### `create.asyncThunk` + +Creates an async thunk instead of an action creator. + +:::caution Setup + +To avoid pulling `createAsyncThunk` into the bundle size of `createSlice` by default, some extra setup is required to use `create.asyncThunk`. + +The version of `createSlice` exported from RTK will throw an error if `create.asyncThunk` is called. + +Instead, import `buildCreateSlice` and `asyncThunkCreator`, and create your own version of `createSlice`: + +```ts +import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit' + +export const createAppSlice = buildCreateSlice({ + creators: { asyncThunk: asyncThunkCreator }, +}) +``` + +Then import this `createAppSlice` as needed instead of the exported version from RTK. + +::: + +**Parameters** + +- **payloadCreator** The thunk [payload creator](./createAsyncThunk#payloadcreator). +- **config** The configuration object. (optional) + +The configuration object can contain case reducers for each of the [lifecycle actions](./createAsyncThunk#promise-lifecycle-actions) (`pending`, `fulfilled`, and `rejected`), as well as a `settled` reducer that will run for both fulfilled and rejected actions (note that this will run _after_ any provided `fulfilled`/`rejected` reducers. Conceptually it can be thought of like a `finally` block.). + +Each case reducer will be attached to the slice's `caseReducers` object, e.g. `slice.caseReducers.fetchTodo.fulfilled`. + +The configuration object can also contain [`options`](./createAsyncThunk#options). + +```ts no-transpile +create.asyncThunk( + async (id: string, thunkApi) => { + const res = await fetch(`myApi/todos?id=${id}`) + return (await res.json()) as Item + }, + { + pending: (state) => { + state.loading = true + }, + rejected: (state, action) => { + state.error = action.payload ?? action.error + }, + fulfilled: (state, action) => { + state.todos.push(action.payload) + }, + settled: (state, action) => { + state.loading = false + } + options: { + idGenerator: uuid, + }, + } +) +``` + +:::note + +Typing for the `create.asyncThunk` works in the same way as [`createAsyncThunk`](../usage/usage-with-typescript#createasyncthunk), with one key difference. + +A type for `state` and/or `dispatch` _cannot_ be provided as part of the `ThunkApiConfig`, as this would cause circular types. + +Instead, it is necessary to assert the type when needed - `getState() as RootState`. You may also include an explicit return type for the payload function as well, in order to break the circular type inference cycle. + +```ts no-transpile +create.asyncThunk( + // highlight-start + // may need to include an explicit return type + async (id: string, thunkApi): Promise => { + // Cast types for `getState` and `dispatch` manually + const state = thunkApi.getState() as RootState + const dispatch = thunkApi.dispatch as AppDispatch + // highlight-end + try { + const todo = await fetchTodo() + return todo + } catch (e) { + throw thunkApi.rejectWithValue({ + error: 'Oh no!', + }) + } + }, +) +``` + +For common thunk API configuration options, a [`withTypes` helper](../usage/usage-with-typescript#defining-a-pre-typed-createasyncthunk) is provided: + +```ts no-transpile +reducers: (create) => { + const createAThunk = create.asyncThunk.withTypes<{ + rejectValue: { error: string } + }>() + + return { + fetchTodo: createAThunk(async (id, thunkApi) => { + throw thunkApi.rejectWithValue({ + error: 'Oh no!', + }) + }), + fetchTodos: createAThunk(async (id, thunkApi) => { + throw thunkApi.rejectWithValue({ + error: 'Oh no, not again!', + }) + }), + } +} +``` + +::: + +### `extraReducers` + +Conceptually, each slice reducer "owns" its slice of state. There's also a natural correspondence between the update logic defined inside `reducers`, and the action types that are generated based on those. + +However, there are many times that a Redux slice may also need to update its own state in response to action types that were defined elsewhere in the application (such as clearing many different kinds of data when a "user logged out" action is dispatched). This can include action types defined by another `createSlice` call, actions generated by a `createAsyncThunk`, RTK Query endpoint matchers, or any other action. In addition, one of the key concepts of Redux is that many slice reducers can independently respond to the same action type. + +**`extraReducers` allows `createSlice` to respond and update its own state in response to other action types besides the types it has generated.** + +As with the `reducers` field, each case reducer in `extraReducers` is [wrapped in Immer and may use "mutating" syntax to safely update the state inside](../usage/immer-reducers.md). + +However, unlike the `reducers` field, each individual case reducer inside of `extraReducers` will _not_ generate a new action type or action creator. + +If two fields from `reducers` and `extraReducers` happen to end up with the same action type string, the function from `reducers` will be used to handle that action type. + +#### The `extraReducers` "builder callback" notation + +Similar to `createReducer`, the `extraReducers` field uses a "builder callback" notation to define handlers for specific action types, matching against a range of actions, or handling a default case. This is conceptually similar to a switch statement, but with better TS support as it can infer the action type from the provided action creator. It's particularly useful for working with actions produced by `createAction` and `createAsyncThunk`. + +[examples](docblock://createSlice.ts?token=CreateSliceOptions.extraReducers) + +See [the "Builder Callback Notation" section of the `createReducer` reference](./createReducer.mdx#usage-with-the-builder-callback-notation) for details on how to use `builder.addCase`, `builder.addMatcher`, and `builder.addDefaultCase` + +### `reducerPath` + +Indicates a preference of where the slice should be located. Defaults to [`name`](#name). + +This is used by `combineSlices` and the default generated `slice.selectors`. + +### `selectors` + +A set of selectors that receive the slice state as their first parameter, and any other parameters. + +Each selector will have a corresponding key in the resulting [`selectors`](#selectors-1) object. + +:::caution Circular types + +It's fairly common to have selectors that use other selectors. This is still possible with slice selectors, but defining a selector without a return type can cause a circular type inference problem: + +```ts no-transpile +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 }, + reducers: {}, + selectors: { + selectValue: (state) => state.value, + // highlight-start + // this creates a cycle, because it's inferring a type from the object we're creating here + selectTimes: (state, times = 1) => + counterSlice.getSelectors().selectValue(state) * times, + // highlight-end + }, +}) +``` + +This cycle can be fixed by providing an explicit return type for the selector: + +```ts no-transpile +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 }, + reducers: {}, + selectors: { + selectValue: (state) => state.value, + // highlight-start + // explicit return type means cycle is broken + selectTimes: (state, times = 1): number => + counterSlice.getSelectors().selectValue(state) * times, + // highlight-end + }, +}) +``` + +This limitation may be also encountered when using a slice's `asyncThunk` creator. +In the same way, the issue is resolved by explicitly providing a type somewhere in the chain and breaking the cycle. + +```ts no-transpile +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 }, + reducers: (create) => ({ + getCountData: create.asyncThunk(async (_arg, { getState }) => { + const currentCount = counterSlice.selectors.selectValue( + getState() as RootState, + ) + // highlight-start + // this would cause a circular type, but the type annotation breaks the circle + const result: Response = await fetch('api/' + currentCount) + // highlight-end + return result.json() + }), + }), + selectors: { + selectValue: (state) => state.value, + }, +}) +``` + +::: + +## Return Value + +`createSlice` will return an object that looks like: + +```ts no-transpile +{ + name: string, + reducer: ReducerFunction, + actions: Record, + caseReducers: Record. + getInitialState: () => State, + reducerPath: string, + selectSlice: Selector; + selectors: Record, + getSelectors: (selectState: (rootState: RootState) => State) => Record + injectInto: (injectable: Injectable, config?: InjectConfig & { reducerPath?: string }) => InjectedSlice +} +``` + +Each function defined in the `reducers` argument will have a corresponding action creator generated using [`createAction`](./createAction.mdx) +and included in the result's `actions` field using the same function name. + +The generated `reducer` function is suitable for passing to the Redux `combineReducers` function as a "slice reducer". + +You may want to consider destructuring the action creators and exporting them individually, for ease of searching +for references in a larger codebase. + +The functions passed to the `reducers` parameter can be accessed through the `caseReducers` return field. This can be particularly useful for testing or direct access to reducers created inline. + +Result's function `getInitialState` provides access to the initial state value given to the slice. If a lazy state initializer was provided, it will be called and a fresh value returned. + +`injectInto` creates an instance of the slice that is aware it's been injected - see [`combineSlices`](./combineSlices#slice-integration). + +:::note +The result object is conceptually similar to a +["Redux duck" code structure](https://redux.js.org/faq/code-structure#what-should-my-file-structure-look-like-how-should-i-group-my-action-creators-and-reducers-in-my-project-where-should-my-selectors-go). +The actual code structure you use is up to you, but it's worth keeping in mind that actions are not exclusively limited to a single slice. +Any part of the reducer logic can (and should!) respond to any dispatched action. +::: + +### Selectors + +Slice selectors are written to expect the slice's state as their first parameter, but the slice may be located anywhere inside the store's root state. + +As a result, there are two ways of getting final selectors: + +#### `selectors` + +Most commonly, the slice is reliably mounted under its [`reducerPath`](#reducerpath). + +Following this, the slice has a `selectSlice` selector attached, which assumes that the slice is located under `rootState[slice.reducerPath]`. + +`slice.selectors` then uses this selector to wrap each of the selectors provided. + +```ts +import { createSlice } from '@reduxjs/toolkit' + +interface CounterState { + value: number +} + +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 } satisfies CounterState as CounterState, + reducers: { + // omitted + }, + selectors: { + selectValue: (sliceState) => sliceState.value, + }, +}) + +console.log(counterSlice.selectSlice({ counter: { value: 2 } })) // { value: 2 } + +const { selectValue } = counterSlice.selectors + +console.log(selectValue({ counter: { value: 2 } })) // 2 +``` + +:::note + +The original selector passed is attached to the wrapped selector as `.unwrapped`. For example: + +```ts +import { createSlice, createSelector } from '@reduxjs/toolkit' + +interface CounterState { + value: number +} + +const counterSlice = createSlice({ + name: 'counter', + initialState: { value: 0 } satisfies CounterState as CounterState, + reducers: { + // omitted + }, + selectors: { + selectDouble: createSelector( + (sliceState: CounterState) => sliceState.value, + (value) => value * 2, + ), + }, +}) + +const { selectDouble } = counterSlice.selectors + +console.log(selectDouble({ counter: { value: 2 } })) // 4 +console.log(selectDouble({ counter: { value: 3 } })) // 6 +console.log(selectDouble.unwrapped.recomputations) // 2 +``` + +::: + +#### `getSelectors` + +`slice.getSelectors` is called with a single parameter, a `selectState` callback. This function should receive the store root state (or whatever you expect to call the resulting selectors with) and return the slice state. + +```ts no-transpile +const { selectValue } = counterSlice.getSelectors( + (rootState: RootState) => rootState.aCounter, +) + +console.log(selectValue({ aCounter: { value: 2 } })) // 2 +``` + +If no `selectState` callback is passed, selectors will be returned as is - expecting the slice state as their first parameter (the same as calling `slice.getSelectors(state => state)`). + +```ts no-transpile +const { selectValue } = counterSlice.getSelectors() + +console.log(selectValue({ value: 2 })) // 2 +``` + +:::note +The [`slice.selectors`](#selectors-2) object is the equivalent of calling + +```ts no-transpile +const { selectValue } = counterSlice.getSelectors(counterSlice.selectSlice) +// or +const { selectValue } = counterSlice.getSelectors( + (state: RootState) => state[counterSlice.reducerPath], +) +``` + +::: + +## Examples + +```ts +import { createSlice, createAction, configureStore } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers } from 'redux' + +const incrementBy = createAction('incrementBy') +const decrementBy = createAction('decrementBy') + +const counter = createSlice({ + name: 'counter', + initialState: 0 satisfies number as number, + reducers: { + increment: (state) => state + 1, + decrement: (state) => state - 1, + multiply: { + reducer: (state, action: PayloadAction) => state * action.payload, + prepare: (value?: number) => ({ payload: value || 2 }), // fallback if the payload is a falsy value + }, + }, + extraReducers: (builder) => { + builder.addCase(incrementBy, (state, action) => { + return state + action.payload + }) + builder.addCase(decrementBy, (state, action) => { + return state - action.payload + }) + }, +}) + +const user = createSlice({ + name: 'user', + initialState: { name: '', age: 20 }, + reducers: { + setUserName: (state, action) => { + state.name = action.payload // mutate the state all you want with immer + }, + }, + extraReducers: (builder) => { + builder.addCase(counter.actions.increment, (state, action) => { + state.age += 1 + }) + }, +}) + +const store = configureStore({ + reducer: { + counter: counter.reducer, + user: user.reducer, + }, +}) + +store.dispatch(counter.actions.increment()) +// -> { counter: 1, user: {name : '', age: 21} } +store.dispatch(counter.actions.increment()) +// -> { counter: 2, user: {name: '', age: 22} } +store.dispatch(counter.actions.multiply(3)) +// -> { counter: 6, user: {name: '', age: 22} } +store.dispatch(counter.actions.multiply()) +// -> { counter: 12, user: {name: '', age: 22} } +console.log(counter.actions.decrement.type) +// -> "counter/decrement" +store.dispatch(user.actions.setUserName('eric')) +// -> { counter: 12, user: { name: 'eric', age: 22} } +``` diff --git a/docs/reference/redux-toolkit/getDefaultEnhancers.mdx b/docs/reference/redux-toolkit/getDefaultEnhancers.mdx new file mode 100644 index 00000000..e44e9d49 --- /dev/null +++ b/docs/reference/redux-toolkit/getDefaultEnhancers.mdx @@ -0,0 +1,114 @@ +--- +id: getDefaultEnhancers +title: getDefaultEnhancers +sidebar_label: getDefaultEnhancers +hide_title: true +--- + +  + +# `getDefaultEnhancers` + +Returns an array containing the default list of enhancers. + +## Intended Usage + +By default, [`configureStore`](./configureStore.mdx) adds some enhancers to the Redux store setup automatically. + +```js +const store = configureStore({ + reducer: rootReducer, +}) + +// Store has enhancers added, because the enhancer list was not customized +``` + +If you want to customise the list of enhancers, you can supply an array of enhancer functions to `configureStore`: + +```js +const store = configureStore({ + reducer: rootReducer, + enhancers: () => new Tuple(offline(offlineConfig)), +}) + +// store specifically has the offline enhancer applied +``` + +However, when you supply the `enhancer` option, you are responsible for defining _all_ the enhancers you want added +to the store (with the exception of the [devtools](./configureStore#devtools)). `configureStore` will not add any extra enhancers beyond what you listed, **including the middleware enhancer**. + +`getDefaultEnhancers` is useful if you want to add some custom enhancers, but also still want to have the default +enhancers added as well: + +```ts no-transpile +import { configureStore } from '@reduxjs/toolkit' +import { offline } from '@redux-offline/redux-offline' +import offlineConfig from '@redux-offline/redux-offline/lib/defaults' + +import rootReducer from './reducer' + +const store = configureStore({ + reducer: rootReducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers().concat(offline(offlineConfig)), +}) + +// Store has all of the default middleware + enhancers added, _plus_ the offline enhancer +``` + +## Included Default Enhancers + +The resulting array will always contain the `applyMiddleware` enhancer created based on the `configureStore`'s `middleware` field. + +Additionally, the [`autoBatchEnhancer`](./autoBatchEnhancer.mdx) is included, to allow for "batching" of low priority action updates. This is used by [RTK Query](/rtk-query/overview.md) and should improve performance when using it. + +Currently, the return value is + +```js +const enhancers = [applyMiddleware, autoBatchEnhancer] +``` + +## Customising the Included Enhancers + +`getDefaultEnhancers` accepts an options object that allows customizing each enhancer (excluding the middleware enhancer) in two ways: + +- Each enhancer can be excluded from the result array by passing `false` for its corresponding field +- Each enhancer can have its options customized by passing the matching options object for its corresponding field + +This example shows customising the autoBatch enhancer: + +```ts +// file: reducer.ts noEmit + +export default function rootReducer(state = {}, action: any) { + return state +} + +// file: store.ts +import rootReducer from './reducer' +import { configureStore } from '@reduxjs/toolkit' + +const store = configureStore({ + reducer: rootReducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ + autoBatch: { type: 'tick' }, + }), +}) +``` + +## API Reference + +```ts no-transpile +interface AutoBatchOptions { + // see "autoBatchEnhancer" page for options +} + +interface GetDefaultEnhancersOptions { + autoBatch?: boolean | AutoBatchOptions +} + +function getDefaultEnhancers>( + options: GetDefaultEnhancersOptions = {}, +): EnhancerArray<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions }>]> +``` diff --git a/docs/reference/redux-toolkit/getDefaultMiddleware.mdx b/docs/reference/redux-toolkit/getDefaultMiddleware.mdx new file mode 100644 index 00000000..921c27c2 --- /dev/null +++ b/docs/reference/redux-toolkit/getDefaultMiddleware.mdx @@ -0,0 +1,171 @@ +--- +id: getDefaultMiddleware +title: getDefaultMiddleware +sidebar_label: getDefaultMiddleware +hide_title: true +--- + +  + +# `getDefaultMiddleware` + +Returns an array containing the default list of middleware. + +## Intended Usage + +By default, [`configureStore`](./configureStore.mdx) adds some middleware to the Redux store setup automatically. + +```js +const store = configureStore({ + reducer: rootReducer, +}) + +// Store has middleware added, because the middleware list was not customized +``` + +If you want to customize the list of middleware, you can supply an array of middleware functions to `configureStore`: + +```js +const store = configureStore({ + reducer: rootReducer, + middleware: () => new Tuple(thunk, logger), +}) + +// Store specifically has the thunk and logger middleware applied +``` + +However, when you supply the `middleware` option, you are responsible for defining _all_ the middleware you want added +to the store. `configureStore` will not add any extra middleware beyond what you listed. + +`getDefaultMiddleware` is useful if you want to add some custom middleware, but also still want to have the default +middleware added as well: + +```ts no-transpile +import { configureStore } from '@reduxjs/toolkit' + +import logger from 'redux-logger' + +import rootReducer from './reducer' + +const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), +}) + +// Store has all of the default middleware added, _plus_ the logger middleware +``` + +It is preferable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `Tuple` instead of the array spread operator, as the latter can lose valuable TS type information under some circumstances. + +## Included Default Middleware + +### Development + +One of the goals of Redux Toolkit is to provide opinionated defaults and prevent common mistakes. As part of that, +`getDefaultMiddleware` includes some middleware that are added **in development builds of your app only** to +provide runtime checks for three common issues: + +- [Immutability check middleware](./immutabilityMiddleware.mdx): deeply compares + state values for mutations. It can detect mutations in reducers during a dispatch, and also mutations that occur between + dispatches (such as in a component or a selector). When a mutation is detected, it will throw an error and indicate the key + path for where the mutated value was detected in the state tree. (Forked from [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant).) + +- [Serializability check middleware](./serializabilityMiddleware.mdx): a custom middleware created specifically for use in Redux Toolkit. Similar in + concept to `immutable-state-invariant`, but deeply checks your state tree and your actions for non-serializable values + such as functions, Promises, Symbols, and other non-plain-JS-data values. When a non-serializable value is detected, a + console error will be printed with the key path for where the non-serializable value was detected. + +- [Action creator check middleware](./actionCreatorMiddleware.mdx): another custom middleware created specifically for use in Redux Toolkit. + Identifies when an action creator was mistakenly dispatched without being called, and warns to console with the action type. + +In addition to these development tool middleware, it also adds [`redux-thunk`](https://github.com/reduxjs/redux-thunk) +by default, since thunks are the basic recommended side effects middleware for Redux. + +Currently, the return value is: + +```js +const middleware = [ + actionCreatorInvariant, + immutableStateInvariant, + thunk, + serializableStateInvariant, +] +``` + +### Production + +Currently, the return value is: + +```js +const middleware = [thunk] +``` + +## Customizing the Included Middleware + +`getDefaultMiddleware` accepts an options object that allows customizing each middleware in two ways: + +- Each middleware can be excluded from the result array by passing `false` for its corresponding field +- Each middleware can have its options customized by passing the matching options object for its corresponding field + +This example shows excluding the serializable state check middleware, and passing a specific value for the thunk +middleware's "extra argument": + +```ts +// file: reducer.ts noEmit + +export default function rootReducer(state = {}, action: any) { + return state +} + +// file: api.ts noEmit + +export declare const myCustomApiService: any + +// file: store.ts + +import { configureStore } from '@reduxjs/toolkit' +import rootReducer from './reducer' +import { myCustomApiService } from './api' + +const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: myCustomApiService, + }, + serializableCheck: false, + }), +}) +``` + +## API Reference + +```ts no-transpile +interface ThunkOptions { + extraArgument: E +} + +interface ImmutableStateInvariantMiddlewareOptions { + // See "Immutability Middleware" page for definition +} + +interface SerializableStateInvariantMiddlewareOptions { + // See "Serializability Middleware" page for definition +} + +interface ActionCreatorInvariantMiddlewareOptions { + // See "Action Creator Middleware" page for definition +} + +interface GetDefaultMiddlewareOptions { + thunk?: boolean | ThunkOptions + immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions + serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions + actionCreatorCheck?: boolean | ActionCreatorInvariantMiddlewareOptions +} + +function getDefaultMiddleware( + options: GetDefaultMiddlewareOptions = {}, +): Middleware<{}, S>[] +``` diff --git a/docs/reference/redux-toolkit/immutabilityMiddleware.mdx b/docs/reference/redux-toolkit/immutabilityMiddleware.mdx new file mode 100644 index 00000000..6360e541 --- /dev/null +++ b/docs/reference/redux-toolkit/immutabilityMiddleware.mdx @@ -0,0 +1,142 @@ +--- +id: immutabilityMiddleware +title: Immutability Middleware +sidebar_label: Immutability Middleware +hide_title: true +--- + +  + +# Immutability Middleware + +A port of the [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant) middleware, customized for use with Redux Toolkit. Any detected mutations will be thrown as errors. + +This middleware is added to the store by default by [`configureStore`](./configureStore.mdx) and [`getDefaultMiddleware`](./getDefaultMiddleware.mdx). + +You can customize the behavior of this middleware by passing any of the supported options as the `immutableCheck` value for `getDefaultMiddleware`. + +## Options + +```ts no-transpile +type IsImmutableFunc = (value: any) => boolean + +interface ImmutableStateInvariantMiddlewareOptions { + /** + Callback function to check if a value is considered to be immutable. + This function is applied recursively to every value contained in the state. + The default implementation will return true for primitive types + (like numbers, strings, booleans, null and undefined). + */ + isImmutable?: IsImmutableFunc + /** + An array of dot-separated path strings or RegExps that match named nodes from + the root state to ignore when checking for immutability. + Defaults to undefined + */ + ignoredPaths?: (string | RegExp)[] + /** Print a warning if checks take longer than N ms. Default: 32ms */ + warnAfter?: number +} +``` + +## Exports + +### `createImmutableStateInvariantMiddleware` + +Creates an instance of the immutability check middleware, with the given options. + +You will most likely not need to call this yourself, as `getDefaultMiddleware` already does so. + +Example: + +```ts +// file: exampleSlice.ts + +import { createSlice } from '@reduxjs/toolkit' + +export const exampleSlice = createSlice({ + name: 'example', + initialState: { + user: 'will track changes', + ignoredPath: 'single level', + ignoredNested: { + one: 'one', + two: 'two', + }, + }, + reducers: {}, +}) + +export default exampleSlice.reducer + +// file: store.ts + +import { + configureStore, + createImmutableStateInvariantMiddleware, + Tuple, +} from '@reduxjs/toolkit' + +import exampleSliceReducer from './exampleSlice' + +const immutableInvariantMiddleware = createImmutableStateInvariantMiddleware({ + ignoredPaths: ['ignoredPath', 'ignoredNested.one', 'ignoredNested.two'], +}) + +const store = configureStore({ + reducer: exampleSliceReducer, + // Note that this will replace all default middleware + middleware: () => new Tuple(immutableInvariantMiddleware), +}) +``` + +doing the same without removing all other middlewares, using [getDetfaultMiddleware](./getDefaultMiddleware): + +```ts +// file: exampleSlice.ts noEmit + +import { createSlice } from '@reduxjs/toolkit' + +export const exampleSlice = createSlice({ + name: 'example', + initialState: { + user: 'will track changes', + ignoredPath: 'single level', + ignoredNested: { + one: 'one', + two: 'two', + }, + }, + reducers: {}, +}) + +export default exampleSlice.reducer + +// file: store.ts +import { configureStore } from '@reduxjs/toolkit' + +import exampleSliceReducer from './exampleSlice' + +const store = configureStore({ + reducer: exampleSliceReducer, + // This replaces the original default middleware with the customized versions + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + immutableCheck: { + ignoredPaths: ['ignoredPath', 'ignoredNested.one', 'ignoredNested.two'], + }, + }), +}) +``` + +### `isImmutableDefault` + +Default implementation of the "is this value immutable?" check. Currently implemented as: + +```js +return ( + typeof value !== 'object' || value === null || typeof value === 'undefined' +) +``` + +This will return true for primitive types (like numbers, strings, booleans, null and undefined) diff --git a/docs/reference/redux-toolkit/matching-utilities.mdx b/docs/reference/redux-toolkit/matching-utilities.mdx new file mode 100644 index 00000000..4c9323ac --- /dev/null +++ b/docs/reference/redux-toolkit/matching-utilities.mdx @@ -0,0 +1,287 @@ +--- +id: matching-utilities +title: Matching Utilities +sidebar_label: Matching Utilities +hide_title: true +--- + +  + +# Matching Utilities + +Redux Toolkit exports several type-safe action matching utilities that you can leverage when checking for specific kinds of actions. These are primarily useful for the `builder.addMatcher()` cases in `createSlice` and `createReducer`, as well as when writing custom middleware. + +### General Purpose + +- [`isAllOf`](#isallof) - returns true when **all** conditions are met +- [`isAnyOf`](#isanyof) - returns true when **at least one of** the conditions are met + +### `createAsyncThunk`-specific matchers + +All these matchers can either be called with one or more thunks as arguments, in which case they will return a matcher function for that condition and thunks, or with one actions, in which case they will match for any thunk action with said condition. + +- [`isAsyncThunkAction`](#isasyncthunkaction) - accepts one or more action creators and returns true when all match +- [`isPending`](#ispending) - accepts one or more action creators and returns true when all match +- [`isFulfilled`](#isfulfilled) - accepts one or more action creators and returns true when all match +- [`isRejected`](#isrejected) - accepts one or more action creators and returns true when all match +- [`isRejectedWithValue`](#isrejectedwithvalue) - accepts one or more action creators and returns true when all match + +## `isAllOf` + +A higher-order function that accepts one or more of: + +- `redux-toolkit` action creator functions such as the ones produced by: + - [`createAction`](./createAction.mdx) + - [`createSlice`](./createSlice.mdx#return-value) + - [`createAsyncThunk`](./createAsyncThunk.mdx#promise-lifecycle-actions) +- type guard functions +- custom action creator functions that have a `.match` property that is a type guard + +It will return a type guard function that returns `true` if _all_ of the provided functions match. + +## `isAnyOf` + +Accepts the same inputs as `isAllOf` and will return a type guard function that returns `true` if at least one of the provided functions match. + +## `isAsyncThunkAction` + +A higher-order function that returns a type guard function that may be used to check whether an action was created by [`createAsyncThunk`](./createAsyncThunk.mdx). + +```ts title="isAsyncThunkAction usage" +import { isAsyncThunkAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' +import { requestThunk1, requestThunk2 } from '@virtual/matchers' + +const isARequestAction = isAsyncThunkAction(requestThunk1, requestThunk2) + +function handleRequestAction(action: UnknownAction) { + if (isARequestAction(action)) { + // action is an action dispatched by either `requestThunk1` or `requestThunk2` + } +} +``` + +## `isPending` + +A higher-order function that returns a type guard function that may be used to check whether an action is a 'pending' action creator from the `createAsyncThunk` promise lifecycle. + +```ts title="isPending usage" +import { isPending } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' +import { requestThunk1, requestThunk2 } from '@virtual/matchers' + +const isAPendingAction = isPending(requestThunk1, requestThunk2) + +function handlePendingAction(action: UnknownAction) { + if (isAPendingAction(action)) { + // action is a pending action dispatched by either `requestThunk1` or `requestThunk2` + } +} +``` + +## `isFulfilled` + +A higher-order function that returns a type guard function that may be used to check whether an action is a 'fulfilled'' action creator from the `createAsyncThunk` promise lifecycle. + +```ts title="isFulfilled usage" +import { isFulfilled } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' +import { requestThunk1, requestThunk2 } from '@virtual/matchers' + +const isAFulfilledAction = isFulfilled(requestThunk1, requestThunk2) + +function handleFulfilledAction(action: UnknownAction) { + if (isAFulfilledAction(action)) { + // action is a fulfilled action dispatched by either `requestThunk1` or `requestThunk2` + } +} +``` + +## `isRejected` + +A higher-order function that returns a type guard function that may be used to check whether an action is a 'rejected' action creator from the `createAsyncThunk` promise lifecycle. + +```ts title="isRejected usage" +import { isRejected } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' +import { requestThunk1, requestThunk2 } from '@virtual/matchers' + +const isARejectedAction = isRejected(requestThunk1, requestThunk2) + +function handleRejectedAction(action: UnknownAction) { + if (isARejectedAction(action)) { + // action is a rejected action dispatched by either `requestThunk1` or `requestThunk2` + } +} +``` + +## `isRejectedWithValue` + +A higher-order function that returns a type guard function that may be used to check whether an action is a 'rejected' action creator from the `createAsyncThunk` promise lifecycle that was created by [`rejectWithValue`](./createAsyncThunk.mdx#handling-thunk-errors). + +```ts title="isRejectedWithValue usage" +import { isRejectedWithValue } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' +import { requestThunk1, requestThunk2 } from '@virtual/matchers' + +const isARejectedWithValueAction = isRejectedWithValue( + requestThunk1, + requestThunk2, +) + +function handleRejectedWithValueAction(action: UnknownAction) { + if (isARejectedWithValueAction(action)) { + // action is a rejected action dispatched by either `requestThunk1` or `requestThunk2` + // where rejectWithValue was used + } +} +``` + +## Using matchers to reduce code complexity, duplication and boilerplate + +When using the `builder` pattern to construct a reducer, we add cases or matchers one at a time. However, by using `isAnyOf` or `isAllOf`, +we're able to easily use the same matcher for several cases in a type-safe manner. + +First, let's examine an unnecessarily complex example: + +```ts title="Example without using a matcher utility" +import { createAsyncThunk, createReducer } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +interface Data { + isInteresting: boolean + isSpecial: boolean +} + +interface Special extends Data { + isSpecial: true +} + +interface Interesting extends Data { + isInteresting: true +} + +function isSpecial( + action: PayloadAction, +): action is PayloadAction { + return action.payload.isSpecial +} + +function isInteresting( + action: PayloadAction, +): action is PayloadAction { + return action.payload.isInteresting +} + +interface ExampleState { + isSpecial: boolean + isInteresting: boolean +} + +const initialState = { + isSpecial: false, + isInteresting: false, +} satisfies ExampleState as ExampleState + +export const isSpecialAndInterestingThunk = createAsyncThunk( + 'isSpecialAndInterestingThunk', + () => { + return { + isSpecial: true, + isInteresting: true, + } + }, +) + +// This has unnecessary complexity +const loadingReducer = createReducer(initialState, (builder) => { + builder.addCase(isSpecialAndInterestingThunk.fulfilled, (state, action) => { + if (isSpecial(action)) { + state.isSpecial = true + } + if (isInteresting(action)) { + state.isInteresting = true + } + }) +}) +``` + +In this scenario, we can use `isAllOf` to simplify our code and reduce some of the boilerplate. + +```ts title="Refactoring with isAllOf" +import { createReducer, isAllOf } from '@reduxjs/toolkit' +import { + isSpecialAndInterestingThunk, + initialState, + isSpecial, + isInteresting, +} from '@virtual/matchers' // This is a fake pkg that provides the types shown above +import type { Data } from '@virtual/matchers' // This is a fake pkg that provides the types shown above + +const loadingReducer = createReducer(initialState, (builder) => { + builder + .addMatcher( + isAllOf(isSpecialAndInterestingThunk.fulfilled, isSpecial), + (state, action) => { + state.isSpecial = true + }, + ) + .addMatcher( + isAllOf(isSpecialAndInterestingThunk.fulfilled, isInteresting), + (state, action) => { + state.isInteresting = true + }, + ) +}) +``` + +## Using matchers as a TypeScript Type Guard + +The function returned by `isAllOf` and `isAnyOf` can also be used as a TypeScript type guard in other contexts. + +```ts title="Using isAllOf as a type guard" +import { isAllOf } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { isSpecial, isInteresting } from '@virtual/matchers' // This is a fake pkg that provides the types shown above +import type { Data } from '@virtual/matchers' // This is a fake pkg that provides the types shown above + +const isSpecialAndInteresting = isAllOf(isSpecial, isInteresting) + +function someFunction(action: PayloadAction) { + if (isSpecialAndInteresting(action)) { + // "action" will be correctly typed as: + // `PayloadAction & PayloadAction` + } +} +``` + +```ts title="Using isAnyOf as a type guard" +import { isAnyOf } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { Data, isSpecial, isInteresting } from '@virtual/matchers' // this is a fake pkg that provides the types shown above + +const isSpecialOrInteresting = isAnyOf(isSpecial, isInteresting) + +function someFunction(action: PayloadAction) { + if (isSpecialOrInteresting(action)) { + // "action" will be correctly typed as: + // `PayloadAction | PayloadAction` + } +} +``` + +## Example + + diff --git a/docs/reference/redux-toolkit/otherExports.mdx b/docs/reference/redux-toolkit/otherExports.mdx new file mode 100644 index 00000000..bc52e040 --- /dev/null +++ b/docs/reference/redux-toolkit/otherExports.mdx @@ -0,0 +1,110 @@ +--- +id: other-exports +title: Other Exports +sidebar_label: Other Exports +hide_title: true +--- + +  + +# Other Exports + +Redux Toolkit exports some of its internal utilities, and re-exports additional functions from other dependencies as well. + +### `nanoid` + +An inlined copy of [`nanoid/nonsecure`](https://github.com/ai/nanoid). Generates a non-cryptographically-secure random ID string. `createAsyncThunk` uses this by default for request IDs. May also be useful for other cases as well. + +```ts +import { nanoid } from '@reduxjs/toolkit' + +console.log(nanoid()) +// 'dgPXxUz_6fWIQBD8XmiSy' +``` + +### `miniSerializeError` + +The default error serialization function used by `createAsyncThunk`, based on https://github.com/sindresorhus/serialize-error. If its argument is an object (such as an `Error` instance), it returns a plain JS `SerializedError` object that copies over any of the listed fields. Otherwise, it returns a stringified form of the value: `{ message: String(value) }`. + +```ts no-transpile +export interface SerializedError { + name?: string + message?: string + stack?: string + code?: string +} + +export function miniSerializeError(value: any): SerializedError {} +``` + +### `copyWithStructuralSharing` + +A utility that will recursively merge two similar objects together, preserving existing references if the values appear to be the same. This is used internally to help ensure that re-fetched data keeps using the same references unless the new data has actually changed, to avoid unnecessary re-renders. Otherwise, every re-fetch would likely cause the entire dataset to be replaced and all consuming components to always re-render. + +If either of the inputs are not plain JS objects or arrays, the new value is returned. + +```ts no-transpile +export function copyWithStructuralSharing(oldObj: any, newObj: T): T +export function copyWithStructuralSharing(oldObj: any, newObj: any): any {} +``` + +## Exports from Other Libraries + +### `createNextState` + +The default immutable update function from the [`immer` library](https://immerjs.github.io/immer/), re-exported here as `createNextState` (also commonly referred to as [`produce`](https://immerjs.github.io/immer/produce)) + +### `current` + +[The `current` function](https://immerjs.github.io/immer/current) from the [`immer` library](https://immerjs.github.io/immer/), which takes a snapshot of the current state of a draft and finalizes it (but without freezing). Current is a great utility to print the current state during debugging, and the output of `current` can also be safely leaked outside the producer. + +```ts +import { createReducer, createAction, current } from '@reduxjs/toolkit' + +interface Todo { + //... +} +const addTodo = createAction('addTodo') + +const initialState = [] satisfies Todo[] as Todo[] + +const todosReducer = createReducer(initialState, (builder) => { + builder.addCase(addTodo, (state, action) => { + state.push(action.payload) + console.log(current(state)) + }) +}) +``` + +### `original` + +[The `original` function](https://immerjs.github.io/immer/original) from the [`immer` library](https://immerjs.github.io/immer/), which returns the original object. This is particularly useful for referential equality check in reducers. + +### `isDraft` + +[The `isDraft` function](https://immerjs.github.io/immer/original) from the [`immer` library](https://immerjs.github.io/immer/), which checks to see if a given value is a Proxy-wrapped "draft" state. + +### `freeze` + +[The `freeze` function](https://immerjs.github.io/immer/api) from the [`immer` library](https://immerjs.github.io/immer/), which [freezes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) draftable objects. + +### `combineReducers` + +Redux's [`combineReducers`](https://redux.js.org/api/combinereducers), re-exported for convenience. While `configureStore` calls this internally, you may wish to call it yourself to compose multiple levels of slice reducers. + +### `compose` + +Redux's [`compose`](https://redux.js.org/api/compose). It composes functions from right to left. +This is a functional programming utility. You might want to use it to apply several store custom enhancers/ functions in a row. + +### `bindActionCreators` + +Redux's [`bindActionCreators`](https://redux.js.org/api/bindactioncreators). It wraps action creators with `dispatch()` so that they dispatch immediately when called. + +### `createStore` + +Redux's [`createStore`](https://redux.js.org/api/createstore). You should not need to use this directly. + +### `applyMiddleware` + +Redux's [`applyMiddleware`](https://redux.js.org/api/applymiddleware). You should not need to use this directly. diff --git a/docs/reference/redux-toolkit/serializabilityMiddleware.mdx b/docs/reference/redux-toolkit/serializabilityMiddleware.mdx new file mode 100644 index 00000000..29893bad --- /dev/null +++ b/docs/reference/redux-toolkit/serializabilityMiddleware.mdx @@ -0,0 +1,146 @@ +--- +id: serializabilityMiddleware +title: Serializability Middleware +sidebar_label: Serializability Middleware +hide_title: true +--- + +  + +# Serializability Middleware + +A custom middleware that detects if any non-serializable values have been included in state or dispatched actions, modeled after `redux-immutable-state-invariant`. Any detected non-serializable values will be logged to the console. + +This middleware is added to the store by default by [`configureStore`](./configureStore.mdx) and [`getDefaultMiddleware`](./getDefaultMiddleware.mdx). + +You can customize the behavior of this middleware by passing any of the supported options as the `serializableCheck` value for `getDefaultMiddleware`. + +## Options + +```ts no-transpile +interface SerializableStateInvariantMiddlewareOptions { + /** + * The function to check if a value is considered serializable. This + * function is applied recursively to every value contained in the + * state. Defaults to `isPlain()`. + */ + isSerializable?: (value: any) => boolean + /** + * The function that will be used to retrieve entries from each + * value. If unspecified, `Object.entries` will be used. Defaults + * to `undefined`. + */ + getEntries?: (value: any) => [string, any][] + + /** + * An array of action types to ignore when checking for serializability. + * Defaults to [] + */ + ignoredActions?: string[] + + /** + * An array of dot-separated path strings or regular expressions to ignore + * when checking for serializability, Defaults to + * ['meta.arg', 'meta.baseQueryMeta'] + */ + ignoredActionPaths?: (string | RegExp)[] + + /** + * An array of dot-separated path strings or regular expressions to ignore + * when checking for serializability, Defaults to [] + */ + ignoredPaths?: (string | RegExp)[] + /** + * Execution time warning threshold. If the middleware takes longer + * than `warnAfter` ms, a warning will be displayed in the console. + * Defaults to 32ms. + */ + warnAfter?: number + + /** + * Opt out of checking state. When set to `true`, other state-related params will be ignored. + */ + ignoreState?: boolean + + /** + * Opt out of checking actions. When set to `true`, other action-related params will be ignored. + */ + ignoreActions?: boolean +} +``` + +## Exports + +### `createSerializableStateInvariantMiddleware` + +Creates an instance of the serializability check middleware, with the given options. + +You will most likely not need to call this yourself, as `getDefaultMiddleware` already does so. + +Example: + +```ts +// file: reducer.ts noEmit + +export default function (state = {}, action: any) { + return state +} + +// file: store.ts + +import { Iterable } from 'immutable' +import { + configureStore, + createSerializableStateInvariantMiddleware, + isPlain, + Tuple, +} from '@reduxjs/toolkit' +import reducer from './reducer' + +// Augment middleware to consider Immutable.JS iterables serializable +const isSerializable = (value: any) => + Iterable.isIterable(value) || isPlain(value) + +const getEntries = (value: any) => + Iterable.isIterable(value) ? value.entries() : Object.entries(value) + +const serializableMiddleware = createSerializableStateInvariantMiddleware({ + isSerializable, + getEntries, +}) + +const store = configureStore({ + reducer, + middleware: () => new Tuple(serializableMiddleware), +}) +``` + +### `isPlain` + +Checks whether the given value is considered a "plain value" or not. + +Currently implemented as: + +```ts +// file: src/isPlainObject.ts noEmit + +declare function isPlainObject(value: unknown): value is object +export default isPlainObject + +// file: src/serializableStateInvariantMiddleware.ts +import isPlainObject from './isPlainObject' + +export function isPlain(val: any) { + return ( + typeof val === 'undefined' || + val === null || + typeof val === 'string' || + typeof val === 'boolean' || + typeof val === 'number' || + Array.isArray(val) || + isPlainObject(val) + ) +} +``` + +This will accept all standard JS objects, arrays, and primitives, but return false for `Date`s, `Map`s, and other similar class instances. diff --git a/docs/reference/rtk-query/ApiProvider.mdx b/docs/reference/rtk-query/ApiProvider.mdx new file mode 100644 index 00000000..48f89c63 --- /dev/null +++ b/docs/reference/rtk-query/ApiProvider.mdx @@ -0,0 +1,35 @@ +--- +id: ApiProvider +title: ApiProvider +sidebar_label: ApiProvider +hide_title: true +description: 'RTK Query > API: ApiProvider reference' +--- + +  + +# `ApiProvider` + +[summary](docblock://query/react/ApiProvider.tsx?token=ApiProvider) + +[examples](docblock://query/react/ApiProvider.tsx?token=ApiProvider) + +:::danger +Using this together with an existing Redux store will cause them to conflict with each other. If you are already using Redux, please follow the instructions as shown in the [Getting Started guide](../../introduction/getting-started). +::: + +### Example + + diff --git a/docs/reference/rtk-query/createApi.mdx b/docs/reference/rtk-query/createApi.mdx new file mode 100644 index 00000000..81c8c16d --- /dev/null +++ b/docs/reference/rtk-query/createApi.mdx @@ -0,0 +1,946 @@ +--- +id: createApi +title: createApi +sidebar_label: createApi +hide_title: true +description: 'RTK Query > API: createApi reference' +--- + +  + +# `createApi` + +`createApi` is the core of RTK Query's functionality. It allows you to define a set of "endpoints" that describe how to retrieve data from backend APIs and other async sources, including the configuration of how to fetch and transform that data. It generates [an "API slice" structure](./created-api/overview.mdx) that contains Redux logic (and optionally React hooks) that encapsulate the data fetching and caching process for you. + +:::tip + +Typically, you should only have one API slice per base URL that your application needs to communicate with. For example, if your site fetches data from both `/api/posts` and `/api/users`, you would have a single API slice with `/api/` as the base URL, and separate endpoint definitions for `posts` and `users`. This allows you to effectively take advantage of [automated re-fetching](../usage/automated-refetching.mdx) by defining [tag](../usage/automated-refetching.mdx#tags) relationships across endpoints. + +This is because: + +- Automatic tag invalidation only works within a single API slice. If you have multiple API slices, the automatic invalidation won't work across them. +- Every `createApi` call generates its own middleware, and each middleware added to the store will run checks against every dispatched action. That has a perf cost that adds up. So, if you called `createApi` 10 times and added 10 separate API middleware to the store, that will be noticeably slower perf-wise. + +For maintainability purposes, you may wish to split up endpoint definitions across multiple files, while still maintaining a single API slice which includes all of these endpoints. See [code splitting](../usage/code-splitting.mdx) for how you can use the `injectEndpoints` property to inject API endpoints from other files into a single API slice definition. + +::: + +```ts title="Example: src/services/pokemon.ts" +// file: src/services/types.ts noEmit +export type Pokemon = {} + +// file: src/services/pokemon.ts +// Need to use the React-specific entry point to allow generating React hooks +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import type { Pokemon } from './types' + +// highlight-start +// Define a service using a base URL and expected endpoints +export const pokemonApi = createApi({ + reducerPath: 'pokemonApi', + baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), + endpoints: (build) => ({ + getPokemonByName: build.query({ + query: (name) => `pokemon/${name}`, + }), + }), +}) +//highlight-end + +// highlight-start +// Export hooks for usage in function components, which are +// auto-generated based on the defined endpoints +export const { useGetPokemonByNameQuery } = pokemonApi +// highlight-end +``` + +## `createApi` Parameters + +`createApi` accepts a single configuration object parameter with the following options: + +```ts no-transpile + baseQuery(args: InternalQueryArgs, api: BaseQueryApi, extraOptions?: DefinitionExtraOptions): any; + endpoints(build: EndpointBuilder): Definitions; + extractRehydrationInfo?: ( + action: UnknownAction, + { + reducerPath, + }: { + reducerPath: ReducerPath + } + ) => + | undefined + | CombinedState + tagTypes?: readonly TagTypes[]; + reducerPath?: ReducerPath; + serializeQueryArgs?: SerializeQueryArgs; + keepUnusedDataFor?: number; // value is in seconds + refetchOnMountOrArgChange?: boolean | number; // value is in seconds + refetchOnFocus?: boolean; + refetchOnReconnect?: boolean; +``` + +### `baseQuery` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.baseQuery) + +#### baseQuery function arguments + +- `args` - The return value of the `query` function for a given endpoint +- `api` - The `BaseQueryApi` object contains: + - `signal` - An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) object that may be used to abort DOM requests and/or read whether the request is aborted. + - `abort` - The [`abort()`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort) method of the AbortController attached to `signal`. + - `dispatch` - The `store.dispatch` method for the corresponding Redux store + - `getState` - A function that may be called to access the current store state + - `extra` - Provided as thunk.extraArgument to the configureStore getDefaultMiddleware option. + - `endpoint` - The name of the endpoint. + - `type` - Type of request (`query` or `mutation`). + - `forced` - Indicates if a query has been forced. + - `queryCacheKey`- The computed query cache key. +- `extraOptions` - The value of the optional `extraOptions` property provided for a given endpoint + +#### baseQuery function signature + +```ts title="Base Query signature" no-transpile +export type BaseQueryFn< + Args = any, + Result = unknown, + Error = unknown, + DefinitionExtraOptions = {}, + Meta = {}, +> = ( + args: Args, + api: BaseQueryApi, + extraOptions: DefinitionExtraOptions, +) => MaybePromise> + +export interface BaseQueryApi { + signal: AbortSignal + abort: (reason?: string) => void + dispatch: ThunkDispatch + getState: () => unknown + extra: unknown + endpoint: string + type: 'query' | 'mutation' + forced?: boolean +} + +export type QueryReturnValue = + | { + error: E + data?: undefined + meta?: M + } + | { + error?: undefined + data: T + meta?: M + } +``` + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.baseQuery) + +### `endpoints` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.endpoints) + +See [Endpoint Definition Parameters](#endpoint-definition-parameters) for details on individual properties. + +#### Query endpoint definition + +Query endpoints (defined with `build.query()`) are used to cache data fetched from the server. + +You must specify either a `query` field (which will use the API's `baseQuery` to make a request), or a `queryFn` function with your own async logic. All other fields are optional. + +```ts title="Query endpoint definition" no-transpile +export type FullTagDescription = { + type: TagType + id?: number | string +} +export type TagDescription = TagType | FullTagDescription + +type TagDescriptionArray = ReadonlyArray< + TagDescription | undefined | null +> + +export type ResultDescription< + TagTypes extends string, + ResultType, + QueryArg, + ErrorType, + MetaType, +> = + | TagDescriptionArray + | ( + result: ResultType | undefined, + error: ErrorType | undefined, + arg: QueryArg, + meta: MetaType, +) => TagDescriptionArray + + +export type QueryDefinition< + QueryArg, + BaseQuery extends BaseQueryFn, + TagTypes extends string, + ResultType, + ReducerPath extends string = string, +> = { + query(arg: QueryArg): BaseQueryArg + + /* either `query` or `queryFn` can be present, but not both simultaneously */ + queryFn( + arg: QueryArg, + api: BaseQueryApi, + extraOptions: BaseQueryExtraOptions, + baseQuery: (arg: Parameters[0]) => ReturnType, + ): MaybePromise>> + + /* transformResponse only available with `query`, not `queryFn` */ + transformResponse?( + baseQueryReturnValue: BaseQueryResult, + meta: BaseQueryMeta, + arg: QueryArg, + ): ResultType | Promise + + /* transformErrorResponse only available with `query`, not `queryFn` */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryError, + meta: BaseQueryMeta, + arg: QueryArg, + ): unknown + + extraOptions?: BaseQueryExtraOptions + + providesTags?: ResultDescription< + TagTypes, + ResultType, + QueryArg, + BaseQueryError + > + + keepUnusedDataFor?: number + + onQueryStarted?( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + queryFulfilled, + getCacheEntry, + updateCachedData, // available for query endpoints only + }: QueryLifecycleApi, + ): Promise + + onCacheEntryAdded?( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + cacheEntryRemoved, + cacheDataLoaded, + getCacheEntry, + 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> +} +``` + +#### Infinite Query endpoint definition + +Infinite query endpoints (defined with `build.infiniteQuery()`) are used to cache multi-page data sets from the server. They have all the same callbacks and options as standard query endpoints, but also require an additional [`infiniteQueryOptions`](#infinitequeryoptions) field to specify how to calculate the unique parameters to fetch each page. + +For infinite query endpoints, there is a separation between the "query arg" used for the cache key, and the "page param" used to fetch a specific page. For example, a Pokemon API endpoint might have a string query arg like `"fire"` , but use a page number as the param to determine which page to fetch out of the results. The `query` and `queryFn` methods will receive a combined `{queryArg, pageParam}` object as the argument, rather than just the `queryArg` by itself. + +```ts title="Infinite Query endpoint definition" no-transpile +export type PageParamFunction = ( + firstPage: DataType, + allPages: Array, + firstPageParam: PageParam, + allPageParams: Array, + queryArg: QueryArg, +) => PageParam | undefined | null + +type InfiniteQueryCombinedArg = { + queryArg: QueryArg + pageParam: PageParam +} + +export type InfiniteQueryDefinition< + QueryArg, + PageParam, + BaseQuery extends BaseQueryFn, + TagTypes extends string, + ResultType, + ReducerPath extends string = string, +> = + // Infinite queries have all the same options as query endpoints, + // but store the `{pages, pageParams}` structure, and receive an object + // with both `{queryArg, pageParam}` as the arg for `query` and `queryFn`. + QueryDefinition< + InfiniteQueryCombinedArg, + BaseQuery, + TagTypes, + InfiniteData + > & { + /** + * Required options to configure the infinite query behavior. + * `initialPageParam` and `getNextPageParam` are required, to + * ensure the infinite query can properly fetch the next page of data. + * `initialPageparam` may be specified when using the + * endpoint, to override the default value. + */ + infiniteQueryOptions: { + /** + * The initial page parameter to use for the first page fetch. + */ + initialPageParam: PageParam + /** + * This function is required to automatically get the next cursor for infinite queries. + * The result will also be used to determine the value of `hasNextPage`. + */ + getNextPageParam: PageParamFunction + /** + * This function can be set to automatically get the previous cursor for infinite queries. + * The result will also be used to determine the value of `hasPreviousPage`. + */ + getPreviousPageParam?: PageParamFunction + /** + * If specified, only keep this many pages in cache at once. + * If additional pages are fetched, older pages in the other + * direction will be dropped from the cache. + */ + maxPages?: number + } + } +``` + +#### Mutation endpoint definition + +Mutation endpoints (defined with `build.mutation()`) are used to send updates to the server, and force invalidation and refetching of query endpoints. + +As with queries, you must specify either the `query` option or the `queryFn` async method. + +```ts title="Mutation endpoint definition" no-transpile +export type MutationDefinition< + QueryArg, + BaseQuery extends BaseQueryFn, + TagTypes extends string, + ResultType, + ReducerPath extends string = string, + Context = Record, +> = { + query(arg: QueryArg): BaseQueryArg + + /* either `query` or `queryFn` can be present, but not both simultaneously */ + queryFn( + arg: QueryArg, + api: BaseQueryApi, + extraOptions: BaseQueryExtraOptions, + baseQuery: (arg: Parameters[0]) => ReturnType, + ): MaybePromise>> + + /* transformResponse only available with `query`, not `queryFn` */ + transformResponse?( + baseQueryReturnValue: BaseQueryResult, + meta: BaseQueryMeta, + arg: QueryArg, + ): ResultType | Promise + + /* transformErrorResponse only available with `query`, not `queryFn` */ + transformErrorResponse?( + baseQueryReturnValue: BaseQueryError, + meta: BaseQueryMeta, + arg: QueryArg, + ): unknown + + extraOptions?: BaseQueryExtraOptions + + invalidatesTags?: ResultDescription + + onQueryStarted?( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + queryFulfilled, + getCacheEntry, + }: MutationLifecycleApi, + ): Promise + + onCacheEntryAdded?( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + cacheEntryRemoved, + cacheDataLoaded, + getCacheEntry, + }: MutationCacheLifecycleApi, + ): Promise +} +``` + +#### How endpoints get used + +When defining a key like `getPosts` as shown below, it's important to know that this name will become exportable from `api` and be able to referenced under `api.endpoints.getPosts.useQuery()`, `api.endpoints.getPosts.initiate()` and `api.endpoints.getPosts.select()`. The same thing applies to `mutation`s but they reference `useMutation` instead of `useQuery`. + +```ts +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +interface Post { + id: number + name: string +} +type PostsResponse = Post[] + +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + tagTypes: ['Posts'], + endpoints: (build) => ({ + getPosts: build.query({ + query: () => 'posts', + providesTags: (result) => + result ? result.map(({ id }) => ({ type: 'Posts', id })) : [], + }), + addPost: build.mutation>({ + query: (body) => ({ + url: `posts`, + method: 'POST', + body, + }), + invalidatesTags: ['Posts'], + }), + }), +}) + +// Auto-generated hooks +export const { useGetPostsQuery, useAddPostMutation } = api + +// Possible exports +export const { endpoints, reducerPath, reducer, middleware } = api +// reducerPath, reducer, middleware are only used in store configuration +// endpoints will have: +// endpoints.getPosts.initiate(), endpoints.getPosts.select(), endpoints.getPosts.useQuery() +// endpoints.addPost.initiate(), endpoints.addPost.select(), endpoints.addPost.useMutation() +// see `createApi` overview for _all exports_ +``` + +### `extractRehydrationInfo` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.extractRehydrationInfo) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.extractRehydrationInfo) + +See also [Server Side Rendering](../usage/server-side-rendering.mdx) and +[Persistence and Rehydration](../usage/persistence-and-rehydration.mdx). + +### `tagTypes` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.tagTypes) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.tagTypes) + +### `reducerPath` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.reducerPath) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.reducerPath) + +### `serializeQueryArgs` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.serializeQueryArgs) + +By default, this function will take the query arguments, sort object keys where applicable, stringify the result, and concatenate it with the endpoint name. This creates a cache key based on the combination of arguments + endpoint name (ignoring object key order), such that calling any given endpoint with the same arguments will result in the same cache key. + +### `invalidationBehavior` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.invalidationBehavior) + +### `keepUnusedDataFor` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.keepUnusedDataFor) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.keepUnusedDataFor) + +### `refetchOnMountOrArgChange` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.refetchOnMountOrArgChange) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnMountOrArgChange` to each individual hook call or similarly by passing `forceRefetch: true` when dispatching the [`initiate`](./created-api/endpoints.mdx#initiate) action. +::: + +### `refetchOnFocus` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.refetchOnFocus) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnFocus` to each individual hook call or when dispatching the [`initiate`](./created-api/endpoints.mdx#initiate) action. + +If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you. +::: + +### `refetchOnReconnect` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.refetchOnReconnect) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `refetchOnReconnect` to each individual hook call or when dispatching the [`initiate`](./created-api/endpoints.mdx#initiate) action. + +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. +::: + +### `catchSchemaFailure` + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure) + +[examples](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure) + +:::note +You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `catchSchemaFailure` 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` + +_(required if no `queryFn` provided)_ + +```ts title="query signature" no-transpile +export type query = ( + arg: QueryArg, +) => string | Record + +// with `fetchBaseQuery` +export type query = (arg: QueryArg) => string | FetchArgs +``` + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.query) + +[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.query) + +### `queryFn` + +_(required if no `query` provided)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQueryFn.queryFn) + +Called with the same arguments as `baseQuery`, as well as the provided `baseQuery` function itself. It is expected to return an object with either a `data` or `error` property, or a promise that resolves to return such an object. + +See also [Customizing queries with queryFn](../usage/customizing-queries.mdx#customizing-queries-with-queryfn). + +```ts title="queryFn signature" no-transpile +queryFn( + arg: QueryArg, + api: BaseQueryApi, + extraOptions: BaseQueryExtraOptions, + baseQuery: (arg: Parameters[0]) => ReturnType +): MaybePromise< +| { + error: BaseQueryError + data?: undefined + } +| { + error?: undefined + data: ResultType + } +> + +export interface BaseQueryApi { + signal: AbortSignal + dispatch: ThunkDispatch + getState: () => unknown +} +``` + +#### `queryFn` function arguments + +- `args` - The argument provided when the query itself is called +- `api` - The `BaseQueryApi` object, containing `signal`, `dispatch` and `getState` properties + - `signal` - An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) object that may be used to abort DOM requests and/or read whether the request is aborted. + - `dispatch` - The `store.dispatch` method for the corresponding Redux store + - `getState` - A function that may be called to access the current store state +- `extraOptions` - The value of the optional `extraOptions` property provided for the endpoint +- `baseQuery` - The `baseQuery` function provided to the api itself + +[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQueryFn.queryFn) + +### `infiniteQueryOptions` + +_(only for `infiniteQuery` endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=InfiniteQueryExtraOptions.infiniteQueryOptions) + +The `infiniteQueryOptions` field includes: + +- `initialPageParam`: the default page param value used for the first request, if this was not specified at the usage site +- `getNextPageParam`: a required callback you must provide to calculate the next page param, given the existing cached pages and page params +- `getPreviousPageParam`: an optional callback that will be used to calculate the previous page param, if you try to fetch backwards. +- `maxPages`: an optional limit to how many fetched pages will be kept in the cache entry at a time + +[examples](docblock://query/endpointDefinitions.ts?token=InfiniteQueryExtraOptions.infiniteQueryOptions) + +### `transformResponse` + +_(optional, not applicable with `queryFn`)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.transformResponse) + +In some cases, you may want to manipulate the data returned from a query before you put it in the cache. In this instance, you can take advantage of `transformResponse`. + +See also [Customizing query responses with `transformResponse`](../usage/customizing-queries.mdx#customizing-query-responses-with-transformresponse) + +```ts title="Unpack a deeply nested collection" no-transpile +transformResponse: (response, meta, arg) => + response.some.deeply.nested.collection +``` + +### `transformErrorResponse` + +_(optional, not applicable with `queryFn`)_ + +[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.transformErrorResponse) + +In some cases, you may want to manipulate the error returned from a query before you put it in the cache. In this instance, you can take advantage of `transformErrorResponse`. + +See also [Customizing query responses with `transformErrorResponse`](../usage/customizing-queries.mdx#customizing-query-responses-with-transformerrorresponse) + +```ts title="Unpack a deeply nested error object" no-transpile +transformErrorResponse: (response, meta, arg) => + response.data.some.deeply.nested.errorObject +``` + +### `extraOptions` + +_(optional)_ + +Passed as the third argument to the supplied `baseQuery` function + +### `providesTags` + +_(optional, only for query endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.providesTags) + +See also [Providing cache data](../usage/automated-refetching.mdx#providing-cache-data). + +[examples](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.providesTags) + +### `invalidatesTags` + +_(optional, only for mutation endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=MutationExtraOptions.invalidatesTags) + +See also [Invalidating cache data](../usage/automated-refetching.mdx#invalidating-cache-data). + +[examples](docblock://query/endpointDefinitions.ts?token=MutationExtraOptions.invalidatesTags) + +### `keepUnusedDataFor` + +_(optional, only for query endpoints)_ + +Overrides the api-wide definition of `keepUnusedDataFor` for this endpoint only. + +[summary](docblock://query/createApi.ts?token=CreateApiOptions.keepUnusedDataFor) + +[examples](docblock://query/core/buildMiddleware/cacheCollection.ts?token=CacheCollectionQueryExtraOptions) + +### `serializeQueryArgs` + +_(optional, only for query endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.serializeQueryArgs) + +[examples](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.serializeQueryArgs) + +### `merge` + +_(optional, only for query endpoints)_ + +[summary](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.merge) + +[examples](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.merge) + +### `forceRefetch` + +_(optional, only for query endpoints)_ + +```ts title="forceRefetch signature" no-transpile +type forceRefetch = (params: { + currentArg: QueryArg | undefined + previousArg: QueryArg | undefined + state: RootState + endpointState?: QuerySubState +}) => boolean +``` + +[summary](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.forceRefetch) + +[examples](docblock://query/endpointDefinitions.ts?token=QueryExtraOptions.forceRefetch) + +### `onQueryStarted` + +_(optional)_ + +Available to both [queries](../usage/queries.mdx) and [mutations](../usage/mutations.mdx). + +A function that is called when you start each individual query or mutation. The function is called with a lifecycle api object containing properties such as `queryFulfilled`, allowing code to be run when a query is started, when it succeeds, and when it fails (i.e. throughout the lifecycle of an individual query/mutation call). + +Can be used in `mutations` for [optimistic updates](../usage/manual-cache-updates.mdx#optimistic-updates). + +#### Lifecycle API properties + +- `dispatch` - The dispatch method for the store. +- `getState` - A method to get the current state for the store. +- `extra` - `extra` as provided as `thunk.extraArgument` to the `configureStore` `getDefaultMiddleware` option. +- `requestId` - A unique ID generated for the query/mutation. +- `queryFulfilled` - A Promise that will resolve with a `data` property (the transformed query result), + and a `meta` property (`meta` returned by the `baseQuery`). + If the query fails, this Promise will reject with the error. This allows you to `await` for the query to finish. +- `getCacheEntry` - A function that gets the current value of the cache entry. +- `updateCachedData` _(query endpoints only)_ - A function that accepts a 'recipe' callback specifying how to update the data for the corresponding cache at the time it is called. This uses `immer` internally, and updates can be written 'mutably' while safely producing the next immutable state. + +```ts title="Mutation onQueryStarted signature" no-transpile +async function onQueryStarted( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + queryFulfilled, + getCacheEntry, + }: MutationLifecycleApi, +): Promise +``` + +```ts title="Query onQueryStarted signature" no-transpile +async function onQueryStarted( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + queryFulfilled, + getCacheEntry, + updateCachedData, // available for query endpoints only + }: QueryLifecycleApi, +): Promise +``` + +```ts title="onQueryStarted query lifecycle example" +// file: notificationsSlice.ts noEmit +export const messageCreated = (msg: string) => ({ + type: 'notifications/messageCreated', + payload: msg, +}) + +// file: api.ts +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' +import { messageCreated } from './notificationsSlice' + +export interface Post { + id: number + name: string +} + +const api = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: '/', + }), + endpoints: (build) => ({ + getPost: build.query({ + query: (id) => `post/${id}`, + async onQueryStarted(id, { dispatch, queryFulfilled }) { + // `onStart` side-effect + dispatch(messageCreated('Fetching post...')) + try { + const { data } = await queryFulfilled + // `onSuccess` side-effect + dispatch(messageCreated('Post received!')) + } catch (err) { + // `onError` side-effect + dispatch(messageCreated('Error fetching post!')) + } + }, + }), + }), +}) +``` + +### `onCacheEntryAdded` + +_(optional)_ + +Available to both [queries](../usage/queries.mdx) and [mutations](../usage/mutations.mdx). + +A function that is called when a new cache entry is added, i.e. when a new subscription for the endpoint + query parameters combination is created. The function is called with a lifecycle api object containing properties such as `cacheDataLoaded` & `cacheDataRemoved`, allowing code to be run when a cache entry is added, when cache data is loaded, and when the cache entry is removed (i.e. throughout the lifecycle of a cache entry). + +Can be used for [streaming updates](../usage/streaming-updates.mdx). + +#### Cache Lifecycle API properties + +- `dispatch` - The dispatch method for the store. +- `getState` - A method to get the current state for the store. +- `extra` - `extra` as provided as `thunk.extraArgument` to the `configureStore` `getDefaultMiddleware` option. +- `requestId` - A unique ID generated for the cache entry. +- `cacheEntryRemoved` - A Promise that allows you to wait for the point in time when the cache entry has been removed from the cache, by not being used/subscribed to any more in the application for too long or by dispatching `api.util.resetApiState`. +- `cacheDataLoaded` - A Promise that will resolve with the first value for this cache key. This allows you to `await` until an actual value is in the cache. + Note: If the cache entry is removed from the cache before any value has ever been resolved, this Promise will reject with `new Error('Promise never resolved before cacheEntryRemoved.')` to prevent memory leaks. You can just re-throw that error (or not handle it at all) - it will be caught outside of `cacheEntryAdded`. +- `getCacheEntry` - A function that gets the current value of the cache entry. +- `updateCachedData` _(query endpoints only)_ - A function that accepts a 'recipe' callback specifying how to update the data at the time it is called. This uses `immer` internally, and updates can be written 'mutably' while safely producing the next immutable state. + +```ts title="Mutation onCacheEntryAdded signature" no-transpile +async function onCacheEntryAdded( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + cacheEntryRemoved, + cacheDataLoaded, + getCacheEntry, + }: MutationCacheLifecycleApi, +): Promise +``` + +```ts title="Query onCacheEntryAdded signature" no-transpile +async function onCacheEntryAdded( + arg: QueryArg, + { + dispatch, + getState, + extra, + requestId, + cacheEntryRemoved, + cacheDataLoaded, + getCacheEntry, + updateCachedData, // available for query endpoints only + }: QueryCacheLifecycleApi, +): Promise +``` + +### Schema Validation + +Endpoints can have schemas for runtime validation of query args, responses, and errors. Any [Standard Schema](https://standardschema.dev/) compliant library can be used. + +When used with TypeScript, schemas can also be used to [infer the type of that value instead of having to declare it](../usage-with-typescript.mdx#schema-validation). + +:::warning + +By default, schema failures are treated as _fatal_, meaning that normal error handling such as tag invalidation will not be executed. + +In order for schema failures to be treated as non-fatal, you must provide a [`catchSchemaFailure`](#catchschemafailure) function, to convert the schema failure into an error shape matching the base query errors. + +```ts title="catchSchemaFailure example" no-transpile +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + catchSchemaFailure: (error, info) => ({ + status: 'CUSTOM_ERROR', + error: error.schemaName + ' failed validation', + data: error, + }), + endpoints: (build) => ({ + // ... + }), +}) +``` + +::: + +#### `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, not applicable with `queryFn`)_ + +[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, not applicable with `queryFn`)_ + +[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/docs/reference/rtk-query/created-api/api-slice-utils.mdx b/docs/reference/rtk-query/created-api/api-slice-utils.mdx new file mode 100644 index 00000000..f9dd4a97 --- /dev/null +++ b/docs/reference/rtk-query/created-api/api-slice-utils.mdx @@ -0,0 +1,519 @@ +--- +id: api-slice-utils +title: 'API Slices: Utilities' +sidebar_label: API Slice Utilities +hide_title: true +--- + +  + +# API Slices: Utilities + +The API slice object includes various utilities that can be used for cache management, +such as implementing [optimistic updates](../../usage/manual-cache-updates.mdx#optimistic-updates), +as well implementing [server side rendering](../../usage/server-side-rendering.mdx). + +These are included as `api.util` inside the API object. + +:::info + +Some of the TS types on this page are pseudocode to illustrate intent, as the actual internal types are fairly complex. + +::: + +### `updateQueryData` + +A Redux thunk action creator that, when dispatched, creates and applies a set of JSON diff/patch objects to the current state. This immediately updates the Redux state with those changes. + +#### Signature + +```ts no-transpile +const updateQueryData = ( + endpointName: string, + arg: any, + updateRecipe: (draft: Draft) => void, + updateProvided?: boolean, +) => ThunkAction + +interface PatchCollection { + patches: Patch[] + inversePatches: Patch[] + undo: () => void +} +``` + +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated +- `updateRecipe`: an Immer `produce` callback that can apply changes to the cached state +- `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. + +#### Description + +The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), any relevant query arguments, and a callback function. The callback receives an Immer-wrapped `draft` of the current state, and may modify the draft to match the expected results after the mutation completes successfully. + +The thunk returns an object containing `{patches: Patch[], inversePatches: Patch[], undo: () => void}`. The `patches` and `inversePatches` are generated using Immer's [`produceWithPatches` method](https://immerjs.github.io/immer/patches). + +This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, arg, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect. + +Note that the first two arguments (`endpointName` and `arg`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run. + +#### Example 1 + +```ts no-transpile +const patchCollection = dispatch( + api.util.updateQueryData('getPosts', undefined, (draftPosts) => { + draftPosts.push({ id: 1, name: 'Teddy' }) + }), +) +``` + +In the example above, `'getPosts'` is provided for the `endpointName`, and `undefined` is provided +for `arg`. This will match a query cache key of `'getPosts(undefined)'`. + +i.e. it will match a cache entry that may have been created via any of the following calls: + +```ts no-transpile +api.endpoints.getPosts.useQuery() + +useGetPostsQuery() + +useGetPostsQuery(undefined, { ...options }) + +dispatch(api.endpoints.getPosts.initiate()) + +dispatch(api.endpoints.getPosts.initiate(undefined, { ...options })) +``` + +#### Example 2 + +```ts no-transpile +const patchCollection = dispatch( + api.util.updateQueryData('getPostById', 1, (draftPost) => { + draftPost.name = 'Lilly' + }), +) +``` + +In the example above, `'getPostById'` is provided for the `endpointName`, and `1` is provided +for `arg`. This will match a query cache key of `'getPostById(1)'`. + +i.e. it will match a cache entry that may have been created via any of the following calls: + +```ts no-transpile +api.endpoints.getPostById.useQuery(1) + +useGetPostByIdQuery(1) + +useGetPostByIdQuery(1, { ...options }) + +dispatch(api.endpoints.getPostById.initiate(1)) + +dispatch(api.endpoints.getPostById.initiate(1, { ...options })) +``` + +### `upsertQueryData` + +A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache. + +#### Signature + +```ts no-transpile +const upsertQueryData = (endpointName: string, arg: any, newEntryData: T) => + ThunkAction>, PartialState, any, UnknownAction> +``` + +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated +- `newEntryValue`: the value to be written into the corresponding cache entry's `data` field + +#### Description + +The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and the data to upsert. + +If no cache entry for that cache key exists, a cache entry will be created and the data added. If a cache entry already exists, this will _overwrite_ the existing cache entry data. + +The thunk executes _asynchronously_, and returns a promise that resolves when the store has been updated. This includes executing the `transformResponse` callback if defined for that endpoint. + +If dispatched while an actual request is in progress, both the upsert and request will be handled as soon as they resolve, resulting in a "last result wins" update behavior. + +#### Example + +```ts no-transpile +await dispatch( + api.util.upsertQueryData('getPost', { id: 1 }, { id: 1, text: 'Hello!' }), +) +``` + +### `patchQueryData` + +A Redux thunk action creator that, when dispatched, applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes. + +#### Signature + +```ts no-transpile +const patchQueryData = ( + endpointName: string, + arg: any + patches: Patch[], + updateProvided?: boolean +) => ThunkAction; +``` + +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: a cache key, used to determine which cached dataset needs to be updated +- `patches`: an array of patches (or inverse patches) to apply to cached state. These would typically be obtained from the result of dispatching [`updateQueryData`](#updatequerydata) +- `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. + +#### Description + +The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and a JSON diff/patch array as produced by Immer's `produceWithPatches`. + +This is typically used as the second step in implementing optimistic updates. If a request fails, the optimistically-applied changes can be reverted by dispatching `patchQueryData` with the `inversePatches` that were generated by `updateQueryData` earlier. + +In cases where it is desired to simply revert the previous changes, it may be preferable to call the `undo` method returned from dispatching `updateQueryData` instead. + +#### Example + +```ts no-transpile +const patchCollection = dispatch( + api.util.updateQueryData('getPosts', undefined, (draftPosts) => { + draftPosts.push({ id: 1, name: 'Teddy' }) + }), +) + +// later +dispatch( + api.util.patchQueryData( + 'getPosts', + undefined, + patchCollection.inversePatches, + ), +) + +// or +patchCollection.undo() +``` + +### `upsertQueryEntries` + +A standard Redux action creator that accepts an array of individual cache entry descriptions, and immediately upserts them into the store. This is designed to efficiently bulk-insert many entries at once. + +#### Signature + +```ts no-transpile +/** + * A typesafe single entry to be upserted into the cache + */ +export type NormalizedQueryUpsertEntry< + Definitions extends EndpointDefinitions, + EndpointName extends QueryKeys, +> = { + endpointName: EndpointName + arg: QueryArgFrom + value: ResultTypeFrom +} + +const upsertQueryEntries = (entries: NormalizedQueryUpsertEntry[]) => + PayloadAction +``` + +#### Parameters + +- `entries`: an array of objects that contain the data needed to upsert individual cache entries: + - `endpointName`: the name of the endpoint, such as `"getPokemon"` + - `arg`: the full query key argument needed to identify this cache entry, such as `"pikachu"` (same as you would pass to a `useQuery` hook or `api.endpoints.someEndpoint.select()`) + - `value`: the data to be upserted into this cache entry, exactly as formatted. + +#### Description + +This method is designed as a more efficient approach to bulk-inserting many entries at once than many individual calls to `upsertQueryData`. As a comparison: + +- `upsertQueryData`: + - upserts one cache entry at a time + - Is async + - Dispatches 2 separate actions, `pending` and `fulfilled` + - Runs the `transformResponse` callback if defined for that endpoint, as well as the `merge` callback if defined +- `upsertQueryEntries`: + - upserts many cache entries at once, and they may be for any combination of endpoints defined in the API + - Is a single synchronous action + - Does _not_ run `transformResponse`, so the provided `value` fields must already be in the final format expected for that endpoint. However, it will still run the `merge` callback if defined + +Currently, this method has two main use cases. The first is prefilling the cache with data retrieved from storage on app startup. The second is to act as a "pseudo-normalization" tool. [RTK Query is _not_ a "normalized" cache](../../usage/cache-behavior.mdx#no-normalized-or-de-duplicated-cache). However, there are times when you may want to prefill other cache entries with the contents of another endpoint, such as taking the results of a `getPosts` list endpoint response and prefilling the individual `getPost(id)` endpoint cache entries. + +If no cache entry for that cache key exists, a cache entry will be created and the data added. If a cache entry already exists, this will _overwrite_ the existing cache entry data. + +If dispatched while an actual request is in progress, both the upsert and request will be handled as soon as they resolve, resulting in a "last result wins" update behavior. + +#### Example + +```ts no-transpile +const api = createApi({ + endpoints: (build) => ({ + getPosts: build.query({ + query: () => '/posts', + async onQueryStarted(_, { dispatch, queryFulfilled }) { + const res = await queryFulfilled + const posts = res.data + + // Pre-fill the individual post entries with the results + // from the list endpoint query + dispatch( + api.util.upsertQueryEntries( + posts.map((post) => ({ + endpointName: 'getPost', + arg: { id: post.id }, + value: post, + })), + ), + ) + }, + }), + getPost: build.query>({ + query: (post) => `post/${post.id}`, + }), + }), +}) +``` + +### `prefetch` + +A Redux thunk action creator that can be used to manually trigger pre-fetching of data. + +#### Signature + +```ts no-transpile +type PrefetchOptions = { ifOlderThan?: false | number } | { force?: boolean } + +const prefetch = (endpointName: string, arg: any, options: PrefetchOptions) => + ThunkAction +``` + +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `args`: a cache key, used to determine which cached dataset needs to be updated +- `options`: options to determine whether the request should be sent for a given situation: + - `ifOlderThan`: if specified, only runs the query if the difference between `new Date()` and the last`fulfilledTimeStamp` is greater than the given value (in seconds) + - `force`: if `true`, it will ignore the `ifOlderThan` value if it is set and the query will be run even if it exists in the cache. + +#### Description + +The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), any relevant query arguments, and a set of options used to determine if the data actually should be re-fetched based on cache staleness. + +React Hooks users will most likely never need to use this directly, as the `usePrefetch` hook will dispatch the thunk action creator result internally as needed when you call the prefetching function supplied by the hook. + +#### Example + +```ts no-transpile +dispatch(api.util.prefetch('getPosts', undefined, { force: true })) +``` + +### `selectInvalidatedBy` + +A selector function that can select query parameters to be invalidated. + +#### Signature + +```ts no-transpile +function selectInvalidatedBy( + state: RootState, + tags: ReadonlyArray>, +): Array<{ + endpointName: string + originalArgs: any + queryCacheKey: QueryCacheKey +}> +``` + +#### Parameters + +- `state`: the root state +- `tags`: a readonly array of invalidated tags, where the provided `TagDescription` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. + - `[TagType]` + - `[{ type: TagType }]` + - `[{ type: TagType, id: number | string }]` + +#### Description + +The function accepts two arguments + +- the root state and +- the cache tags to be invalidated. + +It returns an array that contains + +- the endpoint name, +- the original args and +- the queryCacheKey. + +#### Example + +```ts no-transpile +const entries = api.util.selectInvalidatedBy(state, ['Post']) +const entries = api.util.selectInvalidatedBy(state, [{ type: 'Post', id: 1 }]) +const entries = api.util.selectInvalidatedBy(state, [ + { type: 'Post', id: 1 }, + { type: 'Post', id: 4 }, +]) +``` + +### `invalidateTags` + +A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx). + +#### Signature + +```ts no-transpile +const invalidateTags = ( + tags: Array>, +) => ({ + type: string, + payload: tags, +}) +``` + +#### Parameters + +- `tags`: an array of tags to be invalidated, where the provided `TagType` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. + - `[TagType]` + - `[{ type: TagType }]` + - `[{ type: TagType, id: number | string }]` + +#### Description + +The action creator accepts one argument: the cache tags to be invalidated. It returns an action with those tags as a payload, and the corresponding `invalidateTags` action type for the api. + +Dispatching the result of this action creator will [invalidate](../../usage/automated-refetching.mdx#invalidating-cache-data) the given tags, causing queries to automatically re-fetch if they are subscribed to cache data that [provides](../../usage/automated-refetching.mdx#providing-cache-data) the corresponding tags. + +#### Example + +```ts no-transpile +dispatch(api.util.invalidateTags(['Post'])) +dispatch(api.util.invalidateTags([{ type: 'Post', id: 1 }])) +dispatch( + api.util.invalidateTags([ + { type: 'Post', id: 1 }, + { type: 'Post', id: 'LIST' }, + ]), +) +``` + +### `selectCachedArgsForQuery` + +A selector function that can select arguments for currently cached queries. + +#### Signature + +```ts no-transpile +function selectCachedArgsForQuery( + state: RootState, + queryName: QueryName, +): Array +``` + +#### Parameters + +- `state`: the root state +- `queryName`: a string matching an existing query endpoint name + +#### Description + +The function accepts two arguments + +- the root state and + +- the name of the query + +It returns an array that contains arguments used for each entry. + +#### Example + +```ts no-transpile +const args = api.util.selectCachedArgsForQuery(state, 'getPosts') +``` + +### `resetApiState` + +#### Signature + +```ts no-transpile +const resetApiState = () => ({ + type: string, + payload: undefined, +}) +``` + +#### Description + +A Redux action creator that can be dispatched to manually reset the api state completely. This will immediately remove all existing cache entries, and all queries will be considered 'uninitialized'. + +Note that [hooks](./hooks.mdx) also track state in local component state and might not fully be reset by `resetApiState`. + +#### Example + +```ts no-transpile +dispatch(api.util.resetApiState()) +``` + +## `getRunningQueriesThunk` and `getRunningMutationsThunk` + +#### Signature + +```ts no-transpile +getRunningQueriesThunk(): ThunkWithReturnValue>> +getRunningMutationsThunk(): ThunkWithReturnValue>> +``` + +#### Description + +Thunks that (if dispatched) return either all running queries or mutations. +These returned values can be awaited like promises. + +This is useful for SSR scenarios to await all queries (or mutations) triggered in any way, including via hook calls +or manually dispatching `initiate` actions. + +```ts no-transpile title="Awaiting all currently running queries example" +await Promise.all(dispatch(api.util.getRunningQueriesThunk())) +``` + +## `getRunningQueryThunk` and `getRunningMutationThunk` + +#### Signature + +```ts no-transpile +getRunningQueryThunk>( + endpointName: EndpointName, + args: QueryArgFrom +): ThunkWithReturnValue< + | QueryActionCreatorResult< + Definitions[EndpointName] & { type: 'query' } + > + | undefined +> + +getRunningMutationThunk>( + endpointName: EndpointName, + fixedCacheKeyOrRequestId: string +): ThunkWithReturnValue< + | MutationActionCreatorResult< + Definitions[EndpointName] & { type: 'mutation' } + > + | undefined +> +``` + +#### Description + +Thunks that (if dispatched) return a single running query (or mutation) for a given +endpoint name + argument (or requestId/fixedCacheKey) combination, if it is currently running. +If it is not currently running, the function returns `undefined`. + +These thunks are primarily added to add experimental support for suspense in the future. +They enable writing custom hooks that look up if RTK Query has already got a running query/mutation +for a certain endpoint/argument combination, and retrieving that to `throw` it as a promise. diff --git a/docs/reference/rtk-query/created-api/code-splitting.mdx b/docs/reference/rtk-query/created-api/code-splitting.mdx new file mode 100644 index 00000000..ff3ce318 --- /dev/null +++ b/docs/reference/rtk-query/created-api/code-splitting.mdx @@ -0,0 +1,117 @@ +--- +id: code-splitting +title: 'API Slices: Code Splitting and Generation' +sidebar_label: Code Splitting +hide_title: true +--- + +  + +# API Slices: Code Splitting and Generation + +Each API slice allows [additional endpoint definitions to be injected at runtime](../../usage/code-splitting.mdx) after the initial API slice has been defined. This can be beneficial for apps that may have _many_ endpoints. + +The individual API slice endpoint definitions can also be split across multiple files. This is primarily useful for working with API slices that were [code-generated from an API schema file](../../usage/code-generation.mdx), allowing you to add additional custom behavior and configuration to a set of automatically-generated endpoint definitions. + +Each API slice object has `injectEndpoints` and `enhanceEndpoints` functions to support these use cases. + +## `injectEndpoints` + +#### Signature + +```ts no-transpile +const injectEndpoints = (endpointOptions: InjectedEndpointOptions) => + EnhancedApiSlice + +interface InjectedEndpointOptions { + endpoints: (build: EndpointBuilder) => NewEndpointDefinitions + /** + * Optionally allows endpoints to be overridden if defined by multiple `injectEndpoints` calls. + * + * If set to `true`, will override existing endpoints with the new definition. + * If set to `'throw'`, will throw an error if an endpoint is redefined with a different definition. + * If set to `false` (or unset), will not override existing endpoints with the new definition, and log a warning in development. + */ + overrideExisting?: boolean | 'throw' +} +``` + +#### Description + +Accepts an options object containing the same `endpoints` builder callback you would pass to [`createApi.endpoints`](../createApi.mdx#endpoints). Any endpoint definitions defined using that builder will be merged into the existing endpoint definitions for this API slice using a shallow merge, so any new endpoint definitions will override existing endpoints with the same name. + +Returns an updated and enhanced version of the API slice object, containing the combined endpoint definitions. + +Endpoints will not be overridden unless `overrideExisting` is set to `true`. If not, a development mode warning will be shown to notify you if there is a name clash between endpoint definitions. + +This method is primarily useful for code splitting and hot reloading. + +## `enhanceEndpoints` + +#### Signature + +```ts no-transpile +const enhanceEndpoints = (endpointOptions: EnhanceEndpointsOptions) => + EnhancedApiSlice + +interface EnhanceEndpointsOptions { + addTagTypes?: readonly string[] + endpoints?: Record> +} +``` + +#### Description + +Any provided tag types or endpoint definitions will be merged into the existing endpoint definitions for this API slice. Unlike `injectEndpoints`, the partial endpoint definitions will not _replace_ existing definitions, but are rather merged together on a per-definition basis (ie, `Object.assign(existingEndpoint, newPartialEndpoint)`). + +Returns an updated and enhanced version of the API slice object, containing the combined endpoint definitions. + +This is primarily useful for taking an API slice object that was code-generated from an API schema file like OpenAPI, and adding additional specific hand-written configuration for cache invalidation management on top of the generated endpoint definitions. + +For example, `enhanceEndpoints` can be used to modify caching behavior by changing the values of `providesTags`, `invalidatesTags`, and `keepUnusedDataFor`: + +```ts +// file: api.ts noEmit +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + getUserByUserId: build.query({ + query() { + return '' + }, + }), + patchUserByUserId: build.mutation({ + query() { + return '' + }, + }), + getUsers: build.query({ + query() { + return '' + }, + }), + }), +}) + +// file: enhanceEndpoints.ts +import { api } from './api' + +const enhancedApi = api.enhanceEndpoints({ + addTagTypes: ['User'], + endpoints: { + getUserByUserId: { + providesTags: ['User'], + }, + patchUserByUserId: { + invalidatesTags: ['User'], + }, + // alternatively, define a function which is called with the endpoint definition as an argument + getUsers(endpoint) { + endpoint.providesTags = ['User'] + endpoint.keepUnusedDataFor = 120 + }, + }, +}) +``` diff --git a/docs/reference/rtk-query/created-api/endpoints.mdx b/docs/reference/rtk-query/created-api/endpoints.mdx new file mode 100644 index 00000000..c57cc783 --- /dev/null +++ b/docs/reference/rtk-query/created-api/endpoints.mdx @@ -0,0 +1,275 @@ +--- +id: endpoints +title: 'API Slices: Endpoints' +sidebar_label: Endpoints +hide_title: true +--- + +  + +# API Slices: Endpoints + +The API slice object will have an `endpoints` field inside. This section maps the endpoint names you provided to `createApi` to the core Redux logic (thunks and selectors) used to trigger data fetches and read cached data for that endpoint. If you're using the React-specific version of `createApi`, each endpoint definition will also contain the auto-generated React hooks for that endpoint. + +Each endpoint structure contains the following fields: + +```ts no-transpile +type EndpointLogic = { + initiate: InitiateRequestThunk + select: CreateCacheSelectorFactory + matchPending: Matcher + matchFulfilled: Matcher + matchRejected: Matcher +} +``` + +## `initiate` + +#### Signature + +```ts no-transpile +type InitiateRequestThunk = StartQueryActionCreator | StartMutationActionCreator; + +type StartQueryActionCreator = ( + arg:any, + options?: StartQueryActionCreatorOptions +) => ThunkAction; + +type StartMutationActionCreator> = ( + arg: any + options?: StartMutationActionCreatorOptions +) => ThunkAction, any, any, UnknownAction>; + +type SubscriptionOptions = { + /** + * How frequently to automatically re-fetch data (in milliseconds). Defaults to `0` (off). + */ + pollingInterval?: number; + /** + * Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after regaining a network connection. + * + * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false. + * + * Note: requires `setupListeners` to have been called. + */ + refetchOnReconnect?: boolean; + /** + * Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after the application window regains focus. + * + * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false. + * + * Note: requires `setupListeners` to have been called. + */ + refetchOnFocus?: boolean; +}; + +interface StartQueryActionCreatorOptions { + subscribe?: boolean; + forceRefetch?: boolean | number; + subscriptionOptions?: SubscriptionOptions; +} + +interface StartMutationActionCreatorOptions { + /** + * If this mutation should be tracked in the store. + * If you just want to manually trigger this mutation using `dispatch` and don't care about the + * result, state & potential errors being held in store, you can set this to false. + * (defaults to `true`) + */ + track?: boolean; +} +``` + +#### Description + +A Redux thunk action creator that you can dispatch to trigger data fetch queries or mutations. + +React Hooks users will most likely never need to use these directly, as the hooks automatically dispatch these actions as needed. + +:::note Usage of actions outside of React Hooks +When dispatching an action creator, you're responsible for storing a reference to the promise it returns in the event that you want to update that specific subscription. Also, you have to manually unsubscribe once your component unmounts. To get an idea of what that entails, see the [Svelte Example](../../usage/examples.mdx#svelte) or the [React Class Components Example](../../usage/examples.mdx#react-class-components) +::: + +#### Example + +```tsx no-transpile title="initiate query example" +import { useState } from 'react' +import { useAppDispatch } from './store/hooks' +import { api } from './services/api' + +function App() { + const dispatch = useAppDispatch() + const [postId, setPostId] = useState(1) + + useEffect(() => { + // highlight-start + // Add a subscription + const result = dispatch(api.endpoints.getPost.initiate(postId)) + + // Return the `unsubscribe` callback to be called in the `useEffect` cleanup step + return result.unsubscribe + // highlight-end + }, [dispatch, postId]) + + return ( +
+
Initiate query example
+
+ ) +} +``` + +```tsx no-transpile title="initiate mutation example" +import { useState } from 'react' +import { useAppDispatch } from './store/hooks' +import { api, Post } from './services/api' + +function App() { + const dispatch = useAppDispatch() + const [newPost, setNewPost] = useState>({ name: 'Ash' }) + + function handleClick() { + // highlight-start + // Trigger a mutation + // The `track` property can be set `false` in situations where we aren't + // interested in the result of the mutation + dispatch(api.endpoints.addPost.initiate(newPost), { track: false }) + // highlight-end + } + + return ( +
+
Initiate mutation example
+ +
+ ) +} +``` + +## `select` + +#### Signature + +```ts no-transpile +type CreateCacheSelectorFactory = + | QueryResultSelectorFactory + | MutationResultSelectorFactory + +type QueryResultSelectorFactory = ( + queryArg: QueryArg | SkipToken, +) => (state: RootState) => QueryResultSelectorResult + +type MutationResultSelectorFactory< + Definition extends MutationDefinition, + RootState, +> = ( + requestId: string | SkipToken, +) => (state: RootState) => MutationSubState & RequestStatusFlags + +type SkipToken = typeof Symbol +``` + +#### Description + +A function that accepts a cache key argument, and generates a new memoized selector for reading cached data for this endpoint using the given cache key. The generated selector is memoized using [Reselect's `createSelector`](https://redux-toolkit.js.org/api/createSelector). + +When selecting mutation results rather than queries, the function accepts a request ID instead. + +RTKQ defines a `Symbol` named `skipToken` internally. If `skipToken` is passed as the query argument to these selectors, the selector will return a default uninitialized state. This can be used to avoid returning a value if a given query is supposed to be disabled. + +React Hooks users will most likely never need to use these directly, as the hooks automatically use these selectors as needed. + +:::caution + +Each call to `.select(someCacheKey)` returns a _new_ selector function instance. In order for memoization to work correctly, you should create a given selector function once per cache key and reuse that selector function instance, rather than creating a new selector instance each time. + +::: + +#### Example + +```tsx no-transpile title="select query example" +import { useState, useMemo } from 'react' +import { useAppDispatch, useAppSelector } from './store/hooks' +import { api } from './services/api' + +function App() { + const dispatch = useAppDispatch() + const [postId, setPostId] = useState(1) + // highlight-start + // useMemo is used to only call `.select()` when required. + // Each call will create a new selector function instance + const selectPost = useMemo( + () => api.endpoints.getPost.select(postId), + [postId], + ) + const { data, isLoading } = useAppSelector(selectPost) + // highlight-end + + useEffect(() => { + // Add a subscription + const result = dispatch(api.endpoints.getPost.initiate(postId)) + + // Return the `unsubscribe` callback to be called in the cleanup step + return result.unsubscribe + }, [dispatch, postId]) + + if (isLoading) return
Loading post...
+ + return ( +
+
Initiate query example
+
Post name: {data.name}
+
+ ) +} +``` + +```tsx no-transpile title="select mutation example" +import { useState, useMemo } from 'react' +import { skipToken } from '@reduxjs/toolkit/query' +import { useAppDispatch, useAppSelector } from './store/hooks' +import { api } from './services/api' + +function App() { + const dispatch = useAppDispatch() + const [newPost, setNewPost] = useState({ name: 'Ash' }) + const [requestId, setRequestId] = useState( + skipToken, + ) + // highlight-start + // useMemo is used to only call `.select(..)` when required. + // Each call will create a new selector function instance + const selectMutationResult = useMemo( + () => api.endpoints.addPost.select(requestId), + [requestId], + ) + const { isLoading } = useAppSelector(selectMutationResult) + // highlight-end + + function handleClick() { + // Trigger a mutation + const result = dispatch(api.endpoints.addPost.initiate(newPost)) + // store the requestId to select the mutation result elsewhere + setRequestId(result.requestId) + } + + if (isLoading) return
Adding post...
+ + return ( +
+
Select mutation example
+ +
+ ) +} +``` + +## Matchers + +A set of [Redux Toolkit action matching utilities](https://redux-toolkit.js.org/api/matching-utilities) that match the `pending`, `fulfilled`, and `rejected` actions that will be dispatched by this thunk. These allow you to match on Redux actions for that endpoint, such as in `createSlice.extraReducers` or a custom middleware. Those are implemented as follows: + +```ts no-transpile + matchPending: isAllOf(isPending(thunk), matchesEndpoint(endpoint)), + matchFulfilled: isAllOf(isFulfilled(thunk), matchesEndpoint(endpoint)), + matchRejected: isAllOf(isRejected(thunk), matchesEndpoint(endpoint)), +``` diff --git a/docs/reference/rtk-query/created-api/hooks.mdx b/docs/reference/rtk-query/created-api/hooks.mdx new file mode 100644 index 00000000..333f91ee --- /dev/null +++ b/docs/reference/rtk-query/created-api/hooks.mdx @@ -0,0 +1,837 @@ +--- +id: hooks +title: 'API Slices: React Hooks' +sidebar_label: React Hooks +hide_title: true +--- + +  + +# API Slices: React Hooks + +## Hooks Overview + +The core RTK Query `createApi` method is UI-agnostic, in the same way that the Redux core library and Redux Toolkit are UI-agnostic. They are all plain JS logic that can be used anywhere. So, if you import `createApi` from `'@reduxjs/toolkit/query'`, it does not have any specific UI integrations included. + +However, RTK Query also provides the ability to auto-generate React hooks for each of your endpoints. Since this specifically depends on React itself, RTK Query provides an additional entry point that exposes a customized version of `createApi` that includes that functionality: + +```ts no-transpile +import { createApi } from '@reduxjs/toolkit/query/react' +``` + +If you have used the React-specific version of `createApi`, the generated `api` slice structure will also contain a set of React hooks. The primary endpoint hooks are available as `api.endpoints[endpointName].useQuery`, `api.endpoints[endpointName].useMutation`, and `api.endpoints[endpointName].useInfiniteQuery`, matching how you defined that endpoint. + +### Generated Hook Names + +The same hooks are also added to the `api` object itself, and given auto-generated names based on the endpoint name and query/mutation type. + +For example, if you had endpoints for `getPosts` and `updatePost`, these options would be available: + +```ts title="Generated React Hook names" no-transpile +// Hooks attached to the endpoint definition +const { data } = api.endpoints.getPosts.useQuery() +const [updatePost, { data }] = api.endpoints.updatePost.useMutation() + +// Same hooks, but given unique names and attached to the API slice object +const { data } = api.useGetPostsQuery() +const [updatePost, { data }] = api.useUpdatePostMutation() +``` + +The general format is `use(Endpointname)(Query|Mutation|InfiniteQuery)` - `use` is prefixed, the first letter of your endpoint name is capitalized, then `Query` or `Mutation` or `InfiniteQuery` is appended depending on the type. + +### Available Hooks + +RTK Query provides additional hooks for more advanced use-cases, although not all are generated directly on the `api` object as well. + +Most of the hooks are generated on a per-endpoint basis. + +The full list of hooks generated in the React-specific version of `createApi` is: + +- Endpoint-specific, generated the `api` object with a unique name and on the endpoint object with a generic name: + - [`useQuery`](#usequery) (all standard queries) + - [`useMutation`](#usemutation) (all mutations) + - [`useInfiniteQuery`](#useinfinitequery) (only infinite queries) + - [`useLazyQuery`](#uselazyquery) (all standard queries) +- Endpoint-specific, only generated on the endpoint object with a generic name: + - [`useQueryState`](#usequerystate) + - [`useQuerySubscription`](#usequerysubscription) + - [`useLazyQuerySubscription`](#uselazyquerysubscription) + - [`useInfiniteQueryState`](#useinfinitequerystate) + - [`useInfiniteQuerySubscription`](#useinfinitequerysubscription) +- Endpoint-agnostic, generated on the `api` object: + - [`usePrefetch`](#useprefetch) + +For the example above, the full set of generated hooks for the api would be like so: + +```ts title="Generated React Hooks" no-transpile +/* Hooks attached to the `getPosts` query endpoint definition */ +api.endpoints.getPosts.useQuery(arg, options) +api.endpoints.getPosts.useQueryState(arg, options) +api.endpoints.getPosts.useQuerySubscription(arg, options) +api.endpoints.getPosts.useLazyQuery(options) +api.endpoints.getPosts.useLazyQuerySubscription(options) + +/* hooks attached to the `getManyPosts` infinite query endpoint definition */ +api.endpoints.getManyPosts.useInfiniteQuery(arg, options) +api.endpoints.getManyPosts.useInfiniteQueryState(arg, options) +api.endpoints.getManyPosts.useInfiniteQuerySubscription(arg, options) + +/* Hooks attached to the `updatePost` mutation endpoint definition */ +api.endpoints.updatePost.useMutation(options) + +/* Hooks attached to the `api` object */ +// same as api.endpoints.getPosts.useQuery +api.useGetPostsQuery(arg, options) +// same as api.endpoints.getPosts.useLazyQuery +api.useLazyGetPostsQuery(arg, options) +// same as api.endpoints.updatePost.useMutation +api.useUpdatePostMutation(arg, options) +// same as api.endpoints.getManyPosts.useInfiniteQuery +api.useGetManyPostsInfiniteQuery(arg, options) +// Generic, used for any endpoint +api.usePrefetch(endpointName, options) +``` + +### Feature Comparison + +The provided hooks have a degree of feature overlap in order to provide options optimized for a given situation. The table below provides a comparison of the core features for each hook. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Feature +
+ useQuery +
+
+ + + + + + + + + + + +
Automatically triggers query requests✔️✔️
+ Allows manually triggering query requests + ✔️✔️✔️✔️✔️
+ Allows manually triggering mutation requests + ✔️
+ Subscribes a component to keep cached data in the store + ✔️✔️✔️✔️✔️
+ Returns request status and cached data from the store + ✔️✔️✔️✔️
+ Re-renders as request status and data become available + ✔️✔️✔️✔️
+ Accepts polling/re-fetching options to trigger automatic re-fetches + ✔️✔️✔️✔️
+ +## Primary Hooks + +These hooks are the main methods you will use to interact with RTK Query in your React components. They encapsulate all of logic and options needed for most data fetching and update use cases. + +### `useQuery` + +```ts title="Accessing a useQuery hook" no-transpile +const useQueryResult = api.endpoints.getPosts.useQuery(arg, options) +// or +const useQueryResult = api.useGetPostsQuery(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseQuery) + +#### `useQuery` Signature + +```ts no-transpile +type UseQuery = ( + arg: any | SkipToken, + options?: UseQueryOptions, +) => UseQueryResult + +type UseQueryOptions = { + pollingInterval?: number + skipPollingIfUnfocused?: boolean + refetchOnReconnect?: boolean + refetchOnFocus?: boolean + skip?: boolean + refetchOnMountOrArgChange?: boolean | number + selectFromResult?: (result: UseQueryStateDefaultResult) => any +} + +type UseQueryResult = { + // Base query state + + // Arguments passed to the query + originalArgs?: unknown + // The latest returned result regardless of hook arg, if present + data?: T + // The latest returned result for the current hook arg, if present + currentData?: T + // Error result if present + error?: unknown + // A string generated by RTK Query + requestId?: string + // The name of the given endpoint for the query + endpointName?: string + // Timestamp for when the query was initiated + startedTimeStamp?: number + // Timestamp for when the query was completed + fulfilledTimeStamp?: number + + // Derived request status booleans + + // Query has not started yet. + isUninitialized: boolean + // Query is currently loading for the first time. No data yet. + isLoading: boolean + // Query is currently fetching, but might have data from an earlier request. + isFetching: boolean + // Query has data from a successful load. + isSuccess: boolean + // Query is currently in an "error" state. + isError: boolean + + // A function to force refetch the query - returns a Promise with additional methods + refetch: () => QueryActionCreatorResult +} +``` + +- **Parameters** + - `arg`: The query argument to be used in constructing the query itself, and as a cache key for the query. + You can also pass in `skipToken` here as an alternative way of skipping the query, see [skipToken](#skiptoken) + - `options`: A set of options that control the fetching behavior of the hook +- **Returns** + - A query result object containing the current loading state, the actual data or error returned from the API call, metadata about the request, and a function to `refetch` the data. Can be customized with `selectFromResult` + +#### `skipToken` + +[summary](docblock://query/core/buildSelectors.ts?token=skipToken) + +See also [Skipping queries with TypeScript using `skipToken`](../../usage-with-typescript.mdx#skipping-queries-with-typescript-using-skiptoken) + +### `useMutation` + +```ts title="Accessing a useMutation hook" no-transpile +const useMutationResult = api.endpoints.updatePost.useMutation(options) +// or +const useMutationResult = api.useUpdatePostMutation(options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseMutation) + +#### `useMutation` Signature + +```ts no-transpile +type UseMutation = ( + options?: UseMutationStateOptions, +) => [UseMutationTrigger, UseMutationResult | SelectedUseMutationResult] + +type UseMutationStateOptions = { + // A method to determine the contents of `UseMutationResult` + selectFromResult?: (result: UseMutationStateDefaultResult) => any + // A string used to enable shared results across hook instances which have the same key + fixedCacheKey?: string +} + +type UseMutationTrigger = (arg: any) => Promise< + { data: T } | { error: BaseQueryError | SerializedError } +> & { + requestId: string // A string generated by RTK Query + abort: () => void // A method to cancel the mutation promise + unwrap: () => Promise // A method to unwrap the mutation call and provide the raw response/error + reset: () => void // A method to manually unsubscribe from the mutation call and reset the result to the uninitialized state +} + +type UseMutationResult = { + // Base query state + + // Arguments passed to the latest mutation call. Not available if using the `fixedCacheKey` option + originalArgs?: unknown + // Returned result if present + data?: T + // Error result if present + error?: unknown + // The name of the given endpoint for the mutation + endpointName?: string + // Timestamp for when the mutation was completed + fulfilledTimeStamp?: number + + // Derived request status booleans + + // Mutation has not been fired yet + isUninitialized: boolean + // Mutation has been fired and is awaiting a response + isLoading: boolean + // Mutation has data from a successful call + isSuccess: boolean + // Mutation is currently in an "error" state + isError: boolean + // Timestamp for when the latest mutation was initiated + startedTimeStamp?: number + + // A method to manually unsubscribe from the mutation call and reset the result to the uninitialized state + reset: () => void +} +``` + +:::tip + +The generated `UseMutation` hook will cause a component to re-render by default after the trigger callback is fired, as it affects the properties of the result. If you want to call the trigger but don't care about subscribing to the result with the hook, you can use the `selectFromResult` option to limit the properties that the hook cares about. + +Returning a completely empty object will mean that any individual mutation call will cause only one re-render at most, e.g. + +```ts no-transpile +selectFromResult: () => ({}) +``` + +::: + +- **Parameters** + + - `options`: A set of options that control the subscription behavior of the hook: + - `selectFromResult`: A callback that can be used to customize the mutation result returned as the second item in the tuple + - `fixedCacheKey`: An optional string used to enable shared results across hook instances + +- **Returns**: A tuple containing: + - `trigger`: A function that triggers an update to the data based on the provided argument. The trigger function returns a promise with the properties shown above that may be used to handle the behavior of the promise + - `mutationState`: A query status object containing the current loading state and metadata about the request, or the values returned by the `selectFromResult` option where applicable. + Additionally, this object will contain + - a `reset` method to reset the hook back to its original state and remove the current result from the cache + - an `originalArgs` property that contains the argument passed to the last call of the `trigger` function. + +### `useInfiniteQuery` + +```ts title="Accessing a useQuery hook" no-transpile +const useQueryResult = api.endpoints.getManyPosts.useInfiniteQuery(arg, options) +// or +const useQueryResult = api.useGetManyPostsInfiniteQuery(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseInfiniteQuery) + +#### `useInfiniteQuery` Signature + +```ts no-transpile +type UseInfiniteQuery = ( + arg: any | SkipToken, + options?: UseQueryOptions, +) => UseInfiniteQueryResult + +type InfiniteData = { + pages: Array + pageParams: Array +} + +type UseInfiniteQueryOptions = { + pollingInterval?: number + skipPollingIfUnfocused?: boolean + refetchOnReconnect?: boolean + refetchOnFocus?: boolean + skip?: boolean + refetchOnMountOrArgChange?: boolean | number + selectFromResult?: (result: UseQueryStateDefaultResult) => any + initialPageParam?: PageParam +} + +type UseInfiniteQueryResult = { + // Base query state + + // Arguments passed to the query + originalArgs?: unknown + // The latest returned result regardless of hook arg, if present + data?: InfiniteData + // The latest returned result for the current hook arg, if present + currentData?: InfiniteData + // Error result if present + error?: unknown + // A string generated by RTK Query + requestId?: string + // The name of the given endpoint for the query + endpointName?: string + // Timestamp for when the query was initiated + startedTimeStamp?: number + // Timestamp for when the query was completed + fulfilledTimeStamp?: number + + // Derived request status booleans + + // Query has not started yet. + isUninitialized: boolean + // Query is currently loading for the first time. No data yet. + isLoading: boolean + // Query is currently fetching, but might have data from an earlier request. + isFetching: boolean + // Query has data from a successful load. + isSuccess: boolean + // Query is currently in an "error" state. + isError: boolean + + // Derived request status booleans for infinite query pages + + // There is another page available querying forwards + hasNextPage: boolean + // There is another page available querying backwards + hasPreviousPage: boolean + // The current in-progress fetch is for the next page + isFetchingNextPage: boolean + // The current in-progress fetch is for the previous page + isFetchingPreviousPage: boolean + // The current error occurred fetching the next page + isFetchNextPageError: boolean + // The current error occurred fetching the previous page + isFetchPreviousPageError: boolean + + // A function to force refetch the query - returns a Promise with additional methods + refetch: () => InfiniteQueryActionCreatorResult + + // Triggers a fetch for the next page, based on the current cache + fetchNextPage: () => InfiniteQueryActionCreatorResult + // Triggers a fetch for the previous page, based on the current cache + fetchPreviousPage: () => InfiniteQueryActionCreatorResult +} +``` + +- **Parameters** + - `arg`: The query argument to be used in constructing the query itself, and as a cache key for the query. + You can also pass in `skipToken` here as an alternative way of skipping the query, see [skipToken](#skiptoken) + - `options`: A set of options that control the fetching behavior of the hook +- **Returns** + - A query result object containing the current loading state, the actual data or error returned from the API call, metadata about the request, and a function to `refetch` the data. Can be customized with `selectFromResult` + +## Secondary Hooks + +These hooks are useful for specific additional use cases in your application, but will probably not be used that frequently. + +### `useLazyQuery` + +```ts title="Accessing a useLazyQuery hook" no-transpile +const [trigger, result, lastPromiseInfo] = + api.endpoints.getPosts.useLazyQuery(options) +// or +const [trigger, result, lastPromiseInfo] = api.useLazyGetPostsQuery(options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseLazyQuery) + +#### `useLazyQuery` Signature + +```ts no-transpile +type UseLazyQuery = ( + options?: UseLazyQueryOptions +) => [UseLazyQueryTrigger, UseLazyQueryStateResult, UseLazyQueryLastPromiseInfo] + +type UseLazyQueryOptions = { + pollingInterval?: number + skipPollingIfUnfocused?: boolean + refetchOnReconnect?: boolean + refetchOnFocus?: boolean + selectFromResult?: (result: UseQueryStateDefaultResult) => any +} + +type UseLazyQueryTrigger = (arg: any, preferCacheValue?: boolean) => Promise< + QueryResultSelectorResult +> & { + // Whatever argument was provided to the query + arg: unknown + // A string generated by RTK Query + requestId: string + // The values used for the query subscription + subscriptionOptions: SubscriptionOptions + + // A method to cancel the query promise + abort: () => void + // A method to unwrap the query call and provide the raw response/error + unwrap: () => Promise + // A method used to manually unsubscribe from the query results + unsubscribe: () => void + // A method used to re-run the query. In most cases when using a lazy query, you will never use this and should prefer to call the trigger again. + refetch: () => void + // A method used to update the subscription options (eg. pollingInterval) + updateSubscriptionOptions: (options: SubscriptionOptions) () => void +} + +type UseLazyQueryStateResult = { + // Base query state + + // Arguments passed to the query + originalArgs?: unknown + // The latest returned result regardless of hook arg, if present + data?: T + // The latest returned result for the current hook arg, if present + currentData?: T + // Error result if present + error?: unknown + // A string generated by RTK Query + requestId?: string + // The name of the given endpoint for the query + endpointName?: string + // Timestamp for when the query was initiated + startedTimeStamp?: number + // Timestamp for when the query was completed + fulfilledTimeStamp?: number + + // Derived request status booleans + + // Query has not started yet. + isUninitialized: boolean + // Query is currently loading for the first time. No data yet. + isLoading: boolean + // Query is currently fetching, but might have data from an earlier request. + isFetching: boolean + // Query has data from a successful load. + isSuccess: boolean + // Query is currently in an "error" state. + isError: boolean +} + +type UseLazyQueryLastPromiseInfo = { + lastArg: any +} +``` + +- **Parameters** + + - `options`: A set of options that control the fetching behavior and returned result value of the hook. Options affecting fetching behavior will only have an effect after the lazy query has been triggered at least once. + +- **Returns**: A tuple containing: + - `trigger`: A function that fetches the corresponding data for the endpoint when called + - `result`: A query result object containing the current loading state, the actual data or error returned from the API call and metadata about the request. Can be customized with `selectFromResult` + - `lastPromiseInfo`: An object containing the last argument used to call the trigger function + +### `usePrefetch` + +```ts title="Accessing a usePrefetch hook" no-transpile +const prefetchCallback = api.usePrefetch(endpointName, options) +``` + +A React hook which can be used to initiate fetching data ahead of time. + +##### Features + +- Manual control over firing a request to retrieve data + +##### Signature + +```ts no-transpile +type UsePrefetch = ( + endpointName: string, + options?: UsePrefetchOptions, +) => PrefetchCallback + +type UsePrefetchOptions = + | { + // If specified, only runs the query if the difference between `new Date()` and the last + // `fulfilledTimeStamp` is greater than the given value (in seconds) + ifOlderThan?: false | number + } + | { + // If `force: true`, it will ignore the `ifOlderThan` value if it is set and the query + // will be run even if it exists in the cache. + force?: boolean + } + +type PrefetchCallback = (arg: any, options?: UsePrefetchOptions) => void +``` + +- **Parameters** + + - `endpointName`: The name of the endpoint to prefetch data for + - `options`: A set of options that control whether the prefetch request should occur + +- **Returns** + - A `prefetch` callback that when called, will initiate fetching the data for the provided endpoint + +## Implementation Hooks + +This hooks exist as implementation details of the primary hooks. They may be useful in rare cases, but you should generally use the primary hooks in your apps. + +### `useQueryState` + +```ts title="Accessing a useQuery hook" no-transpile +const useQueryStateResult = api.endpoints.getPosts.useQueryState(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseQueryState) + +##### `useQueryState` Signature + +```ts no-transpile +type UseQueryState = ( + arg: any | SkipToken, + options?: UseQueryStateOptions, +) => UseQueryStateResult | SelectedQueryStateResult + +type UseQueryStateOptions = { + skip?: boolean + selectFromResult?: (result: UseQueryStateDefaultResult) => any +} + +type UseQueryStateResult = { + // Base query state + + // Arguments passed to the query + originalArgs?: unknown + // The latest returned result regardless of hook arg, if present + data?: T + // The latest returned result for the current hook arg, if present + currentData?: T + // Error result if present + error?: unknown + // A string generated by RTK Query + requestId?: string + // The name of the given endpoint for the query + endpointName?: string + // Timestamp for when the query was initiated + startedTimeStamp?: number + // Timestamp for when the query was completed + fulfilledTimeStamp?: number + + // Derived request status booleans + + // Query has not started yet. + isUninitialized: boolean + // Query is currently loading for the first time. No data yet. + isLoading: boolean + // Query is currently fetching, but might have data from an earlier request. + isFetching: boolean + // Query has data from a successful load. + isSuccess: boolean + // Query is currently in an "error" state. + isError: boolean +} +``` + +- **Parameters** + + - `arg`: The argument passed to the query defined in the endpoint. + You can also pass in `skipToken` here as an alternative way of skipping the selection, see [skipToken](#skiptoken) + - `options`: A set of options that control the return value for the hook + +- **Returns** + - A query result object containing the current loading state, the actual data or error returned from the API call and metadata about the request. Can be customized with `selectFromResult` + +### `useQuerySubscription` + +```ts title="Accessing a useQuerySubscription hook" no-transpile +const { refetch } = api.endpoints.getPosts.useQuerySubscription(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseQuerySubscription) + +##### `useQuerySubscription` Signature + +```ts no-transpile +type UseQuerySubscription = ( + arg: any | SkipToken, + options?: UseQuerySubscriptionOptions, +) => UseQuerySubscriptionResult + +type UseQuerySubscriptionOptions = { + skip?: boolean + refetchOnMountOrArgChange?: boolean | number + pollingInterval?: number + skipPollingIfUnfocused?: boolean + refetchOnReconnect?: boolean + refetchOnFocus?: boolean +} + +type UseQuerySubscriptionResult = { + refetch: () => void // A function to force refetch the query +} +``` + +- **Parameters** + + - `arg`: The argument passed to the query defined in the endpoint. + You can also pass in `skipToken` here as an alternative way of skipping the query, see [skipToken](#skiptoken) + - `options`: A set of options that control the fetching behavior of the hook + +- **Returns** + - An object containing a function to `refetch` the data + +### `useInfiniteQueryState` + +```ts title="Accessing a useInfiniteQueryState hook" no-transpile +const useInfiniteQueryStateResult = + api.endpoints.getManyPosts.useInfiniteQueryState(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseInfiniteQueryState) + +### `useInfiniteQuerySubscription` + +```ts title="Accessing a useInfiniteQuerySubscription hook" no-transpile +const useInfiniteQuerySubscriptionResult = + api.endpoints.getManyPosts.useInfiniteQuerySubscription(arg, options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseInfiniteQuerySubscription) + +### `useLazyQuerySubscription` + +```ts title="Accessing a useLazyQuerySubscription hook" no-transpile +const [trigger, lastArg] = + api.endpoints.getPosts.useLazyQuerySubscription(options) +``` + +[summary](docblock://query/react/buildHooks.ts?token=UseLazyQuerySubscription) + +##### `useLazyQuerySubscription` Signature + +```ts no-transpile +type UseLazyQuerySubscription = ( + options?: UseLazyQuerySubscriptionOptions, +) => [UseLazyQuerySubscriptionTrigger, LastArg] + +type UseLazyQuerySubscriptionOptions = { + pollingInterval?: number + skipPollingIfUnfocused?: boolean + refetchOnReconnect?: boolean + refetchOnFocus?: boolean +} + +type UseLazyQuerySubscriptionTrigger = ( + arg: any, + preferCacheValue?: boolean, +) => void +``` + +- **Parameters** + + - `options`: A set of options that control the fetching behavior of the hook. The options will only have an effect after the lazy query has been triggered at least once. + +- **Returns**: A tuple containing: + - `trigger`: A function that fetches the corresponding data for the endpoint when called + - `lastArg`: The last argument used to call the trigger function diff --git a/docs/reference/rtk-query/created-api/overview.mdx b/docs/reference/rtk-query/created-api/overview.mdx new file mode 100644 index 00000000..e911307b --- /dev/null +++ b/docs/reference/rtk-query/created-api/overview.mdx @@ -0,0 +1,183 @@ +--- +id: overview +title: Overview +sidebar_label: API Slice Overview +hide_title: true +--- + +  + +# Generated API Slices + +## API Slice Overview + +When you call [`createApi`](../createApi.mdx), it automatically generates and returns an API service "slice" object structure containing Redux logic you can use to interact with the endpoints you defined. This slice object includes a reducer to manage cached data, a middleware to manage cache lifetimes and subscriptions, and selectors and thunks for each endpoint. If you imported `createApi` from the React-specific entry point, it also includes auto-generated React hooks for use in your components. + +This section documents the contents of that API structure, with the different fields grouped by category. The API types and descriptions are listed on separate pages for each category. + +:::tip + +Typically, you should only have one API slice per base URL that your application needs to communicate with. For example, if your site fetches data from both `/api/posts` and `/api/users`, you would have a single API slice with `/api/` as the base URL, and separate endpoint definitions for `posts` and `users`. This allows you to effectively take advantage of [automated re-fetching](../../usage/automated-refetching.mdx) by defining [tag](../../usage/automated-refetching.mdx#tags) relationships across endpoints. + +This is because: + +- Automatic tag invalidation only works within a single API slice. If you have multiple API slices, the automatic invalidation won't work across them. +- Every `createApi` call generates its own middleware, and each middleware added to the store will run checks against every dispatched action. That has a perf cost that adds up. So, if you called `createApi` 10 times and added 10 separate API middleware to the store, that will be noticeably slower perf-wise. + +For maintainability purposes, you may wish to split up endpoint definitions across multiple files, while still maintaining a single API slice which includes all of these endpoints. See [code splitting](../../usage/code-splitting.mdx) for how you can use the `injectEndpoints` property to inject API endpoints from other files into a single API slice definition. + +::: + +```ts title="API Slice Contents" no-transpile +const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: (build) => ({ + // ... + }), +}) + +type Api = { + // Redux integration + reducerPath: string + reducer: Reducer + middleware: Middleware + + // Endpoint interactions + endpoints: Record + + // Code splitting and generation + injectEndpoints: (options: InjectEndpointsOptions) => UpdatedApi + enhanceEndpoints: (options: EnhanceEndpointsOptions) => UpdatedApi + + // Utilities + utils: { + updateQueryData: UpdateQueryDataThunk + patchQueryData: PatchQueryDataThunk + prefetch: PrefetchThunk + invalidateTags: ActionCreatorWithPayload< + Array>, + string + > + selectInvalidatedBy: ( + state: FullState, + tags: Array>, + ) => Array<{ + endpointName: string + originalArgs: any + queryCacheKey: string + }> + selectCachedArgsForQuery: ( + state: FullState, + endpointName: EndpointName, + ) => Array + resetApiState: ActionCreator + getRunningQueryThunk( + endpointName: EndpointName, + args: QueryArg, + ): ThunkWithReturnValue + getRunningMutationThunk( + endpointName: EndpointName, + fixedCacheKeyOrRequestId: string, + ): ThunkWithReturnValue + getRunningQueriesThunk(): ThunkWithReturnValue< + Array> + > + getRunningMutationsThunk(): ThunkWithReturnValue< + Array> + > + } + + // Internal actions + internalActions: InternalActions + + // React hooks (if applicable) + [key in GeneratedReactHooks]: GeneratedReactHooks[key] +} +``` + +## Redux Integration + +Internally, `createApi` will call [the Redux Toolkit `createSlice` API](https://redux-toolkit.js.org/api/createSlice) to generate a slice reducer and corresponding action creators with the appropriate logic for caching fetched data. It also automatically generates a custom Redux middleware that manages subscription counts and cache lifetimes. + +The generated slice reducer and the middleware both need to be adding to your Redux store setup in `configureStore` in order to work correctly. + +:::info API Reference + +- [API Slices: Redux Integration](./redux-integration.mdx) + +::: + +## Endpoints + +The API slice object will have an `endpoints` field inside. This section maps the endpoint names you provided to `createApi` to the core Redux logic (thunks and selectors) used to trigger data fetches and read cached data for that endpoint. If you're using the React-specific version of `createApi`, each endpoint definition will also contain the auto-generated React hooks for that endpoint. + +:::info API Reference + +- [API Slices: Endpoints](./endpoints.mdx) + +::: + +## Code Splitting and Generation + +Each API slice allows [additional endpoint definitions to be injected at runtime](../../usage/code-splitting.mdx) after the initial API slice has been defined. This can be beneficial for apps that may have _many_ endpoints. + +The individual API slice endpoint definitions can also be split across multiple files. This is primarily useful for working with API slices that were [code-generated from an API schema file](../../usage/code-generation.mdx), allowing you to add additional custom behavior and configuration to a set of automatically-generated endpoint definitions. + +Each API slice object has `injectEndpoints` and `enhanceEndpoints` functions to support these use cases. + +:::info API Reference + +- [API Slices: Code Splitting and Generation](./code-splitting.mdx) + +::: + +## API Slice Utilities + +The `util` field includes various utility functions that can be used to manage the cache, including +manually updating query cache data, triggering pre-fetching of data, manually invalidating tags, +and manually resetting the api state, as well as other utility functions that can be used in +various scenarios, including SSR. + +:::info API Reference + +- [API Slices: Utilities](./api-slice-utils.mdx) + +::: + +## Internal Actions + +The `internalActions` field contains a set of additional thunks that are used for internal behavior, such as managing updates based on focus. + +## React Hooks + +The core RTK Query `createApi` method is UI-agnostic, in the same way that the Redux core library and Redux Toolkit are UI-agnostic. They are all plain JS logic that can be used anywhere. + +However, RTK Query also provides the ability to auto-generate React hooks for each of your endpoints. Since this specifically depends on React itself, RTK Query provides an alternate entry point that exposes a customized version of `createApi` that includes that functionality: + +```js +import { createApi } from '@reduxjs/toolkit/query/react' +``` + +If you have used the React-specific version of `createApi`, the generated `Api` slice structure will also contain a set of React hooks. These endpoint hooks are available as `api.endpoints[endpointName].useQuery` or `api.endpoints[endpointName].useMutation`, matching how you defined that endpoint. + +The same hooks are also added to the `Api` object itself, and given auto-generated names based on the endpoint name and query/mutation type. + +For example, if you had endpoints for `getPosts` and `updatePost`, these options would be available: + +```ts title="Generated React Hook names" no-transpile +// Hooks attached to the endpoint definition +const { data } = api.endpoints.getPosts.useQuery() +const { data } = api.endpoints.updatePost.useMutation() + +// Same hooks, but given unique names and attached to the API slice object +const { data } = api.useGetPostsQuery() +const [updatePost] = api.useUpdatePostMutation() +``` + +The React-specific version of `createApi` also generates a `usePrefetch` hook, attached to the `Api` object, which can be used to initiate fetching data ahead of time. + +:::info API Reference + +- [API Slices: React Hooks](./hooks.mdx) + +::: diff --git a/docs/reference/rtk-query/created-api/redux-integration.mdx b/docs/reference/rtk-query/created-api/redux-integration.mdx new file mode 100644 index 00000000..662c0cc0 --- /dev/null +++ b/docs/reference/rtk-query/created-api/redux-integration.mdx @@ -0,0 +1,67 @@ +--- +id: redux-integration +title: 'API Slices: Redux Integration' +sidebar_label: Redux Integration +hide_title: true +--- + +  + +# API Slices: Redux Integration + +Internally, `createApi` will call [the Redux Toolkit `createSlice` API](https://redux-toolkit.js.org/api/createSlice) to generate a slice reducer and corresponding action creators with the appropriate logic for caching fetched data. It also automatically generates a custom Redux middleware that manages subscription counts and cache lifetimes. + +The generated slice reducer and the middleware both need to be added to your Redux store setup in `configureStore` in order to work correctly: + +```ts title="src/store.ts" +// file: src/services/pokemon.ts noEmit +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' + +export const pokemonApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: () => ({}), +}) + +// file: src/store.ts +import { configureStore } from '@reduxjs/toolkit' +import { setupListeners } from '@reduxjs/toolkit/query' +import { pokemonApi } from './services/pokemon' + +export const store = configureStore({ + reducer: { + // Add the generated reducer as a specific top-level slice + [pokemonApi.reducerPath]: pokemonApi.reducer, + }, + // Adding the api middleware enables caching, invalidation, polling, + // and other useful features of `rtk-query`. + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(pokemonApi.middleware), +}) + +// configure listeners using the provided defaults +setupListeners(store.dispatch) +``` + +## `reducerPath` + +```ts no-transpile +reducerPath: string +``` + +Contains the `reducerPath` option provided to `createApi`. Use this as the root state key when adding the `reducer` function to the store so that the rest of the generated API logic can find the state correctly. + +## `reducer` + +```ts no-transpile +reducer: Reducer +``` + +A standard Redux slice reducer function containing the logic for updating the cached data. Add this to the Redux store using the `reducerPath` you provided as the root state key. + +## `middleware` + +```ts no-transpile +middleware: Middleware +``` + +A custom Redux middleware that contains logic for managing caching, invalidation, subscriptions, polling, and more. Add this to the store setup after other middleware. diff --git a/docs/reference/rtk-query/fetchBaseQuery.mdx b/docs/reference/rtk-query/fetchBaseQuery.mdx new file mode 100644 index 00000000..1cbaee24 --- /dev/null +++ b/docs/reference/rtk-query/fetchBaseQuery.mdx @@ -0,0 +1,403 @@ +--- +id: fetchBaseQuery +title: fetchBaseQuery +sidebar_label: fetchBaseQuery +hide_title: true +hide_table_of_contents: false +description: 'RTK Query > API: fetchBaseQuery reference' +--- + +  + +# `fetchBaseQuery` + +This is a very small wrapper around [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) that aims to simplify HTTP requests. It is not a full-blown replacement for `axios`, `superagent`, or any other more heavyweight library, but it will cover the vast majority of your HTTP request needs. + +`fetchBaseQuery` is a factory function that generates a data fetching method compatible with RTK Query's `baseQuery` configuration option. It takes all standard options from fetch's [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) interface, as well as `baseUrl`, a `prepareHeaders` function, an optional `fetch` function, a `paramsSerializer` function, and a `timeout`. + +## Basic Usage + +To use it, import it when you are [creating an API service definition](../../tutorials/rtk-query#create-an-api-service), call it as `fetchBaseQuery(options)`, and pass the result as the `baseQuery` field in `createApi`: + +```ts title="src/services/pokemon.ts" +// Or from '@reduxjs/toolkit/query/react' +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' + +export const pokemonApi = createApi({ + // Set the baseUrl for every endpoint below + baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), + endpoints: (build) => ({ + getPokemonByName: build.query({ + // Will make a request like https://pokeapi.co/api/v2/pokemon/bulbasaur + query: (name: string) => `pokemon/${name}`, + }), + updatePokemon: build.mutation({ + query: ({ name, patch }) => ({ + url: `pokemon/${name}`, + // When performing a mutation, you typically use a method of + // PATCH/PUT/POST/DELETE for REST endpoints + method: 'PATCH', + // fetchBaseQuery automatically adds `content-type: application/json` to + // the Headers and calls `JSON.stringify(patch)` + body: patch, + }), + }), + }), +}) +``` + +## Signature + +```ts title="fetchBaseQuery signature" no-transpile +type FetchBaseQuery = ( + args: FetchBaseQueryArgs, +) => ( + args: string | FetchArgs, + api: BaseQueryApi, + extraOptions: ExtraOptions, +) => FetchBaseQueryResult + +type FetchBaseQueryArgs = { + baseUrl?: string + prepareHeaders?: ( + headers: Headers, + api: Pick< + BaseQueryApi, + 'getState' | 'extra' | 'endpoint' | 'type' | 'forced' + > & { arg: string | FetchArgs }, + ) => MaybePromise + fetchFn?: ( + input: RequestInfo, + init?: RequestInit | undefined, + ) => Promise + paramsSerializer?: (params: Record) => string + isJsonContentType?: (headers: Headers) => boolean + jsonContentType?: string + timeout?: number +} & RequestInit + +type FetchBaseQueryResult = Promise< + | { + data: any + error?: undefined + meta?: { request: Request; response: Response } + } + | { + error: FetchBaseQueryError + data?: undefined + meta?: { request: Request; response: Response } + } +> + +type FetchBaseQueryError = + | { + /** + * * `number`: + * HTTP status code + */ + status: number + data: unknown + } + | { + /** + * * `"FETCH_ERROR"`: + * An error that occurred during execution of `fetch` or the `fetchFn` callback option + **/ + status: 'FETCH_ERROR' + data?: undefined + error: string + } + | { + /** + * * `"PARSING_ERROR"`: + * An error happened during parsing. + * Most likely a non-JSON-response was returned with the default `responseHandler` "JSON", + * or an error occurred while executing a custom `responseHandler`. + **/ + status: 'PARSING_ERROR' + originalStatus: number + data: string + error: string + } + | { + /** + * * `"TIMEOUT_ERROR"`: + * Request timed out + **/ + status: 'TIMEOUT_ERROR' + data?: undefined + error: string + } + | { + /** + * * `"CUSTOM_ERROR"`: + * A custom error type that you can return from your `queryFn` where another error might not make sense. + **/ + status: 'CUSTOM_ERROR' + data?: unknown + error: string + } +``` + +## Parameters + +### `baseUrl` + +_(required)_ + +Typically a string like `https://api.your-really-great-app.com/v1/`. If you don't provide a `baseUrl`, it defaults to a relative path from where the request is being made. **You should most likely _always_ specify this**. + +### `prepareHeaders` + +_(optional)_ + +Allows you to inject headers on every request. You can specify headers at the endpoint level, but you'll typically want to set common headers like `authorization` here. As a convenience mechanism, the second argument allows you to use `getState` to access your redux store in the event you store information you'll need there such as an auth token. Additionally, it provides access to `arg`, `extra`, `endpoint`, `type`, and `forced` to unlock more granular conditional behaviors. + +You can mutate the `headers` argument directly, and returning it is optional. + +```ts title="prepareHeaders signature" no-transpile +type prepareHeaders = ( + headers: Headers, + api: { + getState: () => unknown + arg: string | FetchArgs + extra: unknown + endpoint: string + type: 'query' | 'mutation' + forced: boolean | undefined + }, +) => Headers | void +``` + +### `paramsSerializer` + +_(optional)_ + +A function that can be used to apply custom transformations to the data passed into [`params`](#setting-the-query-string). If you don't provide this, `params` will be given directly to `new URLSearchParams()`. With some API integrations, you may need to leverage this to use something like the [`query-string`](https://github.com/sindresorhus/query-string) library to support different array types. + +### `fetchFn` + +_(optional)_ + +A fetch function that overrides the default on the window. Can be useful in SSR environments where you may need to leverage `isomorphic-fetch` or `cross-fetch`. + +### `timeout` + +_(optional)_ + +A number in milliseconds that represents the maximum time a request can take before timing out. + +### `isJsonContentType` + +_(optional)_ + +A callback that receives a `Headers` object and determines the `body` field of the `FetchArgs` argument should be stringified via `JSON.stringify()`. + +The default implementation inspects the `content-type` header, and will match values like `"application/json"` and `"application/vnd.api+json"`. + +### `jsonContentType` + +_(optional)_ + +Used when automatically setting the `content-type` header for a request with a jsonifiable body that does not have an explicit `content-type` header. Defaults to `"application/json"`. + +## Common Usage Patterns + +### Setting default headers on requests + +The most common use case for `prepareHeaders` would be to automatically include `authorization` headers for your API requests. + +```ts title="Setting a token from a redux store value +// file: store.ts noEmit +export type RootState = { auth: { token: string } } + +// file: baseQuery.ts +import { fetchBaseQuery } from '@reduxjs/toolkit/query' +import type { RootState } from './store' + +const baseQuery = fetchBaseQuery({ + baseUrl: '/', + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).auth.token + + // If we have a token set in state, let's assume that we should be passing it. + if (token) { + headers.set('authorization', `Bearer ${token}`) + } + + return headers + }, +}) +``` + +## Individual query options + +There is more behavior that you can define on a per-request basis. The `query` field may return an object containing any of the default `fetch` options available to the `RequestInit` interface, as well as these additional options: + +```ts title="endpoint request options" +interface FetchArgs extends RequestInit { + url: string + params?: Record + body?: any + responseHandler?: + | 'json' + | 'text' + | `content-type` + | ((response: Response) => Promise) + validateStatus?: (response: Response, body: any) => boolean + timeout?: number +} + +const defaultValidateStatus = (response: Response) => + response.status >= 200 && response.status <= 299 +``` + +### Setting the body + +By default, `fetchBaseQuery` assumes that every request you make will be `json`, so in those cases all you have to do is set the `url` and pass a `body` object when appropriate. For other implementations, you can manually set the `Headers` to specify the content type. + +#### json + +```ts no-transpile + // omitted + endpoints: (build) => ({ + updateUser: build.query({ + query: (user: Record) => ({ + url: `users`, + method: 'PUT', + body: user // Body is automatically converted to json with the correct headers + }), + }), +``` + +#### text + +```ts no-transpile + // omitted + endpoints: (build) => ({ + updateUser: build.query({ + query: (user: Record) => ({ + url: `users`, + method: 'PUT', + headers: { + 'content-type': 'text/plain', + }, + body: user + }), + }), +``` + +### Setting the query string + +`fetchBaseQuery` provides a simple mechanism that converts an `object` to a serialized query string by passing the object to `new URLSearchParms()`. If this doesn't suit your needs, you have two options: + +1. Pass the `paramsSerializer` option to `fetchBaseQuery` to apply custom transformations +2. Build your own querystring and set it in the `url` + +```ts no-transpile + // omitted + endpoints: (build) => ({ + updateUser: build.query({ + query: (user: Record) => ({ + url: `users`, + // Assuming no `paramsSerializer` is specified, the user object is automatically converted + // and produces a url like /api/users?first_name=test&last_name=example + params: user + }), + }), +``` + +### Parsing a Response + +By default, `fetchBaseQuery` assumes that every `Response` you get will be parsed as `json`. In the event that you don't want that to happen, you can customize the behavior by specifying an alternative response handler like `text`, or take complete control and use a custom function that accepts the raw `Response` object — allowing you to use any [`Response` method](https://developer.mozilla.org/en-US/docs/Web/API/Response). + +The `responseHandler` field can be either: + +```ts +type ResponseHandler = + | 'content-type' + | 'json' + | 'text' + | ((response: Response) => Promise) +``` + +The `"json"` and `"text"` values instruct `fetchBaseQuery` to the corresponding fetch response methods for reading the body. `content-type` will check the header field to first determine if this appears to be JSON, and then use one of those two methods. The callback allows you to process the body yourself. + +```ts title="Parse a Response as text" +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' + +export const customApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), + endpoints: (build) => ({ + getUsers: build.query({ + query: () => ({ + url: `users`, + // This is the same as passing 'text' + responseHandler: (response) => response.text(), + }), + }), + }), +}) +``` + +:::note Note about responses that return an undefined body +If you make a `json` request to an API that only returns a `200` with an undefined body, `fetchBaseQuery` will pass that through as `undefined` and will not try to parse it as `json`. This can be common with some APIs, especially on `delete` requests. +::: + +#### Default response handler + +The default response handler is `"json"`, which is equivalent to the following function: + +```ts title="Default responseHandler" +const defaultResponseHandler = async (res: Response) => { + const text = await res.text() + return text.length ? JSON.parse(text) : null +} +``` + +### Handling non-standard Response status codes + +By default, `fetchBaseQuery` will `reject` any `Response` that does not have a status code of `2xx` and set it to `error`. This is the same behavior you've most likely experienced with `axios` and other popular libraries. In the event that you have a non-standard API you're dealing with, you can use the `validateStatus` option to customize this behavior. + +```ts title="Using a custom validateStatus" +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' + +export const customApi = createApi({ + // Set the baseUrl for every endpoint below + baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), + endpoints: (build) => ({ + getUsers: build.query({ + query: () => ({ + url: `users`, + // Example: we have a backend API always returns a 200, + // but sets an `isError` property when there is an error. + validateStatus: (response, result) => + response.status === 200 && !result.isError, + }), + }), + }), +}) +``` + +### Adding a custom timeout to requests + +By default, `fetchBaseQuery` has no default timeout value set, meaning your requests will stay pending until your api resolves the request(s) or it reaches the browser's default timeout (normally 5 minutes). Most of the time, this isn't what you'll want. When using `fetchBaseQuery`, you have the ability to set a `timeout` on the `baseQuery` or on individual endpoints. When specifying both options, the endpoint value will take priority. + +```ts title="Setting a timeout value" +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' + +export const api = createApi({ + // Set a default timeout of 10 seconds + baseQuery: fetchBaseQuery({ baseUrl: '/api/', timeout: 10000 }), + endpoints: (build) => ({ + getUsers: build.query({ + query: () => ({ + url: `users`, + // Example: we know the users endpoint is _really fast_ because it's always cached. + // We can assume if it's over > 1000ms, something is wrong and we should abort the request. + timeout: 1000, + }), + }), + }), +}) +``` diff --git a/docs/reference/rtk-query/setupListeners.mdx b/docs/reference/rtk-query/setupListeners.mdx new file mode 100644 index 00000000..feb44c12 --- /dev/null +++ b/docs/reference/rtk-query/setupListeners.mdx @@ -0,0 +1,79 @@ +--- +id: setupListeners +title: setupListeners +sidebar_label: setupListeners +hide_title: true +hide_table_of_contents: false +description: 'RTK Query > API: setupListeners reference' +--- + +  + +# `setupListeners` + +A utility used to enable `refetchOnFocus` and `refetchOnReconnect` behaviors. It requires the `dispatch` method from your store. Calling `setupListeners(store.dispatch)` will configure listeners with the recommended defaults, but you have the option of providing a callback for more granular control. + +```ts title="setupListeners default configuration" no-transpile +let initialized = false +export function setupListeners( + dispatch: ThunkDispatch, + customHandler?: ( + dispatch: ThunkDispatch, + actions: { + onFocus: typeof onFocus + onFocusLost: typeof onFocusLost + onOnline: typeof onOnline + onOffline: typeof onOffline + }, + ) => () => void, +) { + function defaultHandler() { + const handleFocus = () => dispatch(onFocus()) + const handleFocusLost = () => dispatch(onFocusLost()) + const handleOnline = () => dispatch(onOnline()) + const handleOffline = () => dispatch(onOffline()) + const handleVisibilityChange = () => { + if (window.document.visibilityState === 'visible') { + handleFocus() + } else { + handleFocusLost() + } + } + + if (!initialized) { + if (typeof window !== 'undefined' && window.addEventListener) { + // Handle focus events + window.addEventListener( + 'visibilitychange', + handleVisibilityChange, + false, + ) + window.addEventListener('focus', handleFocus, false) + + // Handle connection events + window.addEventListener('online', handleOnline, false) + window.addEventListener('offline', handleOffline, false) + initialized = true + } + } + const unsubscribe = () => { + window.removeEventListener('focus', handleFocus) + window.removeEventListener('visibilitychange', handleVisibilityChange) + window.removeEventListener('online', handleOnline) + window.removeEventListener('offline', handleOffline) + initialized = false + } + return unsubscribe + } + + return customHandler + ? customHandler(dispatch, { onFocus, onFocusLost, onOffline, onOnline }) + : defaultHandler() +} +``` + +If you notice, `onFocus`, `onFocusLost`, `onOffline`, `onOnline` are all actions that are provided to the callback. Additionally, these actions are made available to `api.internalActions` and are able to be used by dispatching them like this: + +```ts title="Manual onFocus event" no-transpile +dispatch(api.internalActions.onFocus()) +``` diff --git a/website/sidebars.ts b/website/sidebars.ts index b8ddbac1..b3cb94e3 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -17,6 +17,82 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'API Reference', items: [{ + type: "category", + label: "Redux Toolkit", + items: [ + { + type: 'category', + label: 'Store Setup', + collapsed: true, + items: [ + 'reference/redux-toolkit/configureStore', + 'reference/redux-toolkit/getDefaultMiddleware', + 'reference/redux-toolkit/immutabilityMiddleware', + 'reference/redux-toolkit/serializabilityMiddleware', + 'reference/redux-toolkit/actionCreatorMiddleware', + 'reference/redux-toolkit/createListenerMiddleware', + 'reference/redux-toolkit/createDynamicMiddleware', + 'reference/redux-toolkit/getDefaultEnhancers', + 'reference/redux-toolkit/autoBatchEnhancer', + ], + }, + { + type: 'category', + label: 'Reducers and Actions', + collapsed: true, + items: [ + 'reference/redux-toolkit/createReducer', + 'reference/redux-toolkit/createAction', + 'reference/redux-toolkit/createSlice', + 'reference/redux-toolkit/createAsyncThunk', + 'reference/redux-toolkit/createEntityAdapter', + 'reference/redux-toolkit/combineSlices', + ], + }, + { + type: 'category', + label: 'Other', + collapsed: true, + items: [ + 'reference/redux-toolkit/createSelector', + 'reference/redux-toolkit/matching-utilities', + 'reference/redux-toolkit/other-exports', + 'reference/redux-toolkit/codemods', + { type: 'link', label: 'Error Messages', href: '/errors' }, + ], + }, + ] + }, { + type: 'category', + label: 'RTK Query', + collapsed: true, + items: [ + { + type: 'category', + label: 'Core Concepts', + collapsed: true, + items: [ + 'reference/rtk-query/createApi', + 'reference/rtk-query/fetchBaseQuery', + 'reference/rtk-query/ApiProvider', + 'reference/rtk-query/setupListeners', + ], + }, + { + type: 'category', + label: 'Generated API Slices', + collapsed: true, + items: [ + 'reference/rtk-query/created-api/overview', + 'reference/rtk-query/created-api/redux-integration', + 'reference/rtk-query/created-api/endpoints', + 'reference/rtk-query/created-api/hooks', + 'reference/rtk-query/created-api/code-splitting', + 'reference/rtk-query/created-api/api-slice-utils', + ], + }, + ], + }, { type: "category", label: "Redux", items: [