Where to put business logic in redux? action or store
Quoting the Redux FAQ entry on "how to split business logic between action creators and reducers":
There's no single clear answer to exactly what pieces of logic should go in a reducer or an action creator.
If you put all the logic in the action creator, you end up with fat action objects that declare the updates to the state. Reducers become pure, dumb, add-this, remove that, update these functions. They will be easy to compose. But not much of your business logic will be there. If you put more logic in the reducer, you end up with nice, thin action objects, most of your data logic in one place, but your reducers are harder to compose since you might need info from other branches. You end up with large reducers or reducers that take additional arguments from higher up in the state.
It's valid to dispatch an action that gets ignored by the reducers, and it's also valid to inspect the state first and decide to not dispatch an action. Ultimately, it does come down to what you're comfortable with.
Here's some opinionated answer which goes against redux recommendations.
TL;DR Neither
Longer answer: in so called async action invoked from middleware. In redux community it is known as "thunk" or "saga".
First, some definitions:
- action: a plain object
{ type: 'ACTION_TYPE', payload: { data } }
- action creator: a function that returns action.
- async action: a function that is called from middleware.
- async action creator: a function that returns async action
- middleware: a function which can handle all actions, dispatch other actions and has access to store state.
So, where do we call the business logic from?
If you look carefully, you'll notice that we don't need async action and async action creator. We can have a simple action that is handled directly in the middleware.
In middleware we can have a dedicated handler for each action. This handler behaves like async action but we don't call it so. Let's call it interactor.
So a new definition:
interactor: an abstraction of what is essentially async action in redux, but not redux-specific. Interactor fetches data, calls business logic and dispatches result "actions".
middleware = (...) => {
// if(action.type == 'HIGH_LEVEL')
interactors[action.name]({ dispatch, params: action.payload })
}
const interactors = {
async highLevelAction({ dispatch, params }) {
dispatch({ loading: true });
const data = await api.getData(params.someId);
const processed = myPureLogic(data);
dispatch({ loading: false, data: processed });
}
}
How to dispatch it:
dispatch({ type: 'HIGH_LEVEL', name: 'highLevelAction', { someId: 1 } })
As mentioned, there are multiple ways to perform this action depending on your use case. What I can do is list you what seems more appropriate from speculating your use case.
1. Logic inside the Component.
The state that hold the condition can be brought into the component by mapping the state to props using connect
from react-redux
You also import the action into this component file and map the action to props as well.
The example below demonstrates how you bring the state and action into the Component file. How you use it is up to you. I've put it into a simple context. So you may invoke myFunction()
at the point you wish perform the logic.
MyComponent.js
import React, { Component} from 'react'
import { connect } from 'react-redux'
import { onCheckboxClick } from 'path/to/action'
class MyComponent extends Component {
myFunction() {
const { theConditiion, onCheckboxClick } = this.props
if (theConditiion) {
onCheckboxClick({itemId: 'someItemid'})
}
}
render() {
//...
}
}
const mapStateToProps = (state) => ({
theCondition: state.wherever.the.data.lives.in.store
})
export default connect(
mapStateToProps,
{ onCheckboxClick }
)(MyComponent)
Therefore, you can remove the conditional checks you currently have within your onCheckboxClick function for the example above.
2. Putting logic inside the middleware.
The example below demonstrates how you can dispatch action(s) but first, 'catching' a particular type of action, given that that a condition is true, you can make an api call and dispatch further actions, if false, just pass the action to the next middleware.
myMiddleware.js
const onCheckboxClick = store => next => action => {
if (action.type == 'CHECKBOX_CLICK') {
let theConditiion = store.getState().wherever.the.data.lives.in.store
if (theConditiion) {
// 1. make the api call here, or,
// 2. dispatch an action specific to handling api calls.
// E.g. Create another middleware to catch action type `API_CALL`
// This middleware can then handle all api calls, and dispatch actions for api requests, responses and errors.
const newAction = {...action, type: 'API_CALL' }
store.dispatch(newAction)
// When you use store.dispatch(), the action will be passed back to the top of the middleware chain.
}
return next(action) // this will pass the action to the next middleware in the chain.
}
export default onCheckboxClick
This is a broad overview to help you get thinking what works best. Keep in mind, as your app develops, you will notice where repeated logic can be made into its own functions.