Build search page and add support to Tab to Search
In this day and age, internet search is a habitual behaviour in daily life. People search in medium.com to learn, search videos on YouTube, search products on Amazon and search on Google for decision-making. It makes the search bar become a must-have element in every website.
Why is a search box essential?
According to the survey conducted by the Blue Foundation Media on The satisfaction with current web user experiences, close to one-third of those surveyed say no search box is the biggest website turn-off and more than 40% say a search box is the most important feature on a content website.
Especially for a content-based website or eCommerce platform, a search box is the most efficient way to make your content discoverable and connects the visitors to what they need quickly.
Tab to Search feature
The Tab to Search feature is a UX implementation of Open Search in Google Chrome. A website should specify the Open search description to enable the feature in Google Chrome. The OpenSearch description format lets a website describe a search engine for itself so that a browser or other client application can use that search engine.
We have different implementations in different browsers.
After the user hit tab, a magnifier shows up to indicate a custom search provider is enabled.
Safari also supports Tab to search, but the UX is a bit subtle.
Firefox has no tab to search implementation. Instead, a feature for users to manage custom search providers.
In this story, we are going to share how we build a search page for the Cookbook web application with the Tab to Search feature.
This is the second episode of the Cookbook app. Let’s start by cloning the api-caching branch of this repository.
Build the search bar
We plan to build a search bar component that is only responsible for the style of the search box. The component accepts two props. The query prop is the value of search input passed from parent and the onChange() prop is a callback function when the user types and changes the value of the search box.
We install @svgr/webpack to render the magnifier icon of the search box.
$ yarn add @svgr/webpack
And configure in next.config.js file.
The original color of the magnifier icon is black. To change the color of a svg image, we should change the fill property of the svg path in a svg wrapper.
Build the search page
The query state stores the value of the search box which will be passed to the search box component. Another prop to pass is the onChange() function. When the user makes changes on the search box, the onChange() is called and updates the value of the query state.
However, for a better user experience, we would like to make the search fetch automatically without clicking the search button to submit the query. The search request will be fired after the user finishes typing, like stop changing the input value for 500ms. Therefore we need a debounce function to wrap the query state before we push it to fetch the search result.
The useDebounce custom hook
We created a custom hook called useDebounce() here. The mechanism of the debounce hook is to delay the processing of the keyup event until the user has stopped typing for a predetermined amount of time.
The useEffect() hook is triggered when the value or timeout is changed. The value prop will then be pushed to the state after a timeout of milliseconds. If the value is changed again within the timeout period of time, clearTimeout() function is called while unmounting to clear the setTimeout() handler. Another setTimeout() is set up until the next action.
Trigger search request after debounced query
On the search page, a useEffect() hook is waiting for the debounced searchQuery to change.
The useEffect() hook will trigger the searchRecipes() to fetch the search results with the query value and update the recipes state.
With the Search Bar component, useDebounce hook and the searchRecipes() method, we can list out the recipe titles in the search results simply by <ul> & <li> tags.
Manage the search requests
The network is not always stable, especially when we are integrating a third-party service API, although the Spoonacular Food API is very reliable. We should always be careful when handling the interaction between the frontend and network requests. Here we will bring up a scenario that we need to handle for the search page.
For example, when a user searches for `chicken` recipes, it takes a long time to respond due to network problems. Then, the user changes the mind to search `beef` before the `chicken` search request is finished. Embarrassingly the first `chicken` query responds even later than the latest `beef` query. The `beef` keyword happened to stay in the search bar with `chicken` search results listed.
To handle this problem, we should implement a mechanism to ignore the result from the first `chicken` query when we start the `beef` search query.
Let’s make use of the cancel token feature of axios package. Firstly, we need a variable to store the cancel token source.
And we should modify the searchRecipes() function.
The previous request will be canceled when a new search request starts.
Build and apply loading widget
We should always show a loading indicator to notify the user when something is working in the background. Let’s build a loading widget and show it when fetching.
Next, apply the Loading widget in the RecipeCards component.
In ./pages/search.js, we created a loading state. The flag is pulled up when the search results start to fetch and down when the fetch is finished.
Then, we have the loading state pass to the RecipeCards component as a prop.
Now, the loading indicator will appear when the fetch is processing in the background instead of everything is blank under the search box.
Add search page entry in the header
We added a magnifier icon in the header as the entry of the search page. But the icon is not shown on the search page as users would never find the way to go to a search page when they are already on the search page.
Therefore, we detect the pathname with React useRouter() hook. If the pathname is equal to /search, the magnifier icon is hidden.
There are few things to do before we make the Cookbook application support OpenSearch.
Pull the search keywords from the URL parameter
In the search page, we pass the query URL parameter from the getInitialProps() function as a prop. If the query is not empty, the value will be set as the initial state of the search keywords, and also search recipes request will be triggered.
Add OpenSearch description document
We need to serve a XML file to describe the custom search engine. As we want to generate different paths in the OpenSearch description for different environments, the XML will be served from Next.js pages component.
In the getServerSideProps() function, protocol and host is retrieved from request headers. Then, the request ends with a response object after serving application/xml content type and xml content.
Add the autodiscovery link
After the OpenSearch description file is available, we need to add the autodiscovery link within the head tag for the browsers to discover the OpenSearch description. It is wrapped by the next/head tag added in the ./pages/_document.js file.
With all this hard work, we deployed the repository to vercel. After first access to the page, the browser will discover the custom search engine. For the next time when we type the domain in the URL bar, we will be able to search recipes with Tab and Search.
Bill Gates gave a speech at a private dinner and stated one of the smartest things he has ever said:
The future of search is verbs.
When people search, they are not simply searching for information but usually, they want to carry out an action or decision making.
More and more websites even learn how people behave on their websites with the search box. If you don’t have a search box implemented, you may probably consider adding one for your websites now.
Feel free to clone or fork the repository here.
Happy coding! 💻