Lesson Description
The "Improve Fuzzy Search with Transitions" Lesson is part of the full, React Performance, v2 course featured in this preview video. Here's what you'd learn in this lesson:
Steve discusses using transitions to prioritize user interactions over expensive operations. He also demonstrates separating and updating two search queries with useMemo and useState and shows how to indicate pending operations while leveraging React Fiber for efficient updates.
Transcript from the "Improve Fuzzy Search with Transitions" Lesson
[00:00:00]
>> Steve Kinney: And like I said, this is one of those ones where will you use it every day? No. Will it solve one thing that will make you the hero every so often? Yes. And I mean, who amongst us does not like to be the hero? All right, cool. So let's go ahead and take a look at just a tour of some of the code. So I'll go in here, we'll start an application. All right, so like search query, and then we do—we're doing some things that we learned about, right?
[00:00:34]
We are memoing the filtering that if search query has not changed, like don't do the expensive operation again. That's great. It's kind of pointless in this case because what else would make application re-render other than that, but it's to show that that's not the solution to our problems, right? So we've got that in place, and then on change we set the search query, which again invalidates that memo and makes us go through the entire thing all over again.
[00:01:02]
And so what we want to do is we want to use useTransition to say, hey, showing that change in the input field, do that immediately, right? The filtering—that can wait, right? And so what's amazing is for what feels like a performance bottleneck, the solution—not that bad, right? Arguably would have been bad prior to having React 18 and now 19, but not bad these days, which is all we really want.
[00:01:38]
So one thing we can pull in is we've got useMemo and useState, we're going to pull in useTransition, right? And we'll pull that in, and we've got now—really what we need to do is there are now two search queries that we care about, right? There is the one that we are using to filter the list, and there's the one that we're showing the user, right? One of those we need to update before the other one, right, because the other one has a bunch of implications.
[00:02:12]
So we want to be able to, hey user, I'm going to show you that I got the message. I'm going to do the other thing on a lower priority thing. So everything you do, if you continue typing, that is the top priority—is showing that I understand your interactions, and then I will then do the other stuff kind of effectively. It's not in the background. It's not in a service worker or a web worker or something like that, but it is effectively in one of those lower priority lanes from the user's stuff as well.
[00:02:43]
So what we'll do is we will go ahead—and you always get two things out of useTransition. You will get, again, it's like an array like useState, and you'll get isPending, which is a boolean, which I'll hover in a second, and you'll get startTransition equals useTransition. Cool. As you can see, I hover over pending, it's a boolean. You can take a lucky guess what isPending is—is it in the middle of a transition or not?
[00:03:15]
Why does this exist? It exists so you can show a loading indicator, right, or something to show that stuff is happening, right? We could drop the opacity on the Pokémon or do like a Tailwind animate pulse or something, whatever. The other one is this startTransition, which you can't see the full function signature, but you can get a sense of what it is from TypeScript and IntelliSense, which is a function of what we're going to do in this case.
[00:03:45]
But like I said before, we kind of need to separate out two things, which is the search query we're using to search and what we're showing to the user, right? Because one of those we're going to update immediately, the other one is going to be put on that lower priority queue, then it will then trigger the filter Pokémon function and everything else that we saw before. Give myself a little space just to kind of separate these two.
[00:04:08]
And so what I'm probably going to do, the naming part is always the hardest. I'm going to separate my inputQuery and setInputQuery, these will be the ones we show immediately. And we'll go ahead and we will make these the things that change. So now when they change, I'm going to set that to the setInputQuery. These are my super fast, they're just like any other one that I've used in baby's first React component, what have you.
[00:04:56]
And so I've got the search query, where's that value? Oh, that was searchQuery, now this is inputQuery. I'm literally hovering over the thing I was looking for. So we do that. OK, so now, theoretically, it's going to look fast for a second because I'm not doing anything, right? I've taken the search query, which is what breaks this memoization, and it's not really on the scene at all.
[00:05:27]
Where's that one other search query? Oh, it's in the—cool. All right, so we know, let's see, this one. If the Pokémon itself and the ID haven't changed, so let's go ahead and let's try the beginning—probably don't need to re-render this, let's pop a React.memo on this one because if ideally for this given one, what is going into a given Pokémon component shouldn't be changing, right, because that's pretty static values.
[00:05:56]
And the nice part is that's also the place of key, right, which is we kind of know it's the same one, and these should be the same. So my first thing is I don't want to necessarily render, we can—I can go in there and look at some of the tools. I think this is actually super interesting, so we'll take a look at that in a second. But 145 milliseconds is not great. So let's say, hey, if you've gotten the same arguments, you shouldn't have to re-render.
[00:06:53]
It's going to be my first—what? You sad? You're angry at me? Yeah, yeah, yeah, yeah. All right. The other interesting performance issue that we are going to have that we won't actually be able to solve in this—we're just not seeking to solve it right now—is one of the biggest bottlenecks you'll a lot of times hit is just having a lot of stuff in the DOM, right? Having a lot of stuff in the DOM does tend to slow it down.
[00:07:17]
That's where you'll see stuff like virtualization or windowing, which is really tricky because it introduces a whole new set of problems, which is the idea of virtualization or windowing is you limit the number of DOM things by only showing the ones that are on the screen or will be on the screen soon. By definition, you've broken Control F or Command F at that point. You've run into some accessibility concerns as well.
[00:08:01]
Let's see. Let's turn that off for a sec, we'll pull back on for a moment, but just because right now. OK. So we've got the performance of this case, right, but also, we're not doing anything yet. So it's like, yay, my app is performing because I literally broke it. I didn't break it, I just disabled the expensive part, which I guess turning off the slow part is definitely one way to do this.
[00:08:27]
And I think that's a worthwhile side note for a second there though, like, you know, if you have something that's going to be very expensive, is the answer to not do a search while you type? It's the answer to put a submit button and at least use that to slow things down. Sometimes you can handle it with simple UX. That's not the theme of this workshop, so I'm not going to do that.
[00:08:50]
But it's a valid thing in this case. Cool, cool, cool. So we've got that part in place where we've memoized those so that we know that they are basically the same. Now, what we need to do is we need to have that idea of a startTransition, right? So we've got the filtered Pokémon that's using useMemo on that search query, but that's not doing anything right now. Cool, cool, cool, and then let's say we've got—let's turn this into—let's make a just for our own, just so we can see everything very clearly.
[00:09:53]
We'll call it handleSearch, changeInput, change. Call it handleChange if you want to. For an HTML input element, and with that, we're going to say that what we're going to do is the value is just going to be that event value. What are you angry about? Oh, I'm not using it yet. That's fine. Close that for a second. All right, so now I'm just moving this function because right now it's an inline function and that's no place to write all this.
[00:10:27]
So we've got that, and what we're going to do is we're going to set the input value to that value. And we'll pop this in here. That doesn't really give us much that is different before, setInputQuery. Great. And this gives us basically what we had before. But now what we'll do is we're going to call this startTransition. And startTransition is just going to be a function that says, here's a low priority thing to do later when you get a chance, right?
[00:11:07]
When the time is right, and if you've ever used something like requestIdleCallback, which is kind of like setTimeout, or requestAnimationFrame, which is like, hey browser, when you are sitting around with nothing to do, can you go do this thing? Is effectively what we're telling React. Like, hey, when it comes to time, when the high priority queue is clear of typing or anything along those lines, and it feels good and right, when you come up for air and there's nothing else to do, then can you go do this thing because now we're not dealing with immediately responding to the user.
[00:11:44]
So again, nothing about this is going to be that much faster except it's going to at least feel like we'll update that other piece of state. SetSearchQuery to the value when you get a chance. So if we read this code, and there's not a lot happening here, we haven't used this pending yet. I'll use it in a second. There's not a lot happening here. When the search input changes, right, immediately set the thing that we see in the input field.
[00:12:18]
When you get a chance, go do the expensive thing when we've cleared out all of the urgent things that need to get done, right? And what we'll do here is we'll just put a little—turn this into a template string, and we'll say, isPending and we'll say, if it's pending, we'll give it an opacity of 50. Otherwise, give it opacity of 100. Why are you angry at me now? Oh, because I gotta put these in braces.
[00:13:08]
All right. So, we immediately change in the input field. And then we say in a lower priority lane, then go change the search query, which will trigger that useMemo. So while on one hand, thinking about React Fiber and scheduling and lanes and all this stuff is kind of complicated stuff to wrap our heads around conceptually, the actual implementation—it's shockingly simple. So you're like, I've got a high priority, effectively thing I'm changing immediately, and I'm scheduling work for when we have the ability to do it.
[00:13:44]
It's still going to be labor intensive work, but ideally it's going to happen at a more opportunistic time, right? And so all of that crazy stuff in the inner workings, if we know how to leverage it, we have then the ability to do what would have otherwise been a significant undertaking, right, ourselves, right? So here, hopefully. You can see that it updated immediately. And you can kind of see that when there is something pending, like in the period of time between when we started that transition and set that value, and when it resolved, you can see that everything kind of went a little bit like the opacity dropped for a hot minute there, right?
[00:14:44]
So here we go. Typing fast. There's some visual indicator, maybe you choose to have a loading thing. I think if I wanted to be. Animate pulse. Wasn't like I wanted more of an effect there, right? There's a whole other school of thought or art form, which is, if you show a loading indicator too fast and it goes away too fast, was that worse than never showing a loading indicator at all?
[00:15:07]
And then do you wait a few seconds to show a loading indicator, but if it actually was only there for six seconds, you waited five seconds, did you move the problem around? We're not going down that road today because that's a dark, dark road. All right, so my animation, not great in this case, but the opacity, the actual React parts that we care about, important. I wonder if it's—why, why, why, Steve?
[00:15:46]
Oh, you can see it. You saw it? It animated, it animated, a little slow, a little slow for my taste, but it animated. I think the animation, the animation plus the opacity was just not enough to notice. But my stupid little parlor trick did work after all. The opacity was a better experience, I will say that. I think it made the visual feedback better, but this one amuses me more.
[00:16:10]
Anyway, the point is, not a lot of code for effectively what would have been some amount of maybe debouncing, probably three or four other pieces of say, oh, they started typing, and then we're going to debounce it, and now that the debounce hit, and then we'll do a requestIdleCallback, but you could do this all by hand. And we could spend an hour doing it all by hand. I'm not going to because I don't have to because React Fiber has done all of the heavy lifting.
[00:16:42]
I mostly just need to understand those concepts from earlier in the workshop and then learn the magical incantation of effectively having both the immediate high urgency value that I set, and the thing that can wait until an opportune time to do it. And I also for free, effectively get a kind of loading indicator to give that visual feedback that not only are we going to do it, but you can see a visual indicator that we are in fact going to do it, right?
[00:17:21]
Not a lot of code. A lot of conceptual understanding, hard earned, to be clear, but not a lot of code. So yeah, so that one is one, the other option is this useDeferredValue. Again, I would argue that 90% of the time, unless it's third-party code or you're on the receiving end, in these cases, you will—this is not how I would do it. I would do this approach, but let's see if I can very quickly show that.
[00:18:08]
So what I think I'm going to try to do is, in this case, we should with useDeferredValue, what we do is we basically say, hey, I'm going to give you a new value like we would do with useState. Use the old one until a low priority time has come around, then update it, which should then kick off the same thing, right? Ideally, again, if you are on the receiving end of that prop, like let's say you were the child component and you're getting the search query, right?
[00:18:48]
That's maybe where you would say, hey, use the old one for a little bit, right, and then do it. You will most likely use useTransition a lot more, but let's see if I end up regretting any of this, which is the famous last words before I regret it, to be clear, we all know that. We don't have isPending anymore, but we do have is—we've got the deferred value and we've got the input query.
[00:00:00]
If they're not the same, it means that the deferred one has not caught up to the urgent one. So the act of them not being the same implies that we are awaiting that to happen, right? So here we'll just say if inputQuery does not equal searchQuery, I think that is our heuristic to show that we ought to be pending.
Learn Straight from the Experts Who Shape the Modern Web
- 250+In-depth Courses
- Industry Leading Experts
- 24Learning Paths
- Live Interactive Workshops