Reducers
Now we build our business logic, also known as reducers. Stay calm, this will be a bit more complicated than anything before.
IndexReducer
We begin by creating a file called IndexReducer.ts
in the src/modules/index
-folder
Our reducers follow the naming pattern of
[Foldername]Reducer
import { Action } from 'redux';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mapTo';
import { Epic, combineEpics, ActionsObservable } from 'redux-observable';
import { makeAction, isAction } from '../../redux/guards';
import Todo from '../../common/Todo';
const testDelay = 1000;
export class IndexState {
readonly title: string = '';
readonly todos: Todo[] = [];
readonly loading: boolean = false;
}
export const SET_TITLE = 'boilerplate/Index/SET_TITLE';
export const SAVE_TODO = 'boilerplate/Index/SAVE_TODO';
export const SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS';
export const SET_DONE = 'boilerplate/Index/SET_DONE';
export const SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS';
export const setTitle = makeAction(SET_TITLE)((title: string) => ({ type: SET_TITLE, payload: title }));
export const saveTodo = makeAction(SAVE_TODO)(() => ({ type: SAVE_TODO }));
export const saveTodoSuccess = makeAction(SAVE_TODO_SUCCESS)(() => ({ type: SAVE_TODO_SUCCESS }));
export const setDone = makeAction(SET_DONE)((i: number) => ({ type: SET_DONE, payload: i }));
export const setDoneSuccess = makeAction(SET_DONE_SUCCESS)((i: number) => ({ type: SET_DONE_SUCCESS, payload: i }));
export const saveTodoEpic: Epic<Action, undefined> = action$ =>
action$
.ofType(SAVE_TODO)
.delay(testDelay)
.mapTo(saveTodoSuccess());
export const setDoneEpic: Epic<Action, undefined> = action$ =>
action$
.ofType(SET_DONE)
.delay(testDelay)
.map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic);
const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
if (isAction(action, setTitle)) {
return { ...state, title: action.payload };
} else if (isAction(action, saveTodo)) {
return { ...state, loading: true };
} else if (isAction(action, saveTodoSuccess)) {
return {
...state,
title: '',
todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
loading: false,
};
} else if (isAction(action, setDone)) {
return { ...state, loading: true };
} else if (isAction(action, setDoneSuccess)) {
return {
...state,
todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
loading: false,
};
} else {
return state;
}
};
export default IndexReducer;
First we define the state for our IndexReducer
Reducers usually define their own state and we'll show you later how to connect it to the main reducer and state
import Todo from '../../common/Todo';
export class IndexState {
readonly title: string = '';
readonly todos: Todo[] = [];
readonly loading: boolean = false;
}
which is fairly simple. Here we define the IndexState
as a class, with the given properties (make sure you add default values for required properties so you can instantiate it!), with the title
for the current Todo
the user is creating, todos
for the list of current Todo
s and loading
to show the user whether the application is performing an async call or not.
Next up we define our action types
const SET_TITLE = 'boilerplate/Index/SET_TITLE';
const SAVE_TODO = 'boilerplate/Index/SAVE_TODO';
const SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS';
const SET_DONE = 'boilerplate/Index/SET_DONE';
const SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS';
which redux recommends to be constant string
s, but can be of the type any
. In our case, as we are using redux-guards to facilitate the way TypeScript works with redux we have to make them string
s.
Here we follow the redux-ducks naming pattern of the format
applicationName/ViewName/ACTION_TYPE
Next we define our action creators which are functions that return an action
import { makeAction } from '../../redux/guards';
export const setTitle = makeAction(SET_TITLE)((title: string) => ({ type: SET_TITLE, payload: title }));
export const saveTodo = makeAction(SAVE_TODO)(() => ({ type: SAVE_TODO }));
export const saveTodoSuccess = makeAction(SAVE_TODO_SUCCESS)(() => ({ type: SAVE_TODO_SUCCESS }));
export const setDone = makeAction(SET_DONE)((i: number) => ({ type: SET_DONE, payload: i }));
export const setDoneSuccess = makeAction(SET_DONE_SUCCESS)((i: number) => ({ type: SET_DONE_SUCCESS, payload: i }));
of a specific type with a specific payload
(which is the way to pass new information to the reducer). In this case we use makeAction
as defined by redux-guards to ensure we get proper typings.
If you want to avoid having to type
type: ACTION_TYPE
you can override the redux interface according to this article, but I personally I dislike overriding libraries so I prefer the duplicity here instead.
Next we define our Epics
import { Action } from 'redux';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mapTo';
import { Epic, combineEpics, ActionsObservable } from 'redux-observable';
import { isAction } from '../../redux/guards';
const saveTodoEpic: Epic<Action, undefined> = action$ =>
action$
.ofType(SAVE_TODO)
.delay(testDelay)
.mapTo(saveTodoSuccess());
const setDoneEpic: Epic<Action, undefined> = action$ =>
action$
.ofType(SET_DONE)
.delay(testDelay)
.map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic);
which are redux-observable's way of handling side-effects in Redux (like AJAX calls etc.). At the end we combine all our Epics in this file to a single exportable Epic called IndexEpics
(so we only need to import one variable when we want access to these later).
The importing part may look a little weird, but it's because RxJS is a rather large library, we can either import everything using
import * as RxJS from 'rxjs'
or import only the parts we need as shown above, which will allow any proper minifier like UglifyJS to include only the needed parts from RxJS
The first line
const saveTodoEpic: Epic<Action, undefined> = action$ =>
defines an Epic which takes in as the first type argument the type
for the Actions
the epic takes in (and returns), in this case Action
from redux, and as the second argument the type of the State it takes in (which isn't needed this time, so undefined will do). An Epic is a function that takes in a stream) (in this case of the type ActionsObservable<Action>
) which includes items of the type
given as the first type argument and returns another stream (in this case an Observable<Action>
, which ActionsObservable
is based on) which includes items of the same type
as the input stream.
In JavaScript the convention is to append a
$
to all variables names that are streams, to let the developer know that they are dealing with one
The second and third line
action$
.ofType(SET_DONE)
utilizes the inbuilt function ofType(key: string)
of ActionsObservable
, which basically filters out all actions that do not have the type
-property of the given argument.
A more verbose, but maybe a simpler to understand version would be to write
action$.filter(action => action.type === SET_DONE)
If you find yourself needing to understand the types of the components provided by redux-observable I suggest reading this
The third and fourth line
.delay(1000)
.mapTo(saveTodoSuccess());
include the actual functionality of our Epic. In this case after we receive an action of the type SET_DONE
we wait for 1 second (delay
takes milliseconds as argument) and then we return an action of the type SAVE_TODO_SUCCESS
(in this case using mapTo
as we just want to return a new Action).
If you wanted to return multiple actions, say
SET_DONE_SUCCESS
and an imaginarySEND_PUSH_NOTIFICATION
you could do it usingmergeMap
, which is kind of likeflatMap
, like so:import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/from'; import 'rxjs/add/operator/mergeMap'; ... action$.ofType(SET_DONE) .mergeMap(action => Observable.from([ setDoneSuccess(action.payload), // SEND_PUSH_NOTIFICATION, // OTHER ACTIONS, ]));
The other Epic is otherwirse similar, but it uses map
.delay(1000)
.map(action => isAction(action, setDone) && setDoneSuccess(action.payload));
to return an action of the type SET_DONE_SUCCESS
, using the payload of the incoming action. We use isAction
defined by redux-guards here to ensure TypeScript understands the type of our action, otherwise it would complain that Action does not have key payload
.
If you wanted to do an AJAX call, you would go about it like this:
import { ajax } from 'rxjs/observable/dom/ajax'; ... action$.ofType(AJAX_CALL).mergeMap(action => { // For a get JSON call ajax.getJSON('url', { headers: 'go here' }) .map(response => someAction(response)) .catch(err => errorAction(err)); // For all other calls, just select the correct verb ajax.post('url', payload, { headers: 'go here' }) .map(response => someAction(response)) .catch(err => errorAction(err)); });
Redux-observable is built upon RxJS, the JavaScript implemention of ReactiveX and most issues you will run into will be RxJS issues
Finally we define the rest of our business logic, a.k.a. the reducer itself
const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
if (isAction(action, setTitle)) {
return { ...state, title: action.payload };
} else if (isAction(action, saveTodo)) {
return { ...state, loading: true };
} else if (isAction(action, saveTodoSuccess)) {
return {
...state,
title: '',
todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
loading: false,
};
} else if (isAction(action, setDone)) {
return { ...state, loading: true };
} else if (isAction(action, setDoneSuccess)) {
return {
...state,
todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
loading: false,
};
} else {
return state;
}
};
for which I suggest to break from the redux-ducks pattern by using the naming convention of [Pagename]Reducer
. The important thing to remember with reducers is that they have to be functional, a.k.a. they are not allowed to mutate the incoming information or have side-effects.
On the first line we define the signature of our IndexReducer
const IndexReducer = (state: IndexState = new IndexState(), action: Action): IndexState => {
where we define it to take to parameters (as all reducers), our IndexState
(with a default for the empty state) and an action. IndexReducer
will also return an IndexState
(as all reducers).
Next we do the actual logic which all reducers are built upon
if (isAction(action, setTitle)) {
return { ...state, title: action.payload };
} else if (isAction(action, saveTodo)) {
return { ...state, loading: true };
} else if (isAction(action, saveTodoSuccess)) {
return {
...state,
title: '',
todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)),
loading: false,
};
} else if (isAction(action, setDone)) {
return { ...state, loading: true };
} else if (isAction(action, setDoneSuccess)) {
return {
...state,
todos: state.todos.map(t => (t.id === action.payload ? t.setDone() : t)),
loading: false,
};
} else {
return state;
}
which is usually a switch
-statement, but due to the way we use redux-guards we use an if-else-if-else
-block instead, where we use isAction
to check the function's type (and give TypeScript the type information). In each block
we do something (except the default
-one, where you traditionally just return the incoming state) to add value to that action, such as setting the title
to the payload
in the action in case the action is a SET_TITLE
-action. Notice how we are using the spread syntax to immutably create a new version of the state, thus holding true to the immutability of reducers.
Other options are to use
Object.assign({}, ...)
or Immutable
Connecting the reducer
Remember our root-reducer? Now we connect our IndexReducer
to it.
First the reducer itself
import { combineReducers } from 'redux';
import IndexReducer from '../modulex/index/IndexReducer';
const reducer = combineReducers({
index: IndexReducer,
});
where we add the IndexReducer
under the key index
, which is very important, as when combineReducers
combines included reducers it will put their specific state under the key given, in the global state-object.
Next we add the IndexState
to our global State
-class (this is just to allow us to define the type and initialize it for tests later on)
import { IndexState } from '../modules/index/IndexReducer';
export class State {
readonly index: IndexState = new IndexState();
}
where we define that the global State
-object has a property index
of the type IndexState
(as our combineReducer
already says, but we want to be explicit here).
Then we want to add our Epics into the global epics
constant
import { combineEpics } from 'redux-observable';
import { IndexEpics } from '../modules/index/IndexReducer';
export const epics = combineEpics(IndexEpics);
by including it as a parameter to combineEpics
.