TLDR; Individual entities in a store can be in different states, including loading and error states. How can we correctly reflect this in our UI with a "fire and forget" approach to Redux actions, while preferably keeping the behavior of our action creators consistent?
The convention is to "fire and forget" an action and subscribe to store updates, as opposed to dispatching an action and subsequently dealing with its return value or promise.
Several examples illustrate this:
// This is a web app that lets users create and book events
const loadEvents = () => dispatch => {
dispatch(loadEventsRequest());
return fetchFromApi('https://...')
.then(
json => dispatch(loadEventsSuccess(json)),
error => dispatch(loadEventsFailure(error))
)
}
componentWillMount() {
this.props.loadEvents(); // Fire it; the state will be updated eventually
}
render() {
return this.props.events.map((event) => ( <Event event={event} /> ));
}
Run Code Online (Sandbox Code Playgroud)
The list of events can be in several states, e.g. a loading state. We can accommodate this by designing our state like this:
entities: {
events: {
1: {
title: "Movie night"
},
...
}
},
visibleEvents: [
isFetching: false,
ids: [1, 2, 3]
]
Run Code Online (Sandbox Code Playgroud)
It's easy to show a loading indicator based on the value of visibleEvents.isFetching.
State for specific entities
Let's imagine that events can be booked, canceled and deleted. All of these actions may result in errors from the backend ("The event is fully booked") or success scenarios ("The event was booked"). We can notify the user in two ways:
Alternative 1)
Dispatch the action from the component and respond to it using then/catch. Caught an error? Display it. The state stays local.
For this to work our loadEvents() action creator would need to be changed. Currently it catches errors and fires loadEventsFailure(), so our component doesn't know whether the action failed or succeeded (can't be caught).
We could re-throw the error from the action creator or reject the promise, (or don't catch it at all), so that our component gets a chance to catch and respond to it. My biggest concern is that we end up with inconsistent behavior across our action creators -- some throw errors, some don't. We could make it a convention to always throw errors, but unless the component catches the them we end up with "Uncaught" errors all over the place.
It doesn't feel right to let the component decide the behavior of the action creator (whether to throw/reject), especially if other components want to use it as well. Also, there would be no use for a booking reducer in this case because our state never needs to be updated during the booking process.
Alternative 2)
Store every type of result (error or success state) in the Redux state together with their specific entities (i.e. each event can have multiple states related to booking, deletion and cancellation).
Our component wouldn't need to "respond" to the action creator; it could simply fire an action and read the result off the Redux state (pretty idomatic). Conceptually:
handleBookingButtonClicked() { this.props.bookEvent(id); }
// ...
render() { if (store.entities.events[id].bookingError) return (<div>Booking failed</div>); }
Run Code Online (Sandbox Code Playgroud)
All components that relate to booking of an event can read bookingError. Components related to cancellations can read cancellationError, etc.
(Note: If it's tempting to only have one "global" error object in the store, or one error object per entity which contains any type of error related to it, this would quickly fail if you want to display several components simultaneously.)
A couple of issues with this approach:
Which approach would you recommend, and how would you address the issues described (and the proposal to have consistent action creators)?
| 归档时间: |
|
| 查看次数: |
512 次 |
| 最近记录: |