Redux signal middleware. A place to store your business logic and async code
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples
resources
src
tests
.babelrc
.gitignore
.npmignore
LICENSE
README.md
package.json
yarn.lock

README.md

Signal Middleware

Signal Middleware for Redux. Example: https://xnimorz.github.io/signal-middleware

npm install --save signal-middleware

or

yarn add signal-middleware

Signal-middleware is created to give a place for async logic of your application and an abstraction between View and Data layers.

Contents

Intro

In general, application can be divided into 3 parts:

  • view logic
  • business logic
  • data logic

Signal action provides information between view and business layers. Classic action provides information between business and data layers. Also you can always dispatch classic action from your view layer if no async actions are needed.

Here is an image to represent the work:

MVC using signal-middleware

The business logic here is representented by signal-middleware reactions.

Usage:

Getting started

  1. Import signalMiddleware to your store initialization file and add signalMiddleware to the list of middlewares:
import { createStore, applyMiddleware, combineReducers } from "redux";
import signalMiddleware from "signal-middleware";

const middlewares = [signalMiddleware /* Your other middlewares */];

export default function configureStore() {
  const store = createStore(
    combineReducers({
      /* Your reducers */
    }),
    applyMiddleware(...middlewares)
  );
  return store;
}
  1. Create a signal action:
export const SIGNAL_ACTION_KEY = "SIGNAL_ACTION_KEY";

export const signalActionCreator = data => ({
  signal: SIGNAL_ACTION_KEY,
  payload: data
});
  1. Add a reaction to the SIGNAL_ACTION_KEY signal:
import { addReaction } from "signal-middleware";

addReaction(SIGNAL_ACTION_KEY, ({ getState, dispatch }, payload) => {
  // Paste your code here
  // Dispatch new action via dispatch
  // Get your current store state via getState
  // Your action data is in payload
  // You can return a promise from this function, to handle it from dispatch e.g. dispatch({signal: SIGNAL_ACTION_KEY}).then(() => {do some})
});

Signal-middleware adds abstraction between View and Data layers.

Async actions

Signal-middleware allows you to create async functions:

import signalMiddleware, { addReaction } from "signal-middleware";

const ADD_TODO = "ADD_TODO";
const RECEIVE_TODO = "RECEIVE_TODO";

// Signal Action is an action that has `signal` field instead of `type`
const addTodoSignal = text => ({
  signal: ADD_TODO,
  payload: { text }
});

// Classic action works with store
const receiveTodo = text => ({
  type: RECEIVE_TODO,
  payload: { text }
});

// Add reaction to ADD_TODO signal
addReaction(ADD_TODO, ({ dispatch }, { text }) => {
  setTimeout(() => {
    // Here we can invoke actions using dispatch
    dispatch(receiveTodo(text));
  }, 1000);
});

Getting state

Signal-middleware provides { dispatch, getState } object as the first argument of your reaction. So you can get access to him.

Let's assume we shouldn't add todos which already have the same text:

import signalMiddleware, { addReaction } from "signal-middleware";

const ADD_TODO = "ADD_TODO";
const RECEIVE_TODO = "RECEIVE_TODO";

// Signal Action is an action that has `signal` field instead of `type`
const addTodoSignal = text => ({
  signal: ADD_TODO,
  payload: { text }
});

// Classic action works with store
const receiveTodo = text => ({
  type: RECEIVE_TODO,
  payload: { text }
});

// Reactions are the good place for your project business logic.
// It's separate from view and data logic. View layer works with business logic through the signal actions, and business logic layer works with data logic through the classic actions.
addReaction(ADD_TODO, ({ dispatch, getState }, { text }) => {
  setTimeout(() => {
    // Getting our current state
    if (getState().todos.some(todo => todo.text === text)) {
      return;
    }
    // Here we can invoke actions using dispatch
    dispatch(receiveTodo(text));
  }, 1000);
});

Async-await and business logic

Let's add to our example some async logic and errors handling:

import signalMiddleware, { addReaction } from "signal-middleware";

const ADD_TODO = "ADD_TODO";
const RECEIVE_TODO = "RECEIVE_TODO";
const PENDING_TODO = "PENDING_TODO";
const FAIL_TODO = "FAIL_TODO";

// Signal Action is an action that has `signal` field instead of `type`
const addTodoSignal = text => ({
  signal: ADD_TODO,
  payload: { text }
});

// Classic action works with store
const receiveTodo = text => ({
  type: RECEIVE_TODO,
  payload: { text }
});

const pendingTodo = () => ({
  type: PENDING_TODO
});

