diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 69146d63ba..81f527788f 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -5,9 +5,10 @@ "github/reduxjs/rtk-github-issues-example", "/examples/query/react/basic", "/examples/query/react/advanced", - "/examples/action-listener/counter" + "/examples/action-listener/counter", + "/examples/publish-ci/cra5" ], - "node": "14", + "node": "16", "buildCommand": "build:packages", "packages": [ "packages/toolkit", diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2aec029d2d..9407755fc2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['16.x'] + node: ['18.x'] steps: - name: Checkout repo @@ -47,7 +47,7 @@ jobs: # Read existing version, reuse that, add a Git short hash - name: Set build version to Git commit - run: node scripts/writeGitVersion.js $(git rev-parse --short HEAD) + run: node scripts/writeGitVersion.mjs $(git rev-parse --short HEAD) - name: Check updated version run: jq .version package.json @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['16.x'] + node: ['18.x'] steps: - name: Checkout repo uses: actions/checkout@v2 @@ -91,7 +91,7 @@ jobs: - name: Install build artifact run: yarn workspace @reduxjs/toolkit add $(pwd)/package.tgz - - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.* + - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.ts ./src/tests/*.* ./src/query/tests/*.* - name: Run tests, against dist run: yarn test @@ -104,8 +104,8 @@ jobs: strategy: fail-fast: false matrix: - node: ['16.x'] - ts: ['4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7', '4.8', '4.9.5', '5.0', '5.1', '5.2'] + node: ['18.x'] + ts: ['4.7', '4.8', '4.9', '5.0', '5.1', '5.2', '5.3'] steps: - name: Checkout repo uses: actions/checkout@v2 @@ -133,7 +133,7 @@ jobs: - name: Show installed RTK versions run: yarn info @reduxjs/toolkit - - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.* + - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.ts ./src/tests/*.* ./src/query/tests/*.* - name: Test types run: | @@ -148,17 +148,8 @@ jobs: strategy: fail-fast: false matrix: - node: ['16.x'] - example: - [ - 'cra4', - 'cra5', - 'next', - 'vite', - 'node-standard', - 'node-esm', - 'are-the-types-wrong', - ] + node: ['18.x'] + example: ['cra4', 'cra5', 'next', 'vite', 'node-standard', 'node-esm'] defaults: run: working-directory: ./examples/publish-ci/${{ matrix.example }} @@ -190,10 +181,43 @@ jobs: run: yarn add ./package.tgz - name: Show installed RTK versions - run: yarn info @reduxjs/toolkit + run: yarn info @reduxjs/toolkit && yarn why @reduxjs/toolkit - name: Build example - run: yarn build + run: NODE_OPTIONS=--openssl-legacy-provider yarn build - name: Run test step run: yarn test + + are-the-types-wrong: + name: Check package config with are-the-types-wrong + + needs: [build] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['18.x'] + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Use node ${{ matrix.node }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + + - name: Install deps + run: yarn install + + - uses: actions/download-artifact@v2 + with: + name: package + path: packages/toolkit + + - name: show folder + run: ls -l . + + - name: Run are-the-types-wrong + run: yarn attw ./package.tgz --format table --ignore-rules false-cjs diff --git a/.yarn/patches/console-testing-library-npm-0.6.1-4d9957d402.patch b/.yarn/patches/console-testing-library-npm-0.6.1-4d9957d402.patch new file mode 100644 index 0000000000..74ee5988a2 --- /dev/null +++ b/.yarn/patches/console-testing-library-npm-0.6.1-4d9957d402.patch @@ -0,0 +1,41 @@ +diff --git a/src/index.js b/src/index.js +index 90ff7fa3d7d4fa62dbbf638958ae4e28abd089a8..28434687b5163b7472e86bdb11bed69e0868e660 100644 +--- a/src/index.js ++++ b/src/index.js +@@ -1,4 +1,4 @@ +-import { mockConsole, createConsole } from './pure'; ++import { mockConsole, createConsole } from './pure.js'; + + // Keep an instance of the original console and export it + const originalConsole = global.console; +diff --git a/src/pure.js b/src/pure.js +index b00ea2abbaea833e336676aa46e7ced2d59d6d88..42b83ed83fa16cf2234571500fe09868debd9f01 100644 +--- a/src/pure.js ++++ b/src/pure.js +@@ -228,10 +228,11 @@ export function restore() { + global.console = global.originalConsole; + } + ++/* + if (typeof expect === 'function' && typeof expect.extend === 'function') { + expect.extend({ + toMatchInlineSnapshot(received, ...args) { +- /* ------- Workaround for custom inline snapshot matchers ------- */ ++ // Workaround for custom inline snapshot matchers + const error = new Error(); + const stacks = error.stack.split('\n'); + +@@ -245,7 +246,6 @@ if (typeof expect === 'function' && typeof expect.extend === 'function') { + error.stack = stacks.join('\n'); + + const context = Object.assign(this, { error }); +- /* -------------------------------------------------------------- */ + + const testingConsoleInstance = + (received && received.testingConsole) || received; +@@ -270,3 +270,4 @@ if (typeof expect === 'function' && typeof expect.extend === 'function') { + }, + }); + } ++*/ +\ No newline at end of file diff --git a/docs/api/actionCreatorMiddleware.mdx b/docs/api/actionCreatorMiddleware.mdx index 2248a16482..50570ae109 100644 --- a/docs/api/actionCreatorMiddleware.mdx +++ b/docs/api/actionCreatorMiddleware.mdx @@ -47,6 +47,7 @@ export default function (state = {}, action: any) { import { configureStore, createActionCreatorInvariantMiddleware, + Tuple, } from '@reduxjs/toolkit' import reducer from './reducer' @@ -62,6 +63,6 @@ const actionCreatorMiddleware = createActionCreatorInvariantMiddleware({ const store = configureStore({ reducer, - middleware: [actionCreatorMiddleware], + middleware: () => new Tuple(actionCreatorMiddleware), }) ``` diff --git a/docs/api/autoBatchEnhancer.mdx b/docs/api/autoBatchEnhancer.mdx index e0d9e0dedc..8dd50e9deb 100644 --- a/docs/api/autoBatchEnhancer.mdx +++ b/docs/api/autoBatchEnhancer.mdx @@ -48,14 +48,9 @@ const counterSlice = createSlice({ }) const { incrementBatched, decrementUnbatched } = counterSlice.actions +// includes batch enhancer by default, as of RTK 2.0 const store = configureStore({ reducer: counterSlice.reducer, - // highlight-start - enhancers: (existingEnhancers) => { - // Add the autobatch enhancer to the store setup - return existingEnhancers.concat(autoBatchEnhancer()) - }, - // highlight-end }) ``` @@ -74,6 +69,25 @@ type AutoBatchOptions = 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: @@ -140,4 +154,4 @@ This allows Redux users to selectively tag certain actions for effective batchin ### RTK Query and Batching -RTK Query already marks several of its key internal action types as batchable. If you add the `autoBatchEnhancer` to the store setup, it will improve the overall UI performance, especially when rendering large lists of components that use the RTKQ query hooks. +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/api/codemods.mdx b/docs/api/codemods.mdx index ea81c8b464..912bda223a 100644 --- a/docs/api/codemods.mdx +++ b/docs/api/codemods.mdx @@ -9,11 +9,15 @@ hide_title: true # Codemods -Per [the description in `1.9.0-alpha.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0-alpha.0), we plan to remove the "object" argument from `createReducer` and `createSlice.extraReducers` in the future RTK 2.0 major version. In `1.9.0-alpha.0`, we added a one-shot runtime warning to each of those APIs. +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 two codemods: `createReducerBuilder` and `createSliceBuilder`. +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`. diff --git a/docs/api/combineSlices.mdx b/docs/api/combineSlices.mdx new file mode 100644 index 0000000000..b308a0e36f --- /dev/null +++ b/docs/api/combineSlices.mdx @@ -0,0 +1,370 @@ +--- +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. + +::: + +:::warning + +Like [`combineReducers`](https://redux.js.org/api/combinereducers), `combineSlices` requires at least one reducer at initialisation. + +```ts no-transpile +// will throw an error +const rootReducer = combineSlices() +``` + +::: + +## 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 behaviour, 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/api/configureStore.mdx b/docs/api/configureStore.mdx index c203292b55..08aa0f089c 100644 --- a/docs/api/configureStore.mdx +++ b/docs/api/configureStore.mdx @@ -37,20 +37,15 @@ Redux Toolkit's `configureStore` simplifies that setup process, by doing all tha `configureStore` accepts a single configuration object parameter, with the following options: ```ts no-transpile -type ConfigureEnhancersCallback = ( - defaultEnhancers: EnhancerArray<[StoreEnhancer]> -) => StoreEnhancer[] interface ConfigureStoreOptions< S = any, - A extends Action = AnyAction, - M extends Middlewares = Middlewares + 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 @@ -73,22 +68,26 @@ interface ConfigureStoreOptions< * 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?: DeepPartial + 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 original array (ie, `[applyMiddleware]`), - * and should return a new array (such as `[applyMiddleware, offline]`). + * 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?: StoreEnhancer[] | ConfigureEnhancersCallback + enhancers?: (getDefaultEnhancers: GetDefaultEnhancers) => E | E } -function configureStore( - options: ConfigureStoreOptions -): EnhancedStore +function configureStore< + S = any, + A extends Action = UnknownAction, + M extends Tuple> = Tuple> + E extends Tuple = Tuple, + P = S +>(options: ConfigureStoreOptions): EnhancedStore ``` ### `reducer` @@ -101,21 +100,34 @@ If it is an object of slice reducers, like `{users : usersReducer, posts : posts ### `middleware` -An optional array of Redux middleware functions +A callback which will receive `getDefaultMiddleware` as its argument, +and should return a middleware array. -If this option is provided, it should contain all the middleware functions you +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. -Where you wish to add onto or customize the default middleware, -you may pass a callback function that will receive `getDefaultMiddleware` as its argument, -and should return a middleware array. - 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). @@ -142,17 +154,67 @@ An optional array of Redux store enhancers, or a callback function to customize If defined as an array, these will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`. -This should _not_ include `applyMiddleware()` or the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`. +:::tip Dev Tools +This should _not_ include the Redux DevTools Extension `composeWithDevTools`, as this is already handled by `configureStore`. -Example: `enhancers: [offline]` will result in a final setup of `[applyMiddleware, offline, devToolsExtension]`. +Example: `enhancers: new Tuple(offline)` will result in a final setup of `[offline, devToolsExtension]`. -If defined as a callback function, it will be called with the existing array of enhancers _without_ the DevTools Extension (currently `[applyMiddleware]`), -and should return a new array of enhancers. This is primarily useful for cases where a store enhancer needs to be added -in front of `applyMiddleware`, such as `redux-first-router` or `redux-offline`. +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 provide an array, this `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 Tuple +Typescript users are required to use a `Tuple` instance (if not using a `getDefaultEnhancer` result, which is already a `Tuple`), for better inference. + +``` +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 @@ -223,7 +285,10 @@ const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), devTools: process.env.NODE_ENV !== 'production', preloadedState, - enhancers: [batchedSubscribe(debounceNotify)], + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ + autoBatch: false, + }).concat(batchedSubscribe(debounceNotify)), }) // The store has been created with these options: diff --git a/docs/api/createAction.mdx b/docs/api/createAction.mdx index 9eaded6bec..15776024ae 100644 --- a/docs/api/createAction.mdx +++ b/docs/api/createAction.mdx @@ -31,7 +31,7 @@ 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. Also, the action creator overrides [toString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) so that the action type becomes its string representation. +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' @@ -44,10 +44,7 @@ let action = increment() action = increment(3) // returns { type: 'counter/increment', payload: 3 } -console.log(increment.toString()) -// 'counter/increment' - -console.log(`The action type is: ${increment}`) +console.log(`The action type is: ${increment.type}`) // 'The action type is: counter/increment' ``` @@ -89,7 +86,7 @@ If provided, all arguments from the action creator will be passed to the prepare ## Usage with createReducer() -Because of their `toString()` override, action creators returned by `createAction()` can be used directly as keys for the case reducers passed to [createReducer()](createReducer.mdx). +Action creators can be passed directly to `addCase` in a [createReducer()](createReducer.mdx) build callback. ```ts import { createAction, createReducer } from '@reduxjs/toolkit' @@ -103,43 +100,9 @@ const counterReducer = createReducer(0, (builder) => { }) ``` -## Non-String Action Types - -In principle, Redux lets you use any kind of value as an action type. Instead of strings, you could theoretically use numbers, [symbols](https://developer.mozilla.org/en-US/docs/Glossary/Symbol), or anything else ([although it's recommended that the value should at least be serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)). - -However, Redux Toolkit rests on the assumption that you use string action types. Specifically, some of its features rely on the fact that with strings, the `toString()` method of an `createAction()` action creator returns the matching action type. This is not the case for non-string action types because `toString()` will return the string-converted type value rather than the type itself. - -```js -const INCREMENT = Symbol('increment') -const increment = createAction(INCREMENT) - -increment.toString() -// returns the string 'Symbol(increment)', -// not the INCREMENT symbol itself - -increment.toString() === INCREMENT -// false -``` - -This means that, for instance, you cannot use a non-string-type action creator as a case reducer key for [createReducer()](createReducer.mdx). - -```js -const INCREMENT = Symbol('increment') -const increment = createAction(INCREMENT) - -const counterReducer = createReducer(0, { - // The following case reducer will NOT trigger for - // increment() actions because `increment` will be - // interpreted as a string, rather than being evaluated - // to the INCREMENT symbol. - [increment]: (state, action) => state + action.payload, - - // You would need to use the action type explicitly instead. - [INCREMENT]: (state, action) => state + action.payload, -}) -``` - -For this reason, **we strongly recommend you to only use string action types**. +:::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 diff --git a/docs/api/createAsyncThunk.mdx b/docs/api/createAsyncThunk.mdx index d0a641df99..c44803fda0 100644 --- a/docs/api/createAsyncThunk.mdx +++ b/docs/api/createAsyncThunk.mdx @@ -210,32 +210,38 @@ type RejectedWithValue = ( ) => RejectedWithValueAction ``` -To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using either the object key notation or the "builder callback" notation. (Note that if you use TypeScript, you [should use the "builder callback" notation to ensure the types are inferred correctly](../usage/usage-with-typescript.md#type-safety-with-extrareducers)): +To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using the "builder callback" notation. -```ts no-transpile {2,6,14,23} -const reducer1 = createReducer(initialState, { - [fetchUserById.fulfilled]: (state, action) => {}, -}) - -const reducer2 = createReducer(initialState, (builder) => { +```ts no-transpile {2,10} +const reducer1 = createReducer(initialState, (builder) => { builder.addCase(fetchUserById.fulfilled, (state, action) => {}) }) -const reducer3 = createSlice({ +const reducer2 = createSlice({ name: 'users', initialState, reducers: {}, - extraReducers: { - [fetchUserById.fulfilled]: (state, action) => {}, + 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 reducer4 = createSlice({ +const reducer2 = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: (builder) => { - builder.addCase(fetchUserById.fulfilled, (state, action) => {}) + builder.addMatcher(fetchUserById.settled, (state, action) => {}) }, }) ``` @@ -546,19 +552,20 @@ test('this thunk should always be skipped', async () => { import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { userAPI, User } from './userAPI' -const fetchUserById = createAsyncThunk ( - '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 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', diff --git a/docs/api/createDynamicMiddleware.mdx b/docs/api/createDynamicMiddleware.mdx new file mode 100644 index 0000000000..2f2150f2b6 --- /dev/null +++ b/docs/api/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/api/createEntityAdapter.mdx b/docs/api/createEntityAdapter.mdx index 0cfee25b9e..4b9cbc4134 100644 --- a/docs/api/createEntityAdapter.mdx +++ b/docs/api/createEntityAdapter.mdx @@ -15,11 +15,13 @@ A function that generates a set of prebuilt reducers and selectors for performin 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`. +:::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: @@ -46,9 +48,9 @@ import { type Book = { bookId: string; title: string } -const booksAdapter = createEntityAdapter({ +const booksAdapter = createEntityAdapter({ // Assume IDs are stored in a field other than `book.id` - selectId: (book) => book.bookId, + selectId: (book: Book) => book.bookId, // Keep the "all IDs" array sorted based on book titles sortComparer: (a, b) => a.title.localeCompare(b.title), }) @@ -118,19 +120,11 @@ export type Comparer = (a: T, b: T) => number export type IdSelector = (model: T) => EntityId -export interface DictionaryNum { - [id: number]: T | undefined -} - -export interface Dictionary extends DictionaryNum { - [id: string]: T | undefined -} - export type Update = { id: EntityId; changes: Partial } export interface EntityState { ids: EntityId[] - entities: Dictionary + entities: Record } export interface EntityDefinition { @@ -183,7 +177,7 @@ export interface EntityStateAdapter { export interface EntitySelectors { selectIds: (state: V) => EntityId[] - selectEntities: (state: V) => Dictionary + selectEntities: (state: V) => Record selectAll: (state: V) => T[] selectTotal: (state: V) => number selectById: (state: V, id: EntityId) => T | undefined @@ -228,7 +222,6 @@ All three options will insert _new_ entities into the list. However they differ ::: - Each method has a signature that looks like: ```ts no-transpile @@ -243,7 +236,9 @@ These CRUD methods may be used in multiple ways: - 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. +:::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. @@ -284,9 +279,35 @@ The entity adapter will contain a `getSelectors()` function that returns a set o 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#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, it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from. +- 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: @@ -358,12 +379,8 @@ const booksSlice = createSlice({ }, }) -const { - bookAdded, - booksLoading, - booksReceived, - bookUpdated, -} = booksSlice.actions +const { bookAdded, booksLoading, booksReceived, bookUpdated } = + booksSlice.actions const store = configureStore({ reducer: { diff --git a/docs/api/createListenerMiddleware.mdx b/docs/api/createListenerMiddleware.mdx index e04dd23d67..58dd06dacb 100644 --- a/docs/api/createListenerMiddleware.mdx +++ b/docs/api/createListenerMiddleware.mdx @@ -120,10 +120,10 @@ The "listener middleware instance" returned from `createListenerMiddleware` is a ```ts no-transpile interface ListenerMiddlewareInstance< State = unknown, - Dispatch extends ThunkDispatch = ThunkDispatch< + Dispatch extends ThunkDispatch = ThunkDispatch< State, unknown, - AnyAction + UnknownAction >, ExtraArgument = unknown > { @@ -181,7 +181,7 @@ interface AddListenerOptions { effect: (action: Action, listenerApi: ListenerApi) => void | Promise } -type ListenerPredicate = ( +type ListenerPredicate = ( action: Action, currentState?: State, originalState?: State @@ -321,7 +321,7 @@ The `listenerApi` object is the second argument to each listener callback. It co ```ts no-transpile export interface ListenerEffectAPI< State, - Dispatch extends ReduxDispatch, + Dispatch extends ReduxDispatch, ExtraArgument = unknown > extends MiddlewareAPI { // NOTE: MiddlewareAPI contains `dispatch` and `getState` already @@ -582,12 +582,12 @@ Listeners can use the `condition` and `take` methods in `listenerApi` to wait un The signatures are: ```ts no-transpile -type ConditionFunction = ( +type ConditionFunction = ( predicate: ListenerPredicate | (() => boolean), timeout?: number ) => Promise -type TakeFunction = ( +type TakeFunction = ( predicate: ListenerPredicate | (() => boolean), timeout?: number ) => Promise<[Action, State, State] | null> diff --git a/docs/api/createReducer.mdx b/docs/api/createReducer.mdx index d8e164d70e..2526b9a3d9 100644 --- a/docs/api/createReducer.mdx +++ b/docs/api/createReducer.mdx @@ -37,9 +37,7 @@ function counterReducer(state = initialState, action) { 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 supports two different forms of defining case -reducers to handle actions: a "builder callback" notation and a "map object" notation. Both are equivalent, but the "builder callback" -notation is preferred. +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: @@ -72,17 +70,15 @@ const counterReducer = createReducer(initialState, (builder) => { ## Usage with the "Builder Callback" Notation -[overloadSummary](docblock://createReducer.ts?token=createReducer&overload=0) - -The recommended way of using `createReducer` is the builder callback notation, as it works best with TypeScript and most IDEs. +[overloadSummary](docblock://createReducer.ts?token=createReducer) ### Parameters -[params](docblock://createReducer.ts?token=createReducer&overload=0) +[params](docblock://createReducer.ts?token=createReducer) ### Example Usage -[examples](docblock://createReducer.ts?token=createReducer&overload=0) +[examples](docblock://createReducer.ts?token=createReducer) ### Builder Methods @@ -110,17 +106,6 @@ The recommended way of using `createReducer` is the builder callback notation, a [params,examples](docblock://mapBuilders.ts?token=ActionReducerMapBuilder.addDefaultCase) -## Usage with the "Map Object" Notation - -[overloadSummary](docblock://createReducer.ts?token=createReducer&overload=1) - -While this notation is a bit shorter, it works only in JavaScript, not TypeScript and has less integration with IDEs, -so we recommend the "builder callback" notation in most cases. - -### Parameters - -[params](docblock://createReducer.ts?token=createReducer&overload=1) - ### Returns The generated reducer function. @@ -128,9 +113,10 @@ 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, { - increment: (state, action) => state + action.payload, - decrement: (state, action) => state - action.payload, +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 @@ -138,37 +124,7 @@ console.log(counterReducer.getInitialState()) // 0 ### Example Usage -[examples](docblock://createReducer.ts?token=createReducer&overload=1) - -### Matchers and Default Cases as Arguments - -The most readable approach to define matcher cases and default cases is by using the `builder.addMatcher` and `builder.addDefaultCase` methods described above, but it is also possible to use these with the object notation by passing an array of `{matcher, reducer}` objects as the third argument, and a default case reducer as the fourth argument: - -```js -const isStringPayloadAction = (action) => typeof action.payload === 'string' - -const lengthOfAllStringsReducer = createReducer( - // initial state - { strLen: 0, nonStringActions: 0 }, - // normal reducers - { - /*...*/ - }, - // array of matcher reducers - [ - { - matcher: isStringPayloadAction, - reducer(state, action) { - state.strLen += action.payload.length - }, - }, - ], - // default reducer - (state) => { - state.nonStringActions++ - } -) -``` +[examples](docblock://createReducer.ts?token=createReducer) ## Direct State Mutation diff --git a/docs/api/createSelector.mdx b/docs/api/createSelector.mdx index ec88f8fd3d..b0679f1ad0 100644 --- a/docs/api/createSelector.mdx +++ b/docs/api/createSelector.mdx @@ -18,9 +18,11 @@ For more details on using `createSelector`, see: - [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. +:::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` @@ -37,7 +39,7 @@ All selectors created by `entityAdapter.getSelectors` are "draft safe" selectors Example: -```js +```ts no-transpile const selectSelf = (state: State) => state const unsafeSelector = createSelector(selectSelf, (state) => state.value) const draftSafeSelector = createDraftSafeSelector( @@ -62,3 +64,25 @@ After executing that, `unsafe1` and `unsafe2` will be of the same value, because 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 +) +``` + +::: diff --git a/docs/api/createSlice.mdx b/docs/api/createSlice.mdx index d460f0ad48..53b01a1e4a 100644 --- a/docs/api/createSlice.mdx +++ b/docs/api/createSlice.mdx @@ -56,15 +56,15 @@ function createSlice({ // A name, used in action types name: string, // The initial state for the reducer - initialState: any, + initialState: State, // An object of "case reducers". Key names will be used to generate actions. - reducers: Object - // A "builder callback" function used to add more reducers, or - // an additional object of "case reducers", where the keys should be other - // action types - extraReducers?: - | Object - | ((builder: ActionReducerMapBuilder) => void) + 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>, }) ``` @@ -134,6 +134,223 @@ const todosSlice = createSlice({ }) ``` +### 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: [], + } 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. + +:::warning 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' + +// name is up to you +export const createSliceWithThunks = buildCreateSlice({ + creators: { asyncThunk: asyncThunkCreator }, +}) +``` + +Then import this `createSlice` 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 correspondance between the update logic defined inside `reducers`, and the action types that are generated based on those. @@ -148,63 +365,107 @@ However, unlike the `reducers` field, each individual case reducer inside of `ex 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 - -The recommended way of using `extraReducers` is to use a callback that receives a `ActionReducerMapBuilder` instance. +#### The `extraReducers` "builder callback" notation -This builder notation is also the only way to add matcher reducers and default case reducers to your slice. +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) -We recommend using this API as it has better TypeScript support (and thus, IDE autocomplete even for JavaScript users), as it will correctly infer the action type in the reducer based on the provided action creator. -It's particularly useful for working with actions produced by `createAction` and `createAsyncThunk`. - 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.addDefault` -### The `extraReducers` "map object" notation +### `reducerPath` -:::caution +Indicates a preference of where the slice should be located. Defaults to [`name`](#name). -The "map object" notation is deprecated and will be removed in RTK 2.0. Please migrate to the "builder callback" notation, which offers much better TypeScript support and more flexibility. (There is [a "builder callback" codemod available to help with this migration](./codemods.mdx).) +This is used by `combineSlices` and the default generated `slice.selectors`. -If you do not use the `builder callback` and are using TypeScript, you will need to use `actionCreator.type` or `actionCreator.toString()` to force the TS compiler to accept the computed property. Please see [Usage With TypeScript](./../usage/usage-with-typescript.md#type-safety-with-extraReducers) for further details. +### `selectors` -::: +A set of selectors that receive the slice state as their first parameter, and any other parameters. -Like `reducers`, `extraReducers` can be an object containing Redux case reducer functions. However, the keys should -be other Redux string action type constants, and `createSlice` will _not_ auto-generate action types or action creators -for reducers included in this parameter. +Each selector will have a corresponding key in the resulting [`selectors`](#selectors-1) object. -Action creators that were generated using [`createAction`](./createAction.mdx) may be used directly as the keys here, using -computed property syntax. +:::caution Circular types -```js -const incrementBy = createAction('incrementBy') +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: -createSlice({ +```ts no-transpile +const counterSlice = createSlice({ name: 'counter', - initialState: 0, + initialState: { value: 0 }, reducers: {}, - extraReducers: { - [incrementBy]: (state, action) => { - return state + action.payload - }, - 'some/other/action': (state, action) => {}, + 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, + name: string, + reducer: ReducerFunction, + actions: Record, caseReducers: Record. - getInitialState: () => State + getInitialState: () => State, + reducerPath: string, + selectSlice: Selector; + selectors: Record, + getSelectors: (selectState: (rootState: RootState) => State) => Record + injectInto: (injectable: Injectable, config?: InjectConfig & { reducerPath?: string }) => InjectedSlice } ``` @@ -220,15 +481,120 @@ The functions passed to the `reducers` parameter can be accessed through the `ca 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. -> **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 there are a couple caveats to keep in mind: -> -> - Actions are not exclusively limited to a single slice. Any part of the reducer logic can (and should!) respond -> to any dispatched action. -> - At the same time, circular references can cause import problems. If slices A and B are defined in -> separate files, and each file tries to import the other so it can listen to other actions, unexpected -> behavior may occur. +`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 } 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 } 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 @@ -251,7 +617,6 @@ const counter = createSlice({ prepare: (value?: number) => ({ payload: value || 2 }), // fallback if the payload is a falsy value }, }, - // "builder callback API", recommended for TypeScript users extraReducers: (builder) => { builder.addCase(incrementBy, (state, action) => { return state + action.payload @@ -270,15 +635,10 @@ const user = createSlice({ state.name = action.payload // mutate the state all you want with immer }, }, - // "map object API" - extraReducers: { - // @ts-expect-error in TypeScript, this would need to be [counter.actions.increment.type] - [counter.actions.increment]: ( - state, - action /* action will be inferred as "any", as the map notation does not contain type information */ - ) => { + extraReducers: (builder) => { + builder.addCase(counter.actions.increment, (state, action) => { state.age += 1 - }, + }) }, }) @@ -297,7 +657,7 @@ 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}`) +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/api/getDefaultEnhancers.mdx b/docs/api/getDefaultEnhancers.mdx new file mode 100644 index 0000000000..6863d4168e --- /dev/null +++ b/docs/api/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: [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/api/getDefaultMiddleware.mdx b/docs/api/getDefaultMiddleware.mdx index e44077c301..7e51cf618f 100644 --- a/docs/api/getDefaultMiddleware.mdx +++ b/docs/api/getDefaultMiddleware.mdx @@ -28,7 +28,7 @@ If you want to customize the list of middleware, you can supply an array of midd ```js const store = configureStore({ reducer: rootReducer, - middleware: [thunk, logger], + middleware: () => new Tuple(thunk, logger), }) // Store specifically has the thunk and logger middleware applied @@ -40,14 +40,7 @@ to the store. `configureStore` will not add any extra middleware beyond what you `getDefaultMiddleware` is useful if you want to add some custom middleware, but also still want to have the default middleware added as well: -```ts -// file: reducer.ts noEmit - -export default function rootReducer(state = {}, action: any) { - return state -} - -// file: store.ts +```ts no-transpile import { configureStore } from '@reduxjs/toolkit' import logger from 'redux-logger' @@ -62,7 +55,7 @@ const store = configureStore({ // 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 `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances. +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 diff --git a/docs/api/immutabilityMiddleware.mdx b/docs/api/immutabilityMiddleware.mdx index ae192a7f8a..6360e5417b 100644 --- a/docs/api/immutabilityMiddleware.mdx +++ b/docs/api/immutabilityMiddleware.mdx @@ -36,8 +36,6 @@ interface ImmutableStateInvariantMiddlewareOptions { ignoredPaths?: (string | RegExp)[] /** Print a warning if checks take longer than N ms. Default: 32ms */ warnAfter?: number - // @deprecated. Use ignoredPaths - ignore?: string[] } ``` @@ -76,6 +74,7 @@ export default exampleSlice.reducer import { configureStore, createImmutableStateInvariantMiddleware, + Tuple, } from '@reduxjs/toolkit' import exampleSliceReducer from './exampleSlice' @@ -87,7 +86,7 @@ const immutableInvariantMiddleware = createImmutableStateInvariantMiddleware({ const store = configureStore({ reducer: exampleSliceReducer, // Note that this will replace all default middleware - middleware: [immutableInvariantMiddleware], + middleware: () => new Tuple(immutableInvariantMiddleware), }) ``` diff --git a/docs/api/matching-utilities.mdx b/docs/api/matching-utilities.mdx index f6f86138ec..12808beecc 100644 --- a/docs/api/matching-utilities.mdx +++ b/docs/api/matching-utilities.mdx @@ -49,12 +49,12 @@ A higher-order function that returns a type guard function that may be used to c ```ts title="isAsyncThunkAction usage" import { isAsyncThunkAction } from '@reduxjs/toolkit' -import type { AnyAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' import { requestThunk1, requestThunk2 } from '@virtual/matchers' const isARequestAction = isAsyncThunkAction(requestThunk1, requestThunk2) -function handleRequestAction(action: AnyAction) { +function handleRequestAction(action: UnknownAction) { if (isARequestAction(action)) { // action is an action dispatched by either `requestThunk1` or `requestThunk2` } @@ -67,12 +67,12 @@ A higher-order function that returns a type guard function that may be used to c ```ts title="isPending usage" import { isPending } from '@reduxjs/toolkit' -import type { AnyAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' import { requestThunk1, requestThunk2 } from '@virtual/matchers' const isAPendingAction = isPending(requestThunk1, requestThunk2) -function handlePendingAction(action: AnyAction) { +function handlePendingAction(action: UnknownAction) { if (isAPendingAction(action)) { // action is a pending action dispatched by either `requestThunk1` or `requestThunk2` } @@ -85,12 +85,12 @@ A higher-order function that returns a type guard function that may be used to c ```ts title="isFulfilled usage" import { isFulfilled } from '@reduxjs/toolkit' -import type { AnyAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' import { requestThunk1, requestThunk2 } from '@virtual/matchers' const isAFulfilledAction = isFulfilled(requestThunk1, requestThunk2) -function handleFulfilledAction(action: AnyAction) { +function handleFulfilledAction(action: UnknownAction) { if (isAFulfilledAction(action)) { // action is a fulfilled action dispatched by either `requestThunk1` or `requestThunk2` } @@ -103,12 +103,12 @@ A higher-order function that returns a type guard function that may be used to c ```ts title="isRejected usage" import { isRejected } from '@reduxjs/toolkit' -import type { AnyAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' import { requestThunk1, requestThunk2 } from '@virtual/matchers' const isARejectedAction = isRejected(requestThunk1, requestThunk2) -function handleRejectedAction(action: AnyAction) { +function handleRejectedAction(action: UnknownAction) { if (isARejectedAction(action)) { // action is a rejected action dispatched by either `requestThunk1` or `requestThunk2` } @@ -121,7 +121,7 @@ A higher-order function that returns a type guard function that may be used to c ```ts title="isRejectedWithValue usage" import { isRejectedWithValue } from '@reduxjs/toolkit' -import type { AnyAction } from '@reduxjs/toolkit' +import type { UnknownAction } from '@reduxjs/toolkit' import { requestThunk1, requestThunk2 } from '@virtual/matchers' const isARejectedWithValueAction = isRejectedWithValue( @@ -129,7 +129,7 @@ const isARejectedWithValueAction = isRejectedWithValue( requestThunk2 ) -function handleRejectedWithValueAction(action: AnyAction) { +function handleRejectedWithValueAction(action: UnknownAction) { if (isARejectedWithValueAction(action)) { // action is a rejected action dispatched by either `requestThunk1` or `requestThunk2` // where rejectWithValue was used diff --git a/docs/api/serializabilityMiddleware.mdx b/docs/api/serializabilityMiddleware.mdx index 0aa074a65c..29893bad31 100644 --- a/docs/api/serializabilityMiddleware.mdx +++ b/docs/api/serializabilityMiddleware.mdx @@ -93,6 +93,7 @@ import { configureStore, createSerializableStateInvariantMiddleware, isPlain, + Tuple, } from '@reduxjs/toolkit' import reducer from './reducer' @@ -110,7 +111,7 @@ const serializableMiddleware = createSerializableStateInvariantMiddleware({ const store = configureStore({ reducer, - middleware: [serializableMiddleware], + middleware: () => new Tuple(serializableMiddleware), }) ``` diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md index 9fd3fb6c38..8085db8f3e 100644 --- a/docs/introduction/getting-started.md +++ b/docs/introduction/getting-started.md @@ -34,7 +34,7 @@ you make your Redux code better. ### Create a React Redux App -The recommended way to start new apps with React and Redux is by using [our official Redux+TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux). +The recommended way to start new apps with React and Redux Toolkit is by using [our official Redux Toolkit + TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux). Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features. @@ -85,8 +85,7 @@ yarn add react-redux -It is also available as a precompiled UMD package that defines a `window.RTK` global variable. -The UMD package can be used as a [`