Advanced Remix

Optimistic UI Solution

Kent C. Dodds

Kent C. Dodds

Professional Trainer
Advanced Remix

Check out a free preview of the full Advanced Remix course

The "Optimistic UI Solution" Lesson is part of the full, Advanced Remix course featured in this preview video. Here's what you'd learn in this lesson:

Kent demonstrates the solution to the Optimistic UI exercise.


Transcript from the "Optimistic UI Solution" Lesson

>> We're being optimistic today with optimistic UI. And the UI we want to be optimistic about is this piece, the deposits. So even if the user is on a slow network, I want to be able to submit this deposit and have it appear instantly. We also actually will clear the form and we'll improve that in the next exercise as well.

But we're gonna just add query in the form when the request is ultimately successful as well. So we're in the invoice ID route and there's the deposit form right around here, and this deposit form, when the user submits this form, this fetcher is going to have that submission data on it.

So we actually know everything we need to display that information that the user is gonna get. Sometimes you can't really do this because sometimes what the user is creating and there's gonna be some sort of calculation that happens on the back end. And like, for example, the Consent Podcast, I can't optimistically display the final episode because they're on the back end, we're actually stitching together the two pieces, so I can't do that in the client.

So sometimes you cannot do optimistic UI. But in this case, we have everything we need to display the optimistic UI. And in those cases where it doesn't feel like you can, you might be able to still because you can show just like a little skeleton UI for the piece of data that you don't have yet.

We'll actually look at skeleton UIs later today. So for this, we just need to find out what was it that the user submitted, and then add it into the list of deposits that we render. So that is what we're going to be doing here. Instead of using data dot deposits, I'm actually going to create a new deposits variable, data dot deposits, so now we can just swap this in both of these places for just deposits.

As a straight up refactor, nothing useful going on there until I make a copy of this, because I'm actually going to mutate this. I don't want to mutate something that's coming from I use loader data or use date or any of that stuff, we're not gonna mutate that.

But variables that I create, sure I'll mutate that all day long, and so that's what we're gonna do is we're going to say, hey new deposit fetcher, do you have a submission with a valid deposit, let's just add one of those to the deposits that we're entering. Now that works out nicely.

You wouldn't have to do it this way, you could actually leave it the way it was, and then just go down to the bottom of the deposits and add another one like that if you wanted to. I just find this to be a lot easier. So what we need to do is find out whether there is a submission going on that has the data that we need for a deposit.

So I'm gonna say if new deposit fetcher dot submission, if it's been submitted then, yeah sort of, thanks copilot. Not quite though, so we are going to push and we need to have a couple things on here. We're gonna have an ID. The problem with the ID is that that is generated by the back end.

So the user didn't give that to us. The nice thing is, we actually don't care. We don't display the ID or anything like that. We actually do use it in the link, and there are a couple of ideas you could do with that. You could just have it link to nowhere or whatever.

Hopefully the idea is that we're just trying to be a little extra helpful to the user and this is a situation that is hopefully sort of unusual for the user to look at this for too long of a period. But you could have it linked to a page that says, wow you're really fast, congratulations, or whatever.

But what I'm going to do is just set this to new. And the only situation where this would actually be a problem aside from linking to deposit that doesn't exist yet is if the user submits a bunch of these, we actually use the ID as the key. And so React is gonna be like I don't know what to do about this, probably would actually be okay but yeah, you may wanna generate this if you want it to really be resilient to issues there.

So we need to set the ID, we also need to set the amount and that is going to be new deposit fetcher dot submission dot form data get amount. So the amount that the user submitted, we know that that is how we get that out of the submission because if we go down to our form, we have the input that the user is using has a name amount.

So that's how we know, that's what we're gonna pull out of the form submission because that is what it is called. So we get the amount and then we also have the deposit date formatted. So this is actually what our loader will send back for each one of these deposits.

So what we're actually submitting is the deposit date which is going to actually be stringified to I think it's Y, Y, Y, M, M, D, D. So year, month, day, that's how it's serialized with the way the browser submits and everything. So we actually need to parse that and format it in the way that we display it.

So that's what we're going to do, but it's gonna be called the deposit date. And I mentioned also that when we send back in our loader, we send back the deposit date formatted. We're just using to local date string. In any legitimate application, you're probably going to want to have a date formatting library in everything too.

So that is deposit date formatted, so we're going to get the date out of new deposit, yeah, thank you for that Copilot. So we're gonna get it out of the form data, but again we need to format this and we have a handy parse date and then we can use to locale date string.

So when you take converted to a dot date object via parse date which is a utility that we have that we can just import. Pass that along and then call to locale date string. This actually, it may work differently on your machine because your to locale date string could be different based on your locale.

And so that's why you probably wanna have a date formatting library as part of your application. So in any case, we're getting some errors here because TypeScript isn't super jazzed about the fact that this can be a form data entry value or null, and we're passing that to parse date.

And yeah, the same thing for the amount, actually, but I'm gonna leave that for later. We just wanna make sure that this thing works. This will probably work as expected, but TypeScript is just helping us think about those cases that could absolutely happen. I tweeted a while ago that TypeScript isn't making your life miserable, it's just showing you how miserable your life actually is.

So that is what TypeScript is doing for us. We'll take care of this here in a second. What I want to do is just make sure that this optimistic UI piece is actually working, so that we can deal with it if it's not. Okay, so we'll go slow 3G I give it an amount of 25 bucks and that was today and here's your 25.

