Skip to content

Commit 795af40

Browse files
authored
Add setOne and setMany to entity adapter (#969)
upsertOne and upsertMany merge the passed value with the existing item, but there was no direct way to replace items. setOne and setMany solve this and replace entirely the previous value with the new one.
1 parent 4462647 commit 795af40

File tree

6 files changed

+339
-1
lines changed

6 files changed

+339
-1
lines changed

etc/redux-toolkit.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ export interface EntityStateAdapter<T> {
266266
// (undocumented)
267267
setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: PayloadAction<T[] | Record<EntityId, T>>): S;
268268
// (undocumented)
269+
setMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[] | Record<EntityId, T>): S;
270+
// (undocumented)
271+
setMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: PayloadAction<T[] | Record<EntityId, T>>): S;
272+
// (undocumented)
273+
setOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S;
274+
// (undocumented)
275+
setOne<S extends EntityState<T>>(state: PreventAny<S, T>, action: PayloadAction<T>): S;
276+
// (undocumented)
269277
updateMany<S extends EntityState<T>>(state: PreventAny<S, T>, updates: Update<T>[]): S;
270278
// (undocumented)
271279
updateMany<S extends EntityState<T>>(state: PreventAny<S, T>, updates: PayloadAction<Update<T>[]>): S;

src/entities/models.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ export interface EntityStateAdapter<T> {
7272
entities: PayloadAction<T[] | Record<EntityId, T>>
7373
): S
7474

75+
setOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
76+
setOne<S extends EntityState<T>>(
77+
state: PreventAny<S, T>,
78+
action: PayloadAction<T>
79+
): S
80+
setMany<S extends EntityState<T>>(
81+
state: PreventAny<S, T>,
82+
entities: T[] | Record<EntityId, T>
83+
): S
84+
setMany<S extends EntityState<T>>(
85+
state: PreventAny<S, T>,
86+
entities: PayloadAction<T[] | Record<EntityId, T>>
87+
): S
7588
setAll<S extends EntityState<T>>(
7689
state: PreventAny<S, T>,
7790
entities: T[] | Record<EntityId, T>

src/entities/sorted_state_adapter.test.ts

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
BookModel,
66
TheGreatGatsby,
77
AClockworkOrange,
8-
AnimalFarm
8+
AnimalFarm,
9+
TheHobbit
910
} from './fixtures/book'
1011
import { createNextState } from '..'
1112

@@ -464,6 +465,81 @@ describe('Sorted State Adapter', () => {
464465
})
465466
})
466467

