Counter

We start by the simple example of a counter. A counter does not have asynchronous actions so Redux Ship is not necessary here, but it helps to get started.

Screenshot

Create a React application

Use create-react-app to create a counter application:

create-react-app counter

You should have the following files in src/:

App.css
App.js
App.test.js
index.css
index.js
logo.svg

Install Redux and Redux Ship with their dev tools:

npm install --save redux redux-ship redux-logger redux-ship-logger babel-polyfill

Optionally setup the Flow type system:

npm install -g flow-bin
flow init
flow

Model

We add a model.js file to describe the "Redux part" of the counter:

// @flow
export type State = number;

export const initialState = 0;

export type Commit = {
  type: 'Increment',
} | {
  type: 'Decrement',
};

export function reduce(state: State, commit: Commit): State {
  switch (commit.type) {
  case 'Increment':
    return state + 1;
  case 'Decrement':
    return state - 1;
  default:
    return state;
  }
}

This describes a quite standard reducer with typing. Notice that we name the actions Commit. This is to make clear that these actions are used to modify the state, by opposition to asynchronous actions.

View

In App.js we add the view of the counter:

// @flow
import React, { PureComponent } from 'react';
import './App.css';
import * as Controller from './controller';
import * as Model from './model';

type Props = {
  dispatch: (action: Controller.Action) => void,
  state: Model.State,
};

export default class App extends PureComponent<void, Props, void> {
  handleClickIncrement = (): void => {
    this.props.dispatch({type: 'ClickIncrement'});
  };

  handleClickDecrement = (): void => {
    this.props.dispatch({type: 'ClickDecrement'});
  };

  render() {
    return (
      <div className="App">
        <p>{this.props.state}</p>
        <button onClick={this.handleClickIncrement}>
          +1
        </button>
        <button onClick={this.handleClickDecrement}>
          -1
        </button>
      </div>
    );
  }
}

We display the value of the counter with <p>{this.props.state}</p>. To handle the clicks on the buttons +1 and -1 we dispatch actions to the controller.

Controller

We define the controller in controller.js:

// @flow
import * as Ship from 'redux-ship';
import * as Model from './model';

export type Action = {
  type: 'ClickIncrement',
} | {
  type: 'ClickDecrement',
};

export function* control(action: Action): Ship.Ship<*, Model.Commit, Model.State, void> {
  switch (action.type) {
  case 'ClickIncrement':
    yield* Ship.commit({type: 'Increment'});
    return;
  case 'ClickDecrement':
    yield* Ship.commit({type: 'Decrement'});
    return;
  default:
    return;
  }
}

The controller describes how to react to the application events, here a click on +1 or -1. We define a type Action which is the type of all these application events. The function control handles an action by returning a ship.

A ship is the description of a side effect, including API calls, modifications of the Redux state, timers, url update, or calls to third-party libraries with side effects. By side effect we mean "anything which is not purely functional". We define a ship with a generator and Redux Ship primitives.

We call:

yield* Ship.commit({type: 'Increment'});

to commit a commit to the Redux state. All functions in Redux Ship are called with yield*, you should never encounter a yield. We avoid the yield operator because it is a difficult to type in Flow, and instead call proxy functions with yield*.

The return type of control is:

Ship.Ship<*, Model.Commit, Model.State, void>

meaning that this controller is attached to our model. We could not for example run:

yield* Ship.commit({type: 'Foo'}); // error

as this would result in a Flow type error since {type: 'Foo'} is not of type Model.Commit.

Wrapping everything up

We instantiate a Redux store together with the Redux Ship middleware in store.js:

// @flow
import {applyMiddleware, createStore} from 'redux';
import * as Ship from 'redux-ship';
import createLogger from 'redux-logger';
import {logControl} from 'redux-ship-logger';
import * as Controller from './controller';
import * as Model from './model';

function runEffect() {}

const middlewares = [
  Ship.middleware(runEffect, logControl(Controller.control)),
  createLogger(),
];

export default createStore(
  Model.reduce,
  Model.initialState,
  applyMiddleware(...middlewares)
);

We provide two parameters to the Ship.middleware function:

  • runEffect which is empty for now, as we only have synchronous actions;
  • logControl(Controller.control)) which is our controller. We wrap it with a logControl to add loggin to the Redux Ship actions.

We bootstrap the application in index.js:

// @flow
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import store from './store';

function render(): void {
  ReactDOM.render(
    <App dispatch={store.dispatch} state={store.getState()} />,
    document.getElementById('root')
  );
}

store.subscribe(render);
render();

We define a React render function to render the application. We subscribe to the store to re-render when the Redux store is updated. Note that you can also use react-redux to connect React to Redux.

Snapshots

When we look at our browser's console we see something like:

Logs

We have an action {type: 'Increment'} which takes us from the state 0 to the state 1. This action is logged by redux-logger. We also have a line:

control @ 19:35:41.214 ClickIncrement

which is the log of our controller as given by redux-ship-logger. We see the snapshot of our controller:

[
  {
    type: 'Commit',
    commit: {type: 'Increment'}
  }
]

which is an array of one element, the commit of {type: 'Increment'}, describing all what the controller has done. Nothing fancy there, but the snapshots of our controllers will become increasingly useful as we design more complex controllers.

Let us move to the HTTP Request section to see how to make asynchronous actions.

results matching ""

    No results matching ""