ngrx / platform

Reactive State for Angular
https://ngrx.io
Other
8.02k stars 1.97k forks source link

Collection reducer #2098

Closed Odonno closed 5 years ago

Odonno commented 5 years ago

We often write collection reducers. By collection reducer, I mean pure reducers to add, update or remove an element inside an array.

Source:

export const createCollectionReducer = <TState extends any, TEntity extends any, TEntityKey extends any>(
    collectionSelector: (state: TState) => TEntity[],
    keySelector: (entity: TEntity) => TEntityKey,
    updateStateFunction: (state: TState, updatedCollection: TEntity[]) => TState
) => {
    const upsertOne = (state: TState, entity: TEntity) => {
        return updateStateFunction(
            state,
            collectionSelector(state)
                .filter(e => keySelector(e) !== keySelector(entity))
                .concat(entity)
        );
    };
    const upsertMany = (state: TState, entities: TEntity[]) => {
        const keys = entities.map(keySelector);

        return updateStateFunction(
            state,
            collectionSelector(state)
                .filter(e => keys.some(key => key === keySelector(e)))
                .concat(entities)
        );
    };
    const removeOne = (state: TState, key: TEntityKey) => {
        return updateStateFunction(
            state,
            collectionSelector(state)
                .filter(e => keySelector(e) !== key)
        );
    };
    const removeMany = (state: TState, keys: TEntityKey[]) => {
        return updateStateFunction(
            state,
            collectionSelector(state)
                .filter(e => keys.some(key => key === keySelector(e)))
        );
    };
    const removeAll = (state: TState) => {
        return updateStateFunction(
            state,
            []
        );
    };

    return {
        upsertOne,
        upsertMany,
        removeOne,
        removeMany,
        removeAll
    };
};

Usage:

const todosCollectionReducer = createCollectionReducer(
    (state: AppState) => state.todos,
    entity => entity.id,
    (state, crises) => ({
        ...state,
        todos
    })
);

on(todoCreated, todoUpdated, (state: AppState, { todo }) =>
  todosCollectionReducer.upsertOne(state, todo)
)

And we can of course make it even simpler with the feature pattern.

Source:

export const createFeatureCollectionReducer = <TState extends any, TEntity extends any, TEntityKey extends any>(
    featureName: string,
    keySelector: (entity: TEntity) => TEntityKey
) => createCollectionReducer<TState, TEntity, TEntityKey>(
    state => state[featureName],
    keySelector,
    (state, entities) => ({
        ...state,
        [featureName]: entities
    })
);

Usage:

const todosCollectionReducer = createFeatureCollectionReducer<AppState, Todo, number>(
    'todos',
    entity => entity.id
);

on(todoCreated, todoUpdated, (state: AppState, { todo }) =>
  todosCollectionReducer.upsertOne(state, todo)
)

I know there are packages @ngrx/entity/@ngrx/data that deal with that stuff but the goal of this function is to be that simple.

If accepted, I would be willing to submit a PR for this feature

[x] Yes (Assistance is provided if you need help submitting a pull request) [ ] No

Odonno commented 5 years ago

And why not meta collection reducer with:

And then provide some meta collection selectors:

timdeschryver commented 5 years ago

What's the difference or the benefits in comparison to ngrx/entity?

Odonno commented 5 years ago

It seems I needed to play with ngrx/entity package. I know now how to use it with createAdapter and getSelectors function.

Maybe the only missing part would be to have a state/adapter for loadable entities because I believe ngrx/data is too much specialized on this subject.