Live Search with React

In this blog, we will learn about how to create a live search input field, which fetches the result as the user types in the query.
Of all the results available for that query, we will only fetch 20 results. We will create a pagination for the user to navigate to the next or previous 20 results, until no further results are available.

We will achieve this by working on the following steps:

  1. Create a Search.js component
  2. Create a stylesheet Search.css
  3. Create an onChange Event Handler for Search Input
  4. Install axios and create fetchSearchResults()
  5. Call fetchSearchResults() on user’s query
  6. Create renderSearchResults() to show the results
  7. Add the loader, error message and styles

Pagination:
8. Initialize state for storing page information
9. Update state with pages information
10. Create handlePageClick()Β to handle page click
11. Create a component for Pagination links
12. Include PageNavigation Component into Search.js
13. Add styles for Navigation Links

API link: https://pixabay.com/api/docs/#api_search_images

Tutorial Video:
You can also watch the tutorial to understand in detail.
Part 1:

Part 2:

Step 1: Create a Search.js component

Create a component called Search.js inside the src/components directory.
Initialize the state, and define below properties:
query: to store user’s query.
results: to store our fetched data.
loading: initially set to false, which will help us show loader while the results are fetched.
message: To store any error message
Add an input inside label, wrapped in a container div, for user to enter his query

import React from 'react';

class Search extends  React.Component {

	constructor( props ) {
		super( props );

		this.state = {
			query: '',
                        results: {},
                        loading: false,
                        message: '',
		};

	}

	render() {
		return (
			<div className="container">
				{/*Heading*/}
				<h2 className="heading">Live Search: React Application</h2>

				{/*Search Input*/}
				<label className="search-label" htmlFor="search-input">
					<input
						type="text"
						value=""
						id="search-input"
						placeholder="Search..."
					/>
					<i className="fa fa-search search-icon"/>
				</label>
				
			</div>
			)
	}
}

export default Search;

Now import Search.js into App.js

import React from 'react';
import Search from "./components/Search";

class App extends React.Component {
	render() {
		return (
			<div>
				<Search/>
			</div>
		);
	}
}

export default App;

Include font awesome inside public/index.html

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

Step 2: Create a stylesheet Search.css

Create a stylesheet src/Searc.css. Add styles for the above elements

.heading {
	font-size: 30px;
	padding: 16px 0;
	color: #444;
	text-align: center;
}

/*Container*/
.container {
	margin: 36px auto;
	width: 100%;
	max-width: 800px;
}


.search-label {
	position: relative;
}

.container input {
	width: 100%;
	padding: 16px;
	font-size: 36px;
	font-style: italic;
	color: #444;
	box-sizing: border-box;
	outline: none;
}

.search-icon {
	position: absolute;
	top: -10px;
	font-size: 24px;
	color: #555;
	right: 18px;
}

Now import Style.css into Search.js

import '../Style.css';

So at present you app would look like this:

Step 3: Create an onChange Event Handler for Input

Create an onChange Event Handler function that takes that sets the value of the query property of state to what user entered in the input. We will set loading to true and message as empty which will nullify any message that was printed before.

handleOnInputChange = (event) => {
	const query = event.target.value;
            this.setState({ query, loading: true, message: ''  } );
};

Now add that event to the input.

<input
   type="text"
   value=""
   id="search-input"
   placeholder="Search..."
   onChange={this.handleOnInputChange}
/>

Step 4: Install axios and create fetchSearchResults()

Install and import axios

$ npm install axios

// Search.js
import axios from 'axios';

Now create a function called fetchSearchResults() , which takes two parameters :
updatedPageNumber: Page number to be served which will help us later when we do pagination
query: What user has entered in the search input field.
We then add the searchUrl for which we are using pixabay API, which gives us the images data.
As we don’t want to keep making API call, every time the user enters a character, we would initialize a variable called cancel in our constructor and cancel the previous request before making a new one. We will use CancelToken that axios provides us for this.
1. We cancel the previous request,
2. generate a new token and,
3. Pass this token as a the second parameter in axios.get()
4. We make a get request to Pixabay API and store the response in state, using setState(). We also set the loading to false, and the relevant message when we have received the response.
5. If we receive any error we set that in the message property of state.

constructor( props ) {
	super( props );

	this.state = {
		query: '',
		loading: false,
		message: '',
	};
	this.cancel = '';
}

/**
 * Fetch the search results and update the state with the result.
 *
 * @param {int} updatedPageNo Updated Page No.
 * @param {String} query Search Query.
 *
 */
fetchSearchResults = (updatedPageNo = '', query ) => {

	const pageNumber = updatedPageNo ? `&page=${updatedPageNo}` : '';

	// By default the limit of results is 20
	const searchUrl = `https://pixabay.com/api/?key=12413278-79b713c7e196c7a3defb5330e&q=${query}${pageNumber}`;

	if (this.cancel) {
		// Cancel the previous request before making a new request
		this.cancel.cancel();
	}
	// Create a new CancelToken
	this.cancel = axios.CancelToken.source();

	axios
		.get(searchUrl, {
			cancelToken: this.cancel.token,
		})
		.then((res) => {
			const resultNotFoundMsg = !res.data.hits.length
				? 'There are no more search results. Please try a new search.'
				: '';

			this.setState({
				results: res.data.hits,
				message: resultNotFoundMsg,
				loading: false,
			});
		})
		.catch((error) => {
			if (axios.isCancel(error) || error) {
				this.setState({
					loading: false,
					message: 'Failed to fetch results.Please check network',
				});
			}
		});
};

Step 5: Call fetchSearchResults() on user’s query

When the user types his query in the search box, we will :
1.Check if the user’s query string is empty , if it is set the result to empty object.
2.If its not empty, set the query in the state, set loading to false, message to empty string
3. Call the fetchSearchResults() which will take page number ( 1 for now ) and query,
4. Cancel the previous request, make a new token .
5. When the response is received fetch results and set the result in state, loading to false, and message if any.
6. Set the error message if error received.

handleOnInputChange = (event) => {
	const query = event.target.value;

	if ( ! query ) {
		this.setState({ query, results: {}, message: '' } );
	} else {
		this.setState({ query, loading: true, message: '' }, () => {
			this.fetchSearchResults(1, query);
		});
	}
};

Its important to note that we call fetchSearchResults() after we have set the query and loading to true, by calling it inside the call back function of setState() . This is because setState() is asynchrnous and does not guarantee that the values will be set immediately.

Step 6: Create renderSearchResults() to show the results

Lets create a renderSearchResults() to render the results of the query.
1. We pull the results out of state using ES6 Object restructuring.
2. Check if the results has data and . then loop their each item to display the images using ES6 array map function.

renderSearchResults = () => {
	const {results} = this.state;

	if (Object.keys(results).length && results.length) {
		return (
			<div className="results-container">
				{results.map((result) => {
					return (
						<a key={result.id} href={result.previewURL} className="result-items">
							<h6 className="image-username">{result.user}</h6>
							<div className="image-wrapper">
								<img className="image" src={result.previewURL} alt={result.user}/>
							</div>
						</a>
					);
				})}
			</div>
		);
	}
};

Now lets call this function inside render(). Also pull query from state and set that to the input value

render() {
	const { query } = this.state;
	return (
		<div className="container">
			{/*Heading*/}
			<h2 className="heading">Live Search: React Application</h2>

			{/*Search Input*/}
			<label className="search-label" htmlFor="search-input">
				<input
					type="text"
					value={query}
					id="search-input"
					placeholder="Search..."
					onChange={this.handleOnInputChange}
				/>
				<i className="fa fa-search search-icon"/>
			</label>

			{/*Result*/}
			{ this.renderSearchResults() }
		</div>
		)
}

Step 7: Add the loader, error message and styles

  1. Add loader.gif from Git repo
  2. Pull out message and loading from state using const {message, loading } = this.state;
  3. Import loader in Search.js using import Loader from '../loader.gif';
  4. Include the loader image, and error message inside render method of Search.js just beneath label.
{/*Error Message*/}
{ message && <p className="message">{message}</p> }

{/*Loader*/}
<img  src={Loader} className={`search-loading ${loading ? 'show' : 'hide' }`}  alt="loader"/>