468+
it('should let you add a new entity in the state with setOne() and keep the sorting', () => {
469+
const withMany = adapter.setAll(state, [AnimalFarm, TheHobbit])
470+
const withOneMore = adapter.setOne(withMany, TheGreatGatsby)
471+
expect(withOneMore).toEqual({
472+
ids: [AnimalFarm.id, TheGreatGatsby.id, TheHobbit.id],
473+
entities: {
474+
[AnimalFarm.id]: AnimalFarm,
475+
[TheHobbit.id]: TheHobbit,
476+
[TheGreatGatsby.id]: TheGreatGatsby
477+
}
478+
})
479+
})
480+
481+
it('should let you replace an entity in the state with setOne()', () => {
482+
let withOne = adapter.setOne(state, TheHobbit)
483+
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
484+
withOne = adapter.setOne(withOne, changeWithoutAuthor)
485+
486+
expect(withOne).toEqual({
487+
ids: [TheHobbit.id],
488+
entities: {
489+
[TheHobbit.id]: changeWithoutAuthor
490+
}
491+
})
492+
})
493+
494+
it('should do nothing when setMany is given an empty array', () => {
495+
const withMany = adapter.setAll(state, [TheGreatGatsby])
496+
497+
const withUpserts = adapter.setMany(withMany, [])
498+
499+
expect(withUpserts).toEqual({
500+
ids: [TheGreatGatsby.id],
501+
entities: {
502+
[TheGreatGatsby.id]: TheGreatGatsby
503+
}
504+
})
505+
})
506+
507+
it('should let you set many entities in the state', () => {
508+
const firstChange = { id: TheHobbit.id, title: 'Silmarillion' }
509+
const withMany = adapter.setAll(state, [TheHobbit])
510+
511+
const withSetMany = adapter.setMany(withMany, [
512+
firstChange,
513+
AClockworkOrange
514+
])
515+
516+
expect(withSetMany).toEqual({
517+
ids: [AClockworkOrange.id, TheHobbit.id],
518+
entities: {
519+
[TheHobbit.id]: firstChange,
520+
[AClockworkOrange.id]: AClockworkOrange
521+
}
522+
})
523+
})
524+
525+
it('should let you set many entities in the state when passing in a dictionary', () => {
526+
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
527+
const withMany = adapter.setAll(state, [TheHobbit])
528+
529+
const withSetMany = adapter.setMany(withMany, {
530+
[TheHobbit.id]: changeWithoutAuthor,
531+
[AClockworkOrange.id]: AClockworkOrange
532+
})
533+
534+
expect(withSetMany).toEqual({
535+
ids: [AClockworkOrange.id, TheHobbit.id],
536+
entities: {
537+
[TheHobbit.id]: changeWithoutAuthor,
538+
[AClockworkOrange.id]: AClockworkOrange
539+
}
540+
})
541+
})
542+
467543
describe('can be used mutably when wrapped in createNextState', () => {
468544
test('removeAll', () => {
469545
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
@@ -678,6 +754,79 @@ describe('Sorted State Adapter', () => {
678754
`)
679755
})
680756

757+
test('setOne (insert)', () => {
758+
const result = createNextState(state, draft => {
759+
adapter.setOne(draft, TheGreatGatsby)
760+
})
761+
expect(result).toMatchInlineSnapshot(`
762+
Object {
763+
"entities": Object {
764+
"tgg": Object {
765+
"id": "tgg",
766+
"title": "The Great Gatsby",
767+
},
768+
},
769+
"ids": Array [
770+
"tgg",
771+
],
772+
}
773+
`)
774+
})
775+
776+
test('setOne (update)', () => {
777+
const withOne = adapter.setOne(state, TheHobbit)
778+
const result = createNextState(withOne, draft => {
779+
adapter.setOne(draft, {
780+
id: TheHobbit.id,
781+
title: 'Silmarillion'
782+
})
783+
})
784+
expect(result).toMatchInlineSnapshot(`
785+
Object {
786+
"entities": Object {
787+
"th": Object {
788+
"id": "th",
789+
"title": "Silmarillion",
790+
},
791+
},
792+
"ids": Array [
793+
"th",
794+
],
795+
}
796+
`)
797+
})
798+
799+
test('setMany', () => {
800+
const withOne = adapter.setOne(state, TheHobbit)
801+
const result = createNextState(withOne, draft => {
802+
adapter.setMany(draft, [
803+
{
804+
id: TheHobbit.id,
805+
title: 'Silmarillion'
806+
},
807+
AnimalFarm
808+
])
809+
})
810+
expect(result).toMatchInlineSnapshot(`
811+
Object {
812+
"entities": Object {
813+
"af": Object {
814+
"id": "af",
815+
"title": "Animal Farm",
816+
},
817+
"th": Object {
818+
"id": "th",
819+
"title": "Silmarillion",
820+
},
821+
},
822+
"ids": Array [
823+
"af",
824+
"th",
825+
],
826+
}
827+
`)
828+
})
829+
681830
test('removeOne', () => {
682831
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
683832
const result = createNextState(withTwo, draft => {

src/entities/sorted_state_adapter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ export function createSortedStateAdapter<T>(
4343
}
4444
}
4545

46+
function setOneMutably(entity: T, state: R): void {
47+
return setManyMutably([entity], state)
48+
}
49+
50+
function setManyMutably(
51+
newEntities: T[] | Record<EntityId, T>,
52+
state: R
53+
): void {
54+
newEntities = ensureEntitiesArray(newEntities)
55+
if (newEntities.length !== 0) {
56+
merge(newEntities, state)
57+
}
58+
}
59+
4660
function setAllMutably(
4761
newEntities: T[] | Record<EntityId, T>,
4862
state: R
@@ -140,6 +154,8 @@ export function createSortedStateAdapter<T>(
140154
addOne: createStateOperator(addOneMutably),
141155
updateOne: createStateOperator(updateOneMutably),
142156
upsertOne: createStateOperator(upsertOneMutably),
157+
setOne: createStateOperator(setOneMutably),
158+
setMany: createStateOperator(setManyMutably),
143159
setAll: createStateOperator(setAllMutably),
144160
addMany: createStateOperator(addManyMutably),
145161
updateMany: createStateOperator(updateManyMutably),

src/entities/unsorted_state_adapter.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,65 @@ describe('Unsorted State Adapter', () => {
355355
})
356356
})
357357

358+
it('should let you add a new entity in the state with setOne()', () => {
359+
const withOne = adapter.setOne(state, TheGreatGatsby)
360+
expect(withOne).toEqual({
361+
ids: [TheGreatGatsby.id],
362+
entities: {
363+
[TheGreatGatsby.id]: TheGreatGatsby
364+
}
365+
})
366+
})
367+
368+
it('should let you replace an entity in the state with setOne()', () => {
369+
let withOne = adapter.setOne(state, TheHobbit)
370+
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
371+
withOne = adapter.setOne(withOne, changeWithoutAuthor)
372+
373+
expect(withOne).toEqual({
374+
ids: [TheHobbit.id],
375+
entities: {
376+
[TheHobbit.id]: changeWithoutAuthor
377+
}
378+
})
379+
})
380+
381+
it('should let you set many entities in the state', () => {
382+
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
383+
const withMany = adapter.setAll(state, [TheHobbit])
384+
385+
const withSetMany = adapter.setMany(withMany, [
386+
changeWithoutAuthor,
387+
AClockworkOrange
388+
])
389+
390+
expect(withSetMany).toEqual({
391+
ids: [TheHobbit.id, AClockworkOrange.id],
392+
entities: {
393+
[TheHobbit.id]: changeWithoutAuthor,
394+
[AClockworkOrange.id]: AClockworkOrange
395+
}
396+
})
397+
})
398+
399+
it('should let you set many entities in the state when passing in a dictionary', () => {
400+
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
401+
const withMany = adapter.setAll(state, [TheHobbit])
402+
403+
const withSetMany = adapter.setMany(withMany, {
404+
[TheHobbit.id]: changeWithoutAuthor,
405+
[AClockworkOrange.id]: AClockworkOrange
406+
})
407+
408+
expect(withSetMany).toEqual({
409+
ids: [TheHobbit.id, AClockworkOrange.id],
410+
entities: {
411+
[TheHobbit.id]: changeWithoutAuthor,
412+
[AClockworkOrange.id]: AClockworkOrange
413+
}
414+
})
415+
})
416+
358417
describe('can be used mutably when wrapped in createNextState', () => {
359418
test('removeAll', () => {
360419
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
@@ -583,6 +642,79 @@ describe('Unsorted State Adapter', () => {
583642
`)
584643
})
585644

645+
test('setOne (insert)', () => {
646+
const result = createNextState(state, draft => {
647+
adapter.setOne(draft, TheGreatGatsby)
648+
})
649+
expect(result).toMatchInlineSnapshot(`
650+
Object {
651+
"entities": Object {
652+
"tgg": Object {
653+
"id": "tgg",
654+
"title": "The Great Gatsby",
655+
},
656+
},
657+
"ids": Array [
658+
"tgg",
659+
],
660+
}
661+
`)
662+
})
663+
664+
test('setOne (update)', () => {
665+
const withOne = adapter.setOne(state, TheHobbit)
666+
const result = createNextState(withOne, draft => {
667+
adapter.setOne(draft, {
668+
id: TheHobbit.id,
669+
title: 'Silmarillion'
670+
})
671+
})
672+
expect(result).toMatchInlineSnapshot(`
673+
Object {
674+
"entities": Object {
675+
"th": Object {
676+
"id": "th",
677+
"title": "Silmarillion",
678+
},
679+
},
680+
"ids": Array [
681+
"th",
682+
],
683+
}
684+
`)
685+
})
686+
687+
test('setMany', () => {
688+
const withOne = adapter.setOne(state, TheHobbit)
689+
const result = createNextState(withOne, draft => {
690+
adapter.setMany(draft, [
691+
{
692+
id: TheHobbit.id,
693+
title: 'Silmarillion'
694+
},
695+
AnimalFarm
696+
])
697+
})
698+
expect(result).toMatchInlineSnapshot(`
699+
Object {
700+
"entities": Object {
701+
"af": Object {
702+
"id": "af",
703+
"title": "Animal Farm",
704+
},
705+
"th": Object {
706+
"id": "th",
707+
"title": "Silmarillion",
708+
},
709+
},
710+
"ids": Array [
711+
"th",
712+
"af",
713+
],
714+
}
715+
`)
716+
})
717+
586718
test('removeOne', () => {
587719
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
588720
const result = createNextState(withTwo, draft => {

0 commit comments

Comments
 (0)