DEV Community

Cover image for Redux Typescript Concept for Less Boilerplate
unsignedmind
unsignedmind

Posted on • Updated on

Redux Typescript Concept for Less Boilerplate

GithubRepo

Intro

Hi I am new here and new to react and redux. I worked with NgXS in Angular before but that's it to state management. Please tell me what you think about my approach and what I need to look out for :) Surely there is room for improvement.

Advantages with this setup:

You need a new action that just overrides the state?
Easy! Define the name of the action and 1 call. Overrding is considered default and already implemented for all actions. So you only need 2 lines.

Your action does not override but adds up with the value in the state?
Add function which does that and add it to a map.

New State?
Add the name of that state, definde the state model and add the state to the app state model.

Reducer?
I don't know what you are talking about :)


Redux Setup

This setup aims to remove as much boilerplate code as possible.

The redux setup consists of the following components:

  • Generic Actions (custom actions possible)
  • State Merger (merge of payload and state)

State

export interface State {
    readonly name: StateName;
}

export enum StateName {
    APP_STATE = 'appState',
    PRODUCT_LIST_STATE = 'productListState',
}

export interface AppStateModel extends State {
    [StateName.PRODUCT_LIST_STATE]: ProductListStateModel;
}

export const defaultAppState: AppStateModel = {
    name: StateName.APP_STATE,
    [StateName.PRODUCT_LIST_STATE]: defaultProductState,
};

export interface ProductListStateModel extends State {
    products: Array<ProductDTO>;
    loading: boolean;
    error: string;
}
Enter fullscreen mode Exit fullscreen mode

Each state has a name. The names are stored in an enum. All states are part of the App state.

Actions

Generic action creator

This creator is stored in src/state/utils. To Create an action you need 4 things:

  • StateModel
  • ActionType Enum
  • Payload
  • Name of the state or boolean

The StateModel & ActionType Enum are stored in the component directory.

Use action generator

The payload is type checked. It has a Partial<T> interface of given state model. With the generic creator there are no extra action definitions necessary. Define in the call the attributes that changed.
The constructor of the generic creator has 2 optional parameters. The stateName and a boolean value.

  • useCustomStateMerger(boolean): When the boolean is set to true then the action loads a custom state merger which is intended to hold some business logic added by the developer. Read more about that under State Merger.
  • stateName(StateName): If the useCustomStateMerger flag is false or undefined the default merger is used. The default merger needs to know to which state the payload should be applied to.

Usage

dispatch(new genericAction<ProductStateModel>(ProductListActionTypes.REQUEST_START, { loading: true }), StateName.PRODUCT_LIST_STATE, false);
Enter fullscreen mode Exit fullscreen mode

Create custom actions

If the generated actions reach their limitations then self written actions can be added with the template below.

Usage

export class SomeAction extends Action {
    public readonly type = SomeActionTypes.ActionName;
    public reducer = (state: AppStateModel) => ({ ...state, ...payload });

    constructor(public payload: Pick<SomeStateModel, 'someAttribute'>) {
        super();
    }
}
Enter fullscreen mode Exit fullscreen mode

State Merger

What is this for? It merges the payload with the state.

Default Merger

Based on the state name provided in the genericAction call the default state merger applies the payload to the correct state. That's possible because the state name equal the state attribute name in the app state model.

Custom Merger

The generated actions include the reducer, and the action class has a flag named useCustomStateMerger. If true then the reduce function of that action gets a state merger from a map and runs the merge function. By default the payload of the action overwrites the state. But as there could also be some business logic necessary then a custom merger is needed. These are stored in src/state/state-merger/.
To create a new state merger first add a new class in the state-merger file. They look like this:

export class ProductListRequestStartStateMerger extends DefaultStateMerger {
    merge(state: AppStateModel, payload: Partial<ProductListStateModel>): AppStateModel {
        return {
            ...state,
            productListState: {
                ...state.productListState,
                loading: payload.loading ? payload.loading : defaultProductListState.loading,
                error: 'State Merger working',
            },
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Naming & Types

  • The naming default convention is actionNameStateMerger.
  • The class needs to extend the StateMerger base class.
  • Implement the merge function and set the types to the StateModel
  • Payload must be wrapped in Partial<T>
  • The return type must be AppStateModel.

Merge

  • make sure to make a copy of the app state itself and a copy of the state you want to update to avoid "Common Mistake #2: Only making a shallow copy of one level"

The class must be added to the map state-merger-mapafterwards.

[ProductListActionTypes.REQUEST_START, new ProductListRequestStartStateMerger()]
Enter fullscreen mode Exit fullscreen mode

Each entry is an array that consists of the ActionType Enum, and a new instance of the state merger class

Reducer

A universal reducer exists for all generated actions. It can also handle the custom actions. Since the logic of the reducer is "outsourced" the original reducer is a one-liner.

export const appStateReducer = (state: AppStateModel = defaultAppState, action: Action) => universalReducer(state, action);
Enter fullscreen mode Exit fullscreen mode

Sources

Latest comments (0)