Lets add some CSS to our results container inside Search.css

/* Results */
.results-container {
	display: flex;
	flex-flow: wrap;
}

.result-items {
	position: relative;
	padding: 16px;
	border: 1px solid #898989;
	margin: 16px;
	text-align: center;
	min-width: 200px;
	text-decoration: none;
	box-shadow: 2px 2px 2px #898989;
}

.image-username {
	color: coral;
	font-size: 18px;
	position: absolute;
	bottom: 0;
	right: 0;
	margin: 0;
	background: rgba(0, 0, 0, 0.8);
	padding: 5px 10px;

}

.image {
	width: 100%;
	height: 200px;
}

.image-wrapper {
	display: flex;
	align-items: center;
	box-sizing: border-box;
	height: 100%;
	justify-content: center;
}

/*Loader*/
.search-loading {
	position: absolute;
	left: 0;
	right: 0;
	margin: auto;
}

/*Show and hide*/
.show {
	display: inline-block;
}

.hide {
	display: none;
}

Now if we make a search , we get the results and the loader shows while its fetching the data like so :

Bravo !! πŸ™‚
We have got our app working. Now all we have to do is add pagination.

Pagination

Step 8: Initialize state for storing page information

Add totalResults, totalPages and currentPageNo properties to state, to store these information

constructor( props ) {
	super( props );

	this.state = {
		query: '',
		results: {},
		error: '',
		message: '',
		loading: false,
		totalResults: 0,
		totalPages: 0,
		currentPageNo: 0,
	};
	this.cancel = '';
}

Step 9: Add getPageCount() to calculate the no of page

Create a function getPageCount() to calculate how many pages will exists based on the result we received. So for example if the result total is 61 and we are serving 20 results per page.
total = 61
denominator = 20
This function will return 4 pages. ( 20 each for 3 pages makes a total of 60 and the remaining 1 result will go on the 4th page )
Here Math.floor() converts the number passed to an integer.

// Search.js
/**
 * Get the Total Pages count.
 *
 * @param total
 * @param denominator Count of results per page
 * @return {number}
 */
getPagesCount = (total, denominator) => {
	const divisible = total % denominator === 0;
	const valueToBeAdded = divisible ? 0 : 1;
	return Math.floor(total / denominator) + valueToBeAdded;
};

Step 10: Update state with pages information

  1. Calculate the total results and store it in total
  2. Calculate total pages count using the getPagesCount() and store it in totalPagesCount
  3. Current Page no will be equal to the updated page no received in the parameter of fetchSearchResults()

Now update these information using setState() when we receive the response.

axios
	.get(searchUrl, {
		cancelToken: this.cancel.token,
	})
	.then((res) => {
		const total = res.data.total;
		const totalPagesCount = this.getPagesCount( total, 20 );
		const resultNotFoundMsg = !res.data.hits.length
			? 'There are no more search results. Please try a new search.'
			: '';

		this.setState({
			results: res.data.hits,
			totalResults: res.data.total,
			currentPageNo: updatedPageNo,
			totalPages: totalPagesCount,
			message: resultNotFoundMsg,
			loading: false,
		});
	})

Step 10: Create handlePageClick() to handle page click.

We will now create a function called handlePageClick. This will take a parameter type . We will call this function when the user clicks on Next or Previous button ( which we will create in a moment ) .
If prev is passed, it will add 1 to the current page number, else it would subtract one from the current page number.
Remember that on our initial query we are passing 1 as the updated page no. and setting the current page no to 1. So when the users clicks on Next the current page no would become 2(1+1).
Also we won’t show the the previous button if the current page value is 1

Now we check if loading is false (means the previous request is complete).
Then we call the fetchSearchResults() to get the new search results with the same query but new updated pageno.

/**
 * Fetch results according to the prev or next page requests.
 *
 * @param {String} type 'prev' or 'next'
 */
handlePageClick = (type) => {
	event.preventDefault();
	const updatedPageNo =
		      'prev' === type
			      ? this.state.currentPageNo - 1
			      : this.state.currentPageNo + 1;

	if (!this.state.loading) {
		this.setState({ loading: true, message: '' }, () => {
			// Fetch previous 20 Results
			this.fetchSearchResults(updatedPageNo, this.state.query);
		});
	}
};