// Reactions are the good place for your project business logic.
// It's separated from view and data logic. View layer works with business logic through the signal actions, and business logic layer works with data logic through the classic actions.
addReaction(ADD_TODO, async ({ dispatch, getState }, { text }) => {
  try {
    // You can dispatch any number of actions
    dispatch(pendingTodo(result));
    const result = await axios.post(REMOTE_URL_FOR_TODOS, { text });
    dispatch(receiveTodo(result));
  } catch (e) {
    dispatch({ type: FAIL_TODO, payload: e });
  }
});

The last example shows us how we can implement the business logic using signal-middleware

Completed example

Postpone callback after async request completes

When you write a comment you should clear the text field after server request completes. At the moment you don't know about future id of the comment, so you would add a callback after server request completes. When you use signal-middleware and dispatch a signal action you can use async-await or directly return a promise from signal handler. Your view layer can subscribe to the promise using then. You can see it in our examples:

  1. Return a promise from signal handler directly: https://github.com/xnimorz/signal-middleware/master/examples/src/components/AddComment.js (with direct Promise wrapping)
  2. Declare async function to wrap it with promise (becouse async-await functions return a promise). You can see this example below this text or here:

Let's write a file with actions and actionCreators:

// actions/comments.js

export const ADD_COMMENT_SIGNAL = "ADD_COMMENT_SIGNAL";
export const RECEIVE_NEW_COMMENT = "RECEIVE_NEW_COMMENT";
export const REQUEST_ADD_COMMENT = "REQUEST_ADD_COMMENT";

export const addComment = comment => ({
  signal: ADD_COMMENT_SIGNAL,
  payload: comment
});

export const receiveComment = comments => ({
  type: RECEIVE_NEW_COMMENT,
  payload: comments
});

export const requestComment = () => ({
  type: REQUEST_ADD_COMMENT
});

Now we can handle ADD_COMMENT_SIGNAL using addReaction:

// models/comments.js
import { DIRTY, FETCH, CLEAR } from "../constants/status";
import axios from "axios";
import {
  RECEIVE_NEW_COMMENT,
  REQUEST_ADD_COMMENT,
  ADD_COMMENT_SIGNAL,
  receiveComment,
  requestComment
} from "../actions/comments";

import { addReaction } from "signal-middleware";

addReaction(ADD_COMMENT_SIGNAL, async ({ getState, dispatch }, payload) => {
  // You can dispatch as many actions in signalMiddleware as you need
  dispatch(requestComment());

  try {
    const { data } = await axios.post("/url/to/comments", { comment: payload });
    const comment = { id: data.id, text: data.text };
    // Dispatch new action to store
    dispatch(receiveComment(comment));
    // You can resolve or reject action and
    // handle promise in view layer (look to AddComment.js component)
    return comment;
  } catch (e) {
    return Promise.reject();
  }
});

export default function comments(state = { status: DIRTY, data: [] }, action) {
  switch (action.type) {
    case REQUEST_ADD_COMMENT: {
      return {
        ...state,
        status: FETCH
      };
    }
    case RECEIVE_NEW_COMMENT: {
      return {
        status: CLEAR,
        data: [action.payload, ...state.data]
      };
    }
    default:
      return state;
  }
}

In view layer we can handle a promise:

import React, { PureComponent } from "react";
import { connect } from "react-redux";

import TextArea from "./TextArea";
import Button from "./Button";

import { addComment } from "../actions/comments";

class AddComment extends PureComponent {
  textArea = React.createRef();

  addComment = () => {
    // We created async function as signal handler. Signal handler result will be received as returned value from actions dispatching
    // So we can clear field after async request comes from server
    this.props
      .addComment(this.textArea.current.value)
      .then(() => (this.textArea.current.value = ""));
  };

  render() {
    return (
      <div>
        <TextArea innerRef={this.textArea} />
        <Button onClick={this.addComment}>Add comment</Button>
      </div>
    );
  }
}

export default connect(
  null,
  { addComment }
)(AddComment);

Motivation

Nowadays, building complicated frontend application requires a plenty of business logic with server requests and so on. You can use middlewares such as redux-thunk to implement async actions creator. However after a definite time interval it would be complicated to work with tons of code in action creators. For example, if you want to show an alert to user, when he clicks the button, you wouldn't patch browser engine code, you just add some logic to your own project. You can relate to action similarly. Actions in redux application are similar to events in browser. Consequently, if some event (action) in your project is triggered, you have a reaction for the action. The main goal of signal-middleware is to give an abstraction for the implementation of a separate business logic.

Some more information about middlewares you can find in a lecture (Russian lang): https://docs.google.com/presentation/d/1qFTB--HrXCU0_nVQ_T4ZlB9CsiXpXQsASc14pctoggA/edit?usp=sharing

License

MIT