Make a Search Engine for Any Table in React Using a Custom Hook

Sarthak Saxena
codeburst
Published in
8 min readJul 4, 2020

--

Image Source

Overview

I am going to show you guys how to make a basic search engine that would enable anyone to search for specific data in your table that you’ve built using any package or library in React.

This method is a generic one and would work with any type of table that involves feeding a data source to it in order to render the rows.

I’m going to go a bit slow with the explanation as things start to get rough-rough and we start building our own small search engine, so feel free to have a look at the following sandbox where I have implemented the whole thing :

Sandbox

Give the whole story a read as I will be talking deeply about all the code and the custom hook involved.

It doesn’t matter which library you are using to build your tables. I will be using antd, which is a React library used to design complex interfaces that involve form control and large tables. For the data, I have used the users from https://jsonplaceholder.typicode.com/users. You can have a look at the data while you are going through the code in order to understand stuff better.

Let’s get started.

The file structure is as follows:

The index.js simply renders the App component. I’ll be going through the App component first:

import React, { useState } from "react";import { Table, Input } from "antd";import axios from "axios";import { userColumns } from "./columns";import { useTableSearch } from "./useTableSearch";const { Search } = Input

Here, axios is going to help us fetch data from the jsonplaceholder’s users API. The userColumns are the columns we are going to use in our users table. You can go through them on your own in the sandbox. The useTableSearch is a custom hook we are going to use to get our data filtered depending on our search query.

const fetchUsers = async () => {const { data } = await axios.get(    "https://jsonplaceholder.typicode.com/users/");return { data };};

This is the fetchUsers function which we are going to pass to our hook so that it can fetch the data of all the users which will then be used as the data on which the search can be performed.

export default function App() {   const [searchVal, setSearchVal] = useState(null);   const { filteredData, loading } = useTableSearch({       searchVal,       retrieve: fetchUsers});

This is the body of our App component. First, we will define our local state. searchVal is going to hold the value of the user input from the search bar, and setSearchVal will help us update it’s value inside an onChange event handler that we are going to attach to our search bar element. We set its initial value to null.

The next line shows how we are going to utilize the useTableSearch custom hook to get our filtered data based on the searchVal. We are going to pass the searchVal and the fetchUsers function, that we talked about earlier, as the retrieve parameter, which will be used to fetch the data inside the hook so that the searchVal can be used to filter that data. The hook is going to return the filteredData along with a loading parameter that can be used to display a spinner while the data is being fetched for the first time.

return (   <>     <Search       onChange={e => setSearchVal(e.target.value)}       placeholder="Search"       enterButton       style={{position: 'sticky', top: '0', left: '0'}}     />     <br /> <br />     <Table       dataSource={filteredData}       columns={userColumns}       loading={loading}       pagination={false}       rowKey='name'     />   </>  );}

I am going to use the antd Search component to render the search bar, you can use any type of text input to do the same. The onChange function updates the value of the searchVal state variable whenever the user types something. This leads to the re-render of the whole component and the updated searchVal is passed to our custom hook which in turn returns the filteredData. This is then passed as the dataSource to our table.

Now that you have reached this far, you can go ahead and start using the hook inside your personal projects with your own custom retrieve function. I will now go through the hook and explain how the search engine is working. So if you’re interested to know how stuff works, continue reading!

The Custom Hook

The beauty of a custom hook in React is its reusability and ease of use. I am personally working on a project that includes a large number of tables. This hook has been very useful to add the same functionality to all my tables.

Let’s go through the code

