UNPKG

@gjv/redux-slice-factory

Version:

Generic factory functions for common slice data structures

326 lines (291 loc) 14.5 kB
import { createEntityAdapter, createSelector, createSlice, CaseReducer, Comparer, EntityId, EntitySelectors, EntityState as ReduxEntityState, PayloadAction, Update, SerializedError, createNextState, } from '@reduxjs/toolkit' import StatusEnum from '../../constants/StatusEnum' import EntityState, { IEntityState } from '../../models/entity-state' import { IMetaSliceReducers, IMetaSliceSelectors, ISlice, ISliceName, ISliceOptions, ISliceSelectors } from '../../types' import { getISOString } from '../../utilities' /** * @public */ export interface IEntitySliceReducers < TSliceState, TEntity, TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum, TError extends SerializedError = SerializedError > extends IMetaSliceReducers<TSliceState, TStatusEnum, TError> { /** * This will modify the slice. This adds one entity to the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter} */ addOne: CaseReducer<TSliceState, PayloadAction<TEntity>>; /** * This will modify the slice. This adds many entities to the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter} */ addMany: CaseReducer<TSliceState, PayloadAction<Array<TEntity> | Record<EntityId, TEntity>>>; /** * This will hydrate the slice. This sets one entity of the slice and lastHydrated, and resets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.setOne} */ hydrateOne: CaseReducer<TSliceState, PayloadAction<TEntity>>; /** * This will hydrate the slice. This sets many entities of the slice and lastHydrated, and resets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.setMany} */ hydrateMany: CaseReducer<TSliceState, PayloadAction<Array<TEntity> | Record<EntityId, TEntity>>>; /** * This will hydrate the slice. This sets all entities of the slice and lastHydrated, and resets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.setAll} */ hydrateAll: CaseReducer<TSliceState, PayloadAction<Array<TEntity> | Record<EntityId, TEntity>>>; /** * This will modify the slice. This updates one entity of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.updateOne} */ updateOne: CaseReducer<TSliceState, PayloadAction<Update<TEntity>>>; /** * This will modify the slice. This updates many entities of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.updateMany} */ updateMany: CaseReducer<TSliceState, PayloadAction<Array<Update<TEntity>>>>; /** * This will modify the slice. This updates or inserts one entity of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.upsertOne} */ upsertOne: CaseReducer<TSliceState, PayloadAction<TEntity>>; /** * This will modify the slice. This updates or inserts many entities of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.upsertMany} */ upsertMany: CaseReducer<TSliceState, PayloadAction<Array<TEntity> | Record<EntityId, TEntity>>>; /** * This will modify the slice. This removes one entity of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.removeOne} */ removeOne: CaseReducer<TSliceState, PayloadAction<EntityId>>; /** * This will modify the slice. This removes many entities of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.removeMany} */ removeMany: CaseReducer<TSliceState, PayloadAction<Array<EntityId>>>; /** * This will modify the slice. This removes all entities of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.removeAll} */ removeAll: CaseReducer<TSliceState, PayloadAction>; /** * This will reset the slice to its initial state. */ reset: CaseReducer<TSliceState, PayloadAction>; /** * This will modify the slice. This sets all entities of the slice and sets lastModified. * @see {@link @reduxjs/toolkit#EntityStateAdapter.setAll} */ setAll: CaseReducer<TSliceState, PayloadAction<Array<TEntity> | Record<EntityId, TEntity>>>; } /** * @public */ export interface IEntitySliceSelectors< TGlobalState, TEntity, TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum, TError extends SerializedError = SerializedError > extends EntitySelectors<TEntity, TGlobalState>, ISliceSelectors<TGlobalState, IEntityState<TEntity, TStatusEnum, TError>>, IMetaSliceSelectors<TGlobalState, IEntityState<TEntity, TStatusEnum, TError>, TStatusEnum, TError> {} /** * @public */ export type IEntitySlice< TGlobalState, TEntity, TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum, TError extends SerializedError = SerializedError > = ISlice< TGlobalState, IEntityState<TEntity, TStatusEnum, TError>, IEntitySliceReducers<IEntityState<TEntity, TStatusEnum, TError>, TEntity, TStatusEnum, TError>, IEntitySliceSelectors<TGlobalState, TEntity, TStatusEnum, TError> > /** * @public */ export interface ICreateEntitySliceOptions< TGlobalState, TEntity, TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum, TError extends SerializedError = SerializedError > extends ISliceOptions<TGlobalState, IEntityState<TEntity, TStatusEnum, TError>> { selectId: (o: TEntity) => EntityId; sortComparer: false | Comparer<TEntity>; } /** * @public */ function createEntitySlice< TGlobalState, TEntity, TStatusEnum extends keyof typeof StatusEnum |& string = keyof typeof StatusEnum, TError extends SerializedError = SerializedError >(options: ICreateEntitySliceOptions<TGlobalState, TEntity, TStatusEnum, TError>): IEntitySlice<TGlobalState, TEntity, TStatusEnum, TError> { type ISliceState = IEntityState<TEntity, TStatusEnum, TError> const defaultCanRequestSelector = (sliceState: ISliceState): boolean => sliceState.status !== StatusEnum.Requesting && sliceState.error === null && sliceState.lastModified === null const defaultShouldRequestSelector = (sliceState: ISliceState, canRequest: boolean): boolean => canRequest && sliceState.lastHydrated === null const { name, selectId, sortComparer, selectSliceState, selectShouldRequest = defaultShouldRequestSelector, selectCanRequest = defaultCanRequestSelector, initialState = {}, createTimestamp = getISOString, } = options // intentional, necessary with immer /* eslint-disable no-param-reassign */ const setEntityState = (state: ISliceState, entityState: ReduxEntityState<TEntity>): void => { state.ids = entityState.ids state.entities = entityState.entities } const setError = (state: ISliceState, error: TError | null): void => { state.error = error === null ? null : error } const setStatus = (state: ISliceState, status: TStatusEnum): void => { state.status = status } const setLastModified = (state: ISliceState, lastModified: string | null): void => { state.lastModified = lastModified } const setLastHydrated = (state: ISliceState, lastHydrated: string | null): void => { state.lastHydrated = lastHydrated } /* eslint-enable no-param-reassign */ const modifyState = (state: ISliceState, entityState: ReduxEntityState<TEntity>): void => { setEntityState(state, entityState) // TODO: should not have a side effect: https://redux.js.org/style-guide/style-guide#reducers-must-not-have-side-effects setLastModified(state, createTimestamp()) } const hydrateState = (state: ISliceState, entityState: ReduxEntityState<TEntity>): void => { setEntityState(state, entityState) setLastModified(state, null) // TODO: should not have a side effect: https://redux.js.org/style-guide/style-guide#reducers-must-not-have-side-effects setLastHydrated(state, createTimestamp()) } const entityAdapter = createEntityAdapter({ selectId: selectId, sortComparer: sortComparer, }) const initialEntityState = entityAdapter.getInitialState(initialState) const initialSliceState = EntityState.create<TEntity, TStatusEnum, TError>(initialEntityState) /* eslint-disable no-param-reassign */ const slice = createSlice<ISliceState, IEntitySliceReducers<ISliceState, TEntity, TStatusEnum, TError>, ISliceName<TGlobalState>>({ name: name, initialState: initialSliceState, reducers: { addOne: (state, action) => { const newEntityState = entityAdapter.addOne(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, addMany: (state, action) => { const newEntityState = entityAdapter.addMany(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, hydrateOne: (state, action) => { const newEntityState = entityAdapter.upsertOne(state as ISliceState, action.payload) hydrateState(state as ISliceState, newEntityState) }, hydrateMany: (state, action) => { const newEntityState = entityAdapter.upsertMany(state as ISliceState, action.payload) hydrateState(state as ISliceState, newEntityState) }, hydrateAll: (state, action) => { const newEntityState = entityAdapter.setAll(state as ISliceState, action.payload) hydrateState(state as ISliceState, newEntityState) }, updateOne: (state, action) => { const newEntityState = entityAdapter.updateOne(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, updateMany: (state, action) => { const newEntityState = entityAdapter.updateMany(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, upsertOne: (state, action) => { const newEntityState = entityAdapter.upsertOne(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, upsertMany: (state, action) => { const newEntityState = entityAdapter.upsertMany(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, removeOne: (state, action) => { const newEntityState = entityAdapter.removeOne(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, removeMany: (state, action) => { const newEntityState = entityAdapter.removeMany(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, removeAll: (state) => { const newEntityState = entityAdapter.removeAll(state as ISliceState) modifyState(state as ISliceState, newEntityState) }, reset: () => initialSliceState, setAll: (state, action) => { const newEntityState = entityAdapter.setAll(state as ISliceState, action.payload) modifyState(state as ISliceState, newEntityState) }, setError: (state, action) => { setError(state as ISliceState, action.payload) }, setStatus: (state, action) => { setStatus(state as ISliceState, action.payload) }, setMetaState: (state, action) => { state.status = createNextState(state.status, () => action.payload.status) ?? state.status state.error = createNextState(state.error, () => action.payload.error) ?? state.error }, }, }) /* eslint-disable no-param-reassign */ type ISelectors = IEntitySliceSelectors<TGlobalState, TEntity, TStatusEnum, TError> const entitySelectors = entityAdapter.getSelectors((state: TGlobalState) => selectSliceState(state)) const selectors: ISelectors = { selectIds: entitySelectors.selectIds, selectEntities: entitySelectors.selectEntities, selectAll: entitySelectors.selectAll, selectTotal: entitySelectors.selectTotal, selectById: entitySelectors.selectById, /* TODO remove type assertion */ selectSliceState: createSelector([selectSliceState], (sliceState) => sliceState) as unknown as ISelectors['selectSliceState'], selectStatus: createSelector([selectSliceState], (sliceState) => sliceState.status) as unknown as ISelectors['selectStatus'], selectError: createSelector([selectSliceState], (sliceState) => sliceState.error) as unknown as ISelectors['selectError'], selectLastModified: createSelector([selectSliceState], (sliceState) => sliceState.lastModified) as unknown as ISelectors['selectLastModified'], selectLastHydrated: createSelector([selectSliceState], (sliceState) => sliceState.lastHydrated) as unknown as ISelectors['selectLastHydrated'], selectCanRequest: createSelector([selectSliceState], (sliceState) => selectCanRequest(sliceState)) as unknown as ISelectors['selectCanRequest'], selectShouldRequest: createSelector([selectSliceState], (sliceState) => selectShouldRequest(sliceState, selectCanRequest(sliceState))) as unknown as ISelectors['selectShouldRequest'], } return { name: slice.name, reducer: slice.reducer, actions: slice.actions, selectors: selectors, } } export default createEntitySlice