@b08/redux-generator
v2.0.3
Published
generator for redux members
Downloads
53
Readme
@b08/redux-generator, seeded from @b08/generator-seed, library type: generator
generator for redux members
Flaws of Redux
Redux offers a way to manage application state that has 2 key advantages: 1) your code is pure 2)no change propagation graphs, all code is linear and sequential. This makes your application way more stable and less resistant to modifaction, i.e. reduced risk of regression. However redux comes with a number of flaws, which sometimes shy developers away from using it. Same goes for Flux, and more.
Redux guideline bears antipattern
At first, I wanted "noizy syntax" to be on top, but then I decided that antipattern is worse of a flaw than some extra boilerplate redux makes you write. Because developers will eventually write switch-inside-switch everywhere, given it is allowed here. Worse, it is not even allowed, it is a guideline. Here is how typical reducer is suppesed to be written
function reducer(prev: YourState, action: IAction): YourState {
switch (action.type) {
case actions.type1:
// pile of code
case actions.type2:
// another pile
default: return prev;
}
}
This code violates single responsibility principle. Two piles of code doing 2 different tasks reside inside one function. This becomes even worse when code requires a switch of its own. To avoid SRP violation, it is recommended to decompose contents from the switch. Like this:
function reducer(prev: YourState, action: IAction): YourState {
switch (action.type) {
case actions.type1:
const a1 = action as YourAction1;
return pureFunction1(prev, a1.arg1, a1.arg2);
case actions.type2:
const a2 = action as YourAction2;
return pureFunction2(prev, a2.arg3);
default: return prev;
}
}
// might even want to extract these functions to another file
function pureFunction1(prev: YourState, arg1: Type1, arg2: Type2): YourState {
// pile of code
}
function pureFunction2(prev: YourState, arg3: Type3): YourState {
// another pile of code
}
This way you are free to move the code around, and you are free to decompose it further until it is good enough for your taste.
Noizy syntax
But this decomposition from previous point makes syntax even noizier, there is one more file to touch. To make any, however small change in your application state, you need to write the following:
- pure function implementing your change
- new case for a reducer
- new action and action type
- new dispatcher function, to create action and dispatch it to the store That is a lot of of writing. It looks to be so much of an overhead compared to old good direct assignment - "state.field = newValue;". And it scares away developers from starting to use redux in their application.
Performance
While you state is small, there is no problem with performance, but when it grows, it can become a real issue. And here is why. To handle a list of child reducers a parent reducer is supposed to be created with combine reducers. So for each action, each child reducer will be called and new object is created for each parent reducer in the tree. So for application with 50 pages and average of 5 components on each page, 50 root reducers and 250 "leaf", all of them being called for each action. Sooner or later it will make you want to split the store into several stores. Or attempt some other optimization.
Selectors
Sometimes you would need a tree with depth more than 2, and it will make you write long selectors like this
const componentState = store.getState().pages.myPage.leftPanel.myComponent;
This makes the component to "know" where its state reside in the tree. As an alternative you might want to use libraries like "reselect" which might be useful, but always looked like workaround rather than proper solution.
Reducers are not reusable
If you want to use one component in two places, both states will be changing simultaneously. To work around that, you will need to introduce some sort of id to the state and action, and check that id in the reducer for every action. Also, when one component is in two different nodes of the tree, a component can't know where it resides in the tree, making use of selectors even worse.
Redux generator
The generator, as it is implied, was written to deal with those flaws. First flaw to go is "noizy syntax" since generator writes all the boilerplate code for you. Pure functions(which developer writes) and state description are used as origins for code generation. Reducer, actions, action types, dispatchers and selectors are a projections of those origins. In other words, developer flow looks like this: when you write new function modifying the state, generator will automatically render a new action for it, new action type, new case in the reducer, new method in the dispatcher class and new selector.
State definition
State should be defined before anything is generated. Here is an example
// file1
export interface MyComponentState {
stateId: number;
someFlagComponentNeeds: boolean;
componentData: ComponentDataType[];
textToDisplay: string;
}
// file2
export interface MyAppState {
stateId: number;
component1: MyComponentState;
component2: MyComponentState;
titleText: string;
}
These 2 interfaces define a tree consisting of 3 nodes, even though there are only 2 interfaces, one interface defines 2 nodes of the tree. And, you should've noticed, unlike usual redux guidelines, this guideline allows to define both data and children fields in any node of the tree.
Pure functions changing the state
To modify the state you need to define a function, first parameter of which is the state it changes, as well as return type.
export function setTitle(prev: MyAppState, titleText: string): MyAppState {
return { ...prev, titleText };
}
Alternatively, if there are several functions for one state, you can enclose them into a class with state as a constructor parameter. Much like partial application. All return types should be explicitly defined, arrow function are not supported.
export class MyComponentReduction {
constructor(private prev: MyComponentState) { }
public setFlag(flag: boolean): MyComponentState {
return { ...this.prev, someFlagComponentNeeds: flag };
}
public sortData(): MyComponentState {
return { ...this.prev, componentData: this.prev.componentData.sort((d1, d2) => ... ) };
}
}
That's it, this is all you need to write, regarding managing the state, the rest is generated. As you might have noticed, you do not need to write a switch, thus no more writing "antipatern" code.
Output of the generator
- Based on function name and name of the state it works with, action type is generated.
- Action is generated, having action type set permanently in it. Name of the action looks like A1, A2 and so on. One action per state-changing function. Each action also has stateId as a parameter, regardless of it being defined in corresponding state or not.
All actions and action types are put into the single file called "actions.ts" which is dropped next to your root state. - Only one reducer is generated for the whole tree and placed in "reducer.ts". There is no need for combine reducers anymore. It is known beforehand which action is processed at what node of the tree and by which function. So there is only one switch containing as much cases as there are actions associated to all states in the tree.
And for each state there is a function that changes the state in the tree in most optimal way. No more "performance" flaw.
Also, if state has an id and is present more than once in the tree, action having one id gets one tree path, action with another id gets another tree path. - Selectors are generated for each state. I placed them in the same file as reducer, even though they could be put into their own file. It eliminates the need for the component to know where in the tree its state resides.
When state has id and it is present in several nodes in the tree, selector will return state that corresponds to given id. And not only its own stateId can be passed as a parameter, but also id of its parent state or child state. When id of any other node in tree is passed to a selector, state of specified type, closest to specified node will be returned.
State id
Generated reducer and selectors are aware of the stateId. If you intend to use the same state in two branches of your state tree, add "stateId: number" field to your state. Dispatcher constructor will also be generated with second parameter. First parameter is the store, second parameter will be stateId. And you can provide not only the id of the state itself, but also id of the parent or child of that state and it will be automatically resolved to state's own id. If you pass 0 instead of id, then all the states of that type in the tree will be changed by dispatched action.
Limitations
- Each action can only be served by one function.
- "stateId" field in each state is reserved and can not be used for user data.
- State with id is not recommended to be a child of the state without id.
Generating redux members
import { generate } from "@b08/redux-generator"; import { transformRange, transform } from "@b08/gulp-transform"; import * as changed from "gulp-changed";
const options = { lineFeed: "\n", quotes: """ };
export function reduxMembers(): NodeJS.ReadWriteStream { // this is a gulp task
return gulp.src("./app/**/*.@(state|reduction).ts")
.pipe(transformRange(files => generate(files, options)))
.pipe(changed(dest, { hasChanged: changed.compareContents }))
.pipe(logWrittenFilesToConsole)
.pipe(gulp.dest("./app"));
}
const logWrittenFilesToConsole = transform(file => {
console.log(Writing ${file.folder}/${file.name}${file.extension}
);
return file;
});