Writing Actions and Reducers
┻┳|
┳┻| _
┻┳| •.•) 💬 "Hey, Checkout this awesome documentation for actions and reducers!"
┳┻|⊂ノ ┻┳|
What are actions?
Quoting the redux documentation they are:
Actions are payloads of information that send data from your application to your store.
They are simply plain JavaScript objects
/* trigger the panning action of the map to a center point */
const center = [42.3, 36.5];
export const PAN_TO = 'MAP:PAN_TO';
{
type: PAN_TO,
center
}
They must have type property, typically a constant with a string value, but any other properties are optional
Why we use them
We need them to trigger changes to the application's store via reducers. To do that we use Action Creators
Action Creators
They are simply function that returns actions objects
const defaultValue = [42.3, 36.5];
/*
* by convention, use an initial name (the action filename)
* in order to describe better the action type, in this case MAP
* separated by a colon : and the action constant name
*/
export const PAN_TO = 'MAP:PAN_TO';
export const panTo = (center = defaultValue) => ({
type: PAN_TO,
center
});
Note: Stick to es6 import/export module system and when possible provide a default value for the parameters
These action creators are used in the connected components or in MapStore2 plugins But actions by themselves are not enough we need Reducers that intercepts those actions and change the state accordingly.
Note: Remember to put all the actions .js files in the web/client/actions folder or in js/actions if you are working with custom plugins
Reducers
Again quoting redux documentation they are:
Reducers specify how the application's state changes in response to actions sent to the store.
Reducers are pure functions that take the previous state and an action and return a new state
(previousState, action) => newState
let's see an example:
// @mapstore is an alias for dir_name/web/client (see webpack.config.js)
import {PAN_TO} from '@mapstore/actions/map';
export default function map(state, action) {
switch (action.type) {
case PAN_TO: {
return {
...state,
center: action.center
};
}
default: return state;
}
}
As you can see we are changing the center of the map that triggers the panning action of the mapping library
And that's it we have wrote an action and a reducers that make the map panning around.
Note: Remember to put all the reducers .js files in the web/client/reducers folder or in js/reducers if you are working with custom plugins
Advanced usage and tips
Sometimes you need to change a value of an item which is stored inside an array or in a nested object.
Let's imagine we have this object in the store:
layer: {
features: [object_1, object_2, ...object_n]
}
And we have created an action that holds the id of the object to change and some properties
export const UPDATE_LAYER_FEATURE = 'LAYER:UPDATE_LAYER_FEATURE'
export const updateFeature = (id, props = {}) => ({type: UPDATE_LAYER_FEATURE, id, props})
Then in the reducer we can have different implementations. Here we show the one using arrayUpdate from @mapstore/utils/ImmutableUtils for updating objects in array
import {UPDATE_LAYER_FEATURE} from '@mapstore/actions/layer';
import {find} from 'lodash';
const defaultState = {
features: [{ id: 1, type: "Feature", geometry: { type : "Point", coordinates: [1, 2]}}]
};
export default function layer(state = defaultState, action) {
switch (action.type) {
case UPDATE_LAYER_FEATURE: {
// let's assume that action.props = {newProp: "newValue"}
const feature = find(state.features, {id: action.id});
// merging the old feature object with the new prop while replacing the existing element in the array
const newFeature = {...feature, ...action.props};
return arrayUpdate("features", newFeature, {id: action.id}, state);
// after this you expect to find the new properties in the feature specified by the id
}
default: return state;
}
}
Testing
Tests in mapstore are stored in __tests__
folder at the same level where actions/reducer are.
The file name is the same of the action/reducer with a '-test' suffix
actions/map.js
actions/__tests__/map-test.js
or
reducers/map.js
reducers/__tests__/map-test.js
We use expect as testing library, therefore we suggest to have a look there.
How to test an action
Typically you want to test the type and the params return from the action creator
let's test the mapTo action:
// copyright section
import expect from 'expect';
import {panTo, PAN_TO} from '@mapstore/actions/map';
describe('Test correctness of the map actions', () => {
it('testing panTo', () => {
const center = [2, 3];
const returnValue = panTo(center);
expect(returnValue.type).toEqual(PAN_TO);
expect(returnValue.center).toEqual(center);
});
});
In order to speed up the unit test runner, you can:
- change the path in tests.webpack.js (custom/standard project) or build\tests.webpack.js (framework) to point to the folder parent of tests
for example
'/js/actions'
for custom/standard project or'../web/client/actions'
for framework - then run this command:
npm run test:watch
This allows to run only the tests contained to the specified path. Note: When all tests are successfully passing remember to restore it to its original value.
How to test a reducer
Here things can become more complicated depending on your reducer but in general you want to test all cases
// copyright section
import expect from 'expect';
import {panTo} from '@mapstore/actions/map';
import map from '@js/reducers/map'; // the one created before not the one present in @mapstore/reducers
describe('Test correctness of the map reducers', () => {
it('testing PAN_TO', () => {
const center = [2, 3];
const state = map({}, panTo(center));
// here you have to check that state has changed accordingly
expect(state.center).toEqual(center);
});
});
Here for speedup testing you can again modify the tests.webpack.js (custom/standard project) or build\tests.webpack.js (framework)
in order to point to the reducers folder and then running
npm run test:watch
Actions and epics
Actions are not only used by redux to update the store (through the reducers), but also for triggering side effects workflows managed by epics
For more details see Writing epics