Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
In this article I’m continue to discuss creating react redux app using redux-lazy.
In my experience I found that I spend a lot of time to copy/paste code for working with storage. Each time I should create action types, action creators and reducers. Even the containers are in most cases the same.
That’s why I created a new library — redux-lazy.
I like to write react app in functional style — I create functional components as a pure functions: props => JSX.
It helps to test all code, each function has own single responsibility, no side effects, easy to write and support code. As you know ~80% of time we support existing code. So making code more declarative we save our time and money.
But first I want to share useful links to my previous articles:
React — redux for lazy developers
And
As I use redux-observable for side effects I put all logic to epics. React is just a view layer. All redux stuff I use only as getters/setters for store data and events for epics.
If you look at the relations:
Component (view)<->Store (DB)<->epics (logic)
You can see that view layer is just a template for showing data from our store (redux). Each change in store we should see in components.
And store works not only with view. Each store action we catch in epics and process it to make different stuff like ajax request, change store data (get response and clear form…).
To make code and tests I spend each time ~50% to logic, ~20% to components and ~30% to store.
Each time to get/set data to store we need to create action types, action creators, reducers and containers up to 30% of our time.
After that I tried to save this time and created redux-lazy.
So next I’ll show how to create classic react redux app, add testing to cover all code and switch redux stuff (types, actions, reducers…)to redux-lazy.
But first — I want to repeat:
- React is just a view layer,
- Pure functions: props => jsx,
- No other stuff…
Redux is our store. The same:
- Pure functions (action creators, reducers),
- No side effects,
- No logic,
- Just get/set data to store (CRUD),
- All logic in epics.
We need to install node.js, npm. After that install yarn and create-react-app like in previous article. And create project:
mkdir redux-appcd redux-appcreate-react-app .
And run it:
yarn start
Result:
Let’s install redux and react-redux:
yarn add redux react-redux
I like to create single responsibility modules and keep there all code like react components, redux and epics stuff.
But first about our app…
We will create a form to submit post using JSONPlaceholder service. And next we will create another module to work with comments.
First step: we should create directories and empty files.
mkdir -p src/modules/postmkdir src/modules/post/componentsmkdir src/modules/post/containersmkdir src/modules/post/typesmkdir src/modules/post/actionsmkdir src/modules/post/reducermkdir src/modules/post/epics
touch src/modules/post/index.jstouch src/modules/post/components/index.jsxtouch src/modules/post/containers/index.jstouch src/modules/post/types/index.jstouch src/modules/post/actions/index.jstouch src/modules/post/reducer/index.jstouch src/modules/post/epics/index.js
All code you can find on github.
And our tree:
Don’t forget to install redux-logger. It useful if you need to check current state:
yarn add redux-logger
And prop-types — it helps to validate react component properties:
yarn add prop-types
Next step: we need to create store and reducers.
Reducers:
touch src/reducers.js
With code:
import { combineReducers } from 'redux';import post from './modules/post/reducer';export default combineReducers({ post });
And store:
touch src/store.js
With code:
import { createStore, applyMiddleware } from 'redux';import logger from 'redux-logger';import reducers from './reducers';const store = createStore( reducers, applyMiddleware(logger));export default store;
And update src/index.js:
import React from 'react';import ReactDOM from 'react-dom';import { Provider } from 'react-redux';import './index.css';import App from './App';import store from './store';import registerServiceWorker from './registerServiceWorker';ReactDOM.render(<Provider store={store}> <App /> </Provider>, document.getElementById('root'));registerServiceWorker();
Post module code:
To send a post we need to have possibility to change title, body and submit the form. For each point we need to have action type and action creator:
Action types (src/modules/post/types/index.js):
export const POST_TITLE = '@@post/TITLE';export const POST_BODY = '@@post/BODY';export const POST_SUBMIT = '@@post/SUBMIT';
To avoid collision I like to add prefix to types like @@post/TITLE.
Action creators (src/modules/post/actions/index.js):
import { POST_TITLE, POST_BODY, POST_SUBMIT,} from '../types';export const titleAction = title => ({ type: POST_TITLE, title,});export const bodyAction = body => ({ type: POST_BODY, body,});export const submitAction = () => ({ type: POST_SUBMIT,});
Reducer (src/modules/post/reducer/index.js):
import { POST_TITLE, POST_BODY } from '../types';const defaultState = { title: '', body: '',};export default (state = defaultState, action) => {switch (action.type) {case POST_TITLE:case POST_BODY:return { ...state, ...action };default:return state; }};
Here I change state for title and body. To submit form I don’t need to make any actions (changes) with store.
Container (src/modules/post/containers/index.js):
import { connect } from 'react-redux';import * as actions from '../actions';const mapStateToProps = state => state.post;const mapDispatchToProps = { ...actions };export default connect(mapStateToProps, mapDispatchToProps);
We have a simple container. As we work only with post data, it just takes part of store and all action creators for post.
In next article I’ll show how to combine data from different parts of store and memoize it with reselect.
Component (src/modules/post/components/index.jsx):
import React from 'react';import PropTypes from 'prop-types';const PostComponent = props => ( <form onSubmit={(event) => { event.preventDefault(); props.submitAction(); }}> <h1>Our form example</h1> <div> <input type="text" onChange={event => props.titleAction(event.target.value)} value={props.title} /> </div> <div> <textarea onChange={event => props.bodyAction(event.target.value)} value={props.body} /> </div> <div> <input type="submit" value="Submit" /> </div> </form>);PostComponent.propTypes = { title: PropTypes.string.isRequired, body: PropTypes.string.isRequired, titleAction: PropTypes.func.isRequired, bodyAction: PropTypes.func.isRequired, submitAction: PropTypes.func.isRequired,};export default PostComponent;
Our component is a pure functionm it gets properties and returns JSX code.
Here you can see prop types validation. All properties are required because we have post data in store each time from reducer.
And our post module entry point (src/modules/post/index.js):
import PostComponent from './components';import PostContainer from './containers';export default PostContainer(PostComponent);
Last thing — we need to update src/App.jsx to render our post module:
import React, { Component } from 'react';import logo from './logo.svg';import './App.css';import Post from './modules/post';class App extends Component { render() {return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header><Post /> </div> ); }}export default App;
And result:
Not perfect, but this article is not about UI/UX.
Let’s check our actions…
Title changing:Body changing:And submit:
That’s all for now. We just created a simple react-redux app. Next step is testing.
I like to use single responsibility packages and create own frameworks for development (yeps, wpb, redux-lazy) and testing. I’ll use next packages:
- mocha — a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.
- chai — a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.
- sinon — standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework.
- enzyme — a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.
- nyc — JavaScript test coverage tool.
yarn add mocha chai sinon enzyme nyc enzyme-adapter-react-16 -D
One small thing. To make production ready app you should not forget about code quality and security. For this stuff I use eslint with airbnb config and nsp — node security platform command-line tool:
yarn add eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react nsp jsdom redux-mock-store babel-register -D
Jsdom is a pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js.
Redux-mock-store — a mock store for your testing your redux async action creators and middleware.
To run all commands I use npm-run-all — a CLI tool to run multiple npm-scripts in parallel or sequential:
yarn add npm-run-all
And update package.json scripts section:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "eject": "react-scripts eject","test": "npm-run-all test:*", "test:security": "nsp check", "test:lint": "eslint . --ext .js,.jsx"},
And some configs (just create those files in project root directory):
.eslintignore
buildcoveragesrc/registerServiceWorker.js
.eslintrc
{ "parser": "babel-eslint", "extends": "airbnb", "env": { "browser": true, "node": true, "mocha": true}}
Let’s run it:
yarn test
And we see a lot of errors and warnings from eslint. It helps to keep the same code style for all team. After fix:
All changes you can find in github repository.
Don’t forget about axios:
yarn add axios
I like this client because I can use it on client and backend sides. It can cancel request by timeout. You can even use it as graphQL client.
Next step is testing…
First, we need to create helper for using jsdom:
tests/helper.js:
require('babel-register')({ presets: [ 'react', 'env', ], plugins: [ 'transform-object-rest-spread', ],});
const { configure } = require('enzyme');const Adapter = require('enzyme-adapter-react-16');const axios = require('axios');const httpAdapter = require('axios/lib/adapters/http');axios.defaults.host = 'http://localhost:3000';axios.defaults.adapter = httpAdapter;configure({ adapter: new Adapter() });const { JSDOM } = require('jsdom');const exposedProperties = ['window', 'navigator', 'document'];const { window } = new JSDOM('', { url: 'http://localhost' });global.document = window.document;global.window = window;Object.keys(document.defaultView).forEach((property) => {if (typeof global[property] === 'undefined') { exposedProperties.push(property); global[property] = document.defaultView[property]; }});global.navigator = { userAgent: 'node.js',};global.localStorage = { setItem() {},};global.documentRef = document;
I added babel-register to compile a new ES standards to old ES5 (impors, object spread…). After that I configured enzyme adapter and axios client. And the most important part — jsdom configuration.
.nycrc
{ "extension": [".js",".jsx"], "require": ["./tests/helper.js"], "exclude": [ "node_modules", "build", "coverage", "tests", "src/registerServiceWorker.js" ], "check-coverage": true, "per-file": false, "statements": 80, "branches": 80, "functions": 80, "lines": 80, "reporter": [ "lcov", "text", "text-summary", "html" ], "all": true}
It’s nyc config. We use it for getting code coverage info.
As you can see from config — we need to test js/jsx files, add our helper and exclude some directories and files from testing. Other parameters help with coverage reports.
To run it add line to package.json:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "eject": "react-scripts eject", "test": "npm-run-all test:*", "test:security": "nsp check", "test:lint": "eslint . --ext .js,.jsx","test:coverage": "nyc mocha --timeout 5000 tests/**/*.{js,jsx}"},
Here we run nyc with mocha, we set timeout for testing and path to test files.
If you want to run only mocha without nyc:
mocha --timeout 5000 --require tests/helper.js tests/**/*.{js,jsx}
Now we can run testing:
yarn test
We didn’t pass it because we don’t have any tests.
Let’s create it…
Action types (tests/modules/post/types/index.js):
import { expect } from 'chai';import * as types from '../../../../src/modules/post/types';describe('Testing post module types', () => { it('should test POST_TITLE', () => { expect(types.POST_TITLE).to.be.equal('@@post/TITLE'); }); it('should test POST_BODY', () => { expect(types.POST_BODY).to.be.equal('@@post/BODY'); }); it('should test POST_SUBMIT', () => { expect(types.POST_SUBMIT).to.be.equal('@@post/SUBMIT'); });});
In each test I need to import expect tool from chai package. It helps to make assertation in BDD style. Each test should start from describe section and include it section.
Action creators (tests/modules/post/actions/index.js):
import { expect } from 'chai';import * as actions from '../../../../src/modules/post/actions';import * as types from '../../../../src/modules/post/types';describe('Testing post module actions', () => { it('should test titleAction', () => {const title = 'title';const action = actions.titleAction(title); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_TITLE); expect(action.title).to.be.equal(title); }); it('should test bodyAction', () => {const body = 'body';const action = actions.bodyAction(body); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_BODY); expect(action.body).to.be.equal(body); }); it('should test submitAction', () => {const action = actions.submitAction(); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_SUBMIT); });});
As you can see we work with pure functions. No side effects. All time the same results using the same parameters. Easy to test — just run it and check result.
Action creators always return object with at least one property — action type. It’s required parameter for working with store and we should test it each time.
Container (tests/modules/post/containers/index.jsx):
import React from 'react';import { mount } from 'enzyme';import { expect } from 'chai';import { spy } from 'sinon';import configureMockStore from 'redux-mock-store';import { Provider } from 'react-redux';import PostContainer from '../../../../src/modules/post/containers';describe('Testing post module containers', () => { it('should test containers with login', () => {const Component = spy(() => null);const Container = PostContainer(Component);const title = 'title';const body = 'body';const mockStore = configureMockStore([]);const store = mockStore({ post: { title, body, }, });const wrapper = mount(<Provider store={store}><Container /></Provider>); expect(wrapper.find(Component)).to.have.length(1);const props = wrapper.find(Component).props(); expect(props.title).to.be.equal(title); expect(props.body).to.be.equal(body); expect(props.titleAction).to.be.a('function'); expect(props.bodyAction).to.be.a('function'); expect(props.submitAction).to.be.a('function'); });});
In this test we use mount because we need to work with storage. As redux is a singleton, to run each test we should clear it. Redux-mock-store helps with it.
Next we create store, mount our container and check component props. We don’t need a real component, just use sinon spy.
Component (tests/modules/post/containers/index.jsx):
import React from 'react';import { shallow } from 'enzyme';import { expect } from 'chai';import { spy } from 'sinon';import PostComponent from '../../../../src/modules/post/components';describe('Testing post module PostComponent', () => {const title = 'title';const body = 'body';const titleAction = () => 1;const bodyAction = () => 1;const submitAction = () => 1;const props = { title, body, titleAction, bodyAction, submitAction, }; it('should test PostComponent', () => {const wrapper = shallow(<PostComponent {...props} />); expect(wrapper.find('form')).to.have.length(1); expect(wrapper.find('h1')).to.have.length(1); expect(wrapper.find('h1').props().children).to.be.equal('Our form example'); expect(wrapper.find('div')).to.have.length(3); expect(wrapper.find('input')).to.have.length(2);const titleProps = wrapper.find('input').first().props(); expect(titleProps.type).to.be.equal('text'); expect(titleProps.value).to.be.equal(title); expect(titleProps.onChange).to.be.a('function'); expect(wrapper.find('textarea')).to.have.length(1);const bodyProps = wrapper.find('textarea').props(); expect(bodyProps.value).to.be.equal(body); expect(bodyProps.onChange).to.be.a('function');const submitProps = wrapper.find('input').last().props(); expect(submitProps.type).to.be.equal('submit'); expect(submitProps.value).to.be.equal('Submit'); }); it('should test PostComponent titleAction', () => {const onChange = spy();const wrapper = shallow(<PostComponent {...props} titleAction={onChange} />); expect(wrapper.find('input')).to.have.length(2);const titleProps = wrapper.find('input').first().props(); expect(titleProps.type).to.be.equal('text'); expect(titleProps.value).to.be.equal(title); expect(titleProps.onChange).to.be.a('function'); wrapper.find('input').first().simulate('change', { target: { value: 'test' } }); expect(onChange.withArgs('test').calledOnce).to.be.equal(true); }); it('should test PostComponent bodyAction', () => {const onChange = spy();const wrapper = shallow(<PostComponent {...props} bodyAction={onChange} />); expect(wrapper.find('textarea')).to.have.length(1);const bodyProps = wrapper.find('textarea').props(); expect(bodyProps.value).to.be.equal(body); expect(bodyProps.onChange).to.be.a('function'); wrapper.find('textarea').simulate('change', { target: { value: 'test' } }); expect(onChange.withArgs('test').calledOnce).to.be.equal(true); }); it('should test PostComponent submitAction', () => {const onChange = spy();const preventDefault = spy();const wrapper = shallow(<PostComponent {...props} submitAction={onChange} />); expect(wrapper.find('form')).to.have.length(1); wrapper.find('form').simulate('submit', { preventDefault }); expect(onChange.calledOnce).to.be.equal(true); expect(preventDefault.calledOnce).to.be.equal(true); });});
To test each component (if it’s created as a pure function), we just need to make shallow render and test props. Some times we need to simulate DOM actions like click or submit.
Entry point (tests/modules/post/index.jsx):
import React from 'react';import { mount } from 'enzyme';import { expect } from 'chai';import { spy } from 'sinon';import configureMockStore from 'redux-mock-store';import { Provider } from 'react-redux';import PostModule from '../../../src/modules/post';import PostComponent from '../../../src/modules/post/components';describe('Testing post module', () => { it('should test post module', () => {const title = 'title';const body = 'body';const mockStore = configureMockStore([]);const store = mockStore({ post: { title, body, }, });const wrapper = mount(<Provider store={store}><PostModule /></Provider>); expect(wrapper.find(PostComponent)).to.have.length(1);const props = wrapper.find(PostComponent).props(); expect(props.title).to.be.equal(title); expect(props.body).to.be.equal(body); expect(props.titleAction).to.be.a('function'); expect(props.bodyAction).to.be.a('function'); expect(props.submitAction).to.be.a('function'); });});
The same like container test.
If we disable checking of all other code except our module:
.nycrc
{ "extension": [".js",".jsx"], "require": ["./tests/helper.js"], "exclude": [ "node_modules", "build", "coverage", "tests", "src/registerServiceWorker.js","src/*.*" ], "check-coverage": true, "per-file": false, "statements": 80, "branches": 80, "functions": 80, "lines": 80, "reporter": [ "lcov", "text", "text-summary", "html" ], "all": true}
We have full code coverage:
So, as you see, we spend a lot of time to create and test redux stuff. To save time I created redux-lazy. I described it in previous part. Now I want to delete all redux code and replace it by 7 lines of code.
yarn add redux-lazy
And add it to our module (src/modules/post/rl/index.js):
import RL from 'redux-lazy';const rl = new RL('post');rl.addAction('title', { title: '' });rl.addAction('body', { body: '' });rl.addAction('submit');const result = rl.flush();export default result;
Let’s update our actions and reducer:
Action creators:
import rl from '../rl';const { POST_TITLE, POST_BODY, POST_SUBMIT,} = rl.types;export const titleAction = title => ({ type: POST_TITLE, title,});export const bodyAction = body => ({ type: POST_BODY, body,});export const submitAction = () => ({ type: POST_SUBMIT,});
Reducer:
import rl from '../rl';const { POST_TITLE, POST_BODY,} = rl.types;const defaultState = { title: '', body: '',};export default (state = defaultState, action) => {switch (action.type) {case POST_TITLE:case POST_BODY:return { ...state, ...action };default:return state; }};
Types test:
import { expect } from 'chai';import rl from '../../../../src/modules/post/rl';const { types } = rl;describe('Testing post module types', () => { it('should test POST_TITLE', () => { expect(types.POST_TITLE).to.be.equal('@@post/TITLE'); }); it('should test POST_BODY', () => { expect(types.POST_BODY).to.be.equal('@@post/BODY'); }); it('should test POST_SUBMIT', () => { expect(types.POST_SUBMIT).to.be.equal('@@post/SUBMIT'); });});
And after testing we see the same results. Let’s delete types file and run tests again. It’s ok.
So we can combine at least creating types manual and redux-lazy.
Next step: action creators.
Reducer:
import { connect } from 'react-redux';import rl from '../rl';const { actions } = rl;const mapStateToProps = state => state.post;const mapDispatchToProps = { ...actions };export default connect(mapStateToProps, mapDispatchToProps);
And tests:
import { expect } from 'chai';import rl from '../../../../src/modules/post/rl';const { types, actions } = rl;describe('Testing post module actions', () => { it('should test titleAction', () => {const title = 'title';const action = actions.titleAction({ title }); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_TITLE); expect(action.payload.title).to.be.equal(title); }); it('should test bodyAction', () => {const body = 'body';const action = actions.bodyAction({ body }); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_BODY); expect(action.payload.body).to.be.equal(body); }); it('should test submitAction', () => {const action = actions.submitAction(); expect(action).to.be.a('object'); expect(action.type).to.be.equal(types.POST_SUBMIT); });});
Next step: reducer (src/reducers.js)
import { combineReducers } from 'redux';import rl from './modules/post/rl';const { nameSpace: postNameSpace, reducer: postReducer } = rl;export default combineReducers({ [postNameSpace]: postReducer });
And we can delete it.
And last step is container. If it’s so simple (just part of state from the module and all actions from the module), we can get it from redux-lazy too.
Our entry point in the end:
import PostComponent from './components';import rl from './rl';const { Container: PostContainer } = rl;export default PostContainer(PostComponent);
If we run our app we see the same result:
And if we run tests:
We can see the same tests, the same functionality but less code. Let’s see tree:
You can open coverage report in your browser. Just open coverage/index.html
This article bigger then I planned. So I can give more examples (redux-observable, reselect, recompose) in next part.
But the main goal of this article is to show how fast and easy to work with redux using redux-lazy. We make a lot of code lines to just get/set data to store.
In first code coverage report we had 27 lines of code. Now we have 16. We have 59% less code with the same functionality.
And again... Some developers put a lot of logic to reducer (change data brfore saving). But it’s hard to support. All code should be simple like pure function:
props => JSX
What could be easy?
Keep pure your components, your store and move all logic to special tools like redux-observable and redux-lazy.
React — redux for lazy developers. Part 2 was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.