Re-frame effects implemented as a Redux's middleware
In this, hopefully, short article, I would like to discuss on an alternative approach to solve effects (e.g: API Calls, writing cookies, etc.) when working with react and redux. Replacing sagas with re-frame. You still there? Then I’ll give it a try:
Let’s build a constrained example in which we need to fetch a list of users. The usual way to structure your code with react, redux and sagas is to write a component, a reducer and a watcher, right? Then, sketching a component:
class UsersList componentDidMount() this.props.dispatch({ type: GET_USERS }) render() <div className="user-list"> map(this.props.users, user => <div key={user.id}> user.name </div>) this.props.fetching && <p>Loading</p> </div> connect()(UsersList)
Once the connected component gets mounted, we dispatch the GET_USERS
action, that will set the fetching
property to true
.
const initialState = { fetching: false users: [] } const reducer = (state = initialState, action) => { const { type, payload } = action; switch (type) { case 'GET_USERS': return { ...state fetching: true } case 'GET_USERS_SUCCESS': return { ...state users: payload.users } } }
Then, using sagas we could intercept the GET_USERS
to make an HTTP GET request
and dispatch a GET_USERS_SUCCESS
with the results.
function* getUsersWorker() { const users = yield fetch('users-endpoint') yield put({ type: GET_USERS_SUCCESS payload: { users } }) } export function* getUsersWatcher() { yield takeEvery(GET_USERS, getUsersWorker) }
There. That’s how is done with sagas. Now, re-frame’s approach will be to write all the logic in the reducer:
export const initialState = {
fetching: false
users: []
};
export default {
GET_USERS: ({ state = initialState }) => ({
state: {
...state
fetching: true
},
httpGet: {
path: 'users-endpoint'
then: { type: GET_USERS_SUCCESS }
},
}),
GET_USERS_SUCCESS: ({ state = initialState }, payload) => ({
state: {
...state
users: payload.users
}
})
}
As you can see, now we return the next state wrapped in another object. Now,
Redux will update the state with what is in the state
key, but what’s the
other key (httpGet
) about? It’s an effect. In other words, a non pure function.
Re-frame gives you a way to register new effects, let’s register httpGet
:
regFx('httpGet', (params, dispatch) => {
const { path, then } = params;
fetch(params.path).then(response => {
dispatch({
...then,
payload: response,
})
});
});
The second parameter of regFx
is a function which carries as arguments:
first the value of the key httpGet
in the reducer, and the dispatch
function provided by Redux to emit an event with the response.
Now, let’s be honest, this is a pretty constrained example and both approaches seem quite similar. Let’s analyze a bit this example, with different scenarios:
Imagine you need to send a bunch of data from the state to getUsersAPI
. If
you are working with sagas, then you can use the select
function to get the
state after it has been processed or you can send all the state you
need in the payload, which is, usually, the preferred way. Now, working with
re-frame, you have access to all the state in the current scope before the
state transition. That way your logged payloads will carry only relevant information.
What about testing? because of It’s use of function generators sagas is very testable,
you can inspect each yield
statement. Instead, if you create a re-frame effect
that makes several api calls, you don’t have the same granular control and that’s
why effects should be as simple as possible. In a positive note, in re-frame
you’re encouraged to write your side effects as data in the reducers, in other
words, a more declarative approach. You also have single point of control for
every effect implementation.
Another good point to re-frame is that, using an explicit registration function, you can keep track of each type, and console a warning when there’s an action dispatched with no handler.
This discussion only concerns re-frame effects, but there’s another concept called coeffects which allow us to read other mutable data besides Redux state (e.g: current time). Also I don’t provide any implementation for re-frame, nevertheless if you are interested, the source code is quite elegant.
Finally, this is a subjective comparison between both models and any thoughts or arguments against or in favor are highly appreciated.