React — redux for lazy developers. Part 3

Written by evheniybystrov | Published 2018/06/05
Tech Story Tags: redux | react | redux-lazy | redux-observable | react-redux

TLDRvia the TL;DR App

It’s the last part of series about creating react redux app.

In previous two parts I described how to create classic react redux app, then I made full test coverage of app post module and replaced all redux stuff (action types, action creators, reducer and container) by 7 lines of Redux Lazy — a library created to make redux development more declarative.

In this article I’ll show how to add logic in react redux app using redux-observable, recompose and reselect.

Links to previous parts:

React — redux for lazy developers_Each time working with redux in react app we spend a lot of time to make action types, action creators, reducers… Most…_hackernoon.com

and

React — redux for lazy developers. Part 2_In this article I’m continue to discuss creating react redux app using redux-lazy._medium.com

I’ll use code from the second part. You can find it on github.

I updated redux-lazy: options docs, new tests

After small refactoring you can get 100% the same results like using redux stuff (even better — see form onSubmit):

rl.js (src/modules/post/rl/index.js)

Before:

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;

And now:

import RL from 'redux-lazy';

const rl = new RL('post');rl.addAction('title',{ title: '' },{ isFormElement: true, asParams: 'title' },);rl.addAction('body',{ body: '' },{ isFormElement: true, asParams: 'body' },);rl.addAction('submit',{},{ isForm: true },);

const result = rl.flush();

export default result;

Component (src/modules/post/components/index.jsx)

Before:

import React from 'react';import PropTypes from 'prop-types';

