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;
}
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);
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();
}
}
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',
},
};
}
}
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.
[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.
export const appStateReducer = (state: AppStateModel = defaultAppState, action: Action) => universalReducer(state, action);
Top comments (0)