Sharon Kong - Senior Software Engineer at Workday gives a talk entitled Using Redux Custom Middleware to build a Layered Architecture
I love working at Workday. We have a lot of creative freedom on the development side. I love the tech stack that we're working on. We are working with TypeScripts, React, and Hooks. We will discuss what is off-the-shelf middleware. We will also discuss how to implement middleware, some action processing patterns, and layered architecture using custom middleware. Middleware is a function. The Redux framework provides an extension point where we can inject middleware functions into a chain of functions. The chain begins with dispatch and ends with the reducer. It is often called the pipeline. Think of it as a pipeline through which the action flows. Middleware actually has the opportunity to execute logic in response to an action after the action has been dispatched, and before or after the action is processed by the reducer.
Why use middleware? React components should be focused on presentation logic. Reducers should be pure functions focused on managing state, actions are simply messages with data. Where should we put our application logic? How can we perform side effects? Where should we make asynchronous API calls? Middleware is a flexible way to achieve all of the above. There is some popular off-the-shelf middleware. Redux Logger is a fantastic one that console logs the actions that flow through the pipeline and is handy for debugging. Redux Thunk is another popular middleware that's used for side effects. It executes actions that are sent as functions instead of objects. Also, it's really easy to write your own custom middleware.
To inject middleware into the Redux pipeline, from the Redux library, you can import a function called “Apply Middleware.” Take this function and send it as a parameter to your "Create Store" function, which creates your Redux store. The "Apply Middleware" function takes a list of middleware functions, and the order of this list is the order that the Middleware is executed in, in that pipeline. "Create Custom Middleware" is a function that returns a function. If we break it down, the creator function returns a function that takes the store API as its parameter. The store API is just dispatch and get state from Redux. This returns a function that takes next as its parameter, and next is simply the next function in the pipeline that should be called. That is either the next middleware in the pipeline, or if you're at the end, then the reducer, combined reducer itself. This returns a function that takes the action object, and finally, this returns a function that is the body of this middleware.
I grew up working on desktop UIs and classical MVC, MVP. Those things are a little passe, but in reality, layers themselves, I think, are still a great idea. You can create as many layers as you want, as many layers as you need for your app. When I look for creative ways to use custom middleware, I see that it's actually really a good natural fit for training layers in our layered architecture. Let's say we want to build a collaborative text editor, much like Google Docs. It should be a web app hosted in the Cloud, should allow multiple people to edit the document simultaneously, and everyone's edits should be shown in real-time. You can use a typical web app layout, with a React Redux front end. The Redux store will be the source of truth and will contain all the data needed by the React components, to render. Use a centralized backend server that relays messages from one client to another, via WebSockets. WebSockets is great, because not only can a client make a request to get a response, but WebSockets allows a server to push messages to any of the clients at any time. So it's more of a duplex two-way communication channel compared to REST.
In addition, convert edits from different sources, whether they are local edits, remote edits, or undo-redo edits, into a JSON object that represents a normalized edit. This is going to allow us to have our middleware and reducers operate uniformly, on the normalized edit, regardless of the edit source. Regardless of which type of edit it is, we want the reducers to be able to handle that edit in a uniform way,
Implement this using a layered architecture, and custom middleware. The first middleware layer is called initialization, and it can be responsible for making the WebSocket requests to the server, gathering all the data needed, to initialize the app state when we first open the app. The remote edit middleware will be responsible for processing WebSocket messages from the server, which represent remote edits, and normalizing that into our normalized edit form. The local edit middleware can process actions, which are requests from the React components, and convert these local edit requests into the normalized edit. It can also go ahead and make the WebSocket call out to the server, to persist local edits. Not only will the server persist in those edits, but it will also relay those edits out to other remote clients. The undo-redo middleware is going to determine the undo for a normalized edit.
This is a different approach from spreading logic out over components, action creators, customizable middleware, and large reducers. Using this approach, I reduce the amount of data and business logic and the React components, because actions are dispatched containing minimal information. I avoid putting logic in action creators, so my action creators are simple and I don't have to worry or deal with thunks. I also minimize the amount of logic in reducers, keeping the scope of the reducers to updating state in an immutable fashion. Instead, I put most of my logic into layers of custom middleware, which enhance actions with additional information, as they flow through the pipeline. What are the benefits? I like the idea of creating distinct modules with specific roles and responsibilities. This further achieves the separation of concerns in the app. I can keep related code isolated and co-located, and I like having a clear structure for finding, updating, and adding code.