const PostComponent = props => (<formonSubmit={(event) => {event.preventDefault();props.submitAction();}}

<h1>Our form example</h1>  
<div>  
  <input  
    type="text"  
    **onChange={event => props.titleAction({ title: event.target.value })}**  
    value={props.title}  
  />  
</div>  
<div>  
  <textarea  
    **onChange={event => props.bodyAction({ body: 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;

And now:

import React from 'react';import PropTypes from 'prop-types';

const PostComponent = props => (<form onSubmit={props.submitAction}><h1>Our form example</h1><div><inputtype="text"onChange={props.titleAction}value={props.title}/></div><div><textareaonChange={props.bodyAction}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;

And no payload in action:

And again...

Store is a database. Setters are action creators and reducers, getters are selectors for connect mapStateToProps. All logic should be in other place. No logic inside reducers or actions. It could be like stored procedures — a lot of logic and no way for good testing.

As I said before I like to work with React in a functional way. I use components like a pure functions.

It’s easy to describe as a JS arrow function: props => JSX.

When you need to add local state or react component lifecycle you can use recompose — a react utility belt for function components and higher-order components.

I created another article with some examples:

React: functional way_As you may know working with react you can use functions or classes — work with stateless and stateful components. In…_blog.cloudboost.io

We will use recompose lifecycle to get data about post when component was rendered.

First, we need to install it:

yarn add recompose

Then we should create wrapper for our component — high order component (HOC). Let’s create src/modules/post/containers/lifecycle.js:

import { lifecycle } from 'recompose';

export default lifecycle({componentDidMount() {this.props.loadAction();},});

And update our Redux Lazy model (src/modules/post/rl/index.js):

import RL from 'redux-lazy';

const rl = new RL('post');

rl.addAction('title', { title: '' }, { isFormElement: true, asParams: 'title' });rl.addAction('body', { body: '' }, { isFormElement: true, asParams: 'body' });rl.addAction('submit', {}, { isForm: true });rl.addAction('load');rl.addAction('loaded', { title: '', body: '' }, { asParams: ['title', 'body'] });rl.addAction('error', { error: null }, { asParams: 'error' });

const result = rl.flush();

const {nameSpace,actions: {bodyAction,errorAction,loadAction,loadedAction,submitAction,titleAction,},types: {POST_BODY,POST_ERROR,POST_LOAD,POST_LOADED,POST_SUBMIT,POST_TITLE,},} = result;

export default result;

export {nameSpace,bodyAction,errorAction,loadAction,loadedAction,submitAction,titleAction,POST_BODY,POST_ERROR,POST_LOAD,POST_LOADED,POST_SUBMIT,POST_TITLE,};

I added load action to start loading post data, loaded action — show that we don’t have errors and error action — save error in store if we have a problems, then we can show it on the page.

I export nameSpace, types and actions to avoid magic each time when I need to import it in other place like epics or tests.

And we need to wrap our Post component in module entry point (src/modules/post/index.js):

import PostComponent from './components';import rl from './rl';import lifecycleContainer from './containers/lifecycle';

const { Container: PostContainer } = rl;

export default PostContainer(lifecycleContainer(PostComponent));

Check it:

We can see our loadAction on starting.

Our next step is making ajax request and manage side effects.

There are a lot of tools to work with async action using redux. As redux is totally function it doesn’t have a side effects. And you should manage it using other tools like redux-thunk, redux-promise, redux-saga or redux-observable.

About redux-thunk and redux-promise you can read in article:

Redux side effects and me_Thunks, sagas, effects and loops. My brief opinion on some of the Redux side effect middlewares._medium.com

I don’t use those tools because it’s created for making small actions. For example, make one ajax request for app... But if you are making a great app, you need to manage a lot of stuff like ajax request, making many actions at the same time…

So I see only two ways: redux-saga and redux-observable.

I choose redux observable because it’s implementation of FRP — functional reactive programming using Rx.js. It’s more declarative — less code, more sense.

You can read more about redux-observable:

Introduction · redux-observable_Edit description_redux-observable.js.org

And see a great video presentation:

And if you want to read some articles to compare both tools:

Redux-Saga V.S. Redux-Observable - HackMD_Redux-Saga V.S. Redux-Observable === ### Redux-Saga V.S. Redux-Observable 1. Metal Model 2. Side_hackmd.io

and

Redux-Observable Epics vs Redux-Sagas_And Why Do I Care?_shift.infinite.red

I totally recommend to read first link.

The main pattern of redux-observable is epics. Each time when you dispatch action, redux uses reducers, middlewares and subscribed tools.

Each time when you dispatch action, redux-observable runs epics created as a handlers for the action type.

Before we start to create our first epic we need to install it:

yarn add [email protected] 

While we rare waiting for redux-observable release we should use Rx.js version 5.

To use redux-observable (see docs) we need to create root epic where we can import epics from our modules and add it as middleware to our store.

src/epics.js

import { combineEpics } from 'redux-observable';

import postEpics from './modules/post/epics';

const rootEpic = combineEpics(postEpics);

export default rootEpic;

src/store.js

import { createStore, applyMiddleware } from 'redux';import { createEpicMiddleware } from 'redux-observable';import logger from 'redux-logger';

import reducers from './reducers';**import rootEpic from './epics';**const epicMiddleware = createEpicMiddleware(rootEpic);

const store = createStore(reducers,applyMiddleware(logger, epicMiddleware),);

export default store;

How to use code splitting and dynamic import of reducers and epics I’ll show in next articles thanks to my new library — wpb.

Just to remember, we are using https://jsonplaceholder.typicode.com/ service to get post data. It’s just an example.

And our post module epic:

src/modules/post/epics/index.js

import { combineEpics } from 'redux-observable';

import loadEpic from './load';

export default combineEpics(loadEpic);

If we have more than one epic we need to combine it.

For this example I’ll show only load epic, but you can add submit epic to update story on the server.

src/modules/post/epics/load.js

import 'rxjs';import { Observable } from 'rxjs/Observable';

import axios from 'axios';

import {POST_LOAD,loadedAction,titleAction,bodyAction,errorAction,} from '../rl';

const loadEpic = action$ => action$.ofType(POST_LOAD).switchMap(() => {const request = axios.get('https://jsonplaceholder.typicode.com/posts/1').then(({ data }) => data);

**return** Observable  
  .fromPromise(request)  
  .switchMap(({ title, body }) => Observable  
    .of(loadedAction())  
    .concat(Observable.of(titleAction(title)))  
    .concat(Observable.of(bodyAction(body))));  

}).catch(error => Observable.of(errorAction(error)));

export default loadEpic;

It can be complicated at the first time.

Here I import POST_LOAD action type to run epic (we have event with this action from recompose lifecycleContainer) and actions: loaded — to show that we have a good response or error — if we have a problems. Title and body actions we use to set data to our store.

Using Observable.fromPromise() we can create stream from promise created by axios.

Then we can create a new stream — Observable.of(loadedAction()).

And concat streams for setting title and body from response.

If we have an error we need to catch it and return a new stream with error data (each time we need to return stream).

We can check results:

Now small update of our component:

import React from 'react';import PropTypes from 'prop-types';

const PostComponent = props => (<form onSubmit={props.submitAction}><h1>Our form example</h1><div><inputtype="text"onChange={props.titleAction}value={props.title}/></div><div><textareaonChange={props.bodyAction}value={props.body}/></div><div><input type="submit" value="Submit" /></div>{props.error && (<div>{props.error.message}</div>)}</form>);

PostComponent.propTypes = {title: PropTypes.string.isRequired,body: PropTypes.string.isRequired,error: PropTypes.objectOf(PropTypes.any),titleAction: PropTypes.func.isRequired,bodyAction: PropTypes.func.isRequired,submitAction: PropTypes.func.isRequired,};

PostComponent.defaultProps = {error: null,};

export default PostComponent;

I added error message to the form.

To emulate error I’ll change url to https://jsonplaceholder.typicode.com/posts/test and check result:

Now we can show error on page to see that we have 404 Page Not Found error for wrong url.

And testing:

import nock from 'nock';import { expect } from 'chai';import { ActionsObservable } from 'redux-observable';

import {loadAction,loadedAction,titleAction,bodyAction,POST_ERROR,} from '../../../../src/modules/post/rl';

import epics from '../../../../src/modules/post/epics';

describe('Testing post module loadEpic', () => {it('should test loadEpic without error', (done) => {const title = 'title';const body = 'body';

nock('https://jsonplaceholder.typicode.com')  
  .get('/posts/1')  
  .reply(200, { title, body });  

**const** action$ = ActionsObservable._of_(loadAction());  

**const** expectedOutputActions = \[  
  loadedAction(),  
  titleAction(title),  
  bodyAction(body),  
\];  

epics(action$)  
  .toArray()  
  .subscribe((actualOutputActions) => {  
    expect(actualOutputActions).to.be.eql(expectedOutputActions);  
    done();  
  });  

});

it('should test loadEpic with error', (done) => {const message = 'Request failed with status code 404';

nock('https://jsonplaceholder.typicode.com')  
  .get('/posts/1')  
  .reply(404);  

**const** action$ = ActionsObservable._of_(loadAction());  

epics(action$)  
  .toArray()  
  .subscribe((actualOutputActions) => {  
    expect(actualOutputActions).to.have.length(1);  

    expect(actualOutputActions\[0\].type).to.be.equal(POST\_ERROR);  
    expect(actualOutputActions\[0\].error.message).to.be.equal(message);  
    done();  
  });  

});});

I use nock to mock HTTP requests.

To test epic you just need to create a stream using ActionsObservable. Then put it to epic as the first parameter (the second is a store) and subscribe to output.

You can use epics not only for ajax requests. You can switch a new action:

action$ => actions$.ofType(REQUEST1).mapTo(request2Action)

With store:

(action$, store) => actions$.ofType(REQUEST1).switchMap((action) => {const state = store.getState();

console.log(action, state);

return Observable.of(newAction());  

});

I can stop on this. But I want to say a couple of words about performance.

My last updates (isForm and isFormElement options) help me to use Redux Lazy actions as links in react components. It’s a good stuff to avoid useless rendering.

But we should not forget about react-redux connect HOC. To get data from store we need to use selectors. As we use Redux Lazy Container we should not think about this because it’s to simple and it uses object links to store data:

import { connect } from 'react-redux';import post, { nameSpace } from '../rl';

const mapStateToProps = state => state[nameSpace];const mapDispatchToProps = { ...post.actions };

export default connect(mapStateToProps, mapDispatchToProps);

But If I need to change data before put it to component or get data from other module, each time I’ll get a new object and React will re-render it again and again even if data all time is the same.

Reselect — selector library for Redux, helps us with this problem.

Let’s install it and I’ll show how to use.

yarn add reselect

And our complicated container:

import { connect } from 'react-redux';import { createSelector } from 'reselect';

import post, { nameSpace } from '../rl';

const postSelector = state => ({title: state[nameSpace].title,body: state[nameSpace].body,titleLength: state[nameSpace].title.length,bodyLength: state[nameSpace].body.length,});

const mapStateToProps = createSelector(postSelector,newPost => newPost,);const mapDispatchToProps = { ...post.actions };

export default connect(mapStateToProps, mapDispatchToProps);

I added titleLength and bodyLength as a properties just for example. In real app you can get part from store or some parts and merge it. In any way without reselect you will create a new object each time and react will re-render components even if data is the same.

Reselect checks data inside of a new object and return each time link to the same object from memory. React will see the same link and won’t re-render component.

This is the last part of series about Redux Lazy. Please ask your questions in comments and don’t forget to star it on github.


Published by HackerNoon on 2018/06/05