In this lesson we will refactor an existing UI update from a typical loading approach to an optimistic UI update approach to give our users a faster, more snappy experience. Instead of displaying a "loading" UI to our users while our request is in progress, we will immediately update the UI and account for reverting state and displaying an error in the event of a failure. We can accomplish this relatively easily in React, thanks to the simplicity and power of setState()
combined with making use of Javascript's lexical scoping and closures.
Instructor: [00:00] We had this UI that might be more typical of what you're doing without an optimistic UI update. When you click, we're just going to say, "Hey, user. You're waiting. You clicked this button."
[00:13] It just feels a little bit clunky, whereas, as opposed to using an optimistic UI update, we can make that update to the UI immediately. We get that real snappy feel, and then only when something bad happens, or if something bad happens, we can take care of reverting that state and displaying the error to the user.
[00:33] Let's create a new method here, which is going to be our optimistic approach. We'll call this delete-item-optimistic. It's going to have the same signature where it accepts the ID, and we will make sure we plug that in right here, delete-item-optimistic.
[00:50] For an optimistic update, the first thing we're going to do is we're going to assume that the request we're making to our server succeeds. We'll assume success, and we will immediately update our state.
[01:04] What we're going to need to do then is that step 2(b) on failure is -- if we run into the promise being rejected -- if the request failed, then, we need to revert state and display error to the user.
[01:25] The first thing that we're going to do is we're going to update immediately. This is where we're going to remove the items from our state. We'll just take this. It's going to be pretty much the exact operation, except for we no longer have the concept of loading from the user's perspective.
[01:45] We go ahead and update that state immediately. We just go out and click on this. We're going to see that we're just assuming success here, but now, we need to fire the actual request. I'll do the item request. We need to watch for the failure on that to say when this promise is rejected, first off, we're going to just display that same error to the user.
[02:14] We will set state and display that error right here, similar to what we're doing above. Promise in a parenthesis. Right here, if the request failed, we will display the error. Right now, we have yet to take care of reverting the state. Let's just see if we get that error displaying for item three. We do.
[02:38] The missing piece that we've yet to implement is reverting the state. In order to do that, what we're going to do is, before we do anything, we're going to take a snapshot of what we are going to want to restore, which is going to be our original items, which we can grab out of this.State.items, grab our reference to that there.
[03:01] On this failure event, what we're going to do is we're going to set items and state to our original items. Let's see how this works out. It immediately deletes. Here, we show the error and we've reverted that.
[03:17] To recap, what we're doing with this optimistic UI update is, before we do anything, we're going to snapshot our state and then we go ahead and immediately update the state. We fire our request. We've already assumed it's going to succeed, and we're not depending on anything that is resolving from that promise.
[03:39] All we need is a catch handler here, that if something goes wrong, we're going to revert our state and display that error to the user.
Great question, Massimiliano! The simplified approach used in this video may be fine for a number of scenarios, but is definitely not the most robust solution. This will make a great follow up lesson, but I'll try my best to answer your question here now.
how to avoid inconsistent state due to the fact that between taking the snapshot of current state and restoring it after a failure, the user could perform other actions? For example if you delete item 2 immediately after item 3, item 3 disappears, then disappears item 2, but after item 3 deletion failure both are restored.
There are a few ways to address the problem you've described, but I will keep it simple and comparable to the approach used in the video. Instead of taking a snapshot of the entire current state of items
, we can take a snapshot of only the item being deleted so that we can more precisely restore the single item in the event of a failure.
You can play with this live on this Codesandbox demo and here is the relevant snippet:
class App extends React.Component {
// ...
deleteItemOptimistic = id => {
// 1) Snapshot target item so we can restore it in the case of failure
const deletingItem = this.state.items.find(item => item.id === id);
// 2) Assume success. Immediately update state
this.setState(state => ({
items: state.items.filter(item => item.id !== id),
}));
// 3) If the request failed revert state and display error.
deleteItemRequest(id).catch(() =>
this.setState(state => ({
// Restore the single, deleted item.
// Use sort to ensure it is inserted where expected.
items: [...state.items, deletingItem].sort((a, b) => a.id - b.id),
error: `Request failed for item ${id}`,
}))
);
};
// ...
}
Thank you for the quick reply, I was just playing with something similar. Very interesting subject. I can't wait for the follow up lesson, and I hope there will be more than one if needed (to cover all the ways to address the problem)!
I was screaming in my mind exactly Massimiliano's corner case; its obvious enough that I would have addressed it in the video itself. anyway, nice video!
lol i was too excited to even think about edge cases when i thought: "wth, i should've been doing these techniques a long time ago."
Nice approach! But how to avoid inconsistent state due to the fact that between taking the snapshot of current state and restoring it after a failure, the user could perform other actions?
For example if you delete item 2 immediately after item 3, item 3 disappears, then disappears item 2, but after item 3 deletion failure both are restored.