Hermes JS
Hermes JS is a universal action dispatcher for JavaScript apps. It facilitates the design and management of action flows, both for interacting with the UI and with back-end / third-party services.
In case you wonder, the library name is inspired by the Greek messenger of the gods.
Overview
Let’s consider an “action” as any type of data that can be dispatched, might include metadata, and could expect a response to be returned (e.g. an HTTP request, a WebSocket event, or a Redux action).
Such action can be represented as an object that has three properties:
type
- The action name - Stringdata
- Additional data associated with the action (optional) - Primitive or Objectmeta
- Metadata that is useful to process the action (optional) - Object
For example:
const simpleAction = type: 'INCREMENT_COUNTER'; const completeAction = type: 'NEW_USER' data: email: 'hello@world.com' password: 'pass' meta: method: 'POST' ;
Hermes abstracts the concept of actions being dispatched in an app, decoupling them from the actual dispatcher. It facilitates the generation of action creators, easily allows to include middleware, and simplifies testing.
It supports asynchronous flows, and integrates well with the latest front-end or back-end frameworks, making it a breeze to define your actions logic and cleanly combine different technologies (e.g. Flux / Redux, HTTP, WebSocket, etc).
For example, actions like the ones above can be dispatched with:
;; const data = email: 'hello@world.com' password: 'pass'; async { if await ;};
Or imagine a fairly complex logic, where you want an action to send some data to the back-end, and then only if the response is successful update the UI with the received data. Through a simple middleware (implementation details in the examples below) all this can be expressed with:
await ;
Install
npm install hermes-js
Usage
// Initializeconst dispatcher = ; // Dispatch an action; // Validate an actiondispatcher;
Hermes takes 3 arguments:
- The actions list - Array of strings
- The dispatch function - Function
- The middleware list (optional) - Array of functions
It returns a function to dispatch actions (i.e. a “dispacther”), which:
- When invoked automatically checks that the action type is part of the original actions list.
- If a list of middleware has been provided, before dispatching the action, runs it through each middleware following the original list order.
Each dispatcher takes 3 arguments:
- The action type - String
- The action data (optional) - Primitive or Object
- The action metadata (optional) - Object
Metadata typically is temporary information that is needed in middleware or in the dispatch function, but should be removed before the action is sent to its final destination.
Now let’s go back and take a look at the arguments to initialize Hermes in details.
1. actionsList
Array of strings
A list naming the actions we want to be able to dispatch (having the action names in uppercase leaves less doubts about capitalization, but you can choose whatever format you prefer). For example:
const actionsList = 'NEW_MESSAGE' 'NEW_USER' 'USER_LOGIN';
2. dispatch
Function
The callback that will be invoked to dispatch the action. Here’s a mock example (for more realistic use cases check the “Examples” section at the bottom):
const dispatch = console;
3. middleware
Array of functions
A list of the middleware that we want in the flow (if any). Each middleware function is passed three arguments.
- The action itself.
- The
next
callback, that when invoked inside the middleware executes the next middleware in the list (if there’s no middleware left,next
invokesdispatch
). - A basic action validator. The validator takes one argument, which is the action name (String), and simply returns it or throws an error if it’s not part of the original actions list.
For example:
const middleware = { console; return ; } { if actiontype === console; return ; };
Patterns
It’s useful to distinguish between “sync” and “async” actions.
Sync actions only contain synchronous code and are typically meant for front-end UI rendering (e.g. a Redux action). On the other hand, async actions contain asynchronous code and are mostly used to interact with the back-end or a third part API (e.g. through an HTTP request).
In our experience clearly separating sync and async actions leads to the cleanest logic and the best code scalability / maintainability / development speed.
In both cases, any middleware code that affects an action should be synchronous. Asynchronous code in middleware can be used as long as it doesn’t affect the action at all (e.g. error reporting, analytics, etc.). This means that the next
callback should be invoked independently of the outcome of asynchronous operations.
One of the reasons why this library was created is exactly to avoid asynchronous code in middleware that can affect an action, like other libraries instead enforce. Such code can be moved to async actions. We think you’ll get better results by dispatching sync actions before, after, or depending on the outcome of async actions.
Finally, when you implement your own dispatch function (e.g. for server actions), evaluate whether it should handle all errors locally and return false
(i.e. use a centralized error handler), or return the error and let the callee handle the different cases (i.e. use a distributed error handler).
Above all, keep in mind that Hermes is agnostic about the action flow patterns you decide to implement, so choose what fits best with your project's requirements.
File structure
In case you wonder how to structure your files using Hermes, this is a nice starting point.
/actions /client /dispatcher.js /list.js /middleware /server /dispatcher.js /list.js /middleware
It makes sense to group actions in different sections (e.g. “client” and “server”). This can be more or less granular depending on your project needs. Then each section contains its actions list and dispatcher. Finally, if you use any middleware store it in a separate sub-folder within its section.
Examples
Here you find a few examples of how Hermes can be used, although the power of this library lies in its flexibility and adaptability to different technologies.
Progressing through the examples you’ll find references to code that is defined in previous examples.
React + Redux
Initialize an Hermes dispatcher by passing it store.dispatch
, and then use this dispatcher any time you want to dispatch an action (i.e. store.dispatch
should not be directly used anymore).
// /actions/client/list.js 'INCREMENT_COUNTER' 'NEW_MESSAGE' 'USER_LOGIN'; // /actions/client/dispatcher.js import hermes from 'hermes-js'; import store from '/redux/store';import actionsList from './list'; actionsList storedispatch; // /components/increment-button.js import React from 'react'; import cd from '/actions/client/dispatcher'; <button = > Increment propsamount </button>; // /redux/reducer.js const initialState = counter: 0 user: null; state = initialState action switch actiontype case 'INCREMENT_COUNTER': const amount = actiondata; return ...state counter: statecounter + amount; case 'USER_LOGIN': const user = actiondata; return ...state user; default: return state; ;
HTTP
This is how a dispatcher for server actions could be initialized and used with an HTTP back-end.
// /actions/server/list.js 'GET_MESSAGES' 'SEND_MESSAGE' 'USER_LOGIN'; // /actions/server/dispatcher.js ; ; const dispatch = async { try const res = await ; return await res; catch e console; return false; }; actionsList dispatch; // /components/login.js ;; const formData = email: 'hello@world.com' password: 'pass'; { const userData = await ; if userData ; else ;};
WebSocket
And here’s how a dispatcher could work with a WebSocket back-end.
// /actions/server/dispatcher.js ;; ; const socket = ; const dispatch = { return { socket; };}; actionsList dispatch;
Middleware
Let’s check a few middleware examples.
The first one is an error handler that catches any un-handled exceptions down the chain (i.e. other middleware and the dispatch function), if placed as the first element in the middleware array.
In this case it just logs the error, but you could connect it to any error tracker or crash reporter.
// /actions/client/middleware/error-handler.js { try return ; catch e console; throw e; }; // /actions/client/dispatcher.js ;; ;; actionsList storedispatch errorHandler;
This is a simple analytics middleware that logs every action, but could be easily integrated with any third-party analytics service.
// /actions/client/middleware/analytics.js { console;} { ; return ;};
If your app interacts with an HTTP REST API, the url paths and HTTP methods for each action can be added directly in the dispatch function, or through a middleware that adds the correct metadata (so you don’t need to manually include it each time).
// /actions/server/middleware/http-meta.js { const meta type = action; }; // /actions/server/dispatcher.js ; ;; const dispatch = async { const data meta = action; const method path = meta; const init = method ; if method === 'POST' initbody = data; try const res = await ; catch e console; return false; return await res;}; actionsList dispatch httpMeta;
Another interesting use case for back-end actions middleware is to automatically dispatch the same action on the front-end, but based on the server response outcome and data.
// /actions/server/middleware/reflow.js ; { if actionmetareflow const res = await ; if res ; return res; return ;};
It’s also easy to extend the basic action validation and interrupt the dispatch cycle in case of invalid actions.
// /actions/server/middleware/validate.js { if !str || typeof str !== 'string' throw `The action must provide a (String)`; } { const data type = action; };
Or to debounce actions.
// /actions/server/middleware/debounce.js const timers = {}; { if !timerstype return { const id = ; timerstype = { ; ; }; }; else timerstype; timerstype = null; return ; } { const type = action; };
Finally, more complex logic can be implemented in middleware, for example to interrupt an async action through the use of a generator function.
Testing
Here are a few test examples that simply throw in case of failure, but the same concepts can be easily applied to the testing framework of your choice.
To test a middleware chain, initialize Hermes with a dispatch function that returns the action instead of dispatching it.
// /tests/hermes-middleware.spec.js ;;; ;; const dispatch = action;const sd = ; const formData = email: 'hello@world.com' password: 'pass'; const dispatchedAction = ; const expectedAction = type: 'USER_LOGIN' data: email: 'hello@world.com' password: 'pass' meta: method: 'POST' path: '/login' ; ;
You can also write tests that simulate the full action cycle, using a mock back-end.
// /tests/hermes-middleware.spec.js ;;;; ;; const dispatch = async { const data meta = action; const method path = meta; const init = method ; if method === 'POST' initbody = data; try const res = await ; catch e console; return false; return await res;}; const sd = ; const email = 'hello@world.com';const correctFormData = email password: 'pass' ;const wrongFormData = email password: 'wrong' ; const expectedResponse = username: 'username' token: 'token'; async { const path = '/api/login'; fetchMock; const correctResponse = await ; ; fetchMock; const wrongResponse = await ; ; };
Hermes logo credit: Vincent Montagnana