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.
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 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: users
data and 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
.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.