Redux: Separating Side-Effects (like Redirects) from Actions into Components
Throughout development, especially when new technologies are introduced (most notably in this case: React), a common struggle tends to be related to Separation of Concerns. This relates to our ability to make sure that each piece of our ecosystem is only ever responsible for what it absolutely needs to be in order to function properly, ensuring the delegation of other functionality like side-effects to their responsible parties.
Redux and React have changed the way that we think about programming, designing websites, apps, and software development in general. It's apart of a much larger movement towards the componentization and modularization of softwares. Unfortunately, applications written in React and Redux are far from perfect in relation to Separation of Concerns, albeit the evolution of Facebook's FLUX ideas have certainly helped in many areas.
The Problem
Most recently, a specific item that I’ve been pretty heavy on is never having redirects within your actions. This is important because, if written properly, you should be able to call any action within any component without the worry of any major, jarring side-effects. Side-effects (like redirections) should always occur within the component, because it’s contextual to what you’re doing and WHY you’re calling that action at the time.
The example I'll use is a user switcher
functionality within a management system. While logging in, Action Creators should be called to establish which user our actions are going to be performed under. However, in the case of a User Switcher
functionality in a multiuser environment, the ability to do this on the fly without having to worry about redirections is important, especially if it requires the user to navigate back to where they were.
Future-proofing
In order to future-proof this functionality, we need to make sure that we can call this action in any scenario without having to worry about it sending us to a Dashboard page (because this may not always be an intended side-effect given whatever context it may be used in).
In the case of what is being built right now, however, it is an expected side-effect. By abstracting that out to the component, we can ensure separation of concerns and further make each little piece of our app as versatile and flexible as possible.
After some testing with redux-thunk
, to my surprise, the resulting functionality is exactly what we need in order to ensure these capabilities.
The Implementation
First, the action is tailored to return a predictable response: A promise with the dispatch payload. By using specific action types, we can cast a narrow net to ensure that the side-effects are ensured based on specific scenarios. This includes successful and erroneous responses.
export const switchUser = (targetUser) => (dispatch) => {
dispatch({ type: SWITCH_USER_REQUESTED });
return callSwitchUser(targetUser)
.then(r => r.data)
.then(payload => ({ type: SWITCH_USER_SUCCESSFUL, payload }))
.catch(error => ({ type: SWITCH_USER_FAILED, payload: error }))
.then(dispatch);
};
Notice how we’re returning the promise that we’re using to dispatch.
Next, by also updating our mapDispatchToProps
utilities, we can ensure that the promise is passed along to us inside of our component so that we can utilize it meaningfully within the narrowed scope of our component, as opposed to within our actions or other areas of our application.
// ...
const mapDispatchToProps = dispatch => ({
switchUser: (...args) => dispatch(switchUserAction(...args))
});
// ...
With this passthrough enabled (notice how switchUser
returns dispatch(...)
), a call to switchUser()
within our component will always return a promise (given that we've ensured our uniform, predictable return values in our actions).
switchUser(targetUserId) {
const { switchUser, redirect } = this.props;
// Change the current user
switchUser(targetUserId)
// Throw if switching user failed
.then(({ type, payload }) => type === 'SWITCH_USER_FAILED' && throw payload)
// Otherwise redirect to dashboard page
.then(() => redirect('/dashboard'));
// Catch thrown error and console it!
.catch((e) => console.error('Switching the user failed!', e))
}
This opens up an entirely new world of compositing side-effects based on circumstances, but ensures that the component, which is the consumer of the action, is responsible for handling side-effects.