From 49f808af041504b05560723e31dd8333aac519e6 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Wed, 11 Jan 2023 12:18:11 +0100 Subject: [PATCH 01/21] allow `produce` to be swapped out in createReducer/createSlice --- packages/toolkit/src/createReducer.ts | 161 +++++++++--------- packages/toolkit/src/createSlice.ts | 225 ++++++++++++++------------ packages/toolkit/src/index.ts | 6 + 3 files changed, 218 insertions(+), 174 deletions(-) diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index e22b0a007a..e2530ed29e 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -82,7 +82,8 @@ export type ReducerWithInitialState> = Reducer & { let hasWarnedAboutObjectNotation = false -/** +export type CreateReducer = { + /** * A utility function that allows defining a reducer as a mapping from action * type to *case reducer* functions that handle these action types. The * reducer's initial state is passed as the first argument. @@ -146,90 +147,104 @@ const reducer = createReducer( ``` * @public */ -export function createReducer>( - initialState: S | (() => S), - builderCallback: (builder: ActionReducerMapBuilder) => void -): ReducerWithInitialState - -export function createReducer>( - initialState: S | (() => S), - mapOrBuilderCallback: (builder: ActionReducerMapBuilder) => void -): ReducerWithInitialState { - if (process.env.NODE_ENV !== 'production') { - if (typeof mapOrBuilderCallback === 'object') { - throw new Error( - "The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer" - ) - } - } + >( + initialState: S | (() => S), + builderCallback: (builder: ActionReducerMapBuilder) => void + ): ReducerWithInitialState +} + +export interface BuildCreateReducerConfiguration { + createNextState: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => Base +} - let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] = - executeReducerBuilderCallback(mapOrBuilderCallback) +export function buildCreateReducer({ + createNextState, +}: BuildCreateReducerConfiguration): CreateReducer { + return function createReducer>( + initialState: S | (() => S), + mapOrBuilderCallback: (builder: ActionReducerMapBuilder) => void + ): ReducerWithInitialState { + if (process.env.NODE_ENV !== 'production') { + if (typeof mapOrBuilderCallback === 'object') { + throw new Error( + "The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer" + ) + } + } - // Ensure the initial state gets frozen either way (if draftable) - let getInitialState: () => S - if (isStateFunction(initialState)) { - getInitialState = () => freezeDraftable(initialState()) - } else { - const frozenInitialState = freezeDraftable(initialState) - getInitialState = () => frozenInitialState - } + let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] = + executeReducerBuilderCallback(mapOrBuilderCallback) - function reducer(state = getInitialState(), action: any): S { - let caseReducers = [ - actionsMap[action.type], - ...finalActionMatchers - .filter(({ matcher }) => matcher(action)) - .map(({ reducer }) => reducer), - ] - if (caseReducers.filter((cr) => !!cr).length === 0) { - caseReducers = [finalDefaultCaseReducer] + // Ensure the initial state gets frozen either way (if draftable) + let getInitialState: () => S + if (isStateFunction(initialState)) { + getInitialState = () => freezeDraftable(initialState()) + } else { + const frozenInitialState = freezeDraftable(initialState) + getInitialState = () => frozenInitialState } - return caseReducers.reduce((previousState, caseReducer): S => { - if (caseReducer) { - if (isDraft(previousState)) { - // If it's already a draft, we must already be inside a `createNextState` call, - // likely because this is being wrapped in `createReducer`, `createSlice`, or nested - // inside an existing draft. It's safe to just pass the draft to the mutator. - const draft = previousState as Draft // We can assume this is already a draft - const result = caseReducer(draft, action) - - if (result === undefined) { - return previousState - } + function reducer(state = getInitialState(), action: any): S { + let caseReducers = [ + actionsMap[action.type], + ...finalActionMatchers + .filter(({ matcher }) => matcher(action)) + .map(({ reducer }) => reducer), + ] + if (caseReducers.filter((cr) => !!cr).length === 0) { + caseReducers = [finalDefaultCaseReducer] + } - return result as S - } else if (!isDraftable(previousState)) { - // If state is not draftable (ex: a primitive, such as 0), we want to directly - // return the caseReducer func and not wrap it with produce. - const result = caseReducer(previousState as any, action) + return caseReducers.reduce((previousState, caseReducer): S => { + if (caseReducer) { + if (isDraft(previousState)) { + // If it's already a draft, we must already be inside a `createNextState` call, + // likely because this is being wrapped in `createReducer`, `createSlice`, or nested + // inside an existing draft. It's safe to just pass the draft to the mutator. + const draft = previousState as Draft // We can assume this is already a draft + const result = caseReducer(draft, action) - if (result === undefined) { - if (previousState === null) { + if (result === undefined) { return previousState } - throw Error( - 'A case reducer on a non-draftable value must not return undefined' - ) - } - return result as S - } else { - // @ts-ignore createNextState() produces an Immutable> rather - // than an Immutable, and TypeScript cannot find out how to reconcile - // these two types. - return createNextState(previousState, (draft: Draft) => { - return caseReducer(draft, action) - }) + return result as S + } else if (!isDraftable(previousState)) { + // If state is not draftable (ex: a primitive, such as 0), we want to directly + // return the caseReducer func and not wrap it with produce. + const result = caseReducer(previousState as any, action) + + if (result === undefined) { + if (previousState === null) { + return previousState + } + throw Error( + 'A case reducer on a non-draftable value must not return undefined' + ) + } + + return result as S + } else { + // @ts-ignore createNextState() produces an Immutable> rather + // than an Immutable, and TypeScript cannot find out how to reconcile + // these two types. + return createNextState(previousState, (draft: Draft) => { + return caseReducer(draft, action) + }) + } } - } - return previousState - }, state) - } + return previousState + }, state) + } - reducer.getInitialState = getInitialState + reducer.getInitialState = getInitialState - return reducer as ReducerWithInitialState + return reducer as ReducerWithInitialState + } } + +export const createReducer = buildCreateReducer({ createNextState }) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index b9d3fd0d0f..268a7a74c4 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,4 +1,4 @@ -import type { AnyAction, Reducer } from 'redux' +import type { Reducer } from 'redux' import { createNextState } from '.' import type { ActionCreatorWithoutPayload, @@ -9,11 +9,12 @@ import type { } from './createAction' import { createAction } from './createAction' import type { + BuildCreateReducerConfiguration, CaseReducer, CaseReducers, ReducerWithInitialState, } from './createReducer' -import { createReducer, NotFunction } from './createReducer' +import { buildCreateReducer } from './createReducer' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { NoInfer } from './tsHelpers' @@ -258,120 +259,142 @@ function getType(slice: string, actionKey: string): string { return `${slice}/${actionKey}` } -/** - * A function that accepts an initial state, an object full of reducer - * functions, and a "slice name", and automatically generates - * action creators and action types that correspond to the - * reducers and state. - * - * The `reducer` argument is passed to `createReducer()`. - * - * @public - */ -export function createSlice< - State, - CaseReducers extends SliceCaseReducers, - Name extends string = string ->( - options: CreateSliceOptions -): Slice { - const { name } = options - if (!name) { - throw new Error('`name` is a required option for createSlice') - } +export type CreateSlice = { + /** + * A function that accepts an initial state, an object full of reducer + * functions, and a "slice name", and automatically generates + * action creators and action types that correspond to the + * reducers and state. + * + * The `reducer` argument is passed to `createReducer()`. + * + * @public + */ + < + State, + CaseReducers extends SliceCaseReducers, + Name extends string = string + >( + options: CreateSliceOptions + ): Slice +} - if ( - typeof process !== 'undefined' && - process.env.NODE_ENV === 'development' - ) { - if (options.initialState === undefined) { - console.error( - 'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`' - ) +export interface BuildCreateSliceConfiguration + extends BuildCreateReducerConfiguration {} + +export function buildCreateSlice( + configuration: BuildCreateSliceConfiguration +): CreateSlice { + const createReducer = buildCreateReducer(configuration) + return function createSlice< + State, + CaseReducers extends SliceCaseReducers, + Name extends string = string + >( + options: CreateSliceOptions + ): Slice { + const { name } = options + if (!name) { + throw new Error('`name` is a required option for createSlice') } - } - const initialState = - typeof options.initialState == 'function' - ? options.initialState - : freezeDraftable(options.initialState) + if ( + typeof process !== 'undefined' && + process.env.NODE_ENV === 'development' + ) { + if (options.initialState === undefined) { + console.error( + 'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`' + ) + } + } - const reducers = options.reducers || {} + const initialState = + typeof options.initialState == 'function' + ? options.initialState + : freezeDraftable(options.initialState) - const reducerNames = Object.keys(reducers) + const reducers = options.reducers || {} - const sliceCaseReducersByName: Record = {} - const sliceCaseReducersByType: Record = {} - const actionCreators: Record = {} + const reducerNames = Object.keys(reducers) - reducerNames.forEach((reducerName) => { - const maybeReducerWithPrepare = reducers[reducerName] - const type = getType(name, reducerName) + const sliceCaseReducersByName: Record = {} + const sliceCaseReducersByType: Record = {} + const actionCreators: Record = {} - let caseReducer: CaseReducer - let prepareCallback: PrepareAction | undefined + reducerNames.forEach((reducerName) => { + const maybeReducerWithPrepare = reducers[reducerName] + const type = getType(name, reducerName) - if ('reducer' in maybeReducerWithPrepare) { - caseReducer = maybeReducerWithPrepare.reducer - prepareCallback = maybeReducerWithPrepare.prepare - } else { - caseReducer = maybeReducerWithPrepare - } + let caseReducer: CaseReducer + let prepareCallback: PrepareAction | undefined - sliceCaseReducersByName[reducerName] = caseReducer - sliceCaseReducersByType[type] = caseReducer - actionCreators[reducerName] = prepareCallback - ? createAction(type, prepareCallback) - : createAction(type) - }) - - function buildReducer() { - if (process.env.NODE_ENV !== 'production') { - if (typeof options.extraReducers === 'object') { - throw new Error( - "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice" - ) - } - } - const [ - extraReducers = {}, - actionMatchers = [], - defaultCaseReducer = undefined, - ] = - typeof options.extraReducers === 'function' - ? executeReducerBuilderCallback(options.extraReducers) - : [options.extraReducers] - - const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType } - - return createReducer(initialState, (builder) => { - for (let key in finalCaseReducers) { - builder.addCase(key, finalCaseReducers[key] as CaseReducer) - } - for (let m of actionMatchers) { - builder.addMatcher(m.matcher, m.reducer) - } - if (defaultCaseReducer) { - builder.addDefaultCase(defaultCaseReducer) + if ('reducer' in maybeReducerWithPrepare) { + caseReducer = maybeReducerWithPrepare.reducer + prepareCallback = maybeReducerWithPrepare.prepare + } else { + caseReducer = maybeReducerWithPrepare } + + sliceCaseReducersByName[reducerName] = caseReducer + sliceCaseReducersByType[type] = caseReducer + actionCreators[reducerName] = prepareCallback + ? createAction(type, prepareCallback) + : createAction(type) }) - } - let _reducer: ReducerWithInitialState + function buildReducer() { + if (process.env.NODE_ENV !== 'production') { + if (typeof options.extraReducers === 'object') { + throw new Error( + "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice" + ) + } + } + const [ + extraReducers = {}, + actionMatchers = [], + defaultCaseReducer = undefined, + ] = + typeof options.extraReducers === 'function' + ? executeReducerBuilderCallback(options.extraReducers) + : [options.extraReducers] + + const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType } + + return createReducer(initialState, (builder) => { + for (let key in finalCaseReducers) { + builder.addCase(key, finalCaseReducers[key] as CaseReducer) + } + for (let m of actionMatchers) { + builder.addMatcher(m.matcher, m.reducer) + } + if (defaultCaseReducer) { + builder.addDefaultCase(defaultCaseReducer) + } + }) + } - return { - name, - reducer(state, action) { - if (!_reducer) _reducer = buildReducer() + let _reducer: ReducerWithInitialState - return _reducer(state, action) - }, - actions: actionCreators as any, - caseReducers: sliceCaseReducersByName as any, - getInitialState() { - if (!_reducer) _reducer = buildReducer() + return { + name, + reducer(state, action) { + if (!_reducer) _reducer = buildReducer() - return _reducer.getInitialState() - }, + return _reducer(state, action) + }, + actions: actionCreators as any, + caseReducers: sliceCaseReducersByName as any, + getInitialState() { + if (!_reducer) _reducer = buildReducer() + + return _reducer.getInitialState() + }, + } } } + +export const createSlice = buildCreateSlice({ + createNextState, +}) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 0c8737e8b4..ad505f3d54 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -47,20 +47,26 @@ export type { export { // js createReducer, + buildCreateReducer, } from './createReducer' export type { // types Actions, CaseReducer, CaseReducers, + CreateReducer, + BuildCreateReducerConfiguration, } from './createReducer' export { // js createSlice, + buildCreateSlice, } from './createSlice' export type { // types + BuildCreateSliceConfiguration, + CreateSlice, CreateSliceOptions, Slice, CaseReducerActions, From f0228f906681af02377afff0c457546c62b34fe2 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Feb 2023 23:55:07 -0500 Subject: [PATCH 02/21] Make more Immer utils configurable --- packages/toolkit/src/createReducer.ts | 17 +++++++++++++++-- packages/toolkit/src/createSlice.ts | 8 ++++++-- packages/toolkit/src/utils.ts | 12 +++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index e2530ed29e..3690f274f7 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -4,7 +4,7 @@ import type { AnyAction, Action, Reducer } from 'redux' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { NoInfer } from './tsHelpers' -import { freezeDraftable } from './utils' +import { makeFreezeDraftable } from './utils' /** * Defines a mapping from action types to corresponding action object shapes. @@ -158,11 +158,20 @@ export interface BuildCreateReducerConfiguration { base: Base, recipe: (draft: Draft) => void | Base | Draft ) => Base + isDraft(value: any): boolean + isDraftable(value: any): boolean } export function buildCreateReducer({ createNextState, + isDraft, + isDraftable, }: BuildCreateReducerConfiguration): CreateReducer { + const freezeDraftable = makeFreezeDraftable({ + createNextState, + isDraft, + isDraftable, + }) return function createReducer>( initialState: S | (() => S), mapOrBuilderCallback: (builder: ActionReducerMapBuilder) => void @@ -247,4 +256,8 @@ export function buildCreateReducer({ } } -export const createReducer = buildCreateReducer({ createNextState }) +export const createReducer = buildCreateReducer({ + createNextState, + isDraft, + isDraftable, +}) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index 268a7a74c4..dae0115d0a 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,5 +1,5 @@ import type { Reducer } from 'redux' -import { createNextState } from '.' +import { produce as createNextState, isDraft, isDraftable } from 'immer' import type { ActionCreatorWithoutPayload, PayloadAction, @@ -18,7 +18,7 @@ import { buildCreateReducer } from './createReducer' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { NoInfer } from './tsHelpers' -import { freezeDraftable } from './utils' +import { makeFreezeDraftable } from './utils' let hasWarnedAboutObjectNotation = false @@ -286,6 +286,8 @@ export function buildCreateSlice( configuration: BuildCreateSliceConfiguration ): CreateSlice { const createReducer = buildCreateReducer(configuration) + const freezeDraftable = makeFreezeDraftable(configuration) + return function createSlice< State, CaseReducers extends SliceCaseReducers, @@ -397,4 +399,6 @@ export function buildCreateSlice( export const createSlice = buildCreateSlice({ createNextState, + isDraft, + isDraftable, }) diff --git a/packages/toolkit/src/utils.ts b/packages/toolkit/src/utils.ts index 9697d31bb6..b5ad06f2c5 100644 --- a/packages/toolkit/src/utils.ts +++ b/packages/toolkit/src/utils.ts @@ -1,6 +1,7 @@ -import { produce as createNextState, isDraftable } from 'immer' import type { Middleware } from 'redux' +import type { BuildCreateReducerConfiguration } from './createReducer' + export function getTimeMeasureUtils(maxDelay: number, fnName: string) { let elapsed = 0 return { @@ -70,6 +71,11 @@ export class MiddlewareArray< } } -export function freezeDraftable(val: T) { - return isDraftable(val) ? createNextState(val, () => {}) : val +export function makeFreezeDraftable({ + isDraftable, + createNextState, +}: BuildCreateReducerConfiguration) { + return function freezeDraftable(val: T) { + return isDraftable(val) ? createNextState(val, () => {}) : val + } } From fb9d324ee978d33af123cfd2238f72f86212d59d Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 4 Apr 2023 23:31:12 +0100 Subject: [PATCH 03/21] add buildable createDraftSafeSelector --- .../toolkit/src/createDraftSafeSelector.ts | 29 ++++++++++++++----- packages/toolkit/src/index.ts | 6 +++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/toolkit/src/createDraftSafeSelector.ts b/packages/toolkit/src/createDraftSafeSelector.ts index b1858af2e7..cef33bdc71 100644 --- a/packages/toolkit/src/createDraftSafeSelector.ts +++ b/packages/toolkit/src/createDraftSafeSelector.ts @@ -1,6 +1,11 @@ import { current, isDraft } from 'immer' import { createSelector } from 'reselect' +export interface BuildCreateDraftSafeSelectorConfiguration { + isDraft(value: any): boolean + current(value: T): T +} + /** * "Draft-Safe" version of `reselect`'s `createSelector`: * If an `immer`-drafted object is passed into the resulting selector's first argument, @@ -8,11 +13,21 @@ import { createSelector } from 'reselect' * that might be possibly outdated if the draft has been modified since. * @public */ -export const createDraftSafeSelector: typeof createSelector = ( - ...args: unknown[] -) => { - const selector = (createSelector as any)(...args) - const wrappedSelector = (value: unknown, ...rest: unknown[]) => - selector(isDraft(value) ? current(value) : value, ...rest) - return wrappedSelector as any +export type CreateDraftSafeSelector = typeof createSelector + +export function buildCreateDraftSafeSelector({ + isDraft, + current, +}: BuildCreateDraftSafeSelectorConfiguration): CreateDraftSafeSelector { + return function createDraftSafeSelector(...args: unknown[]) { + const selector = (createSelector as any)(...args) + const wrappedSelector = (value: unknown, ...rest: unknown[]) => + selector(isDraft(value) ? current(value) : value, ...rest) + return wrappedSelector as any + } } + +export const createDraftSafeSelector = buildCreateDraftSafeSelector({ + isDraft, + current, +}) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index ad505f3d54..4e55413d35 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -14,7 +14,11 @@ export type { OutputSelector, ParametricSelector, } from 'reselect' -export { createDraftSafeSelector } from './createDraftSafeSelector' +export type { BuildCreateDraftSafeSelectorConfiguration } from './createDraftSafeSelector' +export { + buildCreateDraftSafeSelector, + createDraftSafeSelector, +} from './createDraftSafeSelector' export type { ThunkAction, ThunkDispatch, ThunkMiddleware } from 'redux-thunk' export { From 293a9b9a1df9ae4c027e4251f3f0847a9305411b Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 4 Apr 2023 23:55:07 +0100 Subject: [PATCH 04/21] make configurable createEntityAdapter --- .../toolkit/src/entities/create_adapter.ts | 80 +++-- .../src/entities/sorted_state_adapter.ts | 253 ++++++++------- .../toolkit/src/entities/state_adapter.ts | 96 +++--- .../toolkit/src/entities/state_selectors.ts | 95 +++--- .../src/entities/unsorted_state_adapter.ts | 302 +++++++++--------- 5 files changed, 445 insertions(+), 381 deletions(-) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index 0d0c77e9d1..e3a98e1f44 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -1,3 +1,5 @@ +import type { Draft } from 'immer' +import { isDraft, current, produce as createNextState } from 'immer' import type { EntityDefinition, Comparer, @@ -5,39 +7,59 @@ import type { EntityAdapter, } from './models' import { createInitialStateFactory } from './entity_state' -import { createSelectorsFactory } from './state_selectors' -import { createSortedStateAdapter } from './sorted_state_adapter' -import { createUnsortedStateAdapter } from './unsorted_state_adapter' +import { buildCreateSelectorsFactory } from './state_selectors' +import { buildCreateSortedStateAdapter } from './sorted_state_adapter' +import { buildCreateUnsortedStateAdapter } from './unsorted_state_adapter' +import type { BuildCreateDraftSafeSelectorConfiguration } from '..' +import type { BuildStateOperatorConfiguration } from './state_adapter' -/** - * - * @param options - * - * @public - */ -export function createEntityAdapter( - options: { +export interface BuildCreateEntityAdapterConfiguration + extends BuildCreateDraftSafeSelectorConfiguration, + BuildStateOperatorConfiguration {} + +export type CreateEntityAdapter = { + (options: { selectId?: IdSelector sortComparer?: false | Comparer - } = {} -): EntityAdapter { - const { selectId, sortComparer }: EntityDefinition = { - sortComparer: false, - selectId: (instance: any) => instance.id, - ...options, - } + }): EntityAdapter +} - const stateFactory = createInitialStateFactory() - const selectorsFactory = createSelectorsFactory() - const stateAdapter = sortComparer - ? createSortedStateAdapter(selectId, sortComparer) - : createUnsortedStateAdapter(selectId) +export function buildCreateEntityAdapter( + config: BuildCreateEntityAdapterConfiguration +): CreateEntityAdapter { + const createSelectorsFactory = buildCreateSelectorsFactory(config) + const createUnsortedStateAdapter = buildCreateUnsortedStateAdapter(config) + const createSortedStateAdapter = buildCreateSortedStateAdapter(config) + return function createEntityAdapter( + options: { + selectId?: IdSelector + sortComparer?: false | Comparer + } = {} + ): EntityAdapter { + const { selectId, sortComparer }: EntityDefinition = { + sortComparer: false, + selectId: (instance: any) => instance.id, + ...options, + } - return { - selectId, - sortComparer, - ...stateFactory, - ...selectorsFactory, - ...stateAdapter, + const stateFactory = createInitialStateFactory() + const selectorsFactory = createSelectorsFactory() + const stateAdapter = sortComparer + ? createSortedStateAdapter(selectId, sortComparer) + : createUnsortedStateAdapter(selectId) + + return { + selectId, + sortComparer, + ...stateFactory, + ...selectorsFactory, + ...stateAdapter, + } } } + +export const createEntityAdapter = buildCreateEntityAdapter({ + isDraft, + current, + createNextState, +}) diff --git a/packages/toolkit/src/entities/sorted_state_adapter.ts b/packages/toolkit/src/entities/sorted_state_adapter.ts index 4f1ed7a372..bc389ab69c 100644 --- a/packages/toolkit/src/entities/sorted_state_adapter.ts +++ b/packages/toolkit/src/entities/sorted_state_adapter.ts @@ -6,163 +6,170 @@ import type { Update, EntityId, } from './models' -import { createStateOperator } from './state_adapter' -import { createUnsortedStateAdapter } from './unsorted_state_adapter' +import type { BuildStateOperatorConfiguration } from './state_adapter' +import { buildCreateStateOperator } from './state_adapter' +import { buildCreateUnsortedStateAdapter } from './unsorted_state_adapter' import { selectIdValue, ensureEntitiesArray, splitAddedUpdatedEntities, } from './utils' -export function createSortedStateAdapter( - selectId: IdSelector, - sort: Comparer -): EntityStateAdapter { - type R = EntityState - - const { removeOne, removeMany, removeAll } = - createUnsortedStateAdapter(selectId) - - function addOneMutably(entity: T, state: R): void { - return addManyMutably([entity], state) - } +export function buildCreateSortedStateAdapter( + config: BuildStateOperatorConfiguration +) { + const createUnsortedStateAdapter = buildCreateUnsortedStateAdapter(config) + const createStateOperator = buildCreateStateOperator(config) + return function createSortedStateAdapter( + selectId: IdSelector, + sort: Comparer + ): EntityStateAdapter { + type R = EntityState + + const { removeOne, removeMany, removeAll } = + createUnsortedStateAdapter(selectId) + + function addOneMutably(entity: T, state: R): void { + return addManyMutably([entity], state) + } - function addManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) + function addManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) - const models = newEntities.filter( - (model) => !(selectIdValue(model, selectId) in state.entities) - ) + const models = newEntities.filter( + (model) => !(selectIdValue(model, selectId) in state.entities) + ) - if (models.length !== 0) { - merge(models, state) + if (models.length !== 0) { + merge(models, state) + } } - } - - function setOneMutably(entity: T, state: R): void { - return setManyMutably([entity], state) - } - function setManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) - if (newEntities.length !== 0) { - merge(newEntities, state) + function setOneMutably(entity: T, state: R): void { + return setManyMutably([entity], state) } - } - function setAllMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) - state.entities = {} - state.ids = [] + function setManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) + if (newEntities.length !== 0) { + merge(newEntities, state) + } + } - addManyMutably(newEntities, state) - } + function setAllMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) + state.entities = {} + state.ids = [] - function updateOneMutably(update: Update, state: R): void { - return updateManyMutably([update], state) - } + addManyMutably(newEntities, state) + } - function updateManyMutably( - updates: ReadonlyArray>, - state: R - ): void { - let appliedUpdates = false + function updateOneMutably(update: Update, state: R): void { + return updateManyMutably([update], state) + } - for (let update of updates) { - const entity = state.entities[update.id] - if (!entity) { - continue + function updateManyMutably( + updates: ReadonlyArray>, + state: R + ): void { + let appliedUpdates = false + + for (let update of updates) { + const entity = state.entities[update.id] + if (!entity) { + continue + } + + appliedUpdates = true + + Object.assign(entity, update.changes) + const newId = selectId(entity) + if (update.id !== newId) { + delete state.entities[update.id] + state.entities[newId] = entity + } } - appliedUpdates = true - - Object.assign(entity, update.changes) - const newId = selectId(entity) - if (update.id !== newId) { - delete state.entities[update.id] - state.entities[newId] = entity + if (appliedUpdates) { + resortEntities(state) } } - if (appliedUpdates) { - resortEntities(state) + function upsertOneMutably(entity: T, state: R): void { + return upsertManyMutably([entity], state) } - } - - function upsertOneMutably(entity: T, state: R): void { - return upsertManyMutably([entity], state) - } - function upsertManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - const [added, updated] = splitAddedUpdatedEntities( - newEntities, - selectId, - state - ) - - updateManyMutably(updated, state) - addManyMutably(added, state) - } - - function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) { - if (a.length !== b.length) { - return false + function upsertManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + const [added, updated] = splitAddedUpdatedEntities( + newEntities, + selectId, + state + ) + + updateManyMutably(updated, state) + addManyMutably(added, state) } - for (let i = 0; i < a.length && i < b.length; i++) { - if (a[i] === b[i]) { - continue + function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) { + if (a.length !== b.length) { + return false + } + + for (let i = 0; i < a.length && i < b.length; i++) { + if (a[i] === b[i]) { + continue + } + return false } - return false + return true } - return true - } - function merge(models: readonly T[], state: R): void { - // Insert/overwrite all new/updated - models.forEach((model) => { - state.entities[selectId(model)] = model - }) + function merge(models: readonly T[], state: R): void { + // Insert/overwrite all new/updated + models.forEach((model) => { + state.entities[selectId(model)] = model + }) - resortEntities(state) - } + resortEntities(state) + } - function resortEntities(state: R) { - const allEntities = Object.values(state.entities) as T[] - allEntities.sort(sort) + function resortEntities(state: R) { + const allEntities = Object.values(state.entities) as T[] + allEntities.sort(sort) - const newSortedIds = allEntities.map(selectId) - const { ids } = state + const newSortedIds = allEntities.map(selectId) + const { ids } = state - if (!areArraysEqual(ids, newSortedIds)) { - state.ids = newSortedIds + if (!areArraysEqual(ids, newSortedIds)) { + state.ids = newSortedIds + } } - } - return { - removeOne, - removeMany, - removeAll, - addOne: createStateOperator(addOneMutably), - updateOne: createStateOperator(updateOneMutably), - upsertOne: createStateOperator(upsertOneMutably), - setOne: createStateOperator(setOneMutably), - setMany: createStateOperator(setManyMutably), - setAll: createStateOperator(setAllMutably), - addMany: createStateOperator(addManyMutably), - updateMany: createStateOperator(updateManyMutably), - upsertMany: createStateOperator(upsertManyMutably), + return { + removeOne, + removeMany, + removeAll, + addOne: createStateOperator(addOneMutably), + updateOne: createStateOperator(updateOneMutably), + upsertOne: createStateOperator(upsertOneMutably), + setOne: createStateOperator(setOneMutably), + setMany: createStateOperator(setManyMutably), + setAll: createStateOperator(setAllMutably), + addMany: createStateOperator(addManyMutably), + updateMany: createStateOperator(updateManyMutably), + upsertMany: createStateOperator(upsertManyMutably), + } } } diff --git a/packages/toolkit/src/entities/state_adapter.ts b/packages/toolkit/src/entities/state_adapter.ts index 220abae40a..4c7dab3509 100644 --- a/packages/toolkit/src/entities/state_adapter.ts +++ b/packages/toolkit/src/entities/state_adapter.ts @@ -1,57 +1,75 @@ -import { produce as createNextState, isDraft } from 'immer' +import type { Draft } from 'immer' import type { EntityState, PreventAny } from './models' import type { PayloadAction } from '../createAction' import { isFSA } from '../createAction' import { IsAny } from '../tsHelpers' -export function createSingleArgumentStateOperator( - mutator: (state: EntityState) => void +export interface BuildStateOperatorConfiguration { + isDraft(value: any): boolean + createNextState: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => Base +} + +export function buildCreateSingleArgumentStateOperator( + config: BuildStateOperatorConfiguration ) { - const operator = createStateOperator((_: undefined, state: EntityState) => - mutator(state) - ) + const createStateOperator = buildCreateStateOperator(config) + return function createSingleArgumentStateOperator( + mutator: (state: EntityState) => void + ) { + const operator = createStateOperator( + (_: undefined, state: EntityState) => mutator(state) + ) - return function operation>( - state: PreventAny - ): S { - return operator(state as S, undefined) + return function operation>( + state: PreventAny + ): S { + return operator(state as S, undefined) + } } } -export function createStateOperator( - mutator: (arg: R, state: EntityState) => void -) { - return function operation>( - state: S, - arg: R | PayloadAction - ): S { - function isPayloadActionArgument( +export function buildCreateStateOperator({ + isDraft, + createNextState, +}: BuildStateOperatorConfiguration) { + return function createStateOperator( + mutator: (arg: R, state: EntityState) => void + ) { + return function operation>( + state: S, arg: R | PayloadAction - ): arg is PayloadAction { - return isFSA(arg) - } + ): S { + function isPayloadActionArgument( + arg: R | PayloadAction + ): arg is PayloadAction { + return isFSA(arg) + } - const runMutator = (draft: EntityState) => { - if (isPayloadActionArgument(arg)) { - mutator(arg.payload, draft) - } else { - mutator(arg, draft) + const runMutator = (draft: EntityState) => { + if (isPayloadActionArgument(arg)) { + mutator(arg.payload, draft) + } else { + mutator(arg, draft) + } } - } - if (isDraft(state)) { - // we must already be inside a `createNextState` call, likely because - // this is being wrapped in `createReducer` or `createSlice`. - // It's safe to just pass the draft to the mutator. - runMutator(state) + if (isDraft(state)) { + // we must already be inside a `createNextState` call, likely because + // this is being wrapped in `createReducer` or `createSlice`. + // It's safe to just pass the draft to the mutator. + runMutator(state) - // since it's a draft, we'll just return it - return state - } else { - // @ts-ignore createNextState() produces an Immutable> rather - // than an Immutable, and TypeScript cannot find out how to reconcile - // these two types. - return createNextState(state, runMutator) + // since it's a draft, we'll just return it + return state + } else { + // @ts-ignore createNextState() produces an Immutable> rather + // than an Immutable, and TypeScript cannot find out how to reconcile + // these two types. + return createNextState(state, runMutator) + } } } } diff --git a/packages/toolkit/src/entities/state_selectors.ts b/packages/toolkit/src/entities/state_selectors.ts index 46f59d3d9e..ed5cf91ff7 100644 --- a/packages/toolkit/src/entities/state_selectors.ts +++ b/packages/toolkit/src/entities/state_selectors.ts @@ -1,5 +1,6 @@ import type { Selector } from 'reselect' -import { createDraftSafeSelector } from '../createDraftSafeSelector' +import type { BuildCreateDraftSafeSelectorConfiguration } from '../createDraftSafeSelector' +import { buildCreateDraftSafeSelector } from '../createDraftSafeSelector' import type { EntityState, EntitySelectors, @@ -7,61 +8,69 @@ import type { EntityId, } from './models' -export function createSelectorsFactory() { - function getSelectors(): EntitySelectors> - function getSelectors( - selectState: (state: V) => EntityState - ): EntitySelectors - function getSelectors( - selectState?: (state: V) => EntityState - ): EntitySelectors { - const selectIds = (state: EntityState) => state.ids +export function buildCreateSelectorsFactory( + config: BuildCreateDraftSafeSelectorConfiguration +) { + return function createSelectorsFactory() { + function getSelectors(): EntitySelectors> + function getSelectors( + selectState: (state: V) => EntityState + ): EntitySelectors + function getSelectors( + selectState?: (state: V) => EntityState + ): EntitySelectors { + const createDraftSafeSelector = buildCreateDraftSafeSelector(config) + const selectIds = (state: EntityState) => state.ids - const selectEntities = (state: EntityState) => state.entities + const selectEntities = (state: EntityState) => state.entities - const selectAll = createDraftSafeSelector( - selectIds, - selectEntities, - (ids, entities): T[] => ids.map((id) => entities[id]!) - ) + const selectAll = createDraftSafeSelector( + selectIds, + selectEntities, + (ids, entities): T[] => ids.map((id) => entities[id]!) + ) - const selectId = (_: unknown, id: EntityId) => id + const selectId = (_: unknown, id: EntityId) => id - const selectById = (entities: Dictionary, id: EntityId) => entities[id] + const selectById = (entities: Dictionary, id: EntityId) => entities[id] + + const selectTotal = createDraftSafeSelector( + selectIds, + (ids) => ids.length + ) + + if (!selectState) { + return { + selectIds, + selectEntities, + selectAll, + selectTotal, + selectById: createDraftSafeSelector( + selectEntities, + selectId, + selectById + ), + } + } - const selectTotal = createDraftSafeSelector(selectIds, (ids) => ids.length) + const selectGlobalizedEntities = createDraftSafeSelector( + selectState as Selector>, + selectEntities + ) - if (!selectState) { return { - selectIds, - selectEntities, - selectAll, - selectTotal, + selectIds: createDraftSafeSelector(selectState, selectIds), + selectEntities: selectGlobalizedEntities, + selectAll: createDraftSafeSelector(selectState, selectAll), + selectTotal: createDraftSafeSelector(selectState, selectTotal), selectById: createDraftSafeSelector( - selectEntities, + selectGlobalizedEntities, selectId, selectById ), } } - const selectGlobalizedEntities = createDraftSafeSelector( - selectState as Selector>, - selectEntities - ) - - return { - selectIds: createDraftSafeSelector(selectState, selectIds), - selectEntities: selectGlobalizedEntities, - selectAll: createDraftSafeSelector(selectState, selectAll), - selectTotal: createDraftSafeSelector(selectState, selectTotal), - selectById: createDraftSafeSelector( - selectGlobalizedEntities, - selectId, - selectById - ), - } + return { getSelectors } } - - return { getSelectors } } diff --git a/packages/toolkit/src/entities/unsorted_state_adapter.ts b/packages/toolkit/src/entities/unsorted_state_adapter.ts index 9113580ba8..b0c6cad2ef 100644 --- a/packages/toolkit/src/entities/unsorted_state_adapter.ts +++ b/packages/toolkit/src/entities/unsorted_state_adapter.ts @@ -5,9 +5,10 @@ import type { Update, EntityId, } from './models' +import type { BuildStateOperatorConfiguration } from './state_adapter' import { - createStateOperator, - createSingleArgumentStateOperator, + buildCreateSingleArgumentStateOperator, + buildCreateStateOperator, } from './state_adapter' import { selectIdValue, @@ -15,184 +16,191 @@ import { splitAddedUpdatedEntities, } from './utils' -export function createUnsortedStateAdapter( - selectId: IdSelector -): EntityStateAdapter { - type R = EntityState +export function buildCreateUnsortedStateAdapter( + config: BuildStateOperatorConfiguration +) { + const createSingleArgumentStateOperator = + buildCreateSingleArgumentStateOperator(config) + const createStateOperator = buildCreateStateOperator(config) + return function createUnsortedStateAdapter( + selectId: IdSelector + ): EntityStateAdapter { + type R = EntityState - function addOneMutably(entity: T, state: R): void { - const key = selectIdValue(entity, selectId) + function addOneMutably(entity: T, state: R): void { + const key = selectIdValue(entity, selectId) - if (key in state.entities) { - return - } + if (key in state.entities) { + return + } - state.ids.push(key) - state.entities[key] = entity - } + state.ids.push(key) + state.entities[key] = entity + } - function addManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) + function addManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) - for (const entity of newEntities) { - addOneMutably(entity, state) + for (const entity of newEntities) { + addOneMutably(entity, state) + } } - } - function setOneMutably(entity: T, state: R): void { - const key = selectIdValue(entity, selectId) - if (!(key in state.entities)) { - state.ids.push(key) + function setOneMutably(entity: T, state: R): void { + const key = selectIdValue(entity, selectId) + if (!(key in state.entities)) { + state.ids.push(key) + } + state.entities[key] = entity } - state.entities[key] = entity - } - function setManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) - for (const entity of newEntities) { - setOneMutably(entity, state) + function setManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) + for (const entity of newEntities) { + setOneMutably(entity, state) + } } - } - function setAllMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - newEntities = ensureEntitiesArray(newEntities) + function setAllMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + newEntities = ensureEntitiesArray(newEntities) - state.ids = [] - state.entities = {} + state.ids = [] + state.entities = {} - addManyMutably(newEntities, state) - } + addManyMutably(newEntities, state) + } - function removeOneMutably(key: EntityId, state: R): void { - return removeManyMutably([key], state) - } + function removeOneMutably(key: EntityId, state: R): void { + return removeManyMutably([key], state) + } - function removeManyMutably(keys: readonly EntityId[], state: R): void { - let didMutate = false + function removeManyMutably(keys: readonly EntityId[], state: R): void { + let didMutate = false - keys.forEach((key) => { - if (key in state.entities) { - delete state.entities[key] - didMutate = true - } - }) + keys.forEach((key) => { + if (key in state.entities) { + delete state.entities[key] + didMutate = true + } + }) - if (didMutate) { - state.ids = state.ids.filter((id) => id in state.entities) + if (didMutate) { + state.ids = state.ids.filter((id) => id in state.entities) + } } - } - - function removeAllMutably(state: R): void { - Object.assign(state, { - ids: [], - entities: {}, - }) - } - function takeNewKey( - keys: { [id: string]: EntityId }, - update: Update, - state: R - ): boolean { - const original = state.entities[update.id] - const updated: T = Object.assign({}, original, update.changes) - const newKey = selectIdValue(updated, selectId) - const hasNewKey = newKey !== update.id - - if (hasNewKey) { - keys[update.id] = newKey - delete state.entities[update.id] + function removeAllMutably(state: R): void { + Object.assign(state, { + ids: [], + entities: {}, + }) } - state.entities[newKey] = updated + function takeNewKey( + keys: { [id: string]: EntityId }, + update: Update, + state: R + ): boolean { + const original = state.entities[update.id] + const updated: T = Object.assign({}, original, update.changes) + const newKey = selectIdValue(updated, selectId) + const hasNewKey = newKey !== update.id + + if (hasNewKey) { + keys[update.id] = newKey + delete state.entities[update.id] + } - return hasNewKey - } + state.entities[newKey] = updated - function updateOneMutably(update: Update, state: R): void { - return updateManyMutably([update], state) - } + return hasNewKey + } - function updateManyMutably( - updates: ReadonlyArray>, - state: R - ): void { - const newKeys: { [id: string]: EntityId } = {} - - const updatesPerEntity: { [id: string]: Update } = {} - - updates.forEach((update) => { - // Only apply updates to entities that currently exist - if (update.id in state.entities) { - // If there are multiple updates to one entity, merge them together - updatesPerEntity[update.id] = { - id: update.id, - // Spreads ignore falsy values, so this works even if there isn't - // an existing update already at this key - changes: { - ...(updatesPerEntity[update.id] - ? updatesPerEntity[update.id].changes - : null), - ...update.changes, - }, + function updateOneMutably(update: Update, state: R): void { + return updateManyMutably([update], state) + } + + function updateManyMutably( + updates: ReadonlyArray>, + state: R + ): void { + const newKeys: { [id: string]: EntityId } = {} + + const updatesPerEntity: { [id: string]: Update } = {} + + updates.forEach((update) => { + // Only apply updates to entities that currently exist + if (update.id in state.entities) { + // If there are multiple updates to one entity, merge them together + updatesPerEntity[update.id] = { + id: update.id, + // Spreads ignore falsy values, so this works even if there isn't + // an existing update already at this key + changes: { + ...(updatesPerEntity[update.id] + ? updatesPerEntity[update.id].changes + : null), + ...update.changes, + }, + } } - } - }) + }) - updates = Object.values(updatesPerEntity) + updates = Object.values(updatesPerEntity) - const didMutateEntities = updates.length > 0 + const didMutateEntities = updates.length > 0 - if (didMutateEntities) { - const didMutateIds = - updates.filter((update) => takeNewKey(newKeys, update, state)).length > - 0 + if (didMutateEntities) { + const didMutateIds = + updates.filter((update) => takeNewKey(newKeys, update, state)) + .length > 0 - if (didMutateIds) { - state.ids = Object.keys(state.entities) + if (didMutateIds) { + state.ids = Object.keys(state.entities) + } } } - } - function upsertOneMutably(entity: T, state: R): void { - return upsertManyMutably([entity], state) - } + function upsertOneMutably(entity: T, state: R): void { + return upsertManyMutably([entity], state) + } - function upsertManyMutably( - newEntities: readonly T[] | Record, - state: R - ): void { - const [added, updated] = splitAddedUpdatedEntities( - newEntities, - selectId, - state - ) - - updateManyMutably(updated, state) - addManyMutably(added, state) - } + function upsertManyMutably( + newEntities: readonly T[] | Record, + state: R + ): void { + const [added, updated] = splitAddedUpdatedEntities( + newEntities, + selectId, + state + ) + + updateManyMutably(updated, state) + addManyMutably(added, state) + } - return { - removeAll: createSingleArgumentStateOperator(removeAllMutably), - addOne: createStateOperator(addOneMutably), - addMany: createStateOperator(addManyMutably), - setOne: createStateOperator(setOneMutably), - setMany: createStateOperator(setManyMutably), - setAll: createStateOperator(setAllMutably), - updateOne: createStateOperator(updateOneMutably), - updateMany: createStateOperator(updateManyMutably), - upsertOne: createStateOperator(upsertOneMutably), - upsertMany: createStateOperator(upsertManyMutably), - removeOne: createStateOperator(removeOneMutably), - removeMany: createStateOperator(removeManyMutably), + return { + removeAll: createSingleArgumentStateOperator(removeAllMutably), + addOne: createStateOperator(addOneMutably), + addMany: createStateOperator(addManyMutably), + setOne: createStateOperator(setOneMutably), + setMany: createStateOperator(setManyMutably), + setAll: createStateOperator(setAllMutably), + updateOne: createStateOperator(updateOneMutably), + updateMany: createStateOperator(updateManyMutably), + upsertOne: createStateOperator(upsertOneMutably), + upsertMany: createStateOperator(upsertManyMutably), + removeOne: createStateOperator(removeOneMutably), + removeMany: createStateOperator(removeManyMutably), + } } } From 83a8931c92492973b120e5b8dc7c486b07a657a6 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 4 Apr 2023 23:55:27 +0100 Subject: [PATCH 05/21] unused type --- packages/toolkit/src/entities/create_adapter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index e3a98e1f44..e8f18bb81e 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -1,4 +1,3 @@ -import type { Draft } from 'immer' import { isDraft, current, produce as createNextState } from 'immer' import type { EntityDefinition, From 411568adef6e28a893dba2d4772a44b935df7737 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 00:16:59 +0100 Subject: [PATCH 06/21] declare immutable helper types once --- packages/toolkit/src/createDraftSafeSelector.ts | 9 +++++---- packages/toolkit/src/createReducer.ts | 14 +++++--------- packages/toolkit/src/entities/state_adapter.ts | 13 +++++-------- packages/toolkit/src/tsHelpers.ts | 12 ++++++++++++ 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/toolkit/src/createDraftSafeSelector.ts b/packages/toolkit/src/createDraftSafeSelector.ts index cef33bdc71..c5577f7faa 100644 --- a/packages/toolkit/src/createDraftSafeSelector.ts +++ b/packages/toolkit/src/createDraftSafeSelector.ts @@ -1,10 +1,11 @@ import { current, isDraft } from 'immer' import { createSelector } from 'reselect' +import type { ImmutableHelpers } from './tsHelpers' -export interface BuildCreateDraftSafeSelectorConfiguration { - isDraft(value: any): boolean - current(value: T): T -} +export type BuildCreateDraftSafeSelectorConfiguration = Pick< + ImmutableHelpers, + 'isDraft' | 'current' +> /** * "Draft-Safe" version of `reselect`'s `createSelector`: diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index 3690f274f7..9d17069a88 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -3,7 +3,7 @@ import { produce as createNextState, isDraft, isDraftable } from 'immer' import type { AnyAction, Action, Reducer } from 'redux' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' -import type { NoInfer } from './tsHelpers' +import type { ImmutableHelpers, NoInfer } from './tsHelpers' import { makeFreezeDraftable } from './utils' /** @@ -153,14 +153,10 @@ const reducer = createReducer( ): ReducerWithInitialState } -export interface BuildCreateReducerConfiguration { - createNextState: ( - base: Base, - recipe: (draft: Draft) => void | Base | Draft - ) => Base - isDraft(value: any): boolean - isDraftable(value: any): boolean -} +export type BuildCreateReducerConfiguration = Pick< + ImmutableHelpers, + 'createNextState' | 'isDraft' | 'isDraftable' +> export function buildCreateReducer({ createNextState, diff --git a/packages/toolkit/src/entities/state_adapter.ts b/packages/toolkit/src/entities/state_adapter.ts index 4c7dab3509..d65f35b68a 100644 --- a/packages/toolkit/src/entities/state_adapter.ts +++ b/packages/toolkit/src/entities/state_adapter.ts @@ -1,16 +1,13 @@ -import type { Draft } from 'immer' import type { EntityState, PreventAny } from './models' import type { PayloadAction } from '../createAction' import { isFSA } from '../createAction' +import type { ImmutableHelpers } from '../tsHelpers' import { IsAny } from '../tsHelpers' -export interface BuildStateOperatorConfiguration { - isDraft(value: any): boolean - createNextState: ( - base: Base, - recipe: (draft: Draft) => void | Base | Draft - ) => Base -} +export type BuildStateOperatorConfiguration = Pick< + ImmutableHelpers, + 'isDraft' | 'createNextState' +> export function buildCreateSingleArgumentStateOperator( config: BuildStateOperatorConfiguration diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 3077037731..922ba511a2 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -1,5 +1,17 @@ import type { Middleware, StoreEnhancer } from 'redux' import type { MiddlewareArray } from './utils' +import type { Draft } from 'immer' + +export interface ImmutableHelpers { + createNextState: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => Base + isDraft(value: any): boolean + isDraftable(value: any): boolean + original(value: T): T | undefined + current(value: T): T +} /** * return True if T is `any`, otherwise return False From d9e134175c523a643b887a95ff7f613afe2ee156 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 00:26:06 +0100 Subject: [PATCH 07/21] build createDraftSafeSelector outside of createSelectorsFactory --- packages/toolkit/src/entities/state_selectors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/src/entities/state_selectors.ts b/packages/toolkit/src/entities/state_selectors.ts index ed5cf91ff7..c7018e4a47 100644 --- a/packages/toolkit/src/entities/state_selectors.ts +++ b/packages/toolkit/src/entities/state_selectors.ts @@ -11,6 +11,8 @@ import type { export function buildCreateSelectorsFactory( config: BuildCreateDraftSafeSelectorConfiguration ) { + const createDraftSafeSelector = buildCreateDraftSafeSelector(config) + return function createSelectorsFactory() { function getSelectors(): EntitySelectors> function getSelectors( @@ -19,7 +21,6 @@ export function buildCreateSelectorsFactory( function getSelectors( selectState?: (state: V) => EntityState ): EntitySelectors { - const createDraftSafeSelector = buildCreateDraftSafeSelector(config) const selectIds = (state: EntityState) => state.ids const selectEntities = (state: EntityState) => state.entities From e5f48f116283dc6305b75ddf6ec5f5a39f90df27 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 10:06:11 +0100 Subject: [PATCH 08/21] allow replacing RTKQ's usage of immer --- .../core/buildMiddleware/batchActions.ts | 12 +++++--- .../src/query/core/buildMiddleware/index.ts | 3 +- .../src/query/core/buildMiddleware/types.ts | 2 ++ packages/toolkit/src/query/core/buildSlice.ts | 4 ++- .../toolkit/src/query/core/buildThunks.ts | 7 +++-- packages/toolkit/src/query/core/module.ts | 29 +++++++++++++++++-- packages/toolkit/src/query/tsHelpers.ts | 19 ++++++++++++ packages/toolkit/src/tsHelpers.ts | 8 ++++- 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts b/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts index 549ade7084..fb76288cef 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/batchActions.ts @@ -5,8 +5,7 @@ import type { QuerySubstateIdentifier, Subscribers, } from '../apiState' -import { produceWithPatches } from 'immer' -import type { AnyAction } from '@reduxjs/toolkit'; +import type { AnyAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit' // Copied from https://github.com/feross/queue-microtask @@ -30,7 +29,12 @@ const queueMicrotaskShim = export const buildBatchedActionsHandler: InternalHandlerBuilder< [actionShouldContinue: boolean, subscriptionExists: boolean] -> = ({ api, queryThunk, internalState }) => { +> = ({ + api, + queryThunk, + internalState, + immutableHelpers: { createWithPatches }, +}) => { const subscriptionsPrefix = `${api.reducerPath}/subscriptions` let previousSubscriptions: SubscriptionState = @@ -125,7 +129,7 @@ export const buildBatchedActionsHandler: InternalHandlerBuilder< JSON.stringify(internalState.currentSubscriptions) ) // Figure out a smaller diff between original and current - const [, patches] = produceWithPatches( + const [, patches] = createWithPatches( previousSubscriptions, () => newSubscriptions ) diff --git a/packages/toolkit/src/query/core/buildMiddleware/index.ts b/packages/toolkit/src/query/core/buildMiddleware/index.ts index 810839e333..f27304c9cb 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/index.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/index.ts @@ -12,6 +12,7 @@ import { buildInvalidationByTagsHandler } from './invalidationByTags' import { buildPollingHandler } from './polling' import type { BuildMiddlewareInput, + BuildSubMiddlewareInput, InternalHandlerBuilder, InternalMiddlewareState, } from './types' @@ -63,7 +64,7 @@ export function buildMiddleware< currentSubscriptions: {}, } - const builderArgs = { + const builderArgs: BuildSubMiddlewareInput = { ...(input as any as BuildMiddlewareInput< EndpointDefinitions, string, diff --git a/packages/toolkit/src/query/core/buildMiddleware/types.ts b/packages/toolkit/src/query/core/buildMiddleware/types.ts index 20e23a4ac8..27e8955935 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/types.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/types.ts @@ -24,6 +24,7 @@ import type { QueryThunkArg, ThunkResult, } from '../buildThunks' +import type { ImmutableHelpers } from '../../tsHelpers' export type QueryStateMeta = Record export type TimeoutId = ReturnType @@ -43,6 +44,7 @@ export interface BuildMiddlewareInput< mutationThunk: MutationThunk api: Api assertTagType: AssertTagTypes + immutableHelpers: Pick } export type SubMiddlewareApi = MiddlewareAPI< diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 3343b6dc3d..c692e19814 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -32,7 +32,6 @@ import type { QueryDefinition, } from '../endpointDefinitions' import type { Patch } from 'immer' -import { applyPatches } from 'immer' import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners' import { isDocumentVisible, @@ -41,6 +40,7 @@ import { } from '../utils' import type { ApiContext } from '../apiTypes' import { isUpsertQuery } from './buildInitiate' +import type { ImmutableHelpers } from '../tsHelpers' function updateQuerySubstateIfExists( state: QueryState, @@ -99,6 +99,7 @@ export function buildSlice({ }, assertTagType, config, + immutableHelpers: { applyPatches }, }: { reducerPath: string queryThunk: QueryThunk @@ -109,6 +110,7 @@ export function buildSlice({ ConfigState, 'online' | 'focused' | 'middlewareRegistered' > + immutableHelpers: Pick }) { const resetApiState = createAction(`${reducerPath}/resetApiState`) const querySlice = createSlice({ diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 458b9edd44..97052c254e 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -32,7 +32,6 @@ import { isRejectedWithValue, } from '@reduxjs/toolkit' import type { Patch } from 'immer' -import { isDraftable, produceWithPatches } from 'immer' import type { AnyAction, ThunkAction, @@ -44,7 +43,7 @@ import { createAsyncThunk, SHOULD_AUTOBATCH } from '@reduxjs/toolkit' import { HandledError } from '../HandledError' import type { ApiEndpointQuery, PrefetchOptions } from './module' -import type { UnwrapPromise } from '../tsHelpers' +import type { ImmutableHelpers, UnwrapPromise } from '../tsHelpers' declare module './module' { export interface ApiEndpointQuery< @@ -222,12 +221,14 @@ export function buildThunks< context: { endpointDefinitions }, serializeQueryArgs, api, + immutableHelpers: { isDraftable, createWithPatches }, }: { baseQuery: BaseQuery reducerPath: ReducerPath context: ApiContext serializeQueryArgs: InternalSerializeQueryArgs api: Api + immutableHelpers: Pick }) { type State = RootState @@ -264,7 +265,7 @@ export function buildThunks< } if ('data' in currentState) { if (isDraftable(currentState.data)) { - const [, patches, inversePatches] = produceWithPatches( + const [, patches, inversePatches] = createWithPatches( currentState.data, updateRecipe ) diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 2cb9ac76e3..53c1771d74 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -40,6 +40,7 @@ import type { QueryActionCreatorResult, } from './buildInitiate' import { buildInitiate } from './buildInitiate' +import type { ImmutableHelpers } from '../tsHelpers' import { assertCast, safeAssign } from '../tsHelpers' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import type { SliceActions } from './buildSlice' @@ -48,7 +49,12 @@ import type { BaseQueryFn } from '../baseQueryTypes' import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle' import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle' import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection' -import { enablePatches } from 'immer' +import { + applyPatches, + enablePatches, + isDraftable, + produceWithPatches, +} from 'immer' /** * `ifOlderThan` - (default: `false` | `number`) - _number is value in seconds_ @@ -71,7 +77,8 @@ export type CoreModule = | ReferenceQueryLifecycle | ReferenceCacheCollection -export interface ThunkWithReturnValue extends ThunkAction {} +export interface ThunkWithReturnValue + extends ThunkAction {} declare module '../apiTypes' { export interface ApiModules< @@ -450,6 +457,13 @@ export type ListenerActions = { export type InternalActions = SliceActions & ListenerActions +interface CoreModuleOptions { + immutableHelpers?: Pick< + ImmutableHelpers, + 'createWithPatches' | 'applyPatches' | 'isDraftable' + > +} + /** * Creates a module containing the basic redux logic for use with `buildCreateApi`. * @@ -458,7 +472,13 @@ export type InternalActions = SliceActions & ListenerActions * const createBaseApi = buildCreateApi(coreModule()); * ``` */ -export const coreModule = (): Module => ({ +export const coreModule = ({ + immutableHelpers = { + createWithPatches: produceWithPatches, + applyPatches, + isDraftable, + }, +}: CoreModuleOptions = {}): Module => ({ name: coreModuleName, init( api, @@ -518,6 +538,7 @@ export const coreModule = (): Module => ({ context, api, serializeQueryArgs, + immutableHelpers, }) const { reducer, actions: sliceActions } = buildSlice({ @@ -533,6 +554,7 @@ export const coreModule = (): Module => ({ keepUnusedDataFor, reducerPath, }, + immutableHelpers, }) safeAssign(api.util, { @@ -551,6 +573,7 @@ export const coreModule = (): Module => ({ mutationThunk, api, assertTagType, + immutableHelpers, }) safeAssign(api.util, middlewareActions) diff --git a/packages/toolkit/src/query/tsHelpers.ts b/packages/toolkit/src/query/tsHelpers.ts index af79f5bd71..656c8b390e 100644 --- a/packages/toolkit/src/query/tsHelpers.ts +++ b/packages/toolkit/src/query/tsHelpers.ts @@ -1,3 +1,22 @@ +import type { Draft, Patch, applyPatches } from 'immer' + +export interface ImmutableHelpers { + createNextState: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => Base + createWithPatches: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => readonly [Base, Patch[], Patch[]] + // depends on an Objectish type that immer doesn't export + applyPatches: typeof applyPatches + isDraft(value: any): boolean + isDraftable(value: any): boolean + original(value: T): T | undefined + current(value: T): T +} + export type Id = { [K in keyof T]: T[K] } & {} export type WithRequiredProp = Omit & Required> diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 922ba511a2..67853c85bb 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -1,12 +1,18 @@ import type { Middleware, StoreEnhancer } from 'redux' +import type { Draft, Patch, applyPatches } from 'immer' import type { MiddlewareArray } from './utils' -import type { Draft } from 'immer' export interface ImmutableHelpers { createNextState: ( base: Base, recipe: (draft: Draft) => void | Base | Draft ) => Base + createWithPatches: ( + base: Base, + recipe: (draft: Draft) => void | Base | Draft + ) => readonly [Base, Patch[], Patch[]] + // depends on an Objectish type that immer doesn't export + applyPatches: typeof applyPatches isDraft(value: any): boolean isDraftable(value: any): boolean original(value: T): T | undefined From 4f6c897201b44a5e7428ea00867a4531a50e44df Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 10:30:30 +0100 Subject: [PATCH 09/21] use immutablehelpers for RTKQ's createSlice --- packages/toolkit/src/query/core/buildSlice.ts | 17 ++++++++++++----- packages/toolkit/src/query/core/module.ts | 12 +++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index c692e19814..ad552e20dd 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -1,13 +1,16 @@ -import type { AnyAction, PayloadAction } from '@reduxjs/toolkit' +import type { + AnyAction, + PayloadAction, + BuildCreateSliceConfiguration, +} from '@reduxjs/toolkit' import { combineReducers, createAction, - createSlice, isAnyOf, isFulfilled, isRejectedWithValue, - createNextState, prepareAutoBatched, + buildCreateSlice, } from '@reduxjs/toolkit' import type { CombinedState as CombinedQueryState, @@ -99,7 +102,8 @@ export function buildSlice({ }, assertTagType, config, - immutableHelpers: { applyPatches }, + immutableHelpers, + immutableHelpers: { applyPatches, createNextState }, }: { reducerPath: string queryThunk: QueryThunk @@ -110,8 +114,11 @@ export function buildSlice({ ConfigState, 'online' | 'focused' | 'middlewareRegistered' > - immutableHelpers: Pick + immutableHelpers: Pick & + BuildCreateSliceConfiguration }) { + const createSlice = buildCreateSlice(immutableHelpers) + const resetApiState = createAction(`${reducerPath}/resetApiState`) const querySlice = createSlice({ name: `${reducerPath}/queries`, diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 53c1771d74..de7d1d3767 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -10,6 +10,7 @@ import { buildThunks } from './buildThunks' import type { ActionCreatorWithPayload, AnyAction, + BuildCreateSliceConfiguration, Middleware, Reducer, ThunkAction, @@ -49,9 +50,10 @@ import type { BaseQueryFn } from '../baseQueryTypes' import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle' import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle' import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection' -import { +import produce, { applyPatches, enablePatches, + isDraft, isDraftable, produceWithPatches, } from 'immer' @@ -458,10 +460,8 @@ export type ListenerActions = { export type InternalActions = SliceActions & ListenerActions interface CoreModuleOptions { - immutableHelpers?: Pick< - ImmutableHelpers, - 'createWithPatches' | 'applyPatches' | 'isDraftable' - > + immutableHelpers?: BuildCreateSliceConfiguration & + Pick } /** @@ -474,8 +474,10 @@ interface CoreModuleOptions { */ export const coreModule = ({ immutableHelpers = { + createNextState: produce, createWithPatches: produceWithPatches, applyPatches, + isDraft, isDraftable, }, }: CoreModuleOptions = {}): Module => ({ From 28a87c99f291ac6a61d697d80e25f6c1e74e2477 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 10:35:33 +0100 Subject: [PATCH 10/21] named export instead of default --- packages/toolkit/src/query/core/module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index de7d1d3767..d221185166 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -50,7 +50,8 @@ import type { BaseQueryFn } from '../baseQueryTypes' import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle' import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle' import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection' -import produce, { +import { + produce, applyPatches, enablePatches, isDraft, From 180d19805dc5aabdcb1ae04cfae773e26922d422 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 11:03:49 +0100 Subject: [PATCH 11/21] use specific freeze function instead of abusing createNextState --- packages/toolkit/src/createReducer.ts | 16 +++++------ packages/toolkit/src/createSlice.ts | 8 +++--- .../toolkit/src/query/core/buildSelectors.ts | 27 +++++++++---------- packages/toolkit/src/query/core/module.ts | 8 +++++- packages/toolkit/src/query/tsHelpers.ts | 1 + packages/toolkit/src/tsHelpers.ts | 1 + 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index 9d17069a88..07b11dcc8c 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -1,10 +1,9 @@ import type { Draft } from 'immer' -import { produce as createNextState, isDraft, isDraftable } from 'immer' +import { produce as createNextState, isDraft, freeze, isDraftable } from 'immer' import type { AnyAction, Action, Reducer } from 'redux' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { ImmutableHelpers, NoInfer } from './tsHelpers' -import { makeFreezeDraftable } from './utils' /** * Defines a mapping from action types to corresponding action object shapes. @@ -155,19 +154,15 @@ const reducer = createReducer( export type BuildCreateReducerConfiguration = Pick< ImmutableHelpers, - 'createNextState' | 'isDraft' | 'isDraftable' + 'createNextState' | 'isDraft' | 'isDraftable' | 'freeze' > export function buildCreateReducer({ createNextState, isDraft, isDraftable, + freeze, }: BuildCreateReducerConfiguration): CreateReducer { - const freezeDraftable = makeFreezeDraftable({ - createNextState, - isDraft, - isDraftable, - }) return function createReducer>( initialState: S | (() => S), mapOrBuilderCallback: (builder: ActionReducerMapBuilder) => void @@ -186,9 +181,9 @@ export function buildCreateReducer({ // Ensure the initial state gets frozen either way (if draftable) let getInitialState: () => S if (isStateFunction(initialState)) { - getInitialState = () => freezeDraftable(initialState()) + getInitialState = () => freeze(initialState(), true) } else { - const frozenInitialState = freezeDraftable(initialState) + const frozenInitialState = freeze(initialState, true) getInitialState = () => frozenInitialState } @@ -256,4 +251,5 @@ export const createReducer = buildCreateReducer({ createNextState, isDraft, isDraftable, + freeze, }) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index dae0115d0a..6dcdb220ad 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,5 +1,5 @@ import type { Reducer } from 'redux' -import { produce as createNextState, isDraft, isDraftable } from 'immer' +import { produce as createNextState, freeze, isDraft, isDraftable } from 'immer' import type { ActionCreatorWithoutPayload, PayloadAction, @@ -18,7 +18,6 @@ import { buildCreateReducer } from './createReducer' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { NoInfer } from './tsHelpers' -import { makeFreezeDraftable } from './utils' let hasWarnedAboutObjectNotation = false @@ -286,7 +285,7 @@ export function buildCreateSlice( configuration: BuildCreateSliceConfiguration ): CreateSlice { const createReducer = buildCreateReducer(configuration) - const freezeDraftable = makeFreezeDraftable(configuration) + const { freeze } = configuration return function createSlice< State, @@ -314,7 +313,7 @@ export function buildCreateSlice( const initialState = typeof options.initialState == 'function' ? options.initialState - : freezeDraftable(options.initialState) + : freeze(options.initialState) const reducers = options.reducers || {} @@ -401,4 +400,5 @@ export const createSlice = buildCreateSlice({ createNextState, isDraft, isDraftable, + freeze, }) diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index a49da366c5..6155e227fa 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -20,6 +20,7 @@ import { expandTagDescription } from '../endpointDefinitions' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import { getMutationCacheKey } from './buildSlice' import { flatten } from '../utils' +import type { ImmutableHelpers } from '../tsHelpers' export type SkipToken = typeof skipToken /** @@ -103,30 +104,28 @@ export type MutationResultSelectorResult< Definition extends MutationDefinition > = MutationSubState & RequestStatusFlags -const initialSubState: QuerySubState = { - status: QueryStatus.uninitialized as const, -} - -// abuse immer to freeze default states -const defaultQuerySubState = /* @__PURE__ */ createNextState( - initialSubState, - () => {} -) -const defaultMutationSubState = /* @__PURE__ */ createNextState( - initialSubState as MutationSubState, - () => {} -) - export function buildSelectors< Definitions extends EndpointDefinitions, ReducerPath extends string >({ serializeQueryArgs, reducerPath, + immutableHelpers: { freeze }, }: { serializeQueryArgs: InternalSerializeQueryArgs reducerPath: ReducerPath + immutableHelpers: Pick }) { + const initialSubState: QuerySubState = { + status: QueryStatus.uninitialized as const, + } + + const defaultQuerySubState = freeze(initialSubState, true) + const defaultMutationSubState = freeze( + initialSubState as MutationSubState, + true + ) + type RootState = _RootState const selectSkippedQuery = (state: RootState) => defaultQuerySubState diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index d221185166..4ca8509456 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -57,6 +57,7 @@ import { isDraft, isDraftable, produceWithPatches, + freeze, } from 'immer' /** @@ -462,7 +463,10 @@ export type InternalActions = SliceActions & ListenerActions interface CoreModuleOptions { immutableHelpers?: BuildCreateSliceConfiguration & - Pick + Pick< + ImmutableHelpers, + 'createWithPatches' | 'applyPatches' | 'isDraftable' | 'freeze' + > } /** @@ -480,6 +484,7 @@ export const coreModule = ({ applyPatches, isDraft, isDraftable, + freeze, }, }: CoreModuleOptions = {}): Module => ({ name: coreModuleName, @@ -586,6 +591,7 @@ export const coreModule = ({ buildSelectors({ serializeQueryArgs: serializeQueryArgs as any, reducerPath, + immutableHelpers, }) safeAssign(api.util, { selectInvalidatedBy }) diff --git a/packages/toolkit/src/query/tsHelpers.ts b/packages/toolkit/src/query/tsHelpers.ts index 656c8b390e..cfb8b07d0c 100644 --- a/packages/toolkit/src/query/tsHelpers.ts +++ b/packages/toolkit/src/query/tsHelpers.ts @@ -15,6 +15,7 @@ export interface ImmutableHelpers { isDraftable(value: any): boolean original(value: T): T | undefined current(value: T): T + freeze(obj: T, deep?: boolean): T } export type Id = { [K in keyof T]: T[K] } & {} diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 67853c85bb..66003dfe5e 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -17,6 +17,7 @@ export interface ImmutableHelpers { isDraftable(value: any): boolean original(value: T): T | undefined current(value: T): T + freeze(obj: T, deep?: boolean): T } /** From 5bd963c152478f10a5df94e1e319f79db3b02db7 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 11:35:15 +0100 Subject: [PATCH 12/21] export buildCreateEntityAdapter --- packages/toolkit/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 4e55413d35..b849861eaf 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -108,7 +108,10 @@ export type { } from './mapBuilders' export { MiddlewareArray } from './utils' -export { createEntityAdapter } from './entities/create_adapter' +export { + buildCreateEntityAdapter, + createEntityAdapter, +} from './entities/create_adapter' export type { Dictionary, EntityState, From 9f9dfffa5baa2312055ac69f27e4271c870ee295 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 11:57:50 +0100 Subject: [PATCH 13/21] export ImmutableHelpers --- packages/toolkit/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index b849861eaf..b11f80c805 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -197,3 +197,5 @@ export { autoBatchEnhancer, } from './autoBatchEnhancer' export type { AutoBatchOptions } from './autoBatchEnhancer' + +export type { ImmutableHelpers } from './tsHelpers' From 19d5b8de65f9c53616e47b3ecd14059d47ba36ab Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 21:06:43 +0100 Subject: [PATCH 14/21] create a defineImmutableHelpers identity function, and use for immer --- .../toolkit/src/createDraftSafeSelector.ts | 8 ++--- packages/toolkit/src/createReducer.ts | 9 ++--- packages/toolkit/src/createSlice.ts | 9 ++--- .../toolkit/src/entities/create_adapter.ts | 8 ++--- packages/toolkit/src/immer.ts | 22 ++++++++++++ packages/toolkit/src/index.ts | 2 ++ packages/toolkit/src/query/core/module.ts | 20 ++--------- packages/toolkit/src/tsHelpers.ts | 35 ++++++++++++++++++- 8 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 packages/toolkit/src/immer.ts diff --git a/packages/toolkit/src/createDraftSafeSelector.ts b/packages/toolkit/src/createDraftSafeSelector.ts index c5577f7faa..047f00461d 100644 --- a/packages/toolkit/src/createDraftSafeSelector.ts +++ b/packages/toolkit/src/createDraftSafeSelector.ts @@ -1,6 +1,6 @@ -import { current, isDraft } from 'immer' import { createSelector } from 'reselect' import type { ImmutableHelpers } from './tsHelpers' +import { immutableHelpers } from './immer' export type BuildCreateDraftSafeSelectorConfiguration = Pick< ImmutableHelpers, @@ -28,7 +28,5 @@ export function buildCreateDraftSafeSelector({ } } -export const createDraftSafeSelector = buildCreateDraftSafeSelector({ - isDraft, - current, -}) +export const createDraftSafeSelector = + buildCreateDraftSafeSelector(immutableHelpers) diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index 07b11dcc8c..5feb0c01d4 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -1,9 +1,9 @@ import type { Draft } from 'immer' -import { produce as createNextState, isDraft, freeze, isDraftable } from 'immer' import type { AnyAction, Action, Reducer } from 'redux' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { ImmutableHelpers, NoInfer } from './tsHelpers' +import { immutableHelpers } from './immer' /** * Defines a mapping from action types to corresponding action object shapes. @@ -247,9 +247,4 @@ export function buildCreateReducer({ } } -export const createReducer = buildCreateReducer({ - createNextState, - isDraft, - isDraftable, - freeze, -}) +export const createReducer = buildCreateReducer(immutableHelpers) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index 0920b99745..3e2f9902c8 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,5 +1,4 @@ import type { Reducer } from 'redux' -import { produce as createNextState, freeze, isDraft, isDraftable } from 'immer' import type { ActionCreatorWithoutPayload, PayloadAction, @@ -18,6 +17,7 @@ import { buildCreateReducer } from './createReducer' import type { ActionReducerMapBuilder } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { NoInfer } from './tsHelpers' +import { immutableHelpers } from './immer' let hasWarnedAboutObjectNotation = false @@ -393,9 +393,4 @@ export function buildCreateSlice( } } -export const createSlice = buildCreateSlice({ - createNextState, - isDraft, - isDraftable, - freeze, -}) +export const createSlice = buildCreateSlice(immutableHelpers) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index e8f18bb81e..d110c274af 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -1,4 +1,3 @@ -import { isDraft, current, produce as createNextState } from 'immer' import type { EntityDefinition, Comparer, @@ -11,6 +10,7 @@ import { buildCreateSortedStateAdapter } from './sorted_state_adapter' import { buildCreateUnsortedStateAdapter } from './unsorted_state_adapter' import type { BuildCreateDraftSafeSelectorConfiguration } from '..' import type { BuildStateOperatorConfiguration } from './state_adapter' +import { immutableHelpers } from '@internal/immer' export interface BuildCreateEntityAdapterConfiguration extends BuildCreateDraftSafeSelectorConfiguration, @@ -57,8 +57,4 @@ export function buildCreateEntityAdapter( } } -export const createEntityAdapter = buildCreateEntityAdapter({ - isDraft, - current, - createNextState, -}) +export const createEntityAdapter = buildCreateEntityAdapter(immutableHelpers) diff --git a/packages/toolkit/src/immer.ts b/packages/toolkit/src/immer.ts new file mode 100644 index 0000000000..81a87da99a --- /dev/null +++ b/packages/toolkit/src/immer.ts @@ -0,0 +1,22 @@ +import { + applyPatches, + current, + freeze, + isDraft, + isDraftable, + original, + produce, + produceWithPatches, +} from 'immer' +import { defineImmutableHelpers } from './tsHelpers' + +export const immutableHelpers = defineImmutableHelpers({ + createNextState: produce, + createWithPatches: produceWithPatches, + applyPatches, + isDraft, + isDraftable, + original, + current, + freeze, +}) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index b11f80c805..04978139c8 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -199,3 +199,5 @@ export { export type { AutoBatchOptions } from './autoBatchEnhancer' export type { ImmutableHelpers } from './tsHelpers' +export { defineImmutableHelpers } from './tsHelpers' +export { immutableHelpers as immerImmutableHelpers } from './immer' diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 4ca8509456..fa7d6064f1 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -16,6 +16,7 @@ import type { ThunkAction, ThunkDispatch, } from '@reduxjs/toolkit' +import { immerImmutableHelpers } from '@reduxjs/toolkit' import type { EndpointDefinitions, QueryArgFrom, @@ -50,15 +51,7 @@ import type { BaseQueryFn } from '../baseQueryTypes' import type { ReferenceCacheLifecycle } from './buildMiddleware/cacheLifecycle' import type { ReferenceQueryLifecycle } from './buildMiddleware/queryLifecycle' import type { ReferenceCacheCollection } from './buildMiddleware/cacheCollection' -import { - produce, - applyPatches, - enablePatches, - isDraft, - isDraftable, - produceWithPatches, - freeze, -} from 'immer' +import { enablePatches } from 'immer' /** * `ifOlderThan` - (default: `false` | `number`) - _number is value in seconds_ @@ -478,14 +471,7 @@ interface CoreModuleOptions { * ``` */ export const coreModule = ({ - immutableHelpers = { - createNextState: produce, - createWithPatches: produceWithPatches, - applyPatches, - isDraft, - isDraftable, - freeze, - }, + immutableHelpers = immerImmutableHelpers, }: CoreModuleOptions = {}): Module => ({ name: coreModuleName, init( diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 66003dfe5e..40c44904bd 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -3,23 +3,56 @@ import type { Draft, Patch, applyPatches } from 'immer' import type { MiddlewareArray } from './utils' export interface ImmutableHelpers { + /** + * Function that receives a base object, and a recipe which is called with a draft that the recipe is allowed to mutate. + * The recipe can return a new state which will replace the existing state, or it can not return (in which case the existing draft is used) + * Returns an immutably modified version of the input object. + */ createNextState: ( base: Base, recipe: (draft: Draft) => void | Base | Draft ) => Base + /** + * Function that receives a base object, and a recipe which is called with a draft that the recipe is allowed to mutate. + * The recipe can return a new state which will replace the existing state, or it can not return (in which case the existing draft is used) + * Returns a tuple of an immutably modified version of the input object, an array of patches describing the changes made, and an array of inverse patches. + */ createWithPatches: ( base: Base, recipe: (draft: Draft) => void | Base | Draft ) => readonly [Base, Patch[], Patch[]] - // depends on an Objectish type that immer doesn't export + /** + * Receives a base object and an array of patches describing changes to apply. + * Returns an immutably modified version of the base object with changes applied. + */ applyPatches: typeof applyPatches + /** + * Indicates whether the value passed is a draft, meaning it's safe to mutate. + */ isDraft(value: any): boolean + /** + * Indicates whether the value passed is possible to turn into a mutable draft. + */ isDraftable(value: any): boolean + /** + * Receives a draft and returns its base object. + */ original(value: T): T | undefined + /** + * Receives a draft and returns an object with any changes to date immutably applied. + */ current(value: T): T + /** + * Receives an object and freezes it, causing runtime errors if mutation is attempted after. + */ freeze(obj: T, deep?: boolean): T } +/** + * Define a config object indicating utilities for RTK packages to use for immutable operations. + */ +export const defineImmutableHelpers = (helpers: ImmutableHelpers) => helpers + /** * return True if T is `any`, otherwise return False * taken from https://github.com/joonhocho/tsdef From 9614298a6d6a45a10b0b4198f26c778b2a46e30a Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 21:09:13 +0100 Subject: [PATCH 15/21] don't use @internal import --- packages/toolkit/src/entities/create_adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index d110c274af..0a954c8df7 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -10,7 +10,7 @@ import { buildCreateSortedStateAdapter } from './sorted_state_adapter' import { buildCreateUnsortedStateAdapter } from './unsorted_state_adapter' import type { BuildCreateDraftSafeSelectorConfiguration } from '..' import type { BuildStateOperatorConfiguration } from './state_adapter' -import { immutableHelpers } from '@internal/immer' +import { immutableHelpers } from '../immer' export interface BuildCreateEntityAdapterConfiguration extends BuildCreateDraftSafeSelectorConfiguration, From 7eb1fa04e18239f6556d85cc2702cc2ce33ff822 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Wed, 5 Apr 2023 22:56:57 +0100 Subject: [PATCH 16/21] import ImmutableHelpers type from RTK --- .../src/query/core/buildMiddleware/types.ts | 2 +- .../toolkit/src/query/core/buildSelectors.ts | 4 ++-- packages/toolkit/src/query/core/buildSlice.ts | 3 +-- .../toolkit/src/query/core/buildThunks.ts | 8 ++++++-- packages/toolkit/src/query/core/module.ts | 2 +- packages/toolkit/src/query/tsHelpers.ts | 20 ------------------- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/types.ts b/packages/toolkit/src/query/core/buildMiddleware/types.ts index 27e8955935..1b8561979f 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/types.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/types.ts @@ -5,6 +5,7 @@ import type { Middleware, MiddlewareAPI, ThunkDispatch, + ImmutableHelpers, } from '@reduxjs/toolkit' import type { Api, ApiContext } from '../../apiTypes' @@ -24,7 +25,6 @@ import type { QueryThunkArg, ThunkResult, } from '../buildThunks' -import type { ImmutableHelpers } from '../../tsHelpers' export type QueryStateMeta = Record export type TimeoutId = ReturnType diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index 6155e227fa..9067a1e2b1 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -1,4 +1,5 @@ -import { createNextState, createSelector } from '@reduxjs/toolkit' +import type { ImmutableHelpers } from '@reduxjs/toolkit' +import { createSelector } from '@reduxjs/toolkit' import type { MutationSubState, QuerySubState, @@ -20,7 +21,6 @@ import { expandTagDescription } from '../endpointDefinitions' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import { getMutationCacheKey } from './buildSlice' import { flatten } from '../utils' -import type { ImmutableHelpers } from '../tsHelpers' export type SkipToken = typeof skipToken /** diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index ad552e20dd..578de663ad 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -1,7 +1,7 @@ import type { - AnyAction, PayloadAction, BuildCreateSliceConfiguration, + ImmutableHelpers, } from '@reduxjs/toolkit' import { combineReducers, @@ -43,7 +43,6 @@ import { } from '../utils' import type { ApiContext } from '../apiTypes' import { isUpsertQuery } from './buildInitiate' -import type { ImmutableHelpers } from '../tsHelpers' function updateQuerySubstateIfExists( state: QueryState, diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 97052c254e..42f52aa7e6 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -23,7 +23,11 @@ import type { } from '../endpointDefinitions' import { isQueryDefinition } from '../endpointDefinitions' import { calculateProvidedBy } from '../endpointDefinitions' -import type { AsyncThunkPayloadCreator, Draft } from '@reduxjs/toolkit' +import type { + AsyncThunkPayloadCreator, + Draft, + ImmutableHelpers, +} from '@reduxjs/toolkit' import { isAllOf, isFulfilled, @@ -43,7 +47,7 @@ import { createAsyncThunk, SHOULD_AUTOBATCH } from '@reduxjs/toolkit' import { HandledError } from '../HandledError' import type { ApiEndpointQuery, PrefetchOptions } from './module' -import type { ImmutableHelpers, UnwrapPromise } from '../tsHelpers' +import type { UnwrapPromise } from '../tsHelpers' declare module './module' { export interface ApiEndpointQuery< diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index fa7d6064f1..2078af0720 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -15,6 +15,7 @@ import type { Reducer, ThunkAction, ThunkDispatch, + ImmutableHelpers, } from '@reduxjs/toolkit' import { immerImmutableHelpers } from '@reduxjs/toolkit' import type { @@ -42,7 +43,6 @@ import type { QueryActionCreatorResult, } from './buildInitiate' import { buildInitiate } from './buildInitiate' -import type { ImmutableHelpers } from '../tsHelpers' import { assertCast, safeAssign } from '../tsHelpers' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import type { SliceActions } from './buildSlice' diff --git a/packages/toolkit/src/query/tsHelpers.ts b/packages/toolkit/src/query/tsHelpers.ts index cfb8b07d0c..af79f5bd71 100644 --- a/packages/toolkit/src/query/tsHelpers.ts +++ b/packages/toolkit/src/query/tsHelpers.ts @@ -1,23 +1,3 @@ -import type { Draft, Patch, applyPatches } from 'immer' - -export interface ImmutableHelpers { - createNextState: ( - base: Base, - recipe: (draft: Draft) => void | Base | Draft - ) => Base - createWithPatches: ( - base: Base, - recipe: (draft: Draft) => void | Base | Draft - ) => readonly [Base, Patch[], Patch[]] - // depends on an Objectish type that immer doesn't export - applyPatches: typeof applyPatches - isDraft(value: any): boolean - isDraftable(value: any): boolean - original(value: T): T | undefined - current(value: T): T - freeze(obj: T, deep?: boolean): T -} - export type Id = { [K in keyof T]: T[K] } & {} export type WithRequiredProp = Omit & Required> From 115b856d49f046504f544817ed7c3fc0ed8b81d7 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 18 Apr 2023 10:25:20 +0100 Subject: [PATCH 17/21] fix entity adapter options not being optional --- packages/toolkit/src/entities/create_adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index 0a954c8df7..31c263362b 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -17,7 +17,7 @@ export interface BuildCreateEntityAdapterConfiguration BuildStateOperatorConfiguration {} export type CreateEntityAdapter = { - (options: { + (options?: { selectId?: IdSelector sortComparer?: false | Comparer }): EntityAdapter From bfa14191a68d3daaadf7048b56f9619f92ca2860 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Fri, 5 May 2023 16:50:10 +0100 Subject: [PATCH 18/21] hope the PR gets happier From 9d8f34471f18024caf045d5140621dd3298f9e2d Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Fri, 5 May 2023 17:00:39 +0100 Subject: [PATCH 19/21] be extra assertive about nicking reselect's types --- .../toolkit/src/createDraftSafeSelector.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/toolkit/src/createDraftSafeSelector.ts b/packages/toolkit/src/createDraftSafeSelector.ts index 047f00461d..bd58ee406a 100644 --- a/packages/toolkit/src/createDraftSafeSelector.ts +++ b/packages/toolkit/src/createDraftSafeSelector.ts @@ -7,19 +7,10 @@ export type BuildCreateDraftSafeSelectorConfiguration = Pick< 'isDraft' | 'current' > -/** - * "Draft-Safe" version of `reselect`'s `createSelector`: - * If an `immer`-drafted object is passed into the resulting selector's first argument, - * the selector will act on the current draft value, instead of returning a cached value - * that might be possibly outdated if the draft has been modified since. - * @public - */ -export type CreateDraftSafeSelector = typeof createSelector - export function buildCreateDraftSafeSelector({ isDraft, current, -}: BuildCreateDraftSafeSelectorConfiguration): CreateDraftSafeSelector { +}: BuildCreateDraftSafeSelectorConfiguration): typeof createSelector { return function createDraftSafeSelector(...args: unknown[]) { const selector = (createSelector as any)(...args) const wrappedSelector = (value: unknown, ...rest: unknown[]) => @@ -28,5 +19,12 @@ export function buildCreateDraftSafeSelector({ } } -export const createDraftSafeSelector = +/** + * "Draft-Safe" version of `reselect`'s `createSelector`: + * If an `immer`-drafted object is passed into the resulting selector's first argument, + * the selector will act on the current draft value, instead of returning a cached value + * that might be possibly outdated if the draft has been modified since. + * @public + */ +export const createDraftSafeSelector: typeof createSelector = buildCreateDraftSafeSelector(immutableHelpers) From b43f5015fa69a8296ebd5fa0d98a0f7304639fed Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Mon, 2 Oct 2023 21:20:56 +0100 Subject: [PATCH 20/21] rtkImports --- packages/toolkit/src/query/core/buildThunks.ts | 4 +++- packages/toolkit/src/query/core/module.ts | 2 +- packages/toolkit/src/query/core/rtkImports.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 3280cdec34..8879937bcc 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -29,6 +29,9 @@ import type { Draft, ImmutableHelpers, UnknownAction, + ThunkAction, + ThunkDispatch, + AsyncThunk, } from '@reduxjs/toolkit' import { isAllOf, @@ -40,7 +43,6 @@ import { SHOULD_AUTOBATCH, } from './rtkImports' import type { Patch } from 'immer' -import type { ThunkAction, ThunkDispatch, AsyncThunk } from '@reduxjs/toolkit' import { HandledError } from '../HandledError' diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 1361114053..50bf08dc88 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -17,7 +17,7 @@ import type { ImmutableHelpers, UnknownAction, } from '@reduxjs/toolkit' -import { immerImmutableHelpers } from '@reduxjs/toolkit' +import { immerImmutableHelpers } from './rtkImports' import type { EndpointDefinitions, QueryArgFrom, diff --git a/packages/toolkit/src/query/core/rtkImports.ts b/packages/toolkit/src/query/core/rtkImports.ts index f2be8536dc..74f4bcdb11 100644 --- a/packages/toolkit/src/query/core/rtkImports.ts +++ b/packages/toolkit/src/query/core/rtkImports.ts @@ -4,7 +4,6 @@ export { createAction, - createSlice, createSelector, createAsyncThunk, combineReducers, @@ -22,4 +21,5 @@ export { isPlainObject, nanoid, buildCreateSlice, + immerImmutableHelpers, } from '@reduxjs/toolkit' From 0888fde1c2d4617ab364afd9503085aca9dc8bda Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Mon, 2 Oct 2023 21:42:18 +0100 Subject: [PATCH 21/21] add missing createEntityAdapter overload --- packages/toolkit/src/entities/create_adapter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/src/entities/create_adapter.ts b/packages/toolkit/src/entities/create_adapter.ts index ba68330c02..c94e3e1c50 100644 --- a/packages/toolkit/src/entities/create_adapter.ts +++ b/packages/toolkit/src/entities/create_adapter.ts @@ -19,9 +19,12 @@ export interface BuildCreateEntityAdapterConfiguration export type CreateEntityAdapter = { (options?: { - selectId?: IdSelector + selectId: IdSelector sortComparer?: false | Comparer }): EntityAdapter + (options?: { + sortComparer?: false | Comparer + }): EntityAdapter } export function buildCreateEntityAdapter(