MV* Patterns with React Redux

By Guy Y.

Feb 27, 2017

The main promise of react & React Native is code reuse. But As Aaron Greenwald states in this Wix talk, it not free. When done right you can achieve a very high code reuse percentage between your IOS, Android and Web apps.

In this blog post, I’ll suggest some guidelines that may help you smell bad code early on.

What code can we share?

When written correctly, React components can be reused. But we’re not going to focus on that aspect of code reuse.

Pure JS has nothing to do with React. Such code is known as Isomorphic JavaScript.

The more code we place outside of the components, the more of it we can reuse.

However “outside of the components” is a pretty big place. When the codebase is a messy, it’s hard to reuse. You can’t reuse what you can’t find. In order to reuse code, our team must keep strict design patterns and architecture guidelines.

Those guidelines are nothing special. They are well-known ages before React ever existed.

  1. Separate your logic from the view
  2. Separation of concerns
  3. DRY code

Separate your logic from the view

The rule of thumb we must always remember is the react is just the View layer in our MV* app.

Imagine you write your app with jQuery, you won’t place the JS code inside your HTML file, right?’ Rahter, you’ll place it in a JS file like a nice web developer.

How about Angular, would you place your logic in the templates? No, you’ll use factories/services.

You never want to place your logic inside a View, this is just a bad MV* practice. Ah, and if your app is written that way, you probably don’t unit test, so good luck with that. It is the sure path to huge debugging cycles and massive code refactoring.

Since react is just JS, it’s very tempting to place logic within your component, but it’s a total anti-pattern.

Separation of concerns

When we need to compute something, it’s easier to split the computation into small functions and then compose them into a bigger one. When dealing with a react component you usually see three concerns it deals with:

  • Linking to external logic/data
  • Inner-state management
  • Rendering

So… there is a lot going on in our component as is. We should keep it as minimal as we can.

We can’t do much about the rendering part except follow React best practices found at Thinking React.

The other two we can minimize by following a design pattern that we describe next.

Example

If you are familiar with redux, you are probably familiar with the following pattern:

class MyComponent extends React.Component {
  .
  .
  .

  doSomething = (user) => {
    this.setState({doingSomething: true})
    .
    . // run some validations
    .
    this.props.actions.doUserAction(user);
  }

  renderUser(user) {
    return (
      <ul>
        <li>{user.name}</li>
        <li>{user.email}</li>
        <button onClick={() => this.doSomething(user)}>Do Something</button>
      </ul>
    )
  }

  render() {
    return (
      <div>
        {this.prop.users.map(u => this.renderUser(u)}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  const users = state.users.filter((u) => u.active)
  return {
    users,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    actions: bindActionCreators(eventActions, dispatch),
  };
};

MyComponent.propTypes = {
  users: React.PropTypes.array.isRequired,
};


export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

Well, this component does quite a few things:

  • mapStateToProps – Prepares our users’ data for the view by filtering users from the global state.
  • mapDispatchToProps – This gives us access to redux action dispatching
  • doSomething – component internal logic

We can move at least some of those concerns out of the component.

The Dispatcher pattern

mapDispatchToProps has nothing to do with our view layer. Rather, it’s a way to link to our Redux dispatch method. Moreover, it’s just boiler-plate code; you’ll find it at every component that needs access to dispatch. We can DRY this piece of code by a simple placing it in a dispatcher:

//_dispatch_access.js
import store from '../store';

export const dispatch = action => store.dispatch(action);
// user_dispatcher.js
import { bindActionCreators } from 'redux';

import { dispatch } from './_dispatch_access';
import * as actions from '../actions/users_actions';

const dispatcher = bindActionCreators(actions, dispatch);
export default dispatcher;

The Presenter pattern

mapStateToProps derives the components data from external data (the global store). We do some manipulation in order to adjust the data to our view. Those manipulations can be DRYed out by using Presenter, most commonly known as selectors. Reselect is very popular optimization for this pattern:

// _state_access.js
import store from '../store';
export const getState = () => store.getState();
// user_selector.js
import { getState } from './_state_access';

export const all = (state = getState()) => state.users;
export const active = (state) => all(state).filter(u => u.active);

You might as well ask yourself why do we need the _state_access since we pass the state into as input to all our selector methods. Well, we don’t really need it.

The only place it comes handy is when we wish to access our state outside of a component, for example, from a helper. But note: It’s probably better to avoid such patterns unless absolutely necessary.

Helpers pattern

doSomethingToUser does too many things, such as changing the state, dispatching an action and validations.

  • As for the first thing, we can’t do much about that.
  • The second was minimized by the dispatcher pattern described above.
  • For validations, we can refactor into a helper module.

Helper is a collection of pure functions that derive/create data from its input.

// user_helper.js
export const canDoSomethingTo = (user) => {
  //do some validations and return true/false
}

Now, we can write our original component in the following pattern:

import * as userDispatcher from '../dispatchers/user_dispatcher';
import * as userSelector from '../selectors/user_selector';
import * as userHelper from '../helpers/user_helper'
class MyComponent extends React.Component {
  .
  .
  .
  doSomething = (user) => {
    this.setState({doingSomething: true})
    if (userHelper.canDoSomethingTo(user)) {
      userDispatcher.doUserAction(user);
    }
  }

  renderUser(user) {
    return (
      <ul>
        <li>{user.name}</li>
        <li>{user.email}</li>
        <button onClick={() => this.doSomething(user)}>Do Something</button>
      </ul>
    )
  }

  render() {
    return (
      <div>
        {this.prop.users.map(u => this.renderUser(u)}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    users: userSelector.active(state)
  };
};

MyComponent.propTypes = {
  users: React.PropTypes.array.isRequired,
};

export default connect(mapStateToProps)(MyComponent);

So, what did we do gain?

Our code is mostly pure functions! There are advantages to this:

  1. It’s easier to test. Pure functions comes straight from unit test heaven. Helpers are a bunch of pure functions.
  2. Selectors are pure just functions that take the global state as input.
  3. Dispatchers are pure and very minimal. As long we test user_actions.js there is no real reason to test them at all.
  4. Pure functions can be composed. In other words, they are easier to reuse.

Code smell tips

  1. React files should end with .jsx.
  2. Isomorphic JS files should end with .js
  3. jsx files should be small, no more than 100 lines.
  4. Structure your folders separation between js and jsx. For example:

core
helpers
user_helper.js
selectors
user_selector.js
dispatchers
user_dispatcher.js
webapp
pages
home_page.jsx
widgets
Native
pages
home_page.jsx
widgets

Summary

We introduced three patterns that organize the Model layer in a React MV* app:

  1. Dispatchers – These dispatches Redux actions
  2. Selectors – These prepare & format data from our Redux store into our React components.
  3. Helpers – These are general-purpose pure functions.

Leave a Reply

Your email address will not be published.