@gjv/redux-slice-factory
Version:
Generic factory functions for common slice data structures
182 lines (165 loc) • 7.88 kB
text/typescript
import {
CaseReducer,
createNextState,
createSelector,
createSlice,
Draft,
PayloadAction,
SerializedError,
} from '@reduxjs/toolkit'
import merge from 'lodash.merge'
import StatusEnum from '../../constants/StatusEnum'
import ModelState, { IModelState } from '../../models/model-state'
import { IMetaSliceReducers, IMetaSliceSelectors, ISlice, ISliceName, ISliceOptions, ISliceSelectors, IOutputSelector } from '../../types'
import { getISOString } from '../../utilities'
/**
* @public
*/
export interface IModelSliceReducers <
TSliceState,
TModel,
TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum,
TError extends SerializedError = SerializedError
> extends IMetaSliceReducers<TSliceState, TStatusEnum, TError> {
/**
* This will hydrate the slice. This sets the model and lastHydrated, and resets lastModified.
*/
hydrate: CaseReducer<TSliceState, PayloadAction<TModel>>;
/**
* This will modify the slice. This updates the model and sets lastModified.
*/
update: CaseReducer<TSliceState, PayloadAction<Partial<TModel>>>;
/**
* This will modify the slice. This sets the model and sets lastModified.
*/
set: CaseReducer<TSliceState, PayloadAction<TModel>>;
/**
* This will reset the slice to its initial state.
*/
reset: CaseReducer<TSliceState, PayloadAction>;
}
/**
* @public
*/
export interface IModelSliceSelectors <
TGlobalState,
TModel,
TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum,
TError extends SerializedError = SerializedError
>
extends
ISliceSelectors<TGlobalState, IModelState<TModel, TStatusEnum, TError>>,
IMetaSliceSelectors<TGlobalState, IModelState<TModel, TStatusEnum, TError>, TStatusEnum, TError> {
/**
* This selects the slice model.
*/
selectModel: IOutputSelector<TGlobalState, IModelState<TModel, TStatusEnum, TError>, TModel>;
}
/**
* @public
*/
export type IModelSlice<
TGlobalState,
TModel,
TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum,
TError extends SerializedError = SerializedError
> = ISlice<
TGlobalState,
IModelState<TModel, TStatusEnum, TError>,
IModelSliceReducers<IModelState<TModel, TStatusEnum, TError>, TModel, TStatusEnum, TError>,
IModelSliceSelectors<TGlobalState, TModel, TStatusEnum, TError>
>
/**
* @public
*/
export interface ICreateModelSliceOptions<
TGlobalState,
TModel,
TStatusEnum extends keyof typeof StatusEnum | & string = keyof typeof StatusEnum,
TError extends SerializedError = SerializedError
> extends ISliceOptions<TGlobalState, IModelState<TModel, TStatusEnum, TError>> {
/**
* This is how the slice will merge the current model and the update model during an update action
* @defaultValue `merge` from {@link https://www.npmjs.com/package/ts-deepmerge}
*/
handleUpdate?: (current: Draft<TModel>, update: Partial<TModel>) => TModel;
}
/**
* @public
*/
function createModelSlice<
TGlobalState,
TModel,
TStatusEnum extends keyof typeof StatusEnum |& string = keyof typeof StatusEnum,
TError extends SerializedError = SerializedError
>(options: ICreateModelSliceOptions<TGlobalState, TModel, TStatusEnum, TError>): IModelSlice<TGlobalState, TModel, TStatusEnum, TError> {
type ISliceState = IModelState<TModel, TStatusEnum, TError>
const defaultCanRequestSelector = (sliceState: ISliceState): boolean => sliceState.status !== 'Requesting'
&& sliceState.error === null
&& sliceState.lastModified === null
const defaultShouldRequestSelector = (sliceState: ISliceState, canRequest: boolean): boolean => canRequest && sliceState.lastHydrated === null
const {
name,
handleUpdate = (current, update) => merge(current, update),
selectSliceState,
selectCanRequest = defaultCanRequestSelector,
selectShouldRequest = defaultShouldRequestSelector,
initialState = {},
createTimestamp = getISOString,
} = options
const initialSliceState = ModelState.create<TModel, TStatusEnum, TError>(initialState)
const slice = createSlice<ISliceState, IModelSliceReducers<ISliceState, TModel, TStatusEnum, TError>, ISliceName<TGlobalState>>({
name: name,
initialState: initialSliceState,
// intentional, necessary with immer
/* eslint-disable no-param-reassign */
reducers: {
hydrate: (state, action) => {
state.model = createNextState(state.model, () => action.payload)
state.lastModified = null
// TODO: should not have a side effect: https://redux.js.org/style-guide/style-guide#reducers-must-not-have-side-effects
state.lastHydrated = createTimestamp()
},
update: (state, action) => {
state.model = createNextState(state.model, () => handleUpdate(state.model, action.payload))
// TODO: should not have a side effect: https://redux.js.org/style-guide/style-guide#reducers-must-not-have-side-effects
state.lastModified = createTimestamp()
},
reset: () => initialSliceState,
set: (state, action) => {
state.model = createNextState(state.model, () => action.payload)
// TODO: should not have a side effect: https://redux.js.org/style-guide/style-guide#reducers-must-not-have-side-effects
state.lastModified = createTimestamp()
},
setError: (state, action) => {
state.error = createNextState(state.error, () => action.payload)
},
setStatus: (state, action) => {
state.status = createNextState(state.status, () => 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-enable no-param-reassign */
})
type ISelectors = IModelSliceSelectors<TGlobalState, TModel, TStatusEnum, TError>
const selectors: ISelectors = {
selectSliceState: createSelector([selectSliceState], (sliceState) => sliceState) as unknown as ISelectors['selectSliceState'],
selectModel: createSelector([selectSliceState], (sliceState) => sliceState.model) as unknown as ISelectors['selectModel'],
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 createModelSlice