Managing data-flows can be quite tricky in javascript. A pattern that really fascinated me over the last years is the flux architecture from facebook. Especially the redux implementation. But redux just tells you how to store your data and how to update the state. It’s really liberal in how to manage data-flows. So really awesome data-flow middleware libs where born like redux-saga and redux-observable.
But all of them work in a really similar way: You listen to some redux-action, do a bunch of things (like data-fetching) and dispatch an action as a reaction. In many situations thats enough, but sometimes you need a more granular control of the data flow.
What is when you want to react to something, that is outside of redux (like react state changes). Or a redux-action has multiple listeners. How can you define the order, the listeners gets executed? What about cancelation of actions or you want to dispatch an action right before another specific action gets dispatched? You will have to handle these edge-cases outside of your redux-middleware.
But that leads to a problematic situation: Your business logic cannot be handled exclusively in your data-flow middleware. You will have to outsource some business-logic to your components. Think of these middlewares as a counterpart of the “Controller” in a classical MVC architecture. It would be a big anti-pattern if you have business logic in a location other than your controller.
So I created my own middleware redux-ruleset that addresses all the problems I was faced with in the last years. That includes:
The basic idea is, that you create small rules, that describe what should happen:
import {addRule} from 'redux-ruleset'
addRule({
id: 'PING_PONG',
target: 'PING',
consequence: () => ({ type: 'PONG' })
})
As you can see, the basic usage is pretty easy. No boilerplate. Readable. I would bet, that you know what happens without knowing redux-ruleset. Each rule you create is a small program within your application. That’s really nice when it comes to refactoring. Let’s look a very common example.
Data-fetching:
import {addRule} from 'redux-ruleset'
addRule({
id: 'FETCH_DATA',
target: 'FETCH_DATA_REQUEST',
concurrency: 'SWITCH',
consequence: () => api.fetchData().then(
result => ({ type: 'FETCH_DATA_SUCCESS', payload: result }),
error => ({ type: 'FETCH_DATA_FAILURE', payload: error.toString() })
)
})
I think even that can be understood without knowing redux-ruleset. When we dispatch ‘
FETCH_DATA_REQUEST
’ we will fetch our data and dispatch the result as ‘FETCH_DATA_SUCCESS
’. We define the concurrency ‘SWITCH
’ so slow requests cannot resolve when a later called api-calls resolve first.You can also define the exact time-window a rule is active:
import {addRule} from 'redux-ruleset'
addRule({
id: 'PING_PONG',
target:'PING',
consequence: () => ({ type: 'PONG' }),
addWhen: function* (next) {
yield next('START_GAME')
return 'ADD_RULE'
},
addUntil: function* (next) {
yield next('STOP_GAME')
return 'RECREATE_RULE'
}
})
// later
dispatch({ type: 'PING' }) // noting happens
dispatch({ type: 'START_GAME' })
dispatch({ type: 'PING' }) // => { type: 'PONG' }
dispatch({ type: 'STOP_GAME' })
dispatch({ type: 'PING' }) // noting happens
dispatch({ type: 'PING' }) // noting happens
When you have a redux-saga background you may understand the above rule.
There is way more. If I have aroused your interest you should check out the docs. My company is already using redux-ruleset. We were able to gain an enormous developing and refactoring performance. I would be glad to hear if that is also true for your project :)