Network Middleware

If your app requires data from a service, the following pattern might get you closer to the aforementioned best practices while keeping all network logic contained, maintainable, debuggable and testable.

Simple Proof of Concept

store => next => async action => {

switch(action.type) {

case SEARCH_REQUEST:

next(action)

const response = await makeSearchRequest(action.query)

store.dispatch(searchResponse(response.payload))

}

}

The snippet above does three things when a search has been initiated by a component.

It passes the action onwards in the middleware chain/reducers. This is optional… perhaps you don’t require the action be run through the reducers. It makes a network request with the event data. In this case, we’re sending the query to a search endpoint. It dispatches a brand new action when the response is received. You could also use next() .

What about Thunk?

Thunk is a popular library which accomplishes basically the same thing (and more) using middleware as well. I prefer the custom middleware pattern because there is less misdirection and Thunk violates the first tenet listed above (action creators should be dumb functions which return simple objects). Another bonus to network middleware is all your network logic (request/response handlers and their invocations) can live in one place so mocking, logging, debugging and maintenance is super easy.

NOTE: I’ve seen a version of this pattern where the original action is decorated with data from the response before being sent onwards. Don’t do this. Keeping actions atomic and opaque means you don’t need to use brain juice when reasoning about your code.

Fun With State Changes

While React’s setState() is asynchronous, Redux’s dispatch() is conveniently not. And since any given middleware has a reference to the store , this means we can get access to both the old and new state trees for a given action.

Simple Proof of Concept

store => next => action => {

const oldState = store.getState()

next(action)

const newState = store.getState()

// do something awesome with your newfound omnipotence

}

Because next() is synchronous, by the time newState is initialized, the action has already run through all reducers and updated state. You can use this pattern to conditionally perform other operations according to what changes in state the action introduced. Essentially, you can both peek into the future and give deeply nested reducers a bird’s eye view of the state tree.

Why would we want this?

Preventing expensive operations for noop actions

if (newState !== oldState) {

performExpensiveOperation()

}

For idempotent actions, we may want to prevent certain operations from occurring on repeated dispatches.

Validating prerequisite state

if (isTutorialMode(store.getState()) {

tutorialModeHandler(action)

} else {

next(action)

}

You may have certain flags kept in state which drastically change the way a given action should be handled. In the example above, I’m pretty sure tutorialModeHandler spawns Clippy.

NOTE: This brings up another good pattern. A middleware’s outer scope should contain only delegation code (switch and conditional statements) while the actual business logic can live in small, testable handler functions.

Web Sockets

We’ve already seen how middleware can work for you in a request/response world. There is also a pattern for interfacing with a web socket connection.

Simple Proof of Concept

store => {

// initialize connection

const socket = new WebSocket()



// set event listeners

socket.on('messageReceived', (e) => {

store.dispatch(messageReceived(e.data))

}) return next => action => {

next(action) // emit events

switch(action.type) {

case MESSAGE_SUBMIT:

socket.send(createMessageEvent(action))

}

}

}

The example creates the socket and registers event listeners when the middleware is initialized. Then, in the innermost function, we can send messages as actions are dispatched.