diff --git a/docs/api/configureStore.mdx b/docs/api/configureStore.mdx index 6f37c89a80..e3199c6660 100644 --- a/docs/api/configureStore.mdx +++ b/docs/api/configureStore.mdx @@ -18,7 +18,7 @@ to the store setup for a better development experience. ```ts no-transpile type ConfigureEnhancersCallback = ( - defaultEnhancers: StoreEnhancer[] + defaultEnhancers: EnhancerArray<[StoreEnhancer]> ) => StoreEnhancer[] interface ConfigureStoreOptions< @@ -107,7 +107,8 @@ a list of the specific options that are available. 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. + +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. @@ -129,7 +130,7 @@ If defined as a callback function, it will be called with the existing array of 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`. -Example: `enhancers: (defaultEnhancers) => [offline, ...defaultEnhancers]` will result in a final setup +Example: `enhancers: (defaultEnhancers) => defaultEnhancers.prepend(offline)` will result in a final setup of `[offline, applyMiddleware, devToolsExtension]`. ## Usage @@ -195,7 +196,7 @@ const preloadedState = { visibilityFilter: 'SHOW_COMPLETED', } -const debounceNotify = _.debounce(notify => notify()); +const debounceNotify = _.debounce((notify) => notify()) const store = configureStore({ reducer, diff --git a/docs/rtk-query/api/created-api/endpoints.mdx b/docs/rtk-query/api/created-api/endpoints.mdx index cbd847c8ae..5d49dd6d71 100644 --- a/docs/rtk-query/api/created-api/endpoints.mdx +++ b/docs/rtk-query/api/created-api/endpoints.mdx @@ -92,7 +92,7 @@ When dispatching an action creator, you're responsible for storing a reference t #### Example -```tsx title="initiate query example" +```tsx no-transpile title="initiate query example" import { useState } from 'react' import { useAppDispatch } from './store/hooks' import { api } from './services/api' @@ -119,7 +119,7 @@ function App() { } ``` -```tsx title="initiate mutation example" +```tsx no-transpile title="initiate mutation example" import { useState } from 'react' import { useAppDispatch } from './store/hooks' import { api, Post } from './services/api' @@ -187,7 +187,7 @@ Each call to `.select(someCacheKey)` returns a _new_ selector function instance. #### Example -```tsx title="select query example" +```tsx no-transpile title="select query example" import { useState, useMemo } from 'react' import { useAppDispatch, useAppSelector } from './store/hooks' import { api } from './services/api' @@ -198,9 +198,10 @@ function App() { // 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 selectPost = useMemo( + () => api.endpoints.getPost.select(postId), + [postId] + ) const { data, isLoading } = useAppSelector(selectPost) // highlight-end @@ -223,7 +224,7 @@ function App() { } ``` -```tsx title="select mutation example" +```tsx no-transpile title="select mutation example" import { useState, useMemo } from 'react' import { skipToken } from '@reduxjs/toolkit/query' import { useAppDispatch, useAppSelector } from './store/hooks' diff --git a/docs/rtk-query/usage-with-typescript.mdx b/docs/rtk-query/usage-with-typescript.mdx index aef8eaba41..7107b83ab3 100644 --- a/docs/rtk-query/usage-with-typescript.mdx +++ b/docs/rtk-query/usage-with-typescript.mdx @@ -467,7 +467,7 @@ export const api = createApi({ export const { useGetPostQuery } = api ``` -```tsx title="Using skip in a component" +```tsx no-transpile title="Using skip in a component" import { useGetPostQuery } from './api' function MaybePost({ id }: { id?: number }) { @@ -486,7 +486,7 @@ While you might be able to convince yourself that the query won't be called unle RTK Query provides a `skipToken` export which can be used as an alternative to the `skip` option in order to skip queries, while remaining type-safe. When `skipToken` is passed as the query argument to `useQuery`, `useQueryState` or `useQuerySubscription`, it provides the same effect as setting `skip: true` in the query options, while also being a valid argument in scenarios where the `arg` might be undefined otherwise. -```tsx title="Using skipToken in a component" +```tsx no-transpile title="Using skipToken in a component" import { skipToken } from '@reduxjs/toolkit/query/react' import { useGetPostQuery } from './api' @@ -566,7 +566,7 @@ export interface SerializedError { When using `fetchBaseQuery`, the `error` property returned from a hook will have the type `FetchBaseQueryError | SerializedError | undefined`. If an error is present, you can access error properties after narrowing the type to either `FetchBaseQueryError` or `SerializedError`. -```tsx +```tsx no-transpile import { api } from './services/api' function PostDetail() { @@ -587,10 +587,9 @@ function PostDetail() {
{errMsg}
) - } - else { - // you can access all properties of `SerializedError` here - return
{error.message}
+ } else { + // you can access all properties of `SerializedError` here + return
{error.message}
} } @@ -617,7 +616,7 @@ In order to safely access properties of the error, you must first narrow the typ This can be done using a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) as shown below. -```tsx title="services/helpers.ts" +```tsx no-transpile title="services/helpers.ts" import { FetchBaseQueryError } from '@reduxjs/toolkit/query' /** @@ -644,7 +643,7 @@ export function isErrorWithMessage( } ``` -```tsx title="addPost.tsx" +```tsx no-transpile title="addPost.tsx" import { useState } from 'react' import { useSnackbar } from 'notistack' import { api } from './services/api' diff --git a/docs/rtk-query/usage/automated-refetching.mdx b/docs/rtk-query/usage/automated-refetching.mdx index 8c26450b01..5d589934cf 100644 --- a/docs/rtk-query/usage/automated-refetching.mdx +++ b/docs/rtk-query/usage/automated-refetching.mdx @@ -232,7 +232,7 @@ const api = createApi({ Note that for the example above, the `id` is used where possible on a successful result. In the case of an error, no result is supplied, and we still consider that it has provided the general `'Post'` tag type rather than any specific instance of that tag. :::tip Advanced List Invalidation -In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such a `'LIST'` for a given tag. See [Advanced Invalidation with abstract tag IDs](#advanced-invalidation-with-abstract-tag-ids) for additional details. +In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such as `'LIST'` for a given tag. See [Advanced Invalidation with abstract tag IDs](#advanced-invalidation-with-abstract-tag-ids) for additional details. ::: ### Invalidating cache data @@ -381,7 +381,7 @@ const api = createApi({ For the example above, rather than invalidating any tag with the type `'Post'`, calling the `editPost` mutation function will now only invalidate a tag for the provided `id`. I.e. if cached data from an endpoint does not provide a `'Post'` for that same `id`, it will remain considered as 'valid', and will not be triggered to automatically re-fetch. :::tip Using abstract tag IDs -In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such a `'LIST'` for a given tag. See [Advanced Invalidation with abstract tag IDs](#advanced-invalidation-with-abstract-tag-ids) for additional details. +In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such as `'LIST'` for a given tag. See [Advanced Invalidation with abstract tag IDs](#advanced-invalidation-with-abstract-tag-ids) for additional details. ::: ## Tag Invalidation Behavior @@ -668,7 +668,7 @@ export const api = createApi({ export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = api ``` -```tsx title="App.tsx" +```tsx no-transpile title="App.tsx" function App() { const { data: posts } = useGetPostsQuery() const [addPost] = useAddPostMutation() @@ -742,7 +742,7 @@ export const api = createApi({ export const { useGetPostsQuery, useAddPostMutation, useGetPostQuery } = api ``` -```tsx title="App.tsx" +```tsx no-transpile title="App.tsx" function App() { const { data: posts } = useGetPostsQuery() const [addPost] = useAddPostMutation() diff --git a/docs/rtk-query/usage/cache-behavior.mdx b/docs/rtk-query/usage/cache-behavior.mdx index fb8e07698c..e6815b5271 100644 --- a/docs/rtk-query/usage/cache-behavior.mdx +++ b/docs/rtk-query/usage/cache-behavior.mdx @@ -120,7 +120,7 @@ Calling the `refetch` function will force refetch the associated query. Alternatively, you can dispatch the `initiate` thunk action for an endpoint, passing the option `forceRefetch: true` to the thunk action creator for the same effect. -```tsx title="Force refetch example" +```tsx no-transpile title="Force refetch example" import { useDispatch } from 'react-redux' import { useGetPostsQuery } from './api' @@ -197,7 +197,7 @@ export const api = createApi({ }) ``` -```tsx title="Forcing refetch on component mount" +```tsx no-transpile title="Forcing refetch on component mount" import { useGetPostsQuery } from './api' const Component = () => { diff --git a/docs/rtk-query/usage/error-handling.mdx b/docs/rtk-query/usage/error-handling.mdx index 5a63588a64..fcd1dd9280 100644 --- a/docs/rtk-query/usage/error-handling.mdx +++ b/docs/rtk-query/usage/error-handling.mdx @@ -16,7 +16,7 @@ If your query or mutation happens to throw an error when using [fetchBaseQuery]( ### Error Display Examples -```tsx title="Query Error" +```tsx no-transpile title="Query Error" function PostsList() { const { data, error } = useGetPostsQuery() @@ -28,7 +28,7 @@ function PostsList() { } ``` -```tsx title="Mutation Error" +```tsx no-transpile title="Mutation Error" function AddPost() { const [addPost, { error }] = useAddPostMutation() @@ -52,7 +52,7 @@ addPost({ id: 1, name: 'Example' }) ::: -```tsx title="Manually selecting an error" +```tsx no-transpile title="Manually selecting an error" function PostsList() { const { error } = useSelector(api.endpoints.getPosts.select()) @@ -88,15 +88,14 @@ import { toast } from 'your-cool-library' /** * Log a warning and show a toast! */ -export const rtkQueryErrorLogger: Middleware = (api: MiddlewareAPI) => ( - next -) => (action) => { - // RTK Query uses `createAsyncThunk` from redux-toolkit under the hood, so we're able to utilize these matchers! - if (isRejectedWithValue(action)) { - console.warn('We got a rejected action!') - toast.warn({ title: 'Async error!', message: action.error.data.message }) +export const rtkQueryErrorLogger: Middleware = + (api: MiddlewareAPI) => (next) => (action) => { + // RTK Query uses `createAsyncThunk` from redux-toolkit under the hood, so we're able to utilize these matchers! + if (isRejectedWithValue(action)) { + console.warn('We got a rejected action!') + toast.warn({ title: 'Async error!', message: action.error.data.message }) + } + + return next(action) } - - return next(action) -} ``` diff --git a/docs/rtk-query/usage/manual-cache-updates.mdx b/docs/rtk-query/usage/manual-cache-updates.mdx index 22ff2cdddc..fa880ecf03 100644 --- a/docs/rtk-query/usage/manual-cache-updates.mdx +++ b/docs/rtk-query/usage/manual-cache-updates.mdx @@ -217,7 +217,7 @@ callback for a mutation without a good reason, as RTK Query is intended to be us your cached data as a reflection of the server-side state. ::: -```tsx title="General manual cache update example" +```tsx no-transpile title="General manual cache update example" import { api } from './api' import { useAppDispatch } from './store/hooks' diff --git a/docs/rtk-query/usage/migrating-to-rtk-query.mdx b/docs/rtk-query/usage/migrating-to-rtk-query.mdx index a85793d192..11b993c78b 100644 --- a/docs/rtk-query/usage/migrating-to-rtk-query.mdx +++ b/docs/rtk-query/usage/migrating-to-rtk-query.mdx @@ -169,7 +169,7 @@ export type RootState = ReturnType In order to have the store accessible within our app, we will wrap our `App` component with a [`Provider`](https://react-redux.js.org/api/provider) component from `react-redux`. -```tsx title="src/index.ts" +```tsx no-transpile title="src/index.ts" import { render } from 'react-dom' // highlight-start import { Provider } from 'react-redux' @@ -223,9 +223,9 @@ export type RootState = { pokemon: typeof initialPokemonSlice } -export declare const store: EnhancedStore +export declare const store: EnhancedStore export type AppDispatch = typeof store.dispatch -export declare const useAppDispatch: () => (...args: any[])=> any; +export declare const useAppDispatch: () => (...args: any[]) => any // file: src/hooks.ts import { useEffect } from 'react' @@ -276,7 +276,7 @@ Our implementation below provides the following behaviour in the component: - When our component is mounted, if a request for the provided pokemon name has not already been sent for the session, send the request off - The hook always provides the latest received `data` when available, as well as the request status booleans `isUninitialized`, `isPending`, `isFulfilled` & `isRejected` in order to determine the current UI at any given moment as a function of our state. -```tsx title="src/App.tsx" +```tsx no-transpile title="src/App.tsx" import * as React from 'react' // highlight-start import { useGetPokemonByNameQuery } from './hooks' diff --git a/docs/rtk-query/usage/mutations.mdx b/docs/rtk-query/usage/mutations.mdx index d820f738e7..739461ca50 100644 --- a/docs/rtk-query/usage/mutations.mdx +++ b/docs/rtk-query/usage/mutations.mdx @@ -52,7 +52,11 @@ const api = createApi({ // Pick out data and prevent nested properties in a hook or selector transformResponse: (response: { data: Post }, meta, arg) => response.data, // Pick out errors and prevent nested properties in a hook or selector - transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status, + transformErrorResponse: ( + response: { status: string | number }, + meta, + arg + ) => response.status, invalidatesTags: ['Post'], // onQueryStarted is useful for optimistic updates // The 2nd parameter is the destructured `MutationLifecycleApi` @@ -177,7 +181,7 @@ When using `fixedCacheKey`, the `originalArgs` property is not able to be shared This is a modified version of the complete example you can see at the bottom of the page to highlight the `updatePost` mutation. In this scenario, a post is fetched with `useQuery`, and then an `EditablePostName` component is rendered that allows us to edit the name of the post. -```tsx title="src/features/posts/PostDetail.tsx" +```tsx no-transpile title="src/features/posts/PostDetail.tsx" export const PostDetail = () => { const { id } = useParams<{ id: any }>() diff --git a/docs/rtk-query/usage/pagination.mdx b/docs/rtk-query/usage/pagination.mdx index c15baedc4a..a9241ad9fb 100644 --- a/docs/rtk-query/usage/pagination.mdx +++ b/docs/rtk-query/usage/pagination.mdx @@ -44,36 +44,35 @@ export const { useListPostsQuery } = api ### Trigger the next page by incrementing the `page` state variable -```tsx title="src/features/posts/PostsManager.tsx" +```tsx no-transpile title="src/features/posts/PostsManager.tsx" const PostList = () => { - const [page, setPage] = useState(1); - const { data: posts, isLoading, isFetching } = useListPostsQuery(page); + const [page, setPage] = useState(1) + const { data: posts, isLoading, isFetching } = useListPostsQuery(page) if (isLoading) { - return
Loading
; + return
Loading
} if (!posts?.data) { - return
No posts :(
; + return
No posts :(
} return (
- {posts.data.map(({ id, title, status }) => ( -
{title} - {status}
- ))} - - + {posts.data.map(({ id, title, status }) => ( +
+ {title} - {status} +
+ ))} + +
- ); -}; + ) +} ``` ### Automated Re-fetching of Paginated Queries @@ -149,6 +148,7 @@ export const postApi = createApi({ }), }) ``` + ## General Pagination Example In the following example, you'll see `Loading` on the initial query, but then as you move forward we'll use the next/previous buttons as a _fetching_ indicator while any non-cached query is performed. When you go back, the cached data will be served instantaneously. diff --git a/docs/rtk-query/usage/polling.mdx b/docs/rtk-query/usage/polling.mdx index ef2bee6c30..2e85c1ed6d 100644 --- a/docs/rtk-query/usage/polling.mdx +++ b/docs/rtk-query/usage/polling.mdx @@ -14,7 +14,7 @@ description: 'RTK Query > Usage > Polling: re-fetching data on a timer' Polling gives you the ability to have a 'real-time' effect by causing a query to run at a specified interval. To enable polling for a query, pass a `pollingInterval` to the `useQuery` hook or action creator with an interval in milliseconds: -```tsx title="src/Pokemon.tsx" no-transpile +```tsx no-transpile title="src/Pokemon.tsx" no-transpile import * as React from 'react' import { useGetPokemonByNameQuery } from './services/pokemon' diff --git a/docs/rtk-query/usage/prefetching.mdx b/docs/rtk-query/usage/prefetching.mdx index dc12e16328..fbdc3b21fb 100644 --- a/docs/rtk-query/usage/prefetching.mdx +++ b/docs/rtk-query/usage/prefetching.mdx @@ -58,7 +58,7 @@ You can specify these prefetch options when declaring the hook or at the call si - **Assuming** you have a `useQuery` hook in the tree that is subscribed to the same query that you are prefetching: - `useQuery` will return `{isLoading: false, isFetching: true, ...rest`} -```tsx title="usePrefetch Example" +```tsx no-transpile title="usePrefetch Example" function User() { const prefetchUser = usePrefetch('getUser') diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index 0a355edbfd..ecfd6f8aca 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -59,7 +59,11 @@ const api = createApi({ // Pick out data and prevent nested properties in a hook or selector transformResponse: (response: { data: Post }, meta, arg) => response.data, // Pick out errors and prevent nested properties in a hook or selector - transformErrorResponse: (response: { status: string | number }, meta, arg) => response.status, + transformErrorResponse: ( + response: { status: string | number }, + meta, + arg + ) => response.status, providesTags: (result, error, id) => [{ type: 'Post', id }], // The 2nd parameter is the destructured `QueryLifecycleApi` async onQueryStarted( @@ -158,7 +162,7 @@ In most cases, you will probably read `data` and either `isLoading` or `isFetchi Here is an example of a `PostDetail` component: -```tsx title="Example" +```tsx no-transpile title="Example" export const PostDetail = ({ id }: { id: string }) => { const { data: post, @@ -199,7 +203,7 @@ For query endpoints, RTK Query maintains a semantic distinction between `isLoadi This distinction allows for greater control when handling UI behavior. For example, `isLoading` can be used to display a skeleton while loading for the first time, while `isFetching` can be used to grey out old data when changing from page 1 to page 2 or when data is invalidated and re-fetched. -```tsx title="Managing UI behavior with Query Loading States" +```tsx no-transpile title="Managing UI behavior with Query Loading States" import { Skeleton } from './Skeleton' import { useGetPostsQuery } from './api' @@ -236,7 +240,7 @@ shown. If posts for the current user have previously been fetched, and are re-fe result of a mutation), the UI will show the previous data, but will grey out the data. If the user changes, it will instead show the skeleton again as opposed to greying out data for the previous user. -```tsx title="Managing UI behavior with currentData" +```tsx no-transpile title="Managing UI behavior with currentData" import { Skeleton } from './Skeleton' import { useGetPostsByUserQuery } from './api' @@ -274,7 +278,7 @@ Sometimes you may have a parent component that is subscribed to a query, and the `selectFromResult` allows you to get a specific segment from a query result in a performant manner. When using this feature, the component will not rerender unless the underlying data of the selected item has changed. If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection. -```tsx title="Using selectFromResult to extract a single result" +```tsx no-transpile title="Using selectFromResult to extract a single result" function PostsList() { const { data: posts } = api.useGetPostsQuery() @@ -301,7 +305,7 @@ function PostById({ id }: { id: number }) { Note that a shallow equality check is performed on the overall return value of `selectFromResult` to determine whether to force a rerender. i.e. it will trigger a rerender if any of the returned object values change reference. If a new array/object is created and used as a return value within the callback, it will hinder the performance benefits due to being identified as a new item each time the callback is run. When intentionally providing an empty array/object, in order to avoid re-creating it each time the callback runs, you can declare an empty array/object outside of the component in order to maintain a stable reference. -```tsx title="Using selectFromResult with a stable empty array" +```tsx no-transpile title="Using selectFromResult with a stable empty array" // An array declared here will maintain a stable reference rather than be re-created again const emptyArray: Post[] = [] diff --git a/docs/tutorials/typescript.md b/docs/tutorials/typescript.md index 12a3405538..ce825cbe37 100644 --- a/docs/tutorials/typescript.md +++ b/docs/tutorials/typescript.md @@ -156,7 +156,7 @@ const initialState = { In component files, import the pre-typed hooks instead of the standard hooks from React-Redux. -```tsx title="features/counter/Counter.tsx" +```tsx no-transpile title="features/counter/Counter.tsx" import React, { useState } from 'react' // highlight-next-line diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index fc7627731b..7ddc49f8ce 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@reduxjs/toolkit", - "version": "1.9.4", + "version": "1.9.5", "description": "The official, opinionated, batteries-included toolset for efficient Redux development", "author": "Mark Erikson ", "license": "MIT", diff --git a/packages/toolkit/src/configureStore.ts b/packages/toolkit/src/configureStore.ts index aae2307c9d..f89a86089a 100644 --- a/packages/toolkit/src/configureStore.ts +++ b/packages/toolkit/src/configureStore.ts @@ -24,7 +24,9 @@ import type { NoInfer, ExtractDispatchExtensions, ExtractStoreExtensions, + ExtractStateExtensions, } from './tsHelpers' +import { EnhancerArray } from './utils' const IS_PRODUCTION = process.env.NODE_ENV === 'production' @@ -34,8 +36,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production' * @public */ export type ConfigureEnhancersCallback = ( - defaultEnhancers: readonly StoreEnhancer[] -) => [...E] + defaultEnhancers: EnhancerArray<[StoreEnhancer<{}, {}>]> +) => E /** * Options for `configureStore()`. @@ -107,7 +109,7 @@ type Enhancers = ReadonlyArray export interface ToolkitStore< S = any, A extends Action = AnyAction, - M extends Middlewares = Middlewares, + M extends Middlewares = Middlewares > extends Store { /** * The `dispatch` method of your store, enhanced by all its middlewares. @@ -128,7 +130,8 @@ export type EnhancedStore< A extends Action = AnyAction, M extends Middlewares = Middlewares, E extends Enhancers = Enhancers -> = ToolkitStore & ExtractStoreExtensions +> = ToolkitStore, A, M> & + ExtractStoreExtensions /** * A friendly abstraction over the standard Redux `createStore()` function. @@ -197,12 +200,13 @@ export function configureStore< }) } - let storeEnhancers: Enhancers = [middlewareEnhancer] + const defaultEnhancers = new EnhancerArray(middlewareEnhancer) + let storeEnhancers: Enhancers = defaultEnhancers if (Array.isArray(enhancers)) { storeEnhancers = [middlewareEnhancer, ...enhancers] } else if (typeof enhancers === 'function') { - storeEnhancers = enhancers(storeEnhancers) + storeEnhancers = enhancers(defaultEnhancers) } const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer diff --git a/packages/toolkit/src/createAction.ts b/packages/toolkit/src/createAction.ts index 0647a04621..ab52d1c4fc 100644 --- a/packages/toolkit/src/createAction.ts +++ b/packages/toolkit/src/createAction.ts @@ -224,7 +224,7 @@ export type PayloadActionCreator< * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function - * will also have its toString() overriden so that it returns the action type, + * will also have its toString() overridden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * * @param type The action type to use for created actions. @@ -241,7 +241,7 @@ export function createAction

( * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function - * will also have its toString() overriden so that it returns the action type, + * will also have its toString() overridden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * * @param type The action type to use for created actions. @@ -286,6 +286,16 @@ export function createAction(type: string, prepareAction?: Function): any { return actionCreator } +/** + * Returns true if value is a plain object with a `type` property. + */ +export function isAction(action: unknown): action is Action { + return isPlainObject(action) && 'type' in action +} + +/** + * Returns true if value is an action with a string type and valid Flux Standard Action keys. + */ export function isFSA(action: unknown): action is { type: string payload?: unknown @@ -293,8 +303,8 @@ export function isFSA(action: unknown): action is { meta?: unknown } { return ( - isPlainObject(action) && - typeof (action as any).type === 'string' && + isAction(action) && + typeof action.type === 'string' && Object.keys(action).every(isValidKey) ) } diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 2ea3e1384c..3ea7634832 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -39,6 +39,8 @@ export { // js createAction, getType, + isAction, + isFSA as isFluxStandardAction, } from './createAction' export type { // types @@ -103,7 +105,7 @@ export type { // types ActionReducerMapBuilder, } from './mapBuilders' -export { MiddlewareArray } from './utils' +export { MiddlewareArray, EnhancerArray } from './utils' export { createEntityAdapter } from './entities/create_adapter' export type { diff --git a/packages/toolkit/src/listenerMiddleware/index.ts b/packages/toolkit/src/listenerMiddleware/index.ts index d96b9257db..56205295ee 100644 --- a/packages/toolkit/src/listenerMiddleware/index.ts +++ b/packages/toolkit/src/listenerMiddleware/index.ts @@ -1,6 +1,6 @@ import type { Dispatch, AnyAction, MiddlewareAPI } from 'redux' import type { ThunkDispatch } from 'redux-thunk' -import { createAction } from '../createAction' +import { createAction, isAction } from '../createAction' import { nanoid } from '../nanoid' import type { @@ -426,6 +426,11 @@ export function createListenerMiddleware< const middleware: ListenerMiddleware = (api) => (next) => (action) => { + if (!isAction(action)) { + // we only want to notify listeners for action objects + return next(action) + } + if (addListener.match(action)) { return startListening(action.payload) } diff --git a/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts b/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts index 2756e09f15..6d9e6abb56 100644 --- a/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts +++ b/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts @@ -367,7 +367,7 @@ describe('fork', () => { }, }) - store.dispatch(increment) + store.dispatch(increment()) expect(await deferredResult).toBe(listenerCompleted) }) diff --git a/packages/toolkit/src/query/react/ApiProvider.tsx b/packages/toolkit/src/query/react/ApiProvider.tsx index 1f2bc7be27..abaef8c2cc 100644 --- a/packages/toolkit/src/query/react/ApiProvider.tsx +++ b/packages/toolkit/src/query/react/ApiProvider.tsx @@ -12,7 +12,7 @@ import type { Api } from '@reduxjs/toolkit/dist/query/apiTypes' * * @example * ```tsx - * // codeblock-meta title="Basic usage - wrap your App with ApiProvider" + * // codeblock-meta no-transpile title="Basic usage - wrap your App with ApiProvider" * import * as React from 'react'; * import { ApiProvider } from '@reduxjs/toolkit/query/react'; * import { Pokemon } from './features/Pokemon'; diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 9d89188514..28e2916bf7 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -140,7 +140,7 @@ interface UseQuerySubscriptionOptions extends SubscriptionOptions { * * @example * ```tsx - * // codeblock-meta title="Skip example" + * // codeblock-meta no-transpile title="Skip example" * const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => { * const { data, error, status } = useGetPokemonByNameQuery(name, { * skip, diff --git a/packages/toolkit/src/tests/EnhancerArray.typetest.ts b/packages/toolkit/src/tests/EnhancerArray.typetest.ts new file mode 100644 index 0000000000..56e89a30d1 --- /dev/null +++ b/packages/toolkit/src/tests/EnhancerArray.typetest.ts @@ -0,0 +1,135 @@ +import { configureStore } from '@reduxjs/toolkit' +import type { StoreEnhancer } from 'redux' + +declare const expectType: (t: T) => T + +declare const enhancer1: StoreEnhancer< + { + has1: true + }, + { stateHas1: true } +> + +declare const enhancer2: StoreEnhancer< + { + has2: true + }, + { stateHas2: true } +> + +{ + // prepend single element + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.prepend(enhancer1), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + + // @ts-expect-error + expectType(store.has2) + // @ts-expect-error + expectType(store.getState().stateHas2) + } + + // prepend multiple (rest) + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.prepend(enhancer1, enhancer2), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + expectType(store.has2) + expectType(store.getState().stateHas2) + + // @ts-expect-error + expectType(store.has3) + // @ts-expect-error + expectType(store.getState().stateHas3) + } + + // prepend multiple (array notation) + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.prepend([enhancer1, enhancer2] as const), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + expectType(store.has2) + expectType(store.getState().stateHas2) + + // @ts-expect-error + expectType(store.has3) + // @ts-expect-error + expectType(store.getState().stateHas3) + } + + // concat single element + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.concat(enhancer1), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + + // @ts-expect-error + expectType(store.has2) + // @ts-expect-error + expectType(store.getState().stateHas2) + } + + // prepend multiple (rest) + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.concat(enhancer1, enhancer2), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + expectType(store.has2) + expectType(store.getState().stateHas2) + + // @ts-expect-error + expectType(store.has3) + // @ts-expect-error + expectType(store.getState().stateHas3) + } + + // concat multiple (array notation) + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.concat([enhancer1, enhancer2] as const), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + expectType(store.has2) + expectType(store.getState().stateHas2) + + // @ts-expect-error + expectType(store.has3) + // @ts-expect-error + expectType(store.getState().stateHas3) + } + + // concat and prepend + { + const store = configureStore({ + reducer: () => 0, + enhancers: (dE) => dE.concat(enhancer1).prepend(enhancer2), + }) + expectType(store.has1) + expectType(store.getState().stateHas1) + expectType(store.has2) + expectType(store.getState().stateHas2) + + // @ts-expect-error + expectType(store.has3) + // @ts-expect-error + expectType(store.getState().stateHas3) + } +} diff --git a/packages/toolkit/src/tests/configureStore.test.ts b/packages/toolkit/src/tests/configureStore.test.ts index 49831e51e6..aa93a5f842 100644 --- a/packages/toolkit/src/tests/configureStore.test.ts +++ b/packages/toolkit/src/tests/configureStore.test.ts @@ -231,9 +231,7 @@ describe('configureStore', () => { const store = configureStore({ reducer, - enhancers: (defaultEnhancers) => { - return [...defaultEnhancers, dummyEnhancer] - }, + enhancers: (defaultEnhancers) => defaultEnhancers.concat(dummyEnhancer), }) expect(dummyEnhancerCalled).toBe(true) diff --git a/packages/toolkit/src/tests/configureStore.typetest.ts b/packages/toolkit/src/tests/configureStore.typetest.ts index ae7d4872c7..d22bb776fd 100644 --- a/packages/toolkit/src/tests/configureStore.typetest.ts +++ b/packages/toolkit/src/tests/configureStore.typetest.ts @@ -194,6 +194,86 @@ const _anyMiddleware: any = () => () => () => {} ) expectType(store.someProperty) expectType(store.anotherProperty) + + const storeWithCallback = configureStore({ + reducer: () => 0, + enhancers: (defaultEnhancers) => + defaultEnhancers + .prepend(anotherPropertyStoreEnhancer) + .concat(somePropertyStoreEnhancer), + }) + + expectType>( + store.dispatch + ) + expectType(storeWithCallback.someProperty) + expectType(storeWithCallback.anotherProperty) + } + + { + type StateExtendingEnhancer = StoreEnhancer<{}, { someProperty: string }> + + const someStateExtendingEnhancer: StateExtendingEnhancer = + (next) => + // @ts-expect-error how do you properly return an enhancer that extends state? + (...args) => { + const store = next(...args) + const getState = () => ({ + ...store.getState(), + someProperty: 'some value', + }) + return { + ...store, + getState, + } + } + + type AnotherStateExtendingEnhancer = StoreEnhancer< + {}, + { anotherProperty: number } + > + + const anotherStateExtendingEnhancer: AnotherStateExtendingEnhancer = + (next) => + // @ts-expect-error any input on this would be great + (...args) => { + const store = next(...args) + const getState = () => ({ + ...store.getState(), + anotherProperty: 123, + }) + return { + ...store, + getState, + } + } + + const store = configureStore({ + reducer: () => ({ aProperty: 0 }), + enhancers: [ + someStateExtendingEnhancer, + anotherStateExtendingEnhancer, + // this doesn't work without the as const + ] as const, + }) + + const state = store.getState() + + expectType(state.aProperty) + expectType(state.someProperty) + expectType(state.anotherProperty) + + const storeWithCallback = configureStore({ + reducer: () => ({ aProperty: 0 }), + enhancers: (dE) => + dE.concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer), + }) + + const stateWithCallback = storeWithCallback.getState() + + expectType(stateWithCallback.aProperty) + expectType(stateWithCallback.someProperty) + expectType(stateWithCallback.anotherProperty) } } diff --git a/packages/toolkit/src/tests/createAction.test.ts b/packages/toolkit/src/tests/createAction.test.ts index b5e8b67b26..5b481b945c 100644 --- a/packages/toolkit/src/tests/createAction.test.ts +++ b/packages/toolkit/src/tests/createAction.test.ts @@ -1,4 +1,4 @@ -import { createAction, getType } from '@reduxjs/toolkit' +import { createAction, getType, isAction } from '@reduxjs/toolkit' describe('createAction', () => { it('should create an action', () => { @@ -122,6 +122,27 @@ describe('createAction', () => { }) }) +describe('isAction', () => { + it('should only return true for plain objects with a type property', () => { + const actionCreator = createAction('anAction') + class Action { + type = 'totally an action' + } + const testCases: [action: unknown, expected: boolean][] = [ + [{ type: 'an action' }, true], + [{ type: 'more props', extra: true }, true], + [actionCreator(), true], + [actionCreator, false], + [Promise.resolve({ type: 'an action' }), false], + [new Action(), false], + ['a string', false], + ] + for (const [action, expected] of testCases) { + expect(isAction(action)).toBe(expected) + } + }) +}) + describe('getType', () => { it('should return the action type', () => { const actionCreator = createAction('A_TYPE') diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 3e8dc20f05..5ffc0196a0 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -1,5 +1,5 @@ import type { Middleware, StoreEnhancer } from 'redux' -import type { MiddlewareArray } from './utils' +import type { EnhancerArray, MiddlewareArray } from './utils' /** * return True if T is `any`, otherwise return False @@ -101,9 +101,56 @@ export type ExtractDispatchExtensions = M extends MiddlewareArray< ? ExtractDispatchFromMiddlewareTuple<[...M], {}> : never -export type ExtractStoreExtensions = E extends any[] - ? UnionToIntersection ? Ext extends {} ? Ext : {} : {}> - : {} +type ExtractStoreExtensionsFromEnhancerTuple< + EnhancerTuple extends any[], + Acc extends {} +> = EnhancerTuple extends [infer Head, ...infer Tail] + ? ExtractStoreExtensionsFromEnhancerTuple< + Tail, + Acc & (Head extends StoreEnhancer ? IsAny : {}) + > + : Acc + +export type ExtractStoreExtensions = E extends EnhancerArray< + infer EnhancerTuple +> + ? ExtractStoreExtensionsFromEnhancerTuple + : E extends ReadonlyArray + ? UnionToIntersection< + E[number] extends StoreEnhancer + ? Ext extends {} + ? IsAny + : {} + : {} + > + : never + +type ExtractStateExtensionsFromEnhancerTuple< + EnhancerTuple extends any[], + Acc extends {} +> = EnhancerTuple extends [infer Head, ...infer Tail] + ? ExtractStateExtensionsFromEnhancerTuple< + Tail, + Acc & + (Head extends StoreEnhancer + ? IsAny + : {}) + > + : Acc + +export type ExtractStateExtensions = E extends EnhancerArray< + infer EnhancerTuple +> + ? ExtractStateExtensionsFromEnhancerTuple + : E extends ReadonlyArray + ? UnionToIntersection< + E[number] extends StoreEnhancer + ? StateExt extends {} + ? IsAny + : {} + : {} + > + : never /** * Helper type. Passes T out again, but boxes it in a way that it cannot diff --git a/packages/toolkit/src/utils.ts b/packages/toolkit/src/utils.ts index 40957c2788..2d3784293c 100644 --- a/packages/toolkit/src/utils.ts +++ b/packages/toolkit/src/utils.ts @@ -1,5 +1,5 @@ import createNextState, { isDraftable } from 'immer' -import type { Middleware } from 'redux' +import type { Middleware, StoreEnhancer } from 'redux' export function getTimeMeasureUtils(maxDelay: number, fnName: string) { let elapsed = 0 @@ -70,6 +70,49 @@ export class MiddlewareArray< } } +/** + * @public + */ +export class EnhancerArray< + Enhancers extends StoreEnhancer[] +> extends Array { + constructor(...items: Enhancers) + constructor(...args: any[]) { + super(...args) + Object.setPrototypeOf(this, EnhancerArray.prototype) + } + + static get [Symbol.species]() { + return EnhancerArray as any + } + + concat>>( + items: AdditionalEnhancers + ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]> + + concat>>( + ...items: AdditionalEnhancers + ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]> + concat(...arr: any[]) { + return super.concat.apply(this, arr) + } + + prepend>>( + items: AdditionalEnhancers + ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]> + + prepend>>( + ...items: AdditionalEnhancers + ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]> + + prepend(...arr: any[]) { + if (arr.length === 1 && Array.isArray(arr[0])) { + return new EnhancerArray(...arr[0].concat(this)) + } + return new EnhancerArray(...arr.concat(this)) + } +} + export function freezeDraftable(val: T) { return isDraftable(val) ? createNextState(val, () => {}) : val } diff --git a/website/package.json b/website/package.json index f6c7954d71..f51edfd685 100644 --- a/website/package.json +++ b/website/package.json @@ -15,7 +15,7 @@ "react": "^18.1.0", "react-dom": "^18.1.0", "react-lite-youtube-embed": "^2.0.3", - "remark-typescript-tools": "^1.0.8", + "remark-typescript-tools": "^1.1.0", "typescript": "~4.2.4" }, "browserslist": { diff --git a/yarn.lock b/yarn.lock index 75cf7af2e1..bbf6061fe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23900,9 +23900,9 @@ fsevents@^1.2.7: languageName: node linkType: hard -"remark-typescript-tools@npm:^1.0.8": - version: 1.0.8 - resolution: "remark-typescript-tools@npm:1.0.8" +"remark-typescript-tools@npm:^1.1.0": + version: 1.1.0 + resolution: "remark-typescript-tools@npm:1.1.0" dependencies: "@microsoft/tsdoc": ^0.12.21 prettier: ^2.1.1 @@ -23910,7 +23910,7 @@ fsevents@^1.2.7: unist-util-visit: ^2.0.3 peerDependencies: typescript: ">=4.0" - checksum: 6d2c1390e73fc1ed2b2d3816fbea34d72bc822ec3bb0e605cabcbdb3624576196242d0a752e6e6a79be2821a2d335c868fe4ef7dd6bb550c41552b49d391d867 + checksum: 3f48c77c449461fffb1d19e85b4a81de3327fae5a98c909330b7ffb85f2600359e7070f73f0b57476eae25b32f57906e3f438bb76bb65b6bd176af025e34ce67 languageName: node linkType: hard @@ -28100,7 +28100,7 @@ fsevents@^1.2.7: react: ^18.1.0 react-dom: ^18.1.0 react-lite-youtube-embed: ^2.0.3 - remark-typescript-tools: ^1.0.8 + remark-typescript-tools: ^1.1.0 typescript: ~4.2.4 languageName: unknown linkType: soft