Boiler Plate eliminator for React projects using Redux
yarn add react-redux-boileroutor
npm install react-redux-boileroutTodoActions.js
import { generateActionDispatchers } from 'react-redux-boilerout';
import { store } from './redux'; //export your store!
export default generateActionDispatchers({ dispatch: store.dispatch,
actions: ['setVisibilityFilter',
'addTodo',
'toggleTodo']
});Action dispatchers are created automatically for specified action names so you can do:
import TodoActions from './TodoActions';
TodoActions.addTodo('buy beer');
TodoActions.toggleTodo(1);
TodoActions.setVisibilityFilter('SHOW_ALL');The Actions dispatched by this dispatchers have the format:
{
type: 'setVisibilityFilter',
payload: ['SHOW_ALL']
}Redux's dispatch function is bound to the resulting dispatchers.
Simply create reducer classes with action listeners
TodosReducer.js
import { sliceReducer } from 'react-redux-boilerout';
class TodosReducer {
constructor() {
this.lastId = 0;
}
static initialState() {
return {
items: [],
filter: 'SHOW_ALL'
};
}
addTodo(state, text) {
const items = [...state.items, {
id: this.lastId++,
text,
completed: false
}];
return { ...state, items }
}
setVisibilityFilter(state, filter) {
return { ...state, filter }
}
toggleTodo(state, id) {
const items = [...state.items
.map(item => item.id === id ? {...item, ...{ completed: !item.completed }} : item)];
return { ...state, items }
}
}
export default sliceReducer('todos')(TodosReducer);And don't forget to export your class using the sliceReducer decorator for which you need specify the store slice name that
it will listen to, like you would with combineReducers.
The initial state of the slice is defined with the special static function initialState.
Each method of the class receives the arguments passed to the action dispatcher when it was called, and the FIRST argument
is the current state. The method must then return the new state of the slice, just like you would with a regular reducer,
without mutating the current state of course, instead you need to make sure it's a new instance with the appropriate changes.
For the above example with an empty preloadedState passed to redux, the store would be initialized with:
{
todos: {
items: [],
filter: 'SHOW_ALL'
}
}And after calling TodosActions.setVisibilityFilter('ACTIVE'), TodosActions.addTodo('Buy Beer') and TodosActions.toggleTodo(1)
it would look like this:
{
todos: {
items: [{
id: 1,
text: 'Buy Beer',
completed: true
}],
filter: 'ACTIVE'
}
}Using a similar approach to react-redux's connect HOC, react-redux-boilerout provides connectSlice to connect
your component to a specific slice of the state directly, without having to write the selection boilerplate.
TodoListContainer.js
import { connectSlice } from 'react-redux-boilerout';
import TodosActions from './TodosActions';
import TodoList from './TodoList';
import TodosReducer from './TodosReducer';
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error('Unknown filter: ' + filter)
}
};
const mapSliceStateToProps = (slice) => ({
todos: getVisibleTodos(slice.items, slice.filter)
});
const VisibleTodoList = connectSlice({
slice: 'todos',
actions: TodosActions
},
mapSliceStateToProps
)(TodoList);
export default VisibleTodoListHere, on the connectSlice call we listen to the todos slice of the store by specifying:
slice: 'todos'And we are mapping the TodosActions action dispatchers to props in the target component with:
actions: TodosActionsWe also pass a mapSliceStateToProps function that we use to narrow down the store slice to the props the specific
component needs in the same fashion of redux's mapStateToProps but with the difference that this function will receive
the slice mapped by TodosReducer only. mapSliceStateToProps is an optional argument, that if not passed
the target component will get the entire store slice as props.
Another optional argument that you can pass after mapSliceStateToProps is mapDispatchToProps shown in the next example:
FilterLinkContainer.js
import { connectSlice } from 'react-redux-boilerout';
import TodosActions from './TodosActions';
import TodosReducer from './TodosReducer';
import FilterLink from './FilterLink';
const mapSliceStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.filter
});
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
TodosActions.setVisibilityFilter(ownProps.filter);
}
});
const FilterLinkContainer = connectSlice({
slice: 'todos'
},
mapSliceStateToProps,
mapDispatchToProps
)(FilterLink);
export default FilterLinkContainerHere, we basically bind a prop onClick to the target component that will dispatch an action, just like you would
with vanilla react-redux, except that we don't have to wrap the call with dispatch. The dispatch argument is there
just in case you need it.
Fortunately no, mapSliceStateToProps and mapDispatchToProps are wrapped with reselect and provided to redux
as functions so each component instance will memoize the props properly.
You need to provide combineSliceReducers any amount of sliceReducer functions and export the store if you're calling
generateActionDispatchers from a different file.
redux.js
import TodosReducer from './TodosReducer';
import { createStore } from 'redux';
import { combineSliceReducers } from 'react-redux-boilerout';
const reducer = combineSliceReducers(TodosReducer);
export const store = createStore(reducer);Last step is to inject the store into your Provider
AppProvider.js
import React from 'react'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './redux'
const AppProvider = () => (
<Provider store={store}>
<App />
</Provider>
);
export default AppProvider;TodoActions.js
class TodosActions {
}
export default actionDispatcher({
dispatch: store.dispatch,
actions: ['setVisibilityFilter', 'addTodo', 'TOGGLE_TODO']
})(TodosActions);That's it, you have dispatch there so you could add any custom actions you would like also to the Class.
TodoListContainer.js
import { sliceContainer } from 'react-redux-boilerout';
class VisibleTodoList {
componentWillMount() {
console.log('Todos Container mounted');
}
static getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error('Unknown filter: ' + filter)
}
};
static mapSliceStateToProps(state) {
return {
todos: this.getVisibleTodos(state.items, state.filter)
}
}
static inject() {
}
}
export default sliceContainer({ slice: 'todos', actions: TodosActions, component: TodoList })(VisibleTodoList);With sliceContainer, static methods can to be defined for mapSliceStateTopProps, mapDispatchToProps and inject. Under the hood, connecetSlice is used.
You can also define any of the following React lifecycle methods: componentWillMount, render, componentDidMount, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, componentDidUpdate, componentWillUnmount
and they will be hoisted into the Container component that will wrap the target component specified to sliceContainer. In this case, TodoList.
One piece of boiler haven't dealt with yet is that every time we add a new Slice to our app we have to go to our combineSliceReducers declaration and add the new slicers there:
const reducer = combineSliceReducers(TodosReducer, AnotherReducer, AndMoreReducer, ThisStartsToSuckReducer, BetterDontForgetToAddHereReducer);You get the idea. For extreme comfort there's yet another decorator, registerSliecReducer and a DynamicSliceReducer class that produces a redux reducer, and also acts as reducer registry.
To use it, replace your existing combineSliceReducers declaration with:
redux.js
import { createStore } from 'redux';
import { DynamicSliceReducer } from 'react-redux-boilerout';
const reducerRegistry = new DynamicSliceReducer();
const store = createStore(reducerRegistry.reducer());
export {
store,
reducerRegistry
}Here we export the store and the dynamic slice reducer as the reducer registry.
Then, on your reducer declaration, you decorate your reducer export with registerSliceReducer:
TodosReducer.js
import { sliceReducer, registerSliceReducer } from 'react-redux-boilerout';
import { reducerRegistry, store } from './redux';
class TodosReducer {
// Class body skipped for brevity
}
export default registerSliceReducer({ store, registry: reducerRegistry })(sliceReducer('todos')(TodosReducer));registerSliceReducer takes 2 arguments: the store and the reducerRegistry we exported in the first step.
And last but not least, change the sliceContainer or connectSlice slice property from a string with the slice name to the actual sliceReducer:
TodoListContainer.js
import TodosReducer from './TodosReducer';
//Rest omitted for brevity
export default sliceContainer({ slice: TodosReducer, actions: TodosActions, component: TodoList })();This is required so that something actually imports the TodosReducer, otherwise the dynamic reducer registration won't kick in.
Like Decorators? Take it to the Ultimate Beast Master level with ES7 (or 8? or 9?? Who knows!) Decorators (needs Babel)
Most of the exports of react-redux-boilerout can be used as ES Decorators. This is what the actions, reducer, and container look like with actual decorators:
TodosActions.js
import { store } from './redux';
import { actionDispatcher } from 'react-redux-boilerout';
@actionDispatcher({
dispatch: store.dispatch,
actions: ['setVisibilityFilter', 'addTodo', 'TOGGLE_TODO']
})
export default class TodosActions {}TodosReducer.js
import { sliceReducer, registerSliceReducer } from 'react-redux-boilerout';
import { reducerRegistry, store } from './redux';
@registerSliceReducer({ store, registry: reducerRegistry})
@sliceReducer('todos')
export default class TodosReducer {
constructor() {
this.lastId = 0;
}
static initialState() {
return {
items: [],
filter: 'SHOW_ALL'
};
}
addTodo(state, text) {
const items = [...state.items, {
id: this.lastId++,
text,
completed: false
}];
return { ...state, items }
}
setVisibilityFilter(state, filter) {
return { ...state, filter }
}
toggleTodo(state, id) {
const items = [...state.items
.map(item => item.id === id ? {...item, ...{ completed: !item.completed }} : item)];
return { ...state, items }
}
}TodoListContainer.js
import { sliceContainer } from 'react-redux-boilerout';
import TodosReducer from './TodosReducer';
@sliceContainer({ slice: TodosReducer, actions: TodosActions, component: TodoList })
export default class VisibleTodoList {
componentWillMount() {
console.log('Todos Container mounted');
}
static getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error('Unknown filter: ' + filter)
}
};
static mapSliceStateToProps(state) {
return {
todos: this.getVisibleTodos(state.items, state.filter)
}
}
static inject() {
}
}Head to redux-todos#with-dynamic-reducer for fully working example.
Head on to redux-todos for a working version of redux's todos example implemented
using react-redux-boilerout.
Generates an object that maps the action names to function properties that are action dispatchers.
If the actions are in the form of ACTION_NAME the dispatcher functions are normalized to the actionName form (camelCase).
For each action, let's say 'sayHello' the resulting function property will also have a defer version that will dispatch
the action asynchronously.
- dispatch: redux store dispatch function to bind the action creators with.
- actions: an arbitrary list of strings to generate the action dispatchers.
Returns an object with the dispatcher function attributes
const Actions = generateActionDispatchers({ dispatch, actions: ['SAY_HELLO']});
Actions.sayHello('Hi There!'); //dispatch action synchronously
Actions.sayHello.defer('Hi There!'); //dispatch action asynchronouslyGenerates a reducer function for the slice portion of the store that will use class instance of the provided class
that upon execution on action dispatch it will select the appropriate method based on the action's type
when dispatching actions.
- slice: the slice the reducer will map, as a string.
- ActionReducerClass: the actual class that will be doing the reducing.
Returns a slice reducer wrapping an instance of ActionReducerClass
Given the followin action:
Actions.sayHello('E.T.', 'call', 'home');the following action reducer will transform the state for the slice earth when sayHello is dispatched:
class EarthReducer {
sayHello(state, who, did, what) {
return {...state, messages: [state.messages, `${who} ${did} ${what}`]};
}
}
const earthReducer = sliceReducer('earth')(EarthReducer);For actions declared with UPPER_CASE style, action reducer methods map to their camelCase counterpart. Also action reducers methods can be named
starting with on + ActionName.
For instance, methods named onSayHello and sayHello will listen to action SAY_HELLO or sayHello.
function connectSlice({slice, actions, inject}, mapSliceStateToProps, mapDispatchToProps): <function (TargetComponent): <hoc>>
Higher Order Component that will decorate TargetComponent to listen for store changes on the slice of the store
mapped by slice. Arguments actions, inject, mapSliceStateToProps and mapStoreDispatchToProps are optional.
- slice: slice name that the target component will map props from. Or the actual slice Reducer as exported with
sliceReducer - actions: an object generated with
generateActionDispatchersoractionDispatchersof which its actions will be injected as props - inject: any arbitrary object of which its attributes will be injected as props
- mapSliceStateToProps: function(slice, props): analog to
redux'smapStateToPropsbut that will receive only the slice mapped by the providedsliceReducer. If this function is not provided, the entire slice is mapped to props. This function is memoize'd - mapDispatchToProps: function(dispatch, props): Same as
redux'smapDispatchToProps. This function is memoize'd - TargetComponent: the actual react component that will be connected to the store
Returns a HOC that connects the target component to the store.
const VisibleTodoList = connectSlice({
slice: 'todos',
actions: TodosActions
},
mapSliceStateToProps,
mapDispatchToProps
)(TodoList);Analog to redux's combineReducers but for reducers generated with the sliceReducer decorator.
- sliceReducers: the slice reducers to combine into one reducer.
Returns a redux reducer.
import { combineSliceReducers } from 'react-redux-boilerout';
const reducer = combineSliceReducers(TodosReducer, SomeOtherSliceReducer);
export const store = createStore(reducer);Similar to generateActionDispatchers for classes. When called, it returns a decorator that will decorate TargetClass
with the specified action methods using the provided dispatch function.
Returns a decorator to be applied to a class.
- dispatch: Redux's
dispatchfunction - actions: An Array of
stringelements for the action names to be generated ass class methods in theTargetClass - TargetClass: the class to decorate with the specified
actions
@actionDispatcher({
dispatch: store.dispatch,
actions: ['setVisibilityFilter', 'addTodo', 'TOGGLE_TODO']
})
export default class TodosActions {}Similar to connectSlice for classes. When called, it returns a decorator that will decorate TargetClass with the
specified behavior connecting to the provided slice, injecting actions into compoment and hoisting into the resulting
hoc the provided lifecycle methods.
- slice: slice name that the target component will map props from. Or the actual slice Reducer as exported with
sliceReducer - actions: an object generated with
generateActionDispatchersoractionDispatchersof which its actions will be injected as props - component: The target component to connect with the store slice.
- TargetClass: The actual class that will contain the
inject,mapDispatchToPropsandmapSliceStateToPropsstatic methods, and any other React lifecycle methods.
Returns an hoc that will wrap the target component connected to the specified slice.
@sliceContainer({ slice: TodosReducer, actions: TodosActions, component: FilterLink })
export default class FilterLinkContainer {
static mapSliceStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.filter
});
static mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
TodosActions.setVisibilityFilter(ownProps.filter);
}
});
}Class that acts as reducer registry for dynamic reducers when developers don't want to register their reducers in advance.
It exports a reducer that needs to be registered on redux store creation. Both store and DynamicSliceReducer instance
need to be exported for use with registerSliceReducer;
const reducerRegistry = new DynamicSliceReducer();
const store = createStore(reducerRegistry.reducer(), enhancer);
export {
store,
reducerRegistry
}Use with DynamicSliceReducer when wanting to dynamically register reducers into redux's store. When called, it returns
a decorator that will register TargetClass as a slice reducer into registry to use with the store.
- store: Redux's store.
- dynamicSliceReducer: an instance of
DynamicSliceReducerthat acts as registry for dynamic reducer registration. - TargetClass: the actual slice reducer class as decorated by
sliceReducer.
Returns a decorator to be applied to a slice reducer class.
@registerSliceReducer({ store, registry: reducerRegistry })
@sliceReducer('todos')
export default class TodosReducer {
// class body omitted for brevity
}MIT ©2017 Ulises Bocchio