Redux Typescript Concept for Less Boilerplate
Tags: redux, typescript, architecture, boilerplate
Published: 2020-11-07 17:10:45

[GithubRepo](https://github.com/unsignedmind/proshop/tree/dev-post)

# 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
```typescript
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;
}
```
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**
```typescript
dispatch(new genericAction<ProductStateModel>(ProductListActionTypes.REQUEST_START, { loading: true }), StateName.PRODUCT_LIST_STATE, false);
``` 
### Create custom actions
If the generated actions reach their limitations then self written actions can be added with the template below. 

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

    constructor(public payload: Pick<SomeStateModel, 'someAttribute'>) {
        super();
    }
}
```

# 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:

```typescript
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',
			},
		};
	}
}
```
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-map`afterwards.

```typescript
[ProductListActionTypes.REQUEST_START, new ProductListRequestStartStateMerger()]
```
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. 
```typescript
export const appStateReducer = (state: AppStateModel = defaultAppState, action: Action) => universalReducer(state, action);
```

# Sources
* [Idea to store reducers in Actions from @krawaller](https://blog.krawaller.se/posts/how-to-reduce-boilerplate-in-redux/)
* [Answers of Stack Overflow](https://stackoverflow.com/questions/35482241/how-to-type-redux-actions-and-redux-reducers-in-typescript#)
* [Redux Docs](https://redux.js.org/recipes/reducing-boilerplate)
* [Original common redux setup](https://www.youtube.com/watch?v=WpvIihorarA)
* Image by — [Alena Aenami](www.artstation.com/artwork/G9VZ4)