import { useState, useEffect } from "react";export const useTableSearch = ({ searchVal, retrieve }) => {   const [filteredData, setFilteredData] = useState([]);   const [origData, setOrigData] = useState([]);   const [searchIndex, setSearchIndex] = useState([]);   const [loading, setLoading] = useState(true);

First, we will be defining our states. filteredData is an array of filtered objects, origData is the original data that we will fetch using our retrieve method that has been passed on to the hook from the App component, searchIndex is the search index that we will build using our origData. This searchIndex will be used to filter the data whenever searchVal changes. Finally, we have loading which tells us when our data is being fetched.

useEffect(() => {   setLoading(true);   const crawl = (user, allValues) => {     if (!allValues) allValues = [];     for (var key in user) {       if (typeof user[key] === "object") crawl(user[key],  
allValues);
else allValues.push(user[key] + " "); } return allValues; }; const fetchData = async () => { const { data: users } = await retrieve(); setOrigData(users); setFilteredData(users); const searchInd = users.map(user => { const allValues = crawl(user); return { allValues: allValues.toString() }; }); setSearchIndex(searchInd); if (users) setLoading(false); }; fetchData();}, [retrieve]);

This is our first useEffect call, it is used to perform 2 major tasks:

  1. To fetch the original users data and store it in our local state.
  2. To generate a Search Index using the data, which can later be used to perform the actual filtering, and to also store it in our local state.

It gets called only when the retrieve function changes. Basically it only gets called when the component is mounted i.e. once.

The Search Index

Now, what is a search index? In layman’s terms, a search index is a modified form of the original data on which we want to perform the search on, such that the modification makes the operation of searching easier / more viable. So, how can we modify our users data in a way that makes it possible to search for a value over the data? One easy method that we can use, and in fact I have used, is to crawl ( or iterate ) over all the values of every user that exists in our users array of objects and convert every user object to a single string. This will enable us to use the indexOf() function in JavaScript to filter our data based on the searchVal as the indexOf() function returns a positive integer ( i.e. the index of the sub-string ) if the string passed to it is a sub-string of the given string and -1 otherwise. Let's dive in deep.

The Crawler

The first thing to keep in mind is that the crawl function is called on each and every user object that is present in the users array and returns an array of all the values of that object. Later we convert that array to a single string using the toString()function that concatenates all the values present in the allValues array. This is done using a simple recursive function which checks whether it needs to crawl further or not based on the value at every key, let’s take an example :

The crawler will start from the id and push it to allValues array. It will keep on pushing the values until it encounters the address key, in which case it needs to crawl further inside to get the values, it will do exactly that. The crawl function will be recursively called for the address object and all the address values will be pushed to allValues. Finally, we will have an array of all the values of the user which we will convert into a string using toString(). This will be done for all the users and will together constitute our searchIndex.

Now, in our fetchData function, we use the retrieve parameter, that has been passed on from the App component, to fetch all the users. After that, we generate our searchIndex by mapping over all our users and crawling on each one of them. We will make sure to setLoading to true before we start fetching the data and then to false when the data has been fetched.

Now that we have our searchIndex, let’s go through the filtering part.

useEffect(() => {   if (searchVal) {     const reqData = searchIndex.map((user, index) => {       if  (user.allValues.toLowerCase().indexOf
(searchVal.toLowerCase()) >= 0)
return origData[index]; return null; }); setFilteredData( reqData.filter(user => { if (user) return true; return false; }) ); } else setFilteredData(origData); }, [searchVal, origData, searchIndex]); return { filteredData, loading };};

Notice that we have the searchVal as a dependency for our hook. This hook will be called every-time the user input i.e. searchVal changes.

We map over all the strings present in our searchIndex. Each string in our searchIndex corresponds to an original user object present in our origData at the same index as the string. Thus, by using the indexOf() function, if we find that the searchVal is a sub-string of a string present at say index=5 of our searchIndex, we can say that the user present at index=5 of our origData should be a part of the filteredData. This is how the filtering takes place. If the searchVal is not a substring of a string in the searchIndex, we return null for the corresponding index value. Finally, we filter our data to remove all those null values and we set our filteredData to the updated value in our local state. This is what is returned to the App component along with loading.

Conclusion

We used a custom hook in React to filter our data using a search query and then used that filtered data to render our table. In the process of filtering our data, we made a small object crawler and a search index to help us in the process.

This was how you can build a search engine for your table. That’s all I have for now. I hope you guys enjoyed the explanation. Thanks for reading! Godspeed.

--

--