This is the one thing that I haven't found a standard solution to yet.
I have my store setup with redux-saga for side effect handling, I dispatch an action (that has async side effects) from a component, and now want the component to do something once the side effects are handled (for example navigate to another route/screen, popup a modal/toast or anything else).
Also I want to display a loading indicator or any errors on failure.
Before redux, this kind of flow was straight forward, it'd look something like this:
try {
this.setState({loading: true});
const result = await doSomeRequest();
this.setState({item: result, loading: false});
} catch (e) {
this.setState({loading: false, error: e});
}
With redux, I'd typically want to dispatch an action initiating the flow and have all related information in the store, to allow many components to listen to what is happening.
I could have 3 actions, 'requested', 'success', 'failed'. In my component I would dispatch the requested action. The responsible saga will then dispatch either the 'success' or 'failed' action upon handling 'requested'.
My Component will reflect on the changes. But I haven't found out a standard way to figure out if the action has completed. Maybe the store hasn't updated as result of the async action (NO-OP, but loading state would still change I guess). But the action still succeeded, and I want to do something like navigate to another screen.
I really tried finding this kind of (seemingly common) scenario in the redux docs, redux-saga docs or Stackoverflow/Google, but no success.
Note: also with redux-thunk I think this behaviour is straight forward to achieve, since I can just .then on an async action dispatch and would receive the success action or the error action in catch (correct me if I'm wrong, never really used thunk). But I haven't seen the same behaviour achieved with redux-saga yet.
I've come up with 3 concrete solutions:
Most primitive solution, handling only the 'success'/'failed' actions from the component. Now this solution I am not a big fan of. In my concrete implementation there is no action that indicates that the async request has been started. The side effects are handled right there in the Component, instead of being abstracted away within a saga. Lots of potential code repitition.
Running a one time saga right before dispatching the request action, that races the 'success'/'failed' actions against each other and allows to react on the first occurring action. For this I've written a helper that abstracts the running of the saga away: https://github.com/milanju/redux-post-handling-example/blob/master/src/watchNext.js This example I like a lot more than 1. since it's simple and declarative. Though I don't know if creating a saga during run time like this has any negative consequences, or maybe there is another 'proper' way to achieve what I'm doing with redux-saga?
Putting everything related to the action (loading, successFlag, error) into the store, and reacting in componentWillReceiveProps on action changes ((!this.props.success && nextProps.success)) means the action has completed successful). This is similar to the second example, but works with whatever side effect handling solution you choose. Maybe I'm overseeing something like the detection of an action succeeding not working if props hail in very fast and props going into componentWillReceiveProps will 'pile up' and the component skips the transition from non-success to success altogether?
Please feel free to have a look at the example project I've created for this question, that has the full example solutions implemented: https://github.com/milanju/redux-post-handling-example
I would love some input on the methods I use to handle the described flow of actions.
Thanks for reading.
If I understand your question correctly, you want your component to take action based on actions fired off by your saga. This would typically happen in componentWillReceiveProps
- that method is called with the new props while the old props are still available via this.props
.
You then compare the state (requested / succeeded / failed) to the old state, and handle the various transitions accordingly.
Let me know if I've misinterpreted something.
I achieved the point of having an asynchronous action callback in a component using saga the following way:
class CallbackableComponent extends Component {
constructor() {
super()
this.state = {
asyncActionId: null,
}
}
onTriggerAction = event => {
if (this.state.asyncActionId) return; // Only once at a time
const asyncActionId = randomHash();
this.setState({
asyncActionId
})
this.props.asyncActionWithId({
actionId: asyncActionId,
...whateverParams
})
}
static getDerivedStateFromProps(newProps, prevState) {
if (prevState.asyncActionId) {
const returnedQuery = newProps.queries.find(q => q.id === prevState.asyncActionId)
return {
asyncActionId: get(returnedQuery, 'status', '') === 'PENDING' ? returnedQuery.id : null
}
}
return null;
}
}
With the queries
reducer like this:
import get from 'lodash.get'
const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const FAIL = 'FAIL'
export default (state = [], action) => {
const id = get(action, 'config.actionId')
if (/REQUEST_DATA_(POST|PUT|DELETE|PATCH)_(.*)/.test(action.type)) {
return state.concat({
id,
status: PENDING,
})
} else if (
/(SUCCESS|FAIL)_DATA_(GET|POST|PUT|PATCH)_(.*)/.test(action.type)
) {
return state
.filter(s => s.status !== PENDING) // Delete the finished ones (SUCCESS/FAIL) in the next cycle
.map(
s =>
s.id === id
? {
id,
status: action.type.indexOf(SUCCESS) === 0 ? SUCCESS : FAIL,
}
: s
)
}
return state
}
In the end, my CallbackableComponent
knows if the query has finished by checking if this.state.asyncActionId
is present.
But this comes at the cost of:
I would expect:
asyncActionId
logic to be held on the saga side (eg. when the async action is connect
ed using mapActionsToProps
, it returns the id
when called: const asyncActionId = this.props.asyncAction(params)
, like setTimeout
)redux-saga
, just like react-router
is in charge of adding the current route to the store.For now, I can't see a cleaner way to achieve this. But I would love to get some insights on this!
Maybe I didn't understand the problem your facing but I guess they meant the client would look something like this:
mapStateToProps = state => {
return {
loading: state.loading,
error : state.error,
data : state.data
};
}
and the component will render like:
return(
{this.props.loading && <span>loading</span>}
{!this.props.loading && <span>{this.props.data}</span>}
{this.props.error && <span>error!</span>}
)
and when requested
action is dispatched the its reducer will update the store state to be {loading: true, error: null}
.
and when succeeded
action is dispatched the its reducer will update the store state to be {loading: false, error: null, data: results}
.
and when failed
action is dispatched the its reducer will update the store state to be {loading: false, error: theError}
.
this way you won't have to use componentWillReceiveProps
.
I hope it was clear.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With