How To Create a Search Engine for Any Table Using a Custom React Hook

Written by pcgamer | Published 2020/07/04
Tech Story Tags: web-development | react | react-hook | admin-dashboard | search-engine | web-design | javascript | programming | web-monetization

TLDR How To Create a Search Engine for Any Table Using a Custom React Hook 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. The 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. The hook is going to use the antd library used to design complex interfaces that involve form control and large tables. For the data, I have used the users from https://://://www.typicode.com/users.via the TL;DR App

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 :
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 it’s 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. 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 it’s 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 performs 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. So, most of the times it only gets called when the component is mounted i.e. once.
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 which 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. Lets 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 changes. 
We map over all the strings present in our
searchIndex
. Each string in our
searchIndex 
corresponds to an original
user 
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
substring 
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.

Written by pcgamer | React Developer at Yantraksh Logistics Pvt. Ltd.
Published by HackerNoon on 2020/07/04