The idea is to create a schema declaration for redux apps and generate ALL the boilerplate with flow typing. The schema will define the entire hierarchy of the store and the reducers.
What I am trying to achieve here.
- Greatly reduce boilerplate. Only say your actions and how the state derives from them.
- Enforce flow typing on everything. Since we have explicit type declaration for the actions, we can generate actions, action creators, reducers and selectors all with the correct types.
I am borrowing the idea from our php repo, which uses gencode heavily to great success.
The following is an artificial boilerplate of what a module's redux code would look like. We have definitions for actions, creators, reducer and selectors.
Note that the reducer handles some foreign actions that are not declared here and the selector will be used in a global selector, which will be available to the application.
// foo/redux.js
// ------ Actions ------
const Actions = keyMirror({
SIMPLE_ACTION: null, // e.g. SET_LOADING
COMPLEX_ACTION: null, // SHOW_NEXT_USER_PAGE
// ...
});
// ------ Creators ------
const FooActionCreators = {
createSimpleAction: (simpleFlag: string) => {
return {
type: Actions.SIMPLE_ACTION,
simpleFlag,
};
},
createComplexAction: (shapeParam: MyShape, boolParam: boolean) => {
return {
type: Actions.COMPLEX_ACTION,
shapeParam,
boolParam,
};
},
};
const state = Immutable.Record(/*...*/);
const INITIAL_STATE = {
simpleFlag: 'some specific initial state';
complexActionField1: null,
complexActionField2: null,
bar: '';
// ...
};
const _myComplexAction2Handler = (state, action) => {
let field1, field2;
// ... do stuff
return state.merge({complexActionField1: field1, complexActionField2: field2});
}
// ------ Reducer ------
const fooReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case Actions.SIMPLE_ACTION: // Simple direct application reducer
return state.set('simpleFlag', action.simpleFlag);
case Actions.COMPLEX_ACTION: // Complex reducer, computes stuff and changes multiple fields
return _myComplexAction2Handler(state, action);
case 'GLOBAL_ACTION_Y': // Reduce on global actions
return state.set('bar', action.param1.field1);
case 'GLOBAL_ACTION_X': // Reduce on global actions
// do whatever
// ... more cases
}
}
// ------ Selectors ------
// A selector that does some logic to derive stuff from multiple fields
const selectComplexActionCombinedFields = (localState) => {
const {complexActionField1, complexActionField2} = localState;
return myFunc(complexActionField1, complexActionField2);
}
module.exports = {
fooReducer,
FooActionCreators,
selectComplexActionCombinedFields,
};
// -----------------------
// rootRedux.js
const {fooReducer} = require('./foo/FooRedux.js')
const Actions = keyMirror({
GLOBAL_ACTION_X: null,
GLOBAL_ACTION_Y: null,
// ...
});
const reducer = combineReducers({
foo: FooReducer,
// ...
});
// More complex global selector
const globalCurrentUsers = (globalState) => {
//from our foo reducer
const currentUserIDs = selectCurrentUserIDs(globalState.localState);
// From another reducer
const usersByID = globalState.usersByID;
const currentUserListForDisplay = currentUserIDs.map(id => usersByID.get(id));
return currentUserListForDisplay;
}
// Global Selector needs to wrap local reducer
const globalSelectsimpleFlag = (globalState) => {
return selectsimpleFlag(globalState.localState);
}
IMO, there is a lot of boilerplate there. Most of which, is unnecessary and could be inferred/generated, such as:
- Reducers that just directly set a field in the state.
- the enum that defines the actions
- the action creators
- the big switch
- the selector
- the Immutable record declaration
- the global selectors
I think that by having a compact schema declarations such as the one below, we can generate all the parts that I mentioned above with full flow typing.
Once you decide to use this schemagen tool in your app, it will be the only source of truth. All the code that you will import in your project will be generated by it.
It will generate some top level files with all the reducers so you can pass to createStore
, as well as the global selectors and the complete list of actions.
As you will notice below, some reducers are delegated to some external file, which will not be generated. You might put all the complex reducing logic there. All simple reducers that just directly set stuff in the state will be generated.
// RootStore.schema.js
const fooStoreSchema = require('FooStore.schema.js');
// ... other sub store requires
module.exports = {
Actions: {
GLOBAL_ACTION_X: 'string', // change route for example
// ... other global actions
},
State: {
route: {
type: 'string',
on: 'GLOBAL_ACTION_X',
direct: true, // Simply sets the field, no need to write a reducer
},
foo: 'fooStoreSchema',
}
};
// FooStore.schema.js
module.exports = {
Actions: {
SIMPLE_ACTION: {
simpleFlag: 'string', // You can name parameters even for single param actions
},
COMPLEX_ACTION: {
shapeParam: {
type: 'MyShape', // Flow defined type
from: './types/MyTypes.js', // Where the flow type is defined
globalSelector: 'getFooShareField', // Name of the global selector
},
boolParam: 'boolean', // just say the type directly
},
// ... other actions
},
State: {
simpleFlag: 'SIMPLE_ACTION', // Directly set the single param of an action
complexActionField1: string, // Doesn't specify handler because it will be manual
complexActionField2: { // Also no handler
type: 'MyOtherShape',
from: './types/MyTypes.js',
},
bar: { // Handle a global action and inline set state
on: 'parent.GLOBAL_ACTION_Y',
// inline function receives an object with the params of the action
inline: (params) => params.param1.field1,
},
// ... Other state fields
},
ManualHandlers: { // You code them and pass [file, function]
COMPLEX_ACTION: ['./handlers.js', 'handleComplexAction'],
// Manually mutate state based on some global action
'parent.GLOBAL_ACTION_X': ['./handlers.js', 'handleGlobalAction'],
// ... Other manual handlers
},
};