From f03c2d9b4a41583c72ce8c1cedf25c25c82adb36 Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Sun, 18 Feb 2024 23:48:18 +0000 Subject: [PATCH 01/15] Revert "there are no entity methods creators in ba sing se" This reverts commit 31c4794a6137dbd42fa2a94012c16aab62e5001a. --- docs/api/createEntityAdapter.mdx | 2 +- docs/api/createSlice.mdx | 99 +++++++++++ packages/toolkit/src/entities/models.ts | 72 ++++++-- .../toolkit/src/entities/slice_creator.ts | 161 ++++++++++++++++++ .../toolkit/src/entities/state_selectors.ts | 92 +++++++--- .../tests/entity_slice_enhancer.test-d.ts | 74 ++++++++ .../tests/entity_slice_enhancer.test.ts | 98 ++++++++++- .../entities/tests/state_selectors.test.ts | 55 ++++++ packages/toolkit/src/index.ts | 5 + 9 files changed, 621 insertions(+), 37 deletions(-) create mode 100644 packages/toolkit/src/entities/slice_creator.ts create mode 100644 packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts diff --git a/docs/api/createEntityAdapter.mdx b/docs/api/createEntityAdapter.mdx index d08d145bc9..e74a7349ab 100644 --- a/docs/api/createEntityAdapter.mdx +++ b/docs/api/createEntityAdapter.mdx @@ -239,7 +239,7 @@ In other words, they accept a state that looks like `{ids: [], entities: {}}`, a These CRUD methods may be used in multiple ways: -- They may be passed as case reducers directly to `createReducer` and `createSlice`. +- They may be passed as case reducers directly to `createReducer` and `createSlice`. (also see the [`create.entityMethods`](./createSlice#createentitymethods-entitymethodscreator) slice creator which can assist with this) - They may be used as "mutating" helper methods when called manually, such as a separate hand-written call to `addOne()` inside of an existing case reducer, if the `state` argument is actually an Immer `Draft` value. - They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array. diff --git a/docs/api/createSlice.mdx b/docs/api/createSlice.mdx index 64326aa2e1..ef5e5a925d 100644 --- a/docs/api/createSlice.mdx +++ b/docs/api/createSlice.mdx @@ -845,6 +845,105 @@ reducers: (create) => { ::: +##### `create.entityMethods` (`entityMethodsCreator`) + +Creates a set of reducers for managing a normalized entity state, based on a provided [adapter](./createEntityAdapter). + +```ts +import { + createEntityAdapter, + buildCreateSlice, + entityMethodsCreator, +} from '@reduxjs/toolkit' + +const createAppSlice = buildCreateSlice({ + creators: { entityMethods: entityMethodsCreator }, +}) + +interface Post { + id: string + text: string +} + +const postsAdapter = createEntityAdapter() + +const postsSlice = createAppSlice({ + name: 'posts', + initialState: postsAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(postsAdapter), + }), +}) + +export const { setOne, upsertMany, removeAll, ...etc } = postsSlice.actions +``` + +:::caution + +Because this creator returns an object of multiple reducer definitions, it should be spread into the final object returned by the `reducers` callback. + +::: + +**Parameters** + +- **adapter** The [adapter](./createEntityAdapter) to use. +- **config** The configuration object. (optional) + +The configuration object can contain some of the following options: + +**`selectEntityState`** + +A selector to retrieve the entity state from the slice state. Defaults to `state => state`, but should be provided if the entity state is nested. + +```ts no-transpile +const postsSlice = createAppSlice({ + name: 'posts', + initialState: { posts: postsAdapter.getInitialState() }, + reducers: (create) => ({ + ...create.entityMethods(postsAdapter, { + selectEntityState: (state) => state.posts, + }), + }), +}) +``` + +**`name`, `pluralName`** + +It's often desirable to modify the reducer names to be specific to the data type being used. These options allow you to do that. + +```ts no-transpile +const postsSlice = createAppSlice({ + name: 'posts', + initialState: postsAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(postsAdapter, { + name: 'post', + }), + }), +}) + +const { addOnePost, upsertManyPosts, removeAllPosts, ...etc } = + postsSlice.actions +``` + +`pluralName` defaults to `name + 's'`, but can be provided if this isn't desired. + +```ts no-transpile +const gooseSlice = createAppSlice({ + name: 'geese', + initialState: gooseAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(gooseAdapter, { + name: 'goose', + pluralName: 'geese', + }), + }), +}) + +const { addOneGoose, upsertManyGeese, removeAllGeese, ...etc } = + gooseSlice.actions +``` + ### Writing your own creators In version v2.2.0 (TODO), we introduced a system for including your own creators. diff --git a/packages/toolkit/src/entities/models.ts b/packages/toolkit/src/entities/models.ts index 76de9872fd..8706a143ed 100644 --- a/packages/toolkit/src/entities/models.ts +++ b/packages/toolkit/src/entities/models.ts @@ -3,6 +3,8 @@ import type { Draft } from 'immer' import type { PayloadAction } from '../createAction' import type { GetSelectorsOptions } from './state_selectors' import type { CastAny, Id as Compute } from '../tsHelpers' +import type { CaseReducerDefinition } from '../createSlice' +import type { CaseReducer } from '../createReducer' /** * @public @@ -158,12 +160,51 @@ export interface EntityStateAdapter { /** * @public */ -export interface EntitySelectors { - selectIds: (state: V) => Id[] - selectEntities: (state: V) => Record - selectAll: (state: V) => T[] - selectTotal: (state: V) => number - selectById: (state: V, id: Id) => Compute> +export type EntitySelectors< + T, + V, + Id extends EntityId, + Single extends string = '', + Plural extends string = DefaultPlural, +> = Compute< + { + [K in `select${Capitalize}Ids`]: (state: V) => Id[] + } & { + [K in `select${Capitalize}Entities`]: (state: V) => Record + } & { + [K in `selectAll${Capitalize}`]: (state: V) => T[] + } & { + [K in `selectTotal${Capitalize}`]: (state: V) => number + } & { + [K in `select${Capitalize}ById`]: ( + state: V, + id: Id, + ) => Compute> + } +> + +export type DefaultPlural = Single extends '' + ? '' + : `${Single}s` + +export type EntityReducers< + T, + Id extends EntityId, + State = EntityState, + Single extends string = '', + Plural extends string = DefaultPlural, +> = { + [K in keyof EntityStateAdapter< + T, + Id + > as `${K}${Capitalize}`]: EntityStateAdapter< + T, + Id + >[K] extends (state: any) => any + ? CaseReducerDefinition + : EntityStateAdapter[K] extends CaseReducer + ? CaseReducerDefinition + : never } /** @@ -187,12 +228,19 @@ export interface EntityAdapter extends EntityStateAdapter, EntityStateFactory, Required> { - getSelectors( + getSelectors< + Single extends string = '', + Plural extends string = DefaultPlural, + >( selectState?: undefined, - options?: GetSelectorsOptions, - ): EntitySelectors, Id> - getSelectors( + options?: GetSelectorsOptions, + ): EntitySelectors, Id, Single, Plural> + getSelectors< + V, + Single extends string = '', + Plural extends string = DefaultPlural, + >( selectState: (state: V) => EntityState, - options?: GetSelectorsOptions, - ): EntitySelectors + options?: GetSelectorsOptions, + ): EntitySelectors } diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts new file mode 100644 index 0000000000..b50ee26d10 --- /dev/null +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -0,0 +1,161 @@ +import type { + CreatorCaseReducers, + ReducerCreator, + ReducerCreatorEntry, +} from '@reduxjs/toolkit' +import { reducerCreator } from '../createSlice' +import type { WithRequiredProp } from '../tsHelpers' +import type { + Update, + EntityAdapter, + EntityId, + EntityState, + DefaultPlural, + EntityReducers, +} from './models' +import { capitalize } from './utils' + +export const entityMethodsCreatorType = /*@__PURE__*/ Symbol() + +export interface EntityMethodsCreatorConfig< + T, + Id extends EntityId, + State, + Single extends string, + Plural extends string, +> { + selectEntityState?: (state: State) => EntityState + name?: Single + pluralName?: Plural +} + +type EntityMethodsCreator = + State extends EntityState + ? { + < + T, + Id extends EntityId, + Single extends string = '', + Plural extends string = DefaultPlural, + >( + adapter: EntityAdapter, + config: WithRequiredProp< + EntityMethodsCreatorConfig, + 'selectEntityState' + >, + ): EntityReducers + < + Single extends string = '', + Plural extends string = DefaultPlural, + >( + adapter: EntityAdapter, + config?: Omit< + EntityMethodsCreatorConfig, + 'selectEntityState' + >, + ): EntityReducers + } + : < + T, + Id extends EntityId, + Single extends string = '', + Plural extends string = DefaultPlural, + >( + adapter: EntityAdapter, + config: WithRequiredProp< + EntityMethodsCreatorConfig, + 'selectEntityState' + >, + ) => EntityReducers + +declare module '@reduxjs/toolkit' { + export interface SliceReducerCreators< + State, + CaseReducers extends CreatorCaseReducers, + Name extends string, + > { + [entityMethodsCreatorType]: ReducerCreatorEntry> + } +} + +export function createEntityMethods< + T, + Id extends EntityId, + State = EntityState, + Single extends string = '', + Plural extends string = DefaultPlural, +>( + adapter: EntityAdapter, + { + selectEntityState = (state) => state as unknown as EntityState, + name: nameParam = '' as Single, + pluralName: pluralParam = (nameParam && `${nameParam}s`) as Plural, + }: EntityMethodsCreatorConfig = {}, +): EntityReducers { + // template literal computed keys don't keep their type if there's an unresolved generic + // so we cast to some intermediate type to at least check we're using the right variables in the right places + + const name = nameParam as 's' + const pluralName = pluralParam as 'p' + const reducer = reducerCreator.create + const reducers: EntityReducers = { + [`addOne${capitalize(name)}` as const]: reducer((state, action) => { + adapter.addOne(selectEntityState(state), action) + }), + [`addMany${capitalize(pluralName)}` as const]: reducer< + readonly T[] | Record + >((state, action) => { + adapter.addMany(selectEntityState(state), action) + }), + [`setOne${capitalize(name)}` as const]: reducer((state, action) => { + adapter.setOne(selectEntityState(state), action) + }), + [`setMany${capitalize(pluralName)}` as const]: reducer< + readonly T[] | Record + >((state, action) => { + adapter.setMany(selectEntityState(state), action) + }), + [`setAll${capitalize(pluralName)}` as const]: reducer< + readonly T[] | Record + >((state, action) => { + adapter.setAll(selectEntityState(state), action) + }), + [`removeOne${capitalize(name)}` as const]: reducer((state, action) => { + adapter.removeOne(selectEntityState(state), action) + }), + [`removeMany${capitalize(pluralName)}` as const]: reducer( + (state, action) => { + adapter.removeMany(selectEntityState(state), action) + }, + ), + [`removeAll${capitalize(pluralName)}` as const]: reducer((state) => { + adapter.removeAll(selectEntityState(state)) + }), + [`upsertOne${capitalize(name)}` as const]: reducer((state, action) => { + adapter.upsertOne(selectEntityState(state), action) + }), + [`upsertMany${capitalize(pluralName)}` as const]: reducer< + readonly T[] | Record + >((state, action) => { + adapter.upsertMany(selectEntityState(state), action) + }), + [`updateOne${capitalize(name)}` as const]: reducer>( + (state, action) => { + adapter.updateOne(selectEntityState(state), action) + }, + ), + [`updateMany${capitalize(pluralName)}` as const]: reducer< + readonly Update[] + >((state, action) => { + adapter.updateMany(selectEntityState(state), action) + }), + } + return reducers as any +} + +export const entityMethodsCreator: ReducerCreator< + typeof entityMethodsCreatorType +> = { + type: entityMethodsCreatorType, + create: createEntityMethods, +} diff --git a/packages/toolkit/src/entities/state_selectors.ts b/packages/toolkit/src/entities/state_selectors.ts index bd64cf001b..eb425aa223 100644 --- a/packages/toolkit/src/entities/state_selectors.ts +++ b/packages/toolkit/src/entities/state_selectors.ts @@ -1,6 +1,12 @@ import type { CreateSelectorFunction, Selector, createSelector } from 'reselect' import { createDraftSafeSelector } from '../createDraftSafeSelector' -import type { EntityState, EntitySelectors, EntityId } from './models' +import type { + EntityState, + EntitySelectors, + EntityId, + DefaultPlural, +} from './models' +import { capitalize } from './utils' type AnyFunction = (...args: any) => any type AnyCreateSelectorFunction = CreateSelectorFunction< @@ -8,25 +14,43 @@ type AnyCreateSelectorFunction = CreateSelectorFunction< (f: F) => F > -export interface GetSelectorsOptions { +export interface GetSelectorsOptions< + Single extends string = '', + Plural extends string = DefaultPlural<''>, +> { createSelector?: AnyCreateSelectorFunction + name?: Single + pluralName?: Plural } export function createSelectorsFactory() { - function getSelectors( + function getSelectors< + Single extends string = '', + Plural extends string = DefaultPlural, + >( selectState?: undefined, - options?: GetSelectorsOptions, - ): EntitySelectors, Id> - function getSelectors( + options?: GetSelectorsOptions, + ): EntitySelectors, Id, Single, Plural> + function getSelectors< + V, + Single extends string = '', + Plural extends string = DefaultPlural, + >( selectState: (state: V) => EntityState, - options?: GetSelectorsOptions, - ): EntitySelectors - function getSelectors( + options?: GetSelectorsOptions, + ): EntitySelectors + function getSelectors< + V, + Single extends string = '', + Plural extends string = DefaultPlural, + >( selectState?: (state: V) => EntityState, - options: GetSelectorsOptions = {}, - ): EntitySelectors { + options: GetSelectorsOptions = {}, + ): EntitySelectors { const { createSelector = createDraftSafeSelector as AnyCreateSelectorFunction, + name = '', + pluralName = name && `${name}s`, } = options const selectIds = (state: EntityState) => state.ids @@ -45,14 +69,25 @@ export function createSelectorsFactory() { const selectTotal = createSelector(selectIds, (ids) => ids.length) + // template literal computed keys don't keep their type if there's an unresolved generic + // so we cast to some intermediate type to at least check we're using the right variables in the right places + + const single = name as 's' + const plural = pluralName as 'p' + if (!selectState) { - return { - selectIds, - selectEntities, - selectAll, - selectTotal, - selectById: createSelector(selectEntities, selectId, selectById), + const selectors: EntitySelectors = { + [`select${capitalize(single)}Ids` as const]: selectIds, + [`select${capitalize(single)}Entities` as const]: selectEntities, + [`selectAll${capitalize(plural)}` as const]: selectAll, + [`selectTotal${capitalize(plural)}` as const]: selectTotal, + [`select${capitalize(single)}ById` as const]: createSelector( + selectEntities, + selectId, + selectById, + ), } + return selectors as any } const selectGlobalizedEntities = createSelector( @@ -60,17 +95,28 @@ export function createSelectorsFactory() { selectEntities, ) - return { - selectIds: createSelector(selectState, selectIds), - selectEntities: selectGlobalizedEntities, - selectAll: createSelector(selectState, selectAll), - selectTotal: createSelector(selectState, selectTotal), - selectById: createSelector( + const selectors: EntitySelectors = { + [`select${capitalize(single)}Ids` as const]: createSelector( + selectState, + selectIds, + ), + [`select${capitalize(single)}Entities` as const]: + selectGlobalizedEntities, + [`selectAll${capitalize(plural)}` as const]: createSelector( + selectState, + selectAll, + ), + [`selectTotal${capitalize(plural)}` as const]: createSelector( + selectState, + selectTotal, + ), + [`select${capitalize(single)}ById` as const]: createSelector( selectGlobalizedEntities, selectId, selectById, ), } + return selectors as any } return { getSelectors } diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts new file mode 100644 index 0000000000..29eca94da9 --- /dev/null +++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts @@ -0,0 +1,74 @@ +import type { PayloadActionCreator } from '../../createAction' +import { + buildCreateSlice, + createEntityAdapter, + entityMethodsCreator, + createEntityMethods, +} from '@reduxjs/toolkit' +import type { BookModel } from './fixtures/book' + +describe('entity slice creator', () => { + const createAppSlice = buildCreateSlice({ + creators: { entityMethods: entityMethodsCreator }, + }) + it('should require selectEntityState if state is not compatible', () => { + const bookAdapter = createEntityAdapter() + const bookSlice = createAppSlice({ + name: 'books', + initialState: { data: bookAdapter.getInitialState() }, + reducers: (create) => ({ + // @ts-expect-error + ...create.entityMethods(bookAdapter), + // @ts-expect-error + ...create.entityMethods(bookAdapter, {}), + ...create.entityMethods(bookAdapter, { + selectEntityState: (state) => state.data, + }), + }), + }) + expectTypeOf(bookSlice.actions.addOne).toEqualTypeOf< + PayloadActionCreator + >() + }) + it('exports createEntityMethods which can be used in object form', () => { + const bookAdapter = createEntityAdapter() + + const initialState = { data: bookAdapter.getInitialState() } + + const bookSlice = createAppSlice({ + name: 'books', + initialState: { data: bookAdapter.getInitialState() }, + // @ts-expect-error + reducers: { + ...createEntityMethods(bookAdapter), + }, + }) + + const bookSlice2 = createAppSlice({ + name: 'books', + initialState, + reducers: { + ...entityMethodsCreator.create(bookAdapter, { + // cannot be inferred, needs annotation + selectEntityState: (state: typeof initialState) => state.data, + }), + }, + }) + + expectTypeOf(bookSlice2.actions.addOne).toEqualTypeOf< + PayloadActionCreator + >() + + const bookSlice3 = createAppSlice({ + name: 'books', + initialState: bookAdapter.getInitialState(), + reducers: { + ...createEntityMethods(bookAdapter), + }, + }) + + expectTypeOf(bookSlice3.actions.addOne).toEqualTypeOf< + PayloadActionCreator + >() + }) +}) diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts index 6f88bf1aa5..af5cab2638 100644 --- a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts +++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts @@ -1,4 +1,10 @@ -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' +import { + buildCreateSlice, + createEntityAdapter, + createSlice, + entityMethodsCreator, + createEntityMethods, +} from '@reduxjs/toolkit' import type { PayloadAction, SliceCaseReducers, @@ -54,3 +60,93 @@ function entitySliceEnhancer< }, }) } + +describe('entity slice creator', () => { + const createAppSlice = buildCreateSlice({ + creators: { entityMethods: entityMethodsCreator }, + }) + + const bookAdapter = createEntityAdapter() + + const bookSlice = createAppSlice({ + name: 'book', + initialState: bookAdapter.getInitialState({ + nested: bookAdapter.getInitialState(), + }), + reducers: (create) => ({ + ...create.entityMethods(bookAdapter, { + name: 'book', + }), + ...create.entityMethods(bookAdapter, { + selectEntityState: (state) => state.nested, + name: 'nestedBook', + pluralName: 'nestedBookies', + }), + }), + }) + + it('should generate correct actions', () => { + expect(bookSlice.actions.addOneBook).toBeTypeOf('function') + expect(bookSlice.actions.addOneNestedBook).toBeTypeOf('function') + expect(bookSlice.actions.addManyBooks).toBeTypeOf('function') + expect(bookSlice.actions.addManyNestedBookies).toBeTypeOf('function') + }) + it('should handle actions', () => { + const withBook = bookSlice.reducer( + undefined, + bookSlice.actions.addOneBook(AClockworkOrange), + ) + expect( + bookAdapter.getSelectors().selectById(withBook, AClockworkOrange.id), + ).toBe(AClockworkOrange) + + const withNestedBook = bookSlice.reducer( + withBook, + bookSlice.actions.addOneNestedBook(AClockworkOrange), + ) + expect( + bookAdapter + .getSelectors( + (state: ReturnType) => state.nested, + ) + .selectById(withNestedBook, AClockworkOrange.id), + ).toBe(AClockworkOrange) + }) + it('should be able to be called without this context', () => { + const bookSlice = createAppSlice({ + name: 'book', + initialState: bookAdapter.getInitialState(), + reducers: ({ entityMethods }) => ({ + ...entityMethods(bookAdapter), + }), + }) + expect(bookSlice.actions.addOne).toBeTypeOf('function') + }) + it('can be called with object syntax', () => { + const bookSlice = createAppSlice({ + name: 'book', + initialState: bookAdapter.getInitialState(), + reducers: { + ...createEntityMethods(bookAdapter, { + name: 'book', + }), + }, + }) + expect(bookSlice.actions.addOneBook).toBeTypeOf('function') + + const initialState = { nested: bookAdapter.getInitialState() } + const nestedBookSlice = createAppSlice({ + name: 'book', + initialState, + reducers: { + ...createEntityMethods(bookAdapter, { + // state can't be inferred, so needs to be annotated + selectEntityState: (state: typeof initialState) => state.nested, + name: 'nestedBook', + pluralName: 'nestedBookies', + }), + }, + }) + expect(nestedBookSlice.actions.addOneNestedBook).toBeTypeOf('function') + }) +}) diff --git a/packages/toolkit/src/entities/tests/state_selectors.test.ts b/packages/toolkit/src/entities/tests/state_selectors.test.ts index dba56cc01d..ef7a48e9eb 100644 --- a/packages/toolkit/src/entities/tests/state_selectors.test.ts +++ b/packages/toolkit/src/entities/tests/state_selectors.test.ts @@ -147,6 +147,61 @@ describe('Entity State Selectors', () => { memoizeSpy.mockClear() }) }) + describe('named selectors', () => { + interface State { + books: EntityState + } + + let adapter: EntityAdapter + let state: State + + beforeEach(() => { + adapter = createEntityAdapter({ + selectId: (book: BookModel) => book.id, + }) + + state = { + books: adapter.setAll(adapter.getInitialState(), [ + AClockworkOrange, + AnimalFarm, + TheGreatGatsby, + ]), + } + }) + it('should use the provided name and pluralName', () => { + const selectors = adapter.getSelectors(undefined, { + name: 'book', + }) + + expect(selectors.selectAllBooks).toBeTypeOf('function') + expect(selectors.selectTotalBooks).toBeTypeOf('function') + expect(selectors.selectBookById).toBeTypeOf('function') + + expect(selectors.selectAllBooks(state.books)).toEqual([ + AClockworkOrange, + AnimalFarm, + TheGreatGatsby, + ]) + expect(selectors.selectTotalBooks(state.books)).toEqual(3) + }) + it('should use the plural of the provided name', () => { + const selectors = adapter.getSelectors((state: State) => state.books, { + name: 'book', + pluralName: 'bookies', + }) + + expect(selectors.selectAllBookies).toBeTypeOf('function') + expect(selectors.selectTotalBookies).toBeTypeOf('function') + expect(selectors.selectBookById).toBeTypeOf('function') + + expect(selectors.selectAllBookies(state)).toEqual([ + AClockworkOrange, + AnimalFarm, + TheGreatGatsby, + ]) + expect(selectors.selectTotalBookies(state)).toEqual(3) + }) + }) }) function expectType(t: T) { diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 678677f7f8..51e044a433 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -132,6 +132,11 @@ export type { IdSelector, Comparer, } from './entities/models' +export { + createEntityMethods, + entityMethodsCreator, + entityMethodsCreatorType, +} from './entities/slice_creator' export { createAsyncThunk, From a6b872791c101b5f3725180dd745e2a842977d81 Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Sun, 18 Feb 2024 23:55:31 +0000 Subject: [PATCH 02/15] Revert "reset more entity files" This reverts commit 82c92627e3a744f40c3b5ad5e194192045af852c. --- .../tests/entity_slice_enhancer.test.ts | 39 +++++++++---------- .../src/entities/tests/entity_state.test.ts | 4 +- .../src/entities/tests/state_adapter.test.ts | 4 +- .../entities/tests/state_selectors.test.ts | 4 +- packages/toolkit/src/entities/utils.ts | 4 ++ 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts index d2fae0e018..af5cab2638 100644 --- a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts +++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts @@ -7,47 +7,44 @@ import { } from '@reduxjs/toolkit' import type { PayloadAction, - Slice, SliceCaseReducers, - UnknownAction, + ValidateSliceCaseReducers, } from '../..' import type { EntityId, EntityState, IdSelector } from '../models' -import type { BookModel } from './fixtures/book' +import { AClockworkOrange, type BookModel } from './fixtures/book' describe('Entity Slice Enhancer', () => { - let slice: Slice> + let slice: ReturnType> beforeEach(() => { - const indieSlice = entitySliceEnhancer({ + slice = entitySliceEnhancer({ name: 'book', selectId: (book: BookModel) => book.id, }) - slice = indieSlice }) it('exposes oneAdded', () => { - const book = { - id: '0', - title: 'Der Steppenwolf', - author: 'Herman Hesse', - } - const action = slice.actions.oneAdded(book) - const oneAdded = slice.reducer(undefined, action as UnknownAction) - expect(oneAdded.entities['0']).toBe(book) + const action = slice.actions.oneAdded(AClockworkOrange) + const oneAdded = slice.reducer(undefined, action) + expect(oneAdded.entities[AClockworkOrange.id]).toBe(AClockworkOrange) }) }) -interface EntitySliceArgs { +interface EntitySliceArgs< + T, + Id extends EntityId, + CaseReducers extends SliceCaseReducers>, +> { name: string selectId: IdSelector - modelReducer?: SliceCaseReducers + modelReducer?: ValidateSliceCaseReducers, CaseReducers> } -function entitySliceEnhancer({ - name, - selectId, - modelReducer, -}: EntitySliceArgs) { +function entitySliceEnhancer< + T, + Id extends EntityId, + CaseReducers extends SliceCaseReducers> = {}, +>({ name, selectId, modelReducer }: EntitySliceArgs) { const modelAdapter = createEntityAdapter({ selectId, }) diff --git a/packages/toolkit/src/entities/tests/entity_state.test.ts b/packages/toolkit/src/entities/tests/entity_state.test.ts index 999ee502b9..b2473f1124 100644 --- a/packages/toolkit/src/entities/tests/entity_state.test.ts +++ b/packages/toolkit/src/entities/tests/entity_state.test.ts @@ -1,5 +1,5 @@ -import type { EntityAdapter } from '../index' -import { createEntityAdapter } from '../index' +import type { EntityAdapter } from '../models' +import { createEntityAdapter } from '../create_adapter' import type { PayloadAction } from '../../createAction' import { createAction } from '../../createAction' import { createSlice } from '../../createSlice' diff --git a/packages/toolkit/src/entities/tests/state_adapter.test.ts b/packages/toolkit/src/entities/tests/state_adapter.test.ts index a05b715ff9..f691639b23 100644 --- a/packages/toolkit/src/entities/tests/state_adapter.test.ts +++ b/packages/toolkit/src/entities/tests/state_adapter.test.ts @@ -1,5 +1,5 @@ -import type { EntityAdapter } from '../index' -import { createEntityAdapter } from '../index' +import type { EntityAdapter } from '../models' +import { createEntityAdapter } from '../create_adapter' import type { PayloadAction } from '../../createAction' import { configureStore } from '../../configureStore' import { createSlice } from '../../createSlice' diff --git a/packages/toolkit/src/entities/tests/state_selectors.test.ts b/packages/toolkit/src/entities/tests/state_selectors.test.ts index a7aa308f7c..ef7a48e9eb 100644 --- a/packages/toolkit/src/entities/tests/state_selectors.test.ts +++ b/packages/toolkit/src/entities/tests/state_selectors.test.ts @@ -1,6 +1,6 @@ import { createDraftSafeSelectorCreator } from '../../createDraftSafeSelector' -import type { EntityAdapter, EntityState } from '../index' -import { createEntityAdapter } from '../index' +import type { EntityAdapter, EntityState } from '../models' +import { createEntityAdapter } from '../create_adapter' import type { EntitySelectors } from '../models' import type { BookModel } from './fixtures/book' import { AClockworkOrange, AnimalFarm, TheGreatGatsby } from './fixtures/book' diff --git a/packages/toolkit/src/entities/utils.ts b/packages/toolkit/src/entities/utils.ts index 5e3fb92fc1..e9c64c8121 100644 --- a/packages/toolkit/src/entities/utils.ts +++ b/packages/toolkit/src/entities/utils.ts @@ -55,3 +55,7 @@ export function splitAddedUpdatedEntities( } return [added, updated] } + +export function capitalize(str: S) { + return str && (str.replace(str[0], str[0].toUpperCase()) as Capitalize) +} From 063f2487a873342acef9801baeed25577ceac4ae Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Sun, 18 Feb 2024 23:56:38 +0000 Subject: [PATCH 03/15] Revert "again" This reverts commit 4db2dcc5c1c7055dd6c2db4ed5c5b6f05e704a76. --- packages/toolkit/src/entities/index.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/toolkit/src/entities/index.ts diff --git a/packages/toolkit/src/entities/index.ts b/packages/toolkit/src/entities/index.ts deleted file mode 100644 index e258527474..0000000000 --- a/packages/toolkit/src/entities/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { createEntityAdapter } from './create_adapter' -export type { - EntityState, - EntityAdapter, - Update, - IdSelector, - Comparer, -} from './models' From 4a95c261daecc4d49b0eb83e9b47c02298cf7ac3 Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Sat, 6 Apr 2024 22:37:38 +0100 Subject: [PATCH 04/15] update type to match --- packages/toolkit/src/entities/slice_creator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index b50ee26d10..77d2456a21 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -73,6 +73,7 @@ declare module '@reduxjs/toolkit' { State, CaseReducers extends CreatorCaseReducers, Name extends string, + ReducerPath extends string, > { [entityMethodsCreatorType]: ReducerCreatorEntry> } From 2fb97e543a97f9a8568be220b6bc3482c3b3c3cb Mon Sep 17 00:00:00 2001 From: EskiMojo14 Date: Mon, 8 Apr 2024 18:30:03 +0100 Subject: [PATCH 05/15] add entity methods creator back to docs --- docs/usage/custom-slice-creators.mdx | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/docs/usage/custom-slice-creators.mdx b/docs/usage/custom-slice-creators.mdx index ac4051593d..e3f17eb260 100644 --- a/docs/usage/custom-slice-creators.mdx +++ b/docs/usage/custom-slice-creators.mdx @@ -324,6 +324,105 @@ reducers: (create) => { ::: +##### `create.entityMethods` (`entityMethodsCreator`) + +Creates a set of reducers for managing a normalized entity state, based on a provided [adapter](./createEntityAdapter). + +```ts +import { + createEntityAdapter, + buildCreateSlice, + entityMethodsCreator, +} from '@reduxjs/toolkit' + +const createAppSlice = buildCreateSlice({ + creators: { entityMethods: entityMethodsCreator }, +}) + +interface Post { + id: string + text: string +} + +const postsAdapter = createEntityAdapter() + +const postsSlice = createAppSlice({ + name: 'posts', + initialState: postsAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(postsAdapter), + }), +}) + +export const { setOne, upsertMany, removeAll, ...etc } = postsSlice.actions +``` + +:::caution + +Because this creator returns an object of multiple reducer definitions, it should be spread into the final object returned by the `reducers` callback. + +::: + +**Parameters** + +- **adapter** The [adapter](../api/createEntityAdapter) to use. +- **config** The configuration object. (optional) + +The configuration object can contain some of the following options: + +**`selectEntityState`** + +A selector to retrieve the entity state from the slice state. Defaults to `state => state`, but should be provided if the entity state is nested. + +```ts no-transpile +const postsSlice = createAppSlice({ + name: 'posts', + initialState: { posts: postsAdapter.getInitialState() }, + reducers: (create) => ({ + ...create.entityMethods(postsAdapter, { + selectEntityState: (state) => state.posts, + }), + }), +}) +``` + +**`name`, `pluralName`** + +It's often desirable to modify the reducer names to be specific to the data type being used. These options allow you to do that simply. + +```ts no-transpile +const postsSlice = createAppSlice({ + name: 'posts', + initialState: postsAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(postsAdapter, { + name: 'post', + }), + }), +}) + +const { addOnePost, upsertManyPosts, removeAllPosts, ...etc } = + postsSlice.actions +``` + +`pluralName` defaults to `name + 's'`, but can be provided if this isn't desired. + +```ts no-transpile +const gooseSlice = createAppSlice({ + name: 'geese', + initialState: gooseAdapter.getInitialState(), + reducers: (create) => ({ + ...create.entityMethods(gooseAdapter, { + name: 'goose', + pluralName: 'geese', + }), + }), +}) + +const { addOneGoose, upsertManyGeese, removeAllGeese, ...etc } = + gooseSlice.actions +``` + ## Writing your own creators In version v2.3.0, we introduced a system for including your own creators. @@ -338,7 +437,7 @@ For example, the `create.preparedReducer` creator uses a definition that looks l The callback form of `reducers` should return an object of reducer definitions, by calling creators and nesting the result of each under a key. -```js no-transpile +```js reducers: (create) => ({ addTodo: create.preparedReducer( (todo) => ({ payload: { id: nanoid(), ...todo } }), From 9febdfe60490462a8c39bf901271222dcce8a56e Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Wed, 22 May 2024 11:13:47 +0100 Subject: [PATCH 06/15] move async thunk creator module augmentation --- packages/toolkit/src/asyncThunkCreator.ts | 48 ++++++++++++++++++++++- packages/toolkit/src/createSlice.ts | 35 ----------------- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/packages/toolkit/src/asyncThunkCreator.ts b/packages/toolkit/src/asyncThunkCreator.ts index d0ae93ed75..52651b7a0c 100644 --- a/packages/toolkit/src/asyncThunkCreator.ts +++ b/packages/toolkit/src/asyncThunkCreator.ts @@ -7,8 +7,54 @@ import type { } from './createAsyncThunk' import { createAsyncThunk } from './createAsyncThunk' import type { CaseReducer } from './createReducer' -import type { ReducerCreator, ReducerDefinition } from './createSlice' +import type { + CreatorCaseReducers, + ReducerCreator, + ReducerCreatorEntry, + ReducerDefinition, +} from './createSlice' import { ReducerType } from './createSlice' +import type { Id } from './tsHelpers' + +declare module '@reduxjs/toolkit' { + export interface SliceReducerCreators< + State, + CaseReducers extends CreatorCaseReducers, + Name extends string, + ReducerPath extends string, + > { + [ReducerType.asyncThunk]: ReducerCreatorEntry< + AsyncThunkCreator, + { + actions: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + infer ThunkArg, + infer Returned, + infer ThunkApiConfig + > + ? AsyncThunk + : never + } + caseReducers: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + any, + any, + any + > + ? Id< + Pick< + Required, + 'fulfilled' | 'rejected' | 'pending' | 'settled' + > + > + : never + } + } + > + } +} export interface AsyncThunkSliceReducerConfig< State, diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index a8819bc1f7..6bc6f3b455 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,9 +1,5 @@ import type { Action, UnknownAction, Reducer } from 'redux' import type { Selector } from 'reselect' -import type { - AsyncThunkCreator, - AsyncThunkSliceReducerDefinition, -} from './asyncThunkCreator' import type { ActionCreatorWithoutPayload, PayloadAction, @@ -12,7 +8,6 @@ import type { _ActionCreatorWithPreparedPayload, } from './createAction' import { createAction } from './createAction' -import type { AsyncThunk } from './createAsyncThunk' import type { ActionMatcherDescriptionCollection, CaseReducer, @@ -135,36 +130,6 @@ export interface SliceReducerCreators< } } > - [ReducerType.asyncThunk]: ReducerCreatorEntry< - AsyncThunkCreator, - { - actions: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - infer ThunkArg, - infer Returned, - infer ThunkApiConfig - > - ? AsyncThunk - : never - } - caseReducers: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - any, - any, - any - > - ? Id< - Pick< - Required, - 'fulfilled' | 'rejected' | 'pending' | 'settled' - > - > - : never - } - } - > } export type ReducerCreators< From b1dd06664e36a3cb1b0ccea8b6016cb9479753bf Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Wed, 22 May 2024 14:50:11 +0100 Subject: [PATCH 07/15] create util to cut down on repetitive code --- .../toolkit/src/entities/slice_creator.ts | 83 ++++++++----------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index 77d2456a21..c4097c0023 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -1,12 +1,13 @@ import type { + CaseReducerDefinition, CreatorCaseReducers, + PayloadAction, ReducerCreator, ReducerCreatorEntry, } from '@reduxjs/toolkit' import { reducerCreator } from '../createSlice' import type { WithRequiredProp } from '../tsHelpers' import type { - Update, EntityAdapter, EntityId, EntityState, @@ -79,6 +80,20 @@ declare module '@reduxjs/toolkit' { } } +const makeWrappedReducerCreator = + ( + selectEntityState: (state: State) => EntityState, + ) => + ( + reducer: ( + state: EntityState, + action: PayloadAction, + ) => void, + ): CaseReducerDefinition> => + reducerCreator.create((state: State, action) => + reducer(selectEntityState(state), action), + ) + export function createEntityMethods< T, Id extends EntityId, @@ -98,58 +113,26 @@ export function createEntityMethods< const name = nameParam as 's' const pluralName = pluralParam as 'p' - const reducer = reducerCreator.create + const reducer = makeWrappedReducerCreator(selectEntityState) const reducers: EntityReducers = { - [`addOne${capitalize(name)}` as const]: reducer((state, action) => { - adapter.addOne(selectEntityState(state), action) - }), - [`addMany${capitalize(pluralName)}` as const]: reducer< - readonly T[] | Record - >((state, action) => { - adapter.addMany(selectEntityState(state), action) - }), - [`setOne${capitalize(name)}` as const]: reducer((state, action) => { - adapter.setOne(selectEntityState(state), action) - }), - [`setMany${capitalize(pluralName)}` as const]: reducer< - readonly T[] | Record - >((state, action) => { - adapter.setMany(selectEntityState(state), action) - }), - [`setAll${capitalize(pluralName)}` as const]: reducer< - readonly T[] | Record - >((state, action) => { - adapter.setAll(selectEntityState(state), action) - }), - [`removeOne${capitalize(name)}` as const]: reducer((state, action) => { - adapter.removeOne(selectEntityState(state), action) - }), - [`removeMany${capitalize(pluralName)}` as const]: reducer( - (state, action) => { - adapter.removeMany(selectEntityState(state), action) - }, + [`addOne${capitalize(name)}` as const]: reducer(adapter.addOne), + [`addMany${capitalize(pluralName)}` as const]: reducer(adapter.addMany), + [`setOne${capitalize(name)}` as const]: reducer(adapter.setOne), + [`setMany${capitalize(pluralName)}` as const]: reducer(adapter.setMany), + [`setAll${capitalize(pluralName)}` as const]: reducer(adapter.setAll), + [`removeOne${capitalize(name)}` as const]: reducer(adapter.removeOne), + [`removeMany${capitalize(pluralName)}` as const]: reducer( + adapter.removeMany, + ), + [`removeAll${capitalize(pluralName)}` as const]: reducer(adapter.removeAll), + [`upsertOne${capitalize(name)}` as const]: reducer(adapter.upsertOne), + [`upsertMany${capitalize(pluralName)}` as const]: reducer( + adapter.upsertMany, ), - [`removeAll${capitalize(pluralName)}` as const]: reducer((state) => { - adapter.removeAll(selectEntityState(state)) - }), - [`upsertOne${capitalize(name)}` as const]: reducer((state, action) => { - adapter.upsertOne(selectEntityState(state), action) - }), - [`upsertMany${capitalize(pluralName)}` as const]: reducer< - readonly T[] | Record - >((state, action) => { - adapter.upsertMany(selectEntityState(state), action) - }), - [`updateOne${capitalize(name)}` as const]: reducer>( - (state, action) => { - adapter.updateOne(selectEntityState(state), action) - }, + [`updateOne${capitalize(name)}` as const]: reducer(adapter.updateOne), + [`updateMany${capitalize(pluralName)}` as const]: reducer( + adapter.updateMany, ), - [`updateMany${capitalize(pluralName)}` as const]: reducer< - readonly Update[] - >((state, action) => { - adapter.updateMany(selectEntityState(state), action) - }), } return reducers as any } From e888b3232d83229cf3ac5fc09ca6c978c1788d6e Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Wed, 22 May 2024 15:00:17 +0100 Subject: [PATCH 08/15] prevent implicit return --- packages/toolkit/src/entities/slice_creator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index c4097c0023..0b20111c03 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -85,14 +85,14 @@ const makeWrappedReducerCreator = selectEntityState: (state: State) => EntityState, ) => ( - reducer: ( + mutator: ( state: EntityState, action: PayloadAction, ) => void, ): CaseReducerDefinition> => - reducerCreator.create((state: State, action) => - reducer(selectEntityState(state), action), - ) + reducerCreator.create((state: State, action) => { + mutator(selectEntityState(state), action) + }) export function createEntityMethods< T, From 88fa75f05481ed07da873bc804f6ae750f4138b1 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Tue, 3 Sep 2024 10:19:03 +0100 Subject: [PATCH 09/15] avoid declare module issue --- packages/toolkit/src/asyncThunkCreator.ts | 68 ++++++++++--------- packages/toolkit/src/createSlice.ts | 13 ++-- .../toolkit/src/entities/slice_creator.ts | 14 +--- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/packages/toolkit/src/asyncThunkCreator.ts b/packages/toolkit/src/asyncThunkCreator.ts index a2a974a1bf..b7e9ec7cb7 100644 --- a/packages/toolkit/src/asyncThunkCreator.ts +++ b/packages/toolkit/src/asyncThunkCreator.ts @@ -10,42 +10,12 @@ import type { CaseReducer } from './createReducer' import type { CreatorCaseReducers, ReducerCreator, + ReducerCreatorEntry, ReducerDefinition, } from './createSlice' import { ReducerType } from './createSlice' import type { Id } from './tsHelpers' -export type AsyncThunkCreatorExposes< - State, - CaseReducers extends CreatorCaseReducers, -> = { - actions: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - infer ThunkArg, - infer Returned, - infer ThunkApiConfig - > - ? AsyncThunk - : never - } - caseReducers: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - any, - any, - any - > - ? Id< - Pick< - Required, - 'fulfilled' | 'rejected' | 'pending' | 'settled' - > - > - : never - } -} - export type AsyncThunkSliceReducerConfig< State, ThunkArg extends any, @@ -145,6 +115,42 @@ export interface AsyncThunkCreator< > } +export type AsyncThunkCreators< + State, + CaseReducers extends CreatorCaseReducers, +> = { + [ReducerType.asyncThunk]: ReducerCreatorEntry< + AsyncThunkCreator, + { + actions: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + infer ThunkArg, + infer Returned, + infer ThunkApiConfig + > + ? AsyncThunk + : never + } + caseReducers: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + any, + any, + any + > + ? Id< + Pick< + Required, + 'fulfilled' | 'rejected' | 'pending' | 'settled' + > + > + : never + } + } + > +} + export const asyncThunkCreator: ReducerCreator = { type: ReducerType.asyncThunk, create: /* @__PURE__ */ (() => { diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index 76dfeeb730..dcf3ee98a3 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,9 +1,6 @@ import type { Action, Reducer, UnknownAction } from 'redux' import type { Selector } from 'reselect' -import type { - AsyncThunkCreator, - AsyncThunkCreatorExposes, -} from './asyncThunkCreator' +import type { AsyncThunkCreators } from './asyncThunkCreator' import type { InjectConfig } from './combineSlices' import type { ActionCreatorWithoutPayload, @@ -20,6 +17,7 @@ import type { ReducerWithInitialState, } from './createReducer' import { createReducer, makeGetInitialState } from './createReducer' +import type { EntityCreators } from './entities/slice_creator' import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { @@ -76,7 +74,8 @@ export interface SliceReducerCreators< CaseReducers extends CreatorCaseReducers, Name extends string, ReducerPath extends string, -> { +> extends AsyncThunkCreators, + EntityCreators { [ReducerType.reducer]: ReducerCreatorEntry< { ( @@ -142,10 +141,6 @@ export interface SliceReducerCreators< } } > - [ReducerType.asyncThunk]: ReducerCreatorEntry< - AsyncThunkCreator, - AsyncThunkCreatorExposes - > } export type ReducerCreators< diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index 0b20111c03..b1fbe29903 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -1,6 +1,5 @@ import type { CaseReducerDefinition, - CreatorCaseReducers, PayloadAction, ReducerCreator, ReducerCreatorEntry, @@ -30,7 +29,7 @@ export interface EntityMethodsCreatorConfig< pluralName?: Plural } -type EntityMethodsCreator = +export type EntityMethodsCreator = State extends EntityState ? { < @@ -69,15 +68,8 @@ type EntityMethodsCreator = >, ) => EntityReducers -declare module '@reduxjs/toolkit' { - export interface SliceReducerCreators< - State, - CaseReducers extends CreatorCaseReducers, - Name extends string, - ReducerPath extends string, - > { - [entityMethodsCreatorType]: ReducerCreatorEntry> - } +export type EntityCreators = { + [entityMethodsCreatorType]: ReducerCreatorEntry> } const makeWrappedReducerCreator = From 11b873c02032f48415f558cab4e89946f9aecd9e Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Tue, 3 Sep 2024 10:28:55 +0100 Subject: [PATCH 10/15] fix creator issue --- packages/toolkit/src/asyncThunkCreator.ts | 56 +++++++++---------- packages/toolkit/src/createSlice.ts | 18 ++++-- .../toolkit/src/entities/slice_creator.ts | 5 -- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/toolkit/src/asyncThunkCreator.ts b/packages/toolkit/src/asyncThunkCreator.ts index b7e9ec7cb7..19fb0380ac 100644 --- a/packages/toolkit/src/asyncThunkCreator.ts +++ b/packages/toolkit/src/asyncThunkCreator.ts @@ -10,7 +10,6 @@ import type { CaseReducer } from './createReducer' import type { CreatorCaseReducers, ReducerCreator, - ReducerCreatorEntry, ReducerDefinition, } from './createSlice' import { ReducerType } from './createSlice' @@ -115,40 +114,35 @@ export interface AsyncThunkCreator< > } -export type AsyncThunkCreators< +export type AsyncThunkCreatorExposes< State, CaseReducers extends CreatorCaseReducers, > = { - [ReducerType.asyncThunk]: ReducerCreatorEntry< - AsyncThunkCreator, - { - actions: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - infer ThunkArg, - infer Returned, - infer ThunkApiConfig - > - ? AsyncThunk - : never - } - caseReducers: { - [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< - State, - any, - any, - any + actions: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + infer ThunkArg, + infer Returned, + infer ThunkApiConfig + > + ? AsyncThunk + : never + } + caseReducers: { + [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends AsyncThunkSliceReducerDefinition< + State, + any, + any, + any + > + ? Id< + Pick< + Required, + 'fulfilled' | 'rejected' | 'pending' | 'settled' + > > - ? Id< - Pick< - Required, - 'fulfilled' | 'rejected' | 'pending' | 'settled' - > - > - : never - } - } - > + : never + } } export const asyncThunkCreator: ReducerCreator = { diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index dcf3ee98a3..5bf5c9744c 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -1,6 +1,9 @@ import type { Action, Reducer, UnknownAction } from 'redux' import type { Selector } from 'reselect' -import type { AsyncThunkCreators } from './asyncThunkCreator' +import type { + AsyncThunkCreator, + AsyncThunkCreatorExposes, +} from './asyncThunkCreator' import type { InjectConfig } from './combineSlices' import type { ActionCreatorWithoutPayload, @@ -17,7 +20,10 @@ import type { ReducerWithInitialState, } from './createReducer' import { createReducer, makeGetInitialState } from './createReducer' -import type { EntityCreators } from './entities/slice_creator' +import type { + EntityMethodsCreator, + entityMethodsCreatorType, +} from './entities/slice_creator' import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { @@ -74,8 +80,7 @@ export interface SliceReducerCreators< CaseReducers extends CreatorCaseReducers, Name extends string, ReducerPath extends string, -> extends AsyncThunkCreators, - EntityCreators { +> { [ReducerType.reducer]: ReducerCreatorEntry< { ( @@ -141,6 +146,11 @@ export interface SliceReducerCreators< } } > + [ReducerType.asyncThunk]: ReducerCreatorEntry< + AsyncThunkCreator, + AsyncThunkCreatorExposes + > + [entityMethodsCreatorType]: ReducerCreatorEntry> } export type ReducerCreators< diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index b1fbe29903..d1fa780985 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -2,7 +2,6 @@ import type { CaseReducerDefinition, PayloadAction, ReducerCreator, - ReducerCreatorEntry, } from '@reduxjs/toolkit' import { reducerCreator } from '../createSlice' import type { WithRequiredProp } from '../tsHelpers' @@ -68,10 +67,6 @@ export type EntityMethodsCreator = >, ) => EntityReducers -export type EntityCreators = { - [entityMethodsCreatorType]: ReducerCreatorEntry> -} - const makeWrappedReducerCreator = ( selectEntityState: (state: State) => EntityState, From a114a444b45c143454d8bb3d210a6f6125fc7cfb Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Fri, 25 Oct 2024 09:48:43 +0100 Subject: [PATCH 11/15] try using a string rather than a symbol for creator type --- packages/toolkit/src/entities/slice_creator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index d1fa780985..f0a329d875 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -14,7 +14,7 @@ import type { } from './models' import { capitalize } from './utils' -export const entityMethodsCreatorType = /*@__PURE__*/ Symbol() +export const entityMethodsCreatorType = '@@rtk/entityMethodsCreator' export interface EntityMethodsCreatorConfig< T, From 07a9acfe5cbeadfefbccdb779ac6ce678cfa6392 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Fri, 25 Oct 2024 10:23:45 +0100 Subject: [PATCH 12/15] rename generic to avoid conflict --- packages/toolkit/src/entities/models.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/toolkit/src/entities/models.ts b/packages/toolkit/src/entities/models.ts index 70c84c638a..346bad0e65 100644 --- a/packages/toolkit/src/entities/models.ts +++ b/packages/toolkit/src/entities/models.ts @@ -2,7 +2,7 @@ import type { UncheckedIndexedAccess } from '../uncheckedindexed' import type { Draft } from 'immer' import type { PayloadAction } from '../createAction' import type { GetSelectorsOptions } from './state_selectors' -import type { CastAny, Id as Compute } from '../tsHelpers' +import type { CastAny, Id } from '../tsHelpers' import type { CaseReducerDefinition } from '../createSlice' import type { CaseReducer } from '../createReducer' @@ -163,14 +163,16 @@ export interface EntityStateAdapter { export type EntitySelectors< T, V, - Id extends EntityId, + IdType extends EntityId, Single extends string = '', Plural extends string = DefaultPlural, > = Compute< { - [K in `select${Capitalize}Ids`]: (state: V) => Id[] + [K in `select${Capitalize}Ids`]: (state: V) => IdType[] } & { - [K in `select${Capitalize}Entities`]: (state: V) => Record + [K in `select${Capitalize}Entities`]: ( + state: V, + ) => Record } & { [K in `selectAll${Capitalize}`]: (state: V) => T[] } & { @@ -178,7 +180,7 @@ export type EntitySelectors< } & { [K in `select${Capitalize}ById`]: ( state: V, - id: Id, + id: IdType, ) => Compute> } > From 0102cc370ce8298079add5626dde0ab40cf158f8 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Fri, 25 Oct 2024 10:37:38 +0100 Subject: [PATCH 13/15] fix Id usage --- packages/toolkit/src/entities/models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/entities/models.ts b/packages/toolkit/src/entities/models.ts index 346bad0e65..2b20b84826 100644 --- a/packages/toolkit/src/entities/models.ts +++ b/packages/toolkit/src/entities/models.ts @@ -166,7 +166,7 @@ export type EntitySelectors< IdType extends EntityId, Single extends string = '', Plural extends string = DefaultPlural, -> = Compute< +> = Id< { [K in `select${Capitalize}Ids`]: (state: V) => IdType[] } & { @@ -181,7 +181,7 @@ export type EntitySelectors< [K in `select${Capitalize}ById`]: ( state: V, id: IdType, - ) => Compute> + ) => Id> } > From 6d0db5ed59ef926ff725ef00ded2d3309cfc7773 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Fri, 25 Oct 2024 14:18:08 +0100 Subject: [PATCH 14/15] add entity methods creator to ReducerType enum --- packages/toolkit/src/createSlice.ts | 8 +++----- packages/toolkit/src/entities/slice_creator.ts | 10 +++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index 5bf5c9744c..af004f2829 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -20,10 +20,7 @@ import type { ReducerWithInitialState, } from './createReducer' import { createReducer, makeGetInitialState } from './createReducer' -import type { - EntityMethodsCreator, - entityMethodsCreatorType, -} from './entities/slice_creator' +import type { EntityMethodsCreator } from './entities/slice_creator' import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { @@ -40,6 +37,7 @@ export enum ReducerType { reducer = 'reducer', reducerWithPrepare = 'reducerWithPrepare', asyncThunk = 'asyncThunk', + entityMethods = 'entityMethods', } export type RegisteredReducerType = keyof SliceReducerCreators< @@ -150,7 +148,7 @@ export interface SliceReducerCreators< AsyncThunkCreator, AsyncThunkCreatorExposes > - [entityMethodsCreatorType]: ReducerCreatorEntry> + [ReducerType.entityMethods]: ReducerCreatorEntry> } export type ReducerCreators< diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index f0a329d875..49f7a0a8a0 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -3,7 +3,7 @@ import type { PayloadAction, ReducerCreator, } from '@reduxjs/toolkit' -import { reducerCreator } from '../createSlice' +import { reducerCreator, ReducerType } from '../createSlice' import type { WithRequiredProp } from '../tsHelpers' import type { EntityAdapter, @@ -14,8 +14,6 @@ import type { } from './models' import { capitalize } from './utils' -export const entityMethodsCreatorType = '@@rtk/entityMethodsCreator' - export interface EntityMethodsCreatorConfig< T, Id extends EntityId, @@ -124,9 +122,7 @@ export function createEntityMethods< return reducers as any } -export const entityMethodsCreator: ReducerCreator< - typeof entityMethodsCreatorType -> = { - type: entityMethodsCreatorType, +export const entityMethodsCreator: ReducerCreator = { + type: ReducerType.entityMethods, create: createEntityMethods, } From 4687c2737931649d3b83cda9052a8c4296ea1bbb Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Wed, 13 Nov 2024 16:55:56 +0000 Subject: [PATCH 15/15] remove export --- packages/toolkit/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 59405818b3..17275f6d5b 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -134,7 +134,6 @@ export type { export { createEntityMethods, entityMethodsCreator, - entityMethodsCreatorType, } from './entities/slice_creator' export {