Redux is a predictable state container for JavaScript apps. It is a framework agnostic library that. It can be used with Angular with ng-redux, Angular 2 with ng2-redux, React with react-redux.
One of the reasons why Rangle.io is starting to adopt Redux on most of our applications, is because we are starting to get more React projects - and wanting to have a core piece of our stack be useable between Angular, Angular 2 and React.
When Redux is used properly - a large part of your application code becomes framework agonostic - and is just pure JavaScript, with the framework specific aspects of your code-base being pretty much the view layer.
- Managing State with Redux and Angular - Blog post I did for Rangle
- ng-sumit-redux - Sample application built for Angular Summit
- ng-summit-slides - Slides from Angular Summit on Redux + Angular
- Redux Documentaton
- Dan Abramov Egghead.io Redux Course
- Awesome Redux Resources
- ng-redux - Angular bindings for Redux
- ng2-redux - Angular2 bindings for Redux
- react-redux - React bindings for Redux
- Angular Redux Starter - Our starter project for Angular + Redux
- React Redux Starter - Our starter project for React + Redux
- Global Application State
- Reducers 101
- Reducers with Redux
- Unit testing Reducers
- Actions with Redux
- Unit testing actions
- Async actions with Redux
- ng-redux
- ng2-redux
- Config
- What's comming..
- Components and Containers
At the core of redux, is the idea of using Reducers to manage application state. So, lets just have a brief recap of what a reducer is.
A reducer is simply a function that iterates over a collection of items, and returns a single result. The reducer function takes in an accumulator - the final value that you want, a value - the current item in the collection, and can also provide an initial value.
The classic example of a reducer is doing a sum:
const result = [1,2,3].reduce((acc,value) => acc+value, 0);
// result is 6
console.log(result);
However, the result of the reducer does not need to be the same type of the items in the collection. For example:
const result = [1,2,3].reduce((acc,value) => {
acc.sum += value;
acc.values.push(value);
return acc;
}, {sum: 0, values: []});
/*
result is:
{
sum: 6,
values: [1, 2, 3]
}
*/
console.log(result);
Redux uses reducers to manage your application state, and expects a function like:
let todoState = (state = [], action = {}) => {
switch (action.type) {
case 'TODO_ADDED':
return [action.payload, ...state];
case 'TODO_COMPLETED':
return state.map(todo =>
todo.id === action.payload.id ?
Object.assign({}, todo, { completed: !todo.completed }) : todo
);
default:
return [...state];
}
}
One of the key things to keep in mind with Reducers in Redux - is that they should be pure functions, have no side effects, and not mutate the state being passed in.
If you mutate the state object, this can cause issues with change detection, and being able to check the equality of objects, and lead to some hard to debug issues later on down the line.
There are various ways to avoid mutating state, and many applications are adopting Immutable to ensure this.
However, it could also be worth watching
- Avoiding Array Mutations with concat, slice and ...spread
- Avoiding Object Mutations with Object.assign and ...spread
Since reducers are pure functions, you can easily setup an initial state and pass in an action. This makes creating unit tests for your reducers easy - and often you do not need to be concerned with Angular, React or Redux in creating your tests.
it('should allow parties to join the lineup', () => {
const initialState = lineup();
const expectedState = [{
partyId: 1,
numberOfPeople: 2
}];
const partyJoined = {
type: PARTY_JOINED,
payload: {
partyId: 1,
numberOfPeople: 2
}
};
const nextState = lineup(initialState, partyJoined);
expect(nextState).to.deep.equal(expectedState);
});
- Should return plain JSON objects
- .....unless using middleware
- Are where your side effects happen
- Are where you deal with async
Actions in Redux should return plain JSON objects that represent something that has happened in the system. When using middleware (which will be covered later), you can have actions that return promises/etc to deal with Async behavior - however even then, the result of the promise should be a plain JSON object.
This is because we want the actions to be repayable and serializable.
I like to think of actions as being broken into two parts:
- Action Creators - or Commands, the request to do something.
- Events - the result of what was done.
The action creator is a wrapper functions that takes in some parameters, does a bit of logic - and the resulting object is a result of what happened.
If you view the result as an event that happened, what Redux does is then re-plays these events over the reducers to be able to form your application state.
const generateId = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
export function joinLine(numberOfPeople) {
return {
type: PARTY_JOINED,
payload: {
partyId: generateId(), // <-- side effect/impure
numberOfPeople: parseInt(numberOfPeople, 10)
}
};
}
We do not want to have the generation of IDs/etc being handled in the reducer. We want to be able to replay the actions and end up in the same application state in a predictable way. If the generateId() was handled in the reducer, this would not be possible.
When your actions are returning plain JavaScript objects, testing the logic in them is very simple. If you need to have async actions, or actions that require access to the state - the complexity will increase slightly and we will cover that in another section.
it('should create an action for joining the line', () => {
const action = lineupActions.joinLine(4);
expect(action.payload.numberOfPeople).to.equal(4);
});
-
Need to use a middleware, such as [redux-thunk], or [redux-promise], that allows you to turn something other than a plain javascript object.
-
Will give you access to dispatch, and getState
import * as types from '../constants/ActionTypes';
function selectReddit(reddit) {
return {
type: types.SELECT_REDDIT,
reddit
};
}
function invalidateReddit(reddit) {
return {
type: types.INVALIDATE_REDDIT,
reddit
};
}
function requestPosts(reddit) {
return {
type: types.REQUEST_POSTS,
reddit
};
}
function receivePosts(reddit, json) {
return {
type: types.RECEIVE_POSTS,
reddit: reddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
};
}
export default function asyncService($http) {
function fetchPosts(reddit) {
return dispatch => {
dispatch(requestPosts(reddit));
return $http.get(`http://www.reddit.com/r/${reddit}.json`)
.then(response => response.data)
.then(json => dispatch(receivePosts(reddit, json)));
};
}
function shouldFetchPosts(state, reddit) {
const posts = state.postsByReddit[reddit];
if (!posts) {
return true;
}
if (posts.isFetching) {
return false;
}
return posts.didInvalidate;
}
function fetchPostsIfNeeded(reddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), reddit)) {
return dispatch(fetchPosts(reddit));
}
};
}
return {
selectReddit,
invalidateReddit,
fetchPostsIfNeeded
};
}
async service example from ng-redux
function someAction(data) {
return function(dispatch) {
const promise = new Promise((resolve, reject) => {
resolve({
type: 'ACTION',
payload: {
data
}
});
});
return promise.then(result => dispatch(result))
}
}
describe('some async action', () => {
it('should check things', done => {
let test = result => {
expect(result.payload.data).to.be.equal('hello');
done();
}
someAction('hello')(test);
});
})
Or, use redux-mock-store
import configureStore from 'redux-mock-store';
const middlewares = []; // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares);
// Test in mocha
it('should dispatch action', (done) => {
const getState = {}; // initial state of the store
const action = { type: 'ADD_TODO' };
const expectedActions = [action];
const store = mockStore(getState, expectedActions, done);
store.dispatch(action);
})
Out of scope for today, but a good blog post on middleware explained.
But for interest sake, a basic logging middleware:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
import reducers from './reducers';
import createLogger from 'redux-logger';
const logger = createLogger({ level: 'info', collapsed: true });
export default angular
.module('app', [ngRedux, ngUiRouter])
.config(($ngReduxProvider) => {
$ngReduxProvider
.createStoreWith(reducers // our application state
, ['ngUiRouterMiddleware', // middleware - that supports DI
logger // middleware - that doesn't need DI
]);
});
$ngRedux.connect(mapStateToTarget, [mapDispatchToTarget])(target)
import lineupActions from '../../actions/lineup-actions';
export default class LineupController {
constructor($ngRedux, $scope) {
let disconnect = $ngRedux.connect(
state => this.onUpdate(state), // What we want to map to our target
lineupActions // Actions we want to map to our target
)(this); // Our target
$scope.$on('$destroy', disconnect); // Cleaning house
}
onUpdate(state) {
return {
parties: state.lineup
};
}
};
Whenever an action gets dispatched in redux, and there is a change in the application state - ngRedux will execute the function that you provided for the mapStateToTarget.
This expects a plain javascript object to be returned. ng-redux will do a shallow check to see if the result of this function has changed since the last time it was called, and if so - will push the object onto your target.
In this case, the state.lineup
property will be mapped onto this.parties
. If you try to return a non-plain object, such as a class, or an immutable object - ngRedux will throw an error.
To access this in your template:
<tr ng-repeat="party in lineup.parties">
<td>
{{party.partyId}}
</td>
<td>
{{party.numberOfPeople}}
</td>
</tr>
ng-redux will also take care of mapping your actions to your target. In this example, the lineup actions exported are:
// from lineup-actions.js
export default {
joinLine, leaveLine
};
This means that on our target, we will be able to access this.joinLine
, and this.leaveLine
, for example:
<button type="button" ng-click="lineup.joinLine(lineup.numberOfPeople)">New Party</button>
- reselect, From their docs...
- Selectors can compute derived data, allowing Redux to store the minimal possible state.
- Selectors are efficient. A selector is not recomputed unless one of its arguments change.
- Selectors are composable. They can be used as input to other selectors.
Selectors can also act as a way to help decouple knowledge about the state tree, from the component using it.
For example, if your component(s) require data in a certian format - the selector can take care of that transformation for you. If you need to change the structure of your state tree, it can be easyier to update your selector to do that transformation, and have unit tests ensuring that the resulting data is the same instead of needing to re-work the entire component to understand the new structure.
Example selector from sad-ui,
import {createSelectorCreator} from 'reselect';
import {is, List, fromJS} from 'immutable';
const immutableCreateSelector = createSelectorCreator(is);
export const filterSelector = state => state.filter.get('ip');
export const dataIpSelector = state => state.data.getIn(['ip', 'data'], List());
export const ipGraphSelector = immutableCreateSelector(
[
filterSelector,
dataIpSelector
],
(filters, dataset) => {
return fromJS({
activeFilters: filters,
dataset: dataset
.reduce((acc, i) => {
return acc.push(List([
i.get('count'),
i.get('ip'),
i.get('country')
]));
}, List())
});
}
);
...and unit testing
import {fromJS, List, Map} from 'immutable';
import {ipGraphSelector} from './ip-graph-selector';
var createState = (state) => ipGraphSelector(state);
describe('ipGraphSelector', () => {
it('should return a list of lists in the format of count, ip, country', () => {
var state = createState({
data: fromJS({
'ip': {
data: [{
ip: '123.456.789.10',
country: 'CA',
count: 100
}, {
country: 'US',
ip: '123.456.789.9',
count: 99
}]
}
}),
filter: fromJS({
filterSource: 'ipGraph',
id: 'ip',
type: 'mustContain',
values: {}
})
});
expect(state.getIn(['dataset', 0, 0])).to.equal(100);
expect(state.getIn(['dataset', 0, 1])).to.equal('123.456.789.10');
expect(state.getIn(['dataset', 0, 2])).to.equal('CA');
expect(state.getIn(['dataset', 1, 0])).to.equal(99);
expect(state.getIn(['dataset', 1, 1])).to.equal('123.456.789.9');
expect(state.getIn(['dataset', 1, 2])).to.equal('US');
});
});
Container Components | Presentational Components | |
---|---|---|
Location | Top level, route handlers | Middle and leaf components |
Aware of Redux | Yes | No |
To read data | Subscribe to Redux state | Read data from props |
To change data | Dispatch Redux actions | Invoke callbacks from props |
Keeping with this separation seems to be a little more natural/easy with React, but the same concepts can apply to Angular.
- Smart containers that are aware of redux
- Pass down the data and actions that need to be used
- The dumb components are responsible for just displaying the data
An example from SAD-UI:
Smart Component - the IP Graph:
/* beautify preserve:start */
import {List} from 'immutable';
import {ipGraphSelector} from '../../selectors/ip-graph/ip-graph-selector';
import filterActions from '../../actions/filter/filter-actions';
/* beautify preserve:end */
export default class IpGraphController {
constructor($ngRedux, $scope) {
let _onChange = (state) => {
const data = ipGraphSelector(state);
return {
activeFilters: data.get('activeFilters'),
dataset: data.get('dataset')
};
};
let disconnect = $ngRedux.connect(_onChange, filterActions)(this);
$scope.$on('$destroy', () => disconnect());
this.columnHeaders = List([
'IP_GRAPH.HEADERS.RANK',
'IP_GRAPH.HEADERS.ATTEMPTS',
'IP_GRAPH.HEADERS.IP',
'IP_GRAPH.HEADERS.COUNTY'
]);
this.fnColumnCallbacks = [
angular.noop,
(value) => this.toggleFilter('ip', value),
(value) => this.toggleFilter('country', value)
];
this.classList = List([
null,
'list-graph__list--hover',
'list-graph__list--hover'
]);
}
isSelectedOrEmpty(obj) {
return this.activeFilters.getIn(['values', obj.get(1)]) ||
this.activeFilters.get('values').size <= 0;
}
}
<list-graph
column-headers="ipGraph.columnHeaders"
dataset="ipGraph.dataset"
active-filters="ipGraph.activeFilters"
fn-column-callbacks="ipGraph.fnColumnCallbacks"
class-list="ipGraph.classList"
fn-check-selected="ipGraph.isSelectedOrEmpty">
</list-graph>
The list-graph dumb component:
import angular from 'angular';
import invariant from 'invariant';
export default class ListGraphController {
constructor() {
invariant(
!(angular.isDefined(this.fnRowCallback) && angular.isDefined(this.fnColumnCallbacks)),
'Inside the List Graph component, both fnRowCallback and fnColumnCallbacks are defined, only one is allowed.',
this
);
invariant(
!(angular.isDefined(this.classList) && angular.isDefined(this.rowClass)),
'Inside the List Graph component, both classList and rowClass are defined, only one is allowed.',
this
);
}
}
ListGraphController.$inject = [];
<table class="list-graph__content">
<thead>
<tr class="list-graph__list list-graph__list--title">
<th
ng-repeat="header in listGraph.columnHeaders | mutable"
class="list-graph__list--entry-title"
ng-class="{'list-graph__list--num-title': $index === 0}">
{{ header | translate }}
</th>
</tr>
</thead>
<tbody>
<tr
class="list-graph__list {{ listGraph.rowClass }}"
ng-class="{'list-graph__list--selected': listGraph.fnCheckSelected(data)}"
ng-repeat="data in listGraph.dataset | mutable"
ng-click="listGraph.fnRowCallback(data)">
<td class="list-graph__list--num">
{{ $index + 1 }}
</td>
<td
class="list-graph__list--entry {{ listGraph.classList.get($index) }}"
ng-click="listGraph.fnColumnCallbacks[$index](item)"
ng-repeat="item in data | mutable track by $index"
title="{{ item }}">
{{ item }}
</td>
</tr>
</tbody>
</table>
The current version of ng2-redux is very much a direct port of ng-redux. Michael and myself, with input from Cosmin, Abdella, Chris and others at Rangle are working on a TypeScript version of ng2-redux with better type support, and an improved API for working with observables.
the ngRedux.connect
API will remain the same. However, we are also introducing a ngRedux.select
that allows you to expose segments of the state as an observable, and works better with the Angular 2 OnPush change detection.
this.owner$ = this.stateService
.select(state => state.filters.get('owner'));
this.status$ = this.stateService
.select(state => state.filters.get('taskStatus'));
this.tasks$ = this.stateService
.select(state => state.tasks)
.combineLatest(this.owner$, this.status$,
(tasks, owner, status) => {
// do stuff
});