Now lets pull out currentPage and totalPages and create constants showPrevLink and showNextLink to help us decide when to show the Prev or Next links.
So if the current page is greater than 1 means user is either on the second or high page, and we should show the Prev link.
If the current page is less than the no of total pages, that means we still have more pages to show and we can show Next link

const { query, loading, message, currentPageNo, totalPages } = this.state;


// showPrevLink will be false, when on the 1st page, hence Prev link be shown on 1st page.
const showPrevLink = 1 < currentPageNo;

// showNextLink will be false, when on the last page, hence Next link wont be shown last page.
const showNextLink = totalPages > currentPageNo;

Step 11: Create a component for Pagination links

Lets create a component called PageNavigation.js inside src/component directory. We will create two links one to navigate to Prev page and the other to the Next page. This component will receive props from the Search.js which we will define in a moment when we import this component there.
showPrevLink and showNextLink props will be boolean values and will decide whether to show these links or not.
-And handlePrevClick and handleNextClick functions will called when the Prev and Next links are clicked respectively , to show results of the relevant pages.
-We will grey out the color of the these links to show the user they are not clickable until their previous request is served, using the loading prop. Loading will be true while the request is being served.

import React from 'react';

export default (props) => {
	const {
		      showPrevLink,
		      showNextLink,
		      handlePrevClick,
		      handleNextClick,
		      loading,
	      } = props;

	return (
		<div className="nav-link-container">
			<a
				href="#"
				className={`
				nav-link 
				${ showPrevLink ? 'show' : 'hide'}
				${ loading ? 'greyed-out' : '' }
				`}
				onClick={handlePrevClick}
				>
				Prev
			</a>
			<a
				href="#"
				className={`
				nav-link
				${showNextLink ? 'show' : 'hide'}
				${ loading ? 'greyed-out' : '' }
				`}
				onClick={handleNextClick}
				>
				Next
			</a>
		</div>
	);
};

Step 12: Include PageNavigation Component into Search.js

  1. Lets import it into our Search.js using import PageNavigation from './PageNavigation';
  2. And add PageNavigation component one before and the other after this.renderSearchResults(). This is so that we have navigation available before and after search results and the user doesn’t have to scroll up or down to go the next page.
{/*Navigation Top*/}
<PageNavigation
	loading={loading}
	showPrevLink={showPrevLink}
	showNextLink={showNextLink}
	handlePrevClick={() => this.handlePageClick('prev')}
	handleNextClick={() => this.handlePageClick('next')}
/>

{/*Result*/}
{ this.renderSearchResults() }

{/*Navigation Bottom*/}
<PageNavigation
	loading={loading}
	showPrevLink={showPrevLink}
	showNextLink={showNextLink}
	handlePrevClick={() => this.handlePageClick('prev')}
	handleNextClick={() => this.handlePageClick('next')}
/>

Step 13: Add styles for Navigation Links

Lets add styles in Search.js for these navigation links

/*Nav Links*/
.nav-link-container {
	margin: 20px 0;
	display: flex;
	justify-content: flex-end;
}

.nav-link {
	color: #555;
	text-decoration: none;
	border: 1px solid #898989;
	padding: 10px 20px;
	margin-right: 10px;
}
.greyed-out {
	background: #f1f1f1;
	opacity: 0.5;
}

Now we should update our handelOnInputChange(), by setting totalResults, totalPages, and currentPageNo to zero when there query is empty

handleOnInputChange = (event) => {
	const query = event.target.value;

	if ( ! query ) {
		this.setState({ query, results: {}, totalResults: 0, totalPages: 0, currentPageNo: 0, message: '' } );
	} else {
		this.setState({ query, loading: true, message: '' }, () => {
			this.fetchSearchResults(1, query);
		});
	}
};

Awesome!! πŸ™‚
Thats all guys we have our entire live search application ready with pagination.

If you liked my blog, please give a thumbs up and follow me on twitter

Git Repo: https://github.com/imranhsayed/react-workshop/tree/live-search-react/src

2+

Leave a Reply