Building a React Search Bar in the App Header

In this lesson, we'll work on something slightly related to the `/listings/:location?` page and is a big factor of our app. We'll be working on the search input that we'll place in the app header that will allow users to search for listings in a location while within any part of our app.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two

At this moment in time, our listings page behaves the way we want it to. However, in this lesson, we'll work on something slightly related to the listings page and is a pretty big factor of our app. And that is the search inputs we're going to place in the app header. Having the search input in the app header itself is useful since it would allow the user to search for listings in a certain location while navigating any part of our application. The search input is going to behave very similar to the search input we have in the home hero. It'll simply be an input component from Antesign and will ensure that when the user searches for something, it'll take the user directly to the listings route and append whatever's been searched as the URL parameter. And the listings page will retrieve that parameter and make the necessary query . There's a few other things we're going to handle as well, but we're going to address them in a second. For now, let's look to provide the search input functionality that's very similar to what we have in the home page. We'll first head over to the component that we've created for the app header, which we've called the app header component and that we've kept within a section folder. In the app header index file, we'll import the input component from Antesign and we'll destruct the search sub component from the input component. Let's now prepare our search inputs. We'll place the search input right after the div element that encompasses the logo in our app header. And for the placeholder of the search inputs, we'll say search San Francisco. We'll provide the enter button prop that helps display the call to action search button. And for the on search callback prop that gets called when a search is actually submitted, we'll call a function in the component called the search input. The component called on search. When a search is now going to be made, we'll do similar to what we've done before and trim the value that's been searched to remove any white space characters in the beginning or end of the submitted string. If the user was attempting to simply search for a value that contained just white space characters, the trimmed value will be an empty string, with which will then use the display error message utility function to display an error message that tells the user please enter a valid search. We'll then import the display error message function from the libutels file. If we now take a look at our app, we'll see the search input in our header. If we try to simply search for a value that contained just white space characters, we'll get the error message shown to us prompting us to search for a suitable location. Great. Now let's look to take the user to the listings route with the appropriate path name when a search is going to be successfully made. Recall in the home component, we used the history object available in the component to help add a new entry to the browser session stack or in simpler words to help directly user to the appropriate route. This history object we mentioned was available as a prop only because the component here, home, was rendered as part of the route component from React Router. Here's where we have a slight problem. The app header component isn't rendered as a route component, but we're interested in using this history object. This is why React Router also provides the capability to gain access to the history object for components that aren't rendered as part of the routes. Now this can be done in a few different ways. We're going to talk about some of the changes React Router is currently underway with, closer to the end of this lesson, but for now we'll use the former older standard way of using a higher order components function called with the router. That provides additional props related to the routes for components that aren't rendered as part of the routes from React Router. So with that said, let's import the with router function and the route component props interface from React Router done. We'll wrap our app header components function with the with router function with which we'll then be able to have our app header components function access the history object. And we can help define the shape of this object by using the route component props interface. And we'll say our app header component props is going to have the shape of the props and route component props interfaces. Now a higher order component is a function that accepts a component and returns a new modified component. In this case, we're passing the app header component into the with router function and from there we're getting a new component with which the history object is now available as a prop. And in our component on search function, we'll use the push method in the history object to push the new the user to the new location of slash listings and will append whatever the trimmed search value was. Now let's take a look at our app. If we're at some other page in our application and we used the search input in the app header, we'll find ourselves being navigated to the listings page and we'll have the appropriate URL parameter appended in the routes. The listings page will now take that parameter and query for the listings that we're looking or searching for. Great. We're in a good spot now. However, there's a few improvements we can make with regards to the search input. When we search for something, we're taken to the new listings route, but the search input isn't cleared out and that's okay. It helps tells us what we've recently searched for. However, as a preference, we'll also want this search input to sort of be two- way binded to the URL parameter in our route. And what we mean by this is if I at this moment in time simply visited the listings page directly in the browser with a certain parameter, though we're taken to the correct listings view and we're seeing the correct amount of listings, the search input remains blank. Not an incredibly important thing to handle, but we'll like the search input to reflect what's being searched for, even if we used the URL route to navigate to the page. So in this case, what we want to do is check for when the component first renders, and if so, we'll see if the listings route has an appropriate URL parameter. If it does, we'll update the value in our search input. Since we need to keep track of the value or a value in the search input, we'll import and use the use state hook from React. And in the beginning of our component function, we'll declare a new state property called search and a function called set search, which will be used to update this particular search property. We'll initialize the value of this search property with a blank string. And we'll place the search state property or the value of that as the value of the value prop in our search component. If we took a look at our app and at the search input, we'll see that it is initialized with a blank string, however, we're unable to actually type anything in this input. This is because by providing a state property as the value of the input, we haven't provided the means to update this state value. So even if we try to type something here, the state value remains as a blank string. So in our search input, we'll use another prop labeled onChange that receives an event object. From this event object, we can access the value property with event.target. value with which we can then use to update the search state property with the set state function. Now note that this value and onChangeProps are just normal traditional props or attributes that can be used for any normal input field. They aren't relevant to just the design search inputs. Event or the event object constitutes the actual input event. From where we can get other information, but we're interested in getting the value at the time with which we access with event.target.value. And our onSearch callback function still does as intended and only calls the on Search method in our component when the input has been submitted. If we took a look at our app, we'll notice that we're able to actually type something in our inputs. With every change we make, the onChange event is being triggered, the new values being obtained, and it's updating the state property with which we're dancing. We want to check for when the app header components first renders, see if the user is in the listings route. If so, we'll take the sub path and look to update the value of the search input with it. Since we want to do something on the component or the app header component first renders, this will be useful to use the use effect hook from React, so we 'll import the use effect hook. And we'll use the use effect hook to construct our effect callback with an intention of having the effect run only on first render, so we'll provide an empty dependency array. Our approach here would depend on us checking what URL route the user is in. If the user is in the listings route, we'll want to update the inputs with whatever the path name is after slash listings. React Router provides access to an object called location, which is useful if we ever need to know the current URL at any moment in time. If a component is rendered as part of a routes component, this location object is available as props. However, in this particular case, the app header component isn't a routes component, but we're already using the width router higher order function to provide these routes properties or routes objects. So we're able to destruct the location prop object as well. And in our use effect hook, we can access the path name from our URL with the location object. Note that the use effect hook warns us here and tells us that the location property should be a dependency since we're using it in our effect. This is a valid warning, since we'll actually want this effect callback to run any time the location ever changes. Keep in mind, the app header component is going to be rendered for every page in our app. So if the user was to navigate away from listings to another page in the app or vice versa, we'll want this effect to run and do the work we plan on doing right now. The first thing we can actually do here is the opposite of what we just talked about. So what we'll say is if the user is in the listings page and navigates away from the listings page to another location in our app, we'll want the search input to be cleared out. That's because there's no reason to have the search input still contain the most recent search. To achieve this, what we can do is check if the URL path name does not contain the string slash listings. If it doesn't, it probably means the user isn't visiting the listings page and is trying to visit some other page in our app. If that's the case, we can use the set search state function we have to set the state property to an empty string and then we can return early. And if we now take a look at our app and search for something in the app header and then try to navigate elsewhere in our app, the search input will be cleared out. Great. Now we can try and achieve the opposite of this particular use case. And what we'll say in our use effect hook is we'll place an if statement to see if the user is actually visiting the listings route, with which we're able to do by checking if the path name is not in the list. And if the path name includes slash listings. Our URL listings route when the user is searching for a location will look something like this. Locally, it'll be localhost 3000 slash listings slash Toronto. We want to sort of grab the URL parameter Toronto. Since we have the location object available to us, we can use some simple JavaScript to try and retrieve the path name at the end of the string. One way of doing this is we can first try and get all the substrings in our URL by separating every portion with the slash. And we can do this with path name dot split slash. Path name of the location essentially refers to the path of the URL after the domain name. So for the particular URL example we've talked about before, localhost 3000 slash listing slash Toronto, the path name essentially refers to slash listings slash Toronto. The substrings we're creating here is we're essentially splitting this particular path name with every portion of the string that contains the slash. So this particular substrings would be an array that contains three items. The first item will be the item before the slash in the beginning for the path name, which would be an empty string. The second item would be simply just listings and the third item would just simply be Toronto. That's essentially the three pieces within the path name substrings. So in our new if statements, we can add a further check to make sure the users attempting to visit a certain location by seeing if path name substrings has a length of three. If it has only a length of two, it probably means the user is just trying to visit slash listings. If it has a length of three, we can take the last item in the array and use it to update the search state property with it. And we can then return early as well. As a side note, you might be wondering why don't we just use the match param object we've seen elsewhere. The match object is actually only route relative and is only inherited from the nearest route, which is why if we attempted to access the match object here and the params within this match object in the app header component, it would have no context as to what URL parameter exists and just be an empty object. So in this case, we're using the actual URL routes and attempting to pull the location parameter manually. If we'd visited our app right now and we tried to access a URL route directly for listings in a certain location, something like listings slash Toronto. When our page loads, we'll see the Toronto path name now populating our search inputs. Amazing. And we've almost covered pretty much most of what we wanted to hear. There's one other small quirk we'll look to handle, and this can be observed when we actually have pagination elements in our listings page and the ability to search for listings in the app header. Now, for example, in this case with our app showing listings in Toronto, let's change the page limits in our listings query in the listings component file to four, just so we're able to see the pagination element again for the number of mock listings we have for this certain city. If I go back to the app right now, and if I attempt to go to the second page, we'll see the new listings in this second page. However, at this moment in time, if I try and search for another location in the search bar, I'll notice that the new search is being made, but I'm still in the second page. As a preference, I wouldn't prefer this. I'll prefer whenever a search is being made for a certain location, we bring the page back to one. And this is even more important if we had dozens and dozens of pages for a location. If, for example, I was in the 30th page and I tried to search for a different location, I wouldn't want to stay in the 30th page of that new location. Now with that said, why is this happening? When our listings component is first rendered, the page is initialized with one . When we change the page and change the page state value, then attempt to search for a different location, we're still in the same listings component. So the page value remains the same as we were before, but the location value in our query changes, which is why we get a new set of listings, but stay in the same page. So to resolve this, what we need to do is simply check if the location ever changes while still in the listings page, and if that happens, simply set the page number back to one. This is a perfect use case for using the use effect hook, so we'll import and attempt to use the use effect hook in our listings component file. We've mentioned that we want to have an effect that brings our page state value back to one. We can achieve this with the set page function and providing a value of one. Now this comes to the fact that when do we actually want this effect to run? We want it to run at any moment the URL location value changes while still in the listings route. In other words, we can say we'll want this effect to run at any moment of the location URL parameter changes. If it changes and the listings component is still being rendered, this probably means that the user has used the search bar to find listings for another location. So what we'll do is we'll add the location URL parameter available in our match .perams object as the dependency of this effect. Let's take a look at our app and see how this now behaves. If we are in a certain location, go to another page and attempt to search for listings in a different location, we'll be brought back to page one. Amazing. Now if we took a look at our network logs, we'll notice something pretty strange happening. When we go to a new page and search for a different location, we'll see two network requests being made. Why? This is because the variables in our query is being changed twice. First, the location actually changes and a query is being made. Then an effect is being run to set the page back to one and the second query is being made. This happens pretty much instantly from a UI perspective, which is why we only just see the final outcome. But it's unnecessary to make two requests in this particular case. So what we can do here is try and skip that first request under the condition that the page is going to be updated back to one. And we can achieve this by using Apollo's skip property, which helps tell the query to skip making that particular query under certain conditions. So now what would this condition be? We can say if the location is being updated and the page isn't equal to one, this means that when the location is updated, the page will eventually be set back to one. So let's skip that very first query and only try to make the query when the page is back to one. How do we keep context of when the location is being changed? We can use the use ref hook. The use ref hook and react helps return a mutable ref object that persists for the lifetime of the component. So with that said, let's import the use ref hook. In the beginning of our listings components function, we'll create a new ref, or in other words, reference object, that we'll call location ref and we'll pass in the location parameter as the value. In our skip clause, we can check to see if the location ref current value, which is the referenced value we passed in, isn't equal to the new match params location, and page is not equal to one. And in our use effect hook, we'll be sure to update the location ref current value with the new location after the page has already been set back to one. So what's going to happen here? Let's assume we first visit the listings/toronto route. The location reference will be Toronto. Let's say we switch to page number two, then in our search bar, search for Los Angeles. Our reference value won't be equal to the new location value. Toronto won't be equal to Los Angeles and we won't be in page number one, so we 'll skip making that very first query when the location just changes. However, our use effect hook will pick up the fact that our location has just changed and will attempt to set our page state value back to one. Since it isn't already one, it's being set to one, which means the value of the page variable in our query is going to change and the second query is going to be made. So if we take a look at our UI right now and attempt to replicate the situation to have two queries be made, we won't be able to. We'll only be making a single query request with the latest variables when we 've changed the page and then updated our search bar to search for a new location. Fantastic. One other observation you might notice is that the filter value in our components is going to have a very similar situation with the location being changed. If I change the location while a different filter is applied, let's say high to low pricing, that filter will remain. I personally don't find this an issue and I don't think it's as jarring as staying within a certain page after the location is updated. So I won't change this if the user wants to stay within the high to low pricing context that makes sense to me. But you're more than welcome to take the filter into account as well if you want to. And that's pretty much it. The search functionality in our app header now works the way we expect it to. As a quick note before we close, the React Router team is currently underway in creating hooks for accessing the different route specific objects within components. The React Router team is still progressing through this and have mentioned further changes are still in the works. When we began building this app, this was before any of the work the React Router team has made in introducing some of these new hooks. So for the rest of this application, we're going to continue using the higher order component with Router to gain access to route specific information. We're going to provide documentation in the manuscript to reflect how the newer version of React Router can be adapted or when it's officially available and ready, we'll show a spinoff video of the next major release that React Router has. [no audio]