I hit Create, we're on a slow 3G network so this should show up instantly which it does, that works great, and so we're in a good spot. And when revalidation happens, all of that optimistic UI or the optimistic data deposit that we just created, that actually is gone now because the submission no longer exists.

And so this if statement will not happen because there's no longer a submission. And now data deposits actually includes the real deposit, and if I hover over this you can see the link to the specific deposit. If I submit this again and then I hover over quickly, you'll see it actually links to slash new, which is the problem that I described earlier.

But when revalidation happens, then it gets updated to the correct URL. Okay, so next we're gonna do a little bit of work to make TypeScript happier about this and just kind of talk about how to deal with some of that, as well as actually do a little validation too.

>> If the client ID matches one that's already in the deposits array, could we avoid submitting another deposit to prevent accidental double deposit for instance?
>> Yeah, so I don't know how you could possibly set this ID, that optimistic ID, to something that already exists in the deposits array.

That would be like these IDs are nuts, so I'm not sure that is a practical problem. I might be misunderstanding the question though. In general, I don't think that this is going to surface as an actual issue for people. And we'll just focus on this last bit, which is resetting the form when the submission is finished.

So we're going to use a use effect from React. We're not going to use the plus symbol. We're gonna type correctly. We're gonna add our dependency array. And now what I need to do is nope, nope, you know what, I'm gonna disable Copilot because as awesome as it is sometimes it's not awesome.

So what I need to do is first I need to add a ref to the form so I can reset the form. So let's do that first here got my deposit form ref. We'll just call it the form ref wherein the deposits components is gonna be, our deposit form of course.

Use ref, that is used reducer, not what I wanted to type, use ref, there we go. And set that to know initially, we're gonna make this an HTML form element. And then down here on my HTML form element I'll say ref equals my form ref, and then I'll first just do a little thing that I often do in these types of things.

If there's not a form ref dot current, then I'm gonna return. So what that does for me is now form ref dot current totally exists and I don't have to do any more null checking and whatever, so that's nice. The next thing that I wanna do is, ultimately what I'm trying to accomplish here is form ref dot reset current dot reset.

And that will reset the form to its original state to all of its default values and things. But I only want to do that when the new deposit fetcher state is idle, so when the submission is finished then we wanna reset the form. We don't wanna do that in any other rerender like as users typing or anything else, we don't wanna do that.

So we'll say if it's not idle, then we'll return, you can write that however you'd like. And then I'll let Dan Abramoff fill that in for me. Thank you, Dan. Cool, so now if I come back over here and we wait for this slow three gen network to load the app, and we say 45, 45, and we get the automatic or that optimistic UI.

And when revalidation is all finished, then that gets reset for us. Next exercise we'll talk about focusing back on the right field and stuff like that. Okay, great. So now the last thing is just to make TypeScript happy, and actually we're gonna do some handy dandy validation as well.

Before we do that, I'm gonna say what if I set this is missing an amount. So we're gonna skip the amount and hit Create. Well thank you browser for that. What if I change that to say no validate? What do you think about that browser? Boom, okay, so we're gonna get zero over there.

And that will roll back actually because that is an error. And if we go to our network and look at that post request, we are going to get in our response this error, it says amount must be greater than zero. And the deposit date didn't have an error, so we're fine there.

So in that case, we can actually get that error out of the submission or the fetcher dot data that will have it there and we could display that, we're not gonna do that right now. But what I want to note is the fact that it rolled back for us.

So user submitted something, our optimism was misplaced, that optimistic UI is gone, and we didn't code anything that thought about that even a little bit. So that's neat. That bug is is not a problem for us, which I think is cool. We can hit Create and right because they're all new, so if I generated the ID you could actually hit Create a bunch of times and the right thing will happen.

It will say all of these optimistic UI things should go away and they will not ultimately be created. So that's cool. So let's just make TypeScript happy here, and then we can move on to the next exercise. So for our amount, TypeScript is angry about this because this returns a form data entry value or null.

So what I'm gonna do here is we'll say form amount, and now I'll just say if the type form amount is equal to a number and then we'll do this, and now Typescript's happy about that. For this piece, we'll say this is our deposit or form deposit date.

And we can say, let's actually put that up here and the type of this is equal to a string. And actually you know what, it just occurs to me, this will never be a number, it's either a form data entry value or a string. So this is going to be a string.

I'm going to say we'll numberfy this thing, number, there. So now this is going to be of type number. It could be num potentially, so like you could add a bunch of extra checks here, but I think you get the idea. You're just doing a little bit of validation before you do optimistic UI.

Same thing for our form deposit date, we're just gonna check that it's a string. You could also make sure that parse date results in an actual validate and all of that, but hopefully you get the idea, you can do all sorts of validation before, that's all client side validation before actually doing the optimistic UI.

Because it's doesn't make any sense to actually send the request and show the optimistic UI if you know for certain that this is not going to work anyway. Additionally, what's really neat about this is you can actually use the same validation logic that we're using in our action.

So you could literally just say, hey, validate my amount and don't show optimistic UI if this validation is gonna fail. Because I know it's going to fail, why show that optimism? Same thing for our deposit date, and so we could do all that, I think you get the idea so I'm not gonna bother.

It would all just go as part of the if statements that we have before we actually add an item to the deposits.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now