Skip to content

Commit

Permalink
Feature/entity selectors (#440)
Browse files Browse the repository at this point in the history
* Add a `selectById` selector

* Export Reselect types

* Update API
  • Loading branch information
markerikson authored Mar 21, 2020
1 parent f64f750 commit 3025d77
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 6 deletions.
4 changes: 3 additions & 1 deletion docs/api/createEntityAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export interface EntitySelectors<T, V> {
selectEntities: (state: V) => Dictionary<T>
selectAll: (state: V) => T[]
selectTotal: (state: V) => number
selectById: (state: V, id: EntityId) => T | undefined
}

export interface EntityAdapter<T> extends EntityStateAdapter<T> {
Expand Down Expand Up @@ -247,12 +248,13 @@ const booksSlice = createSlice({

### Selector Functions

The entity adapter will contain a `getSelectors()` function that returns a set of four selectors that know how to read the contents of an entity state object:
The entity adapter will contain a `getSelectors()` function that returns a set of selectors that know how to read the contents of an entity state object:

- `selectIds`: returns the `state.ids` array
- `selectEntities`: returns the `state.entities` lookup table
- `selectAll`: maps over the `state.ids` array, and returns an array of entities in the same order
- `selectTotal`: returns the total number of entities being stored in this state
- `selectById`: given the state and an entity ID, returns the entity with that ID or `undefined`

Each selector function will be created using the `createSelector` function from Reselect, to enable memoizing calculation of the results.

Expand Down
12 changes: 12 additions & 0 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import { DeepPartial } from 'redux';
import { Dispatch } from 'redux';
import { Draft } from 'immer';
import { Middleware } from 'redux';
import { OutputParametricSelector } from 'reselect';
import { OutputSelector } from 'reselect';
import { ParametricSelector } from 'reselect';
import { Reducer } from 'redux';
import { ReducersMapObject } from 'redux';
import { Selector } from 'reselect';
import { Store } from 'redux';
import { StoreEnhancer } from 'redux';
import { ThunkAction } from 'redux-thunk';
Expand Down Expand Up @@ -212,6 +216,12 @@ export type IdSelector<T> = (model: T) => EntityId;
// @public
export function isPlain(val: any): boolean;

export { OutputParametricSelector }

export { OutputSelector }

export { ParametricSelector }

// @public
export type PayloadAction<P = void, T extends string = string, M = never, E = never> = {
payload: P;
Expand Down Expand Up @@ -240,6 +250,8 @@ export type PrepareAction<P> = ((...args: any[]) => {
error: any;
});

export { Selector }

// @public
export interface SerializableStateInvariantMiddlewareOptions {
getEntries?: (value: any) => [string, any][];
Expand Down
1 change: 1 addition & 0 deletions src/entities/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export interface EntitySelectors<T, V> {
selectEntities: (state: V) => Dictionary<T>
selectAll: (state: V) => T[]
selectTotal: (state: V) => number
selectById: (state: V, id: EntityId) => T | undefined
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/entities/state_selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ describe('Entity State Selectors', () => {

expect(total).toEqual(3)
})

it('should create a selector for selecting a single item by ID', () => {
const first = selectors.selectById(state, AClockworkOrange.id)
expect(first).toBe(AClockworkOrange)
const second = selectors.selectById(state, AnimalFarm.id)
expect(second).toBe(AnimalFarm)
})
})

describe('Uncomposed Selectors', () => {
Expand Down Expand Up @@ -110,5 +117,12 @@ describe('Entity State Selectors', () => {

expect(total).toEqual(3)
})

it('should create a selector for selecting a single item by ID', () => {
const first = selectors.selectById(state, AClockworkOrange.id)
expect(first).toBe(AClockworkOrange)
const second = selectors.selectById(state, AnimalFarm.id)
expect(second).toBe(AnimalFarm)
})
})
})
18 changes: 14 additions & 4 deletions src/entities/state_selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { EntityState, EntitySelectors, Dictionary } from './models'
import { EntityState, EntitySelectors, Dictionary, EntityId } from './models'

export function createSelectorsFactory<T>() {
function getSelectors(): EntitySelectors<T, EntityState<T>>
Expand All @@ -10,30 +10,40 @@ export function createSelectorsFactory<T>() {
selectState?: (state: any) => EntityState<T>
): EntitySelectors<T, any> {
const selectIds = (state: any) => state.ids

const selectEntities = (state: EntityState<T>) => state.entities

const selectAll = createSelector(
selectIds,
selectEntities,
(ids: T[], entities: Dictionary<T>): any =>
ids.map((id: any) => (entities as any)[id])
)

const selectId = (_: any, id: EntityId) => id

const selectById = (entities: Dictionary<T>, id: EntityId) => entities[id]

const selectTotal = createSelector(selectIds, ids => ids.length)

if (!selectState) {
return {
selectIds,
selectEntities,
selectAll,
selectTotal
selectTotal,
selectById: createSelector(selectEntities, selectId, selectById)
}
}

const selectGlobalizedEntities = createSelector(selectState, selectEntities)

return {
selectIds: createSelector(selectState, selectIds),
selectEntities: createSelector(selectState, selectEntities),
selectEntities: selectGlobalizedEntities,
selectAll: createSelector(selectState, selectAll),
selectTotal: createSelector(selectState, selectTotal)
selectTotal: createSelector(selectState, selectTotal),
selectById: createSelector(selectGlobalizedEntities, selectId, selectById)
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { enableES5 } from 'immer'
export * from 'redux'
export { default as createNextState, Draft } from 'immer'
export { createSelector } from 'reselect'
export {
createSelector,
Selector,
OutputParametricSelector,
OutputSelector,
ParametricSelector
} from 'reselect'
export { ThunkAction } from 'redux-thunk'

// We deliberately enable Immer's ES5 support, on the grounds that
Expand Down

0 comments on commit 3025d77

Please sign in to comment.