Skip to content

Instantly share code, notes, and snippets.

@altbdoor
Last active January 10, 2020 07:22
Show Gist options
  • Select an option

  • Save altbdoor/6ce86f0eceb5282aa2758dec0206844e to your computer and use it in GitHub Desktop.

Select an option

Save altbdoor/6ce86f0eceb5282aa2758dec0206844e to your computer and use it in GitHub Desktop.
Proposed basis of NgRx reducer files
import { Action, createReducer, on } from '@ngrx/store';
export const featureKey = 'niceFeature';
export interface State {}
export const initialState: State = {};
const reducerFn = createReducer(
initialState,
// on(fooAction, (state, { fooVal }) => ({ ...state, fooVal })),
);
export function reducer(state: State, action: Action) {
return reducerFn(state, action);
}
// export const getSection = (state: State) => state.fooVal;

Proposed basis of NgRx reducer files

Problem

We are confused as to how to properly structure reducer files in NgRx projects. As far as we could see, there are no clear guidelines as to how these files should be structured.

Reducer files are also confusing, as the file can contain selectors, and we could cross import selectors, to combine under createSelectors.

Proposal

Let us assume a slice of state called User. We will then create user.reducer.ts and user.reducer.spec.ts.

// reducers/user.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';

export const featureKey = 'user';

export interface State {
    name?: string;
    age?: number;
}

export const initialState = {};

const reducerFn = createReducer(
    initialState,
    on(setName, (state, { name }) => ({ ...state, name })),
);

export function reducer(state: State, action: Action) {
    return reducerFn(state, action);
}

export const getName = (state: State) => state.name;

A number of variables are fixed as exports, which will be consistent across different reducer files. They are:

  • featureKey, which will define the name of the slice
  • State, an interface that defines how the slice will be structured
  • initialState, which will serve as the initial state for the slice
  • reducer, which will only be used in the root reducer file (will be elaborated later)

With the use of a barrel file, or import * as foo, we can reliably refer to the variables uniquely. As an example:

import * as fromUser from './user.reducer';  // via import all
import fromVehicles from './vehicles.reducer';  // via barrel file

fromUser.State
fromVehicles.State
fromVehicles.initialState

The import prefix will serve as an identifier to the slice of data. Also, should two different reducer have same variable names for the selector, the import prefix will help to clear the confusion.

Selectors in the reducer file will be as simple as possible, and should never import any external selectors. Usage of createSelector is allowed, but only to combine or perform complex selection with the available selectors within the reducer file itself. Any complex selection that requires selectors from other reducer file, needs to be done in the root reducer file.

export const getName = (state: State) => state.name;
export const getAge = (state: State) => state.age;
export const getFullProfile = createSelector(
    getName,
    getAge,
    (name, age) => ({ name, age })
);

In the root reducer file (reducers/index.ts), it will contain the complete logic of structuring the selectors.

// reducers/index.ts
import { ActionReducerMap, createFeatureSelector, createSelector } from '@ngrx/store';
import { InjectionToken } from '@angular/core';

import * as fromUser from './user.reducer';
import * as fromVehicle from './vehicle.reducer';

export interface AppState {  // or any descriptive state name for project
    [fromUser.featureKey]?: fromUser.State,
    [fromVehicle.featureKey]?: fromVehicle.State
}

export const reducers: ActionReducerMap<AppState> = {
    [fromUser.featureKey]: fromUser.reducer,
    [fromVehicle.featureKey]: fromVehicle.reducer
}

export const reducerToken = new InjectionToken<ActionReducerMap<AppState>>('Registered Reducers');

// to be parked in the providers, in app.module.ts
export const reducerProvider = [{ provide: reducerToken, useValue: reducers }];

// and here lies all the selectors to be used throughout the whole project

// the first selector will be the main selector to get a slice of data
export const getUserState = createFeatureSelector<AppState, fromUser.State>(fromUser.featureKey);
// `createFeatureSelector` will be memoized, which is good, but alternatively you can use a simple pure function
export const getUserStatePure = (state: AppState) => state.user;
export const getUserName = createSelector(getUserState, fromUser.getName);
export const getUserAge = createSelector(getUserState, fromUser.getAge);

export const getVehicleState = (state: AppState) => state.vehicle
// as far as this file is concerned, it only needs to call `getAll`
// as to whether we want to use NgRx entity store or such, that is dependant on the vehicle reducer file
export const getAllVehicles = createSelector(getVehicleState, fromVehicle.getAll);

// complex selectors that require selectors from other reducers will be defined here
export const getVehicleForUser = createSelector(
    getUserName,
    getAllVehicles,
    (name, vehicles) => vehicles.find(v => v.owner.name === name)
);

The clear drawback here is that the root reducer file will increase in length, as the project grows, and more selectors are added into the file.

When writing tests for each respective reducer, it is easily scoped, as the selectors for each reducer will not go out of bounds to creep on other reducer file. So in theory, tests will be simple and straightforward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment