Create the fastest search for your website in minutes, without any dependencies βš‘πŸ”Ž

Dhravya - May 10 '22 - - Dev Community

The project I'm working on is written in Gatsby JS, but the solution itself is vanilla react and will work everywhere.

Jump to main content

The project I'm working on is written in Gatsby JS, but the solution itself is vanilla react and will work everywhere.

Today, I spent most of my time updating my blog, and thought to add more features like search, tags, MDX support, and a few design changes, including the sidebar.

I was deciding how I would implement the search function, because the only time I have done it, was using a
Self hosted version of Typesense

But well, that was costly to host server-side, especially for
something as simple as a blog. and their hosted solutions aren't that great price-wise either.

So one thing was sure, there is no need to use any API for this. After a quick google search, I came across this documentation on Gatsby's website which is about adding search to Gatsby

From that guide, under the Client Side section, here's what they recommend:

It is possible to do all the work in your Gatsby site without needing a third-party solution. This involves writing a bit of code, but using less services. With large amounts of content to index, it can also increase the bundle size significantly.

One way of doing this is to use the js-search library:

Adding Search with JS Search

There are two Gatsby plugins that support this as well:

gatsby-plugin-elasticlunr-search
gatsby-plugin-local-search

Now these search methods index everything which means higher bundle size. And they are also a hassle to set up.

The solution I went with

Now for my use case, it was probably a good idea to just make something simple by myself, and I can build on it as I keep updating this blog.

The idea is really simple, I just need to make a search box, and on every keystroke, loop through the contents and filter them like that.

const BlogIndex = ({ data, location }) => {
  // These posts can be anything,
  // I've just used the posts from a gatsby query
    const posts = data.allMdx.edges;

  // We need to filter the posts by the search query.
  // by default, we have all posts
    const [filteredPosts, setFilteredPosts] = useState(posts);

  // This will be the search query
  const [search, setSearch] = useState('');

  return (
    <div>
      {/* Our search bar */}
      <input
        type="text"
        placeholder="Search"
        onChange={(e) => {
          e.preventDefault();
          setSearch(e.target.value)}
      }/>

      {/* Simply mapping through everything and rendering blogs */}
      {filteredPosts.map(({ node }) => {
        <BlogPost post={node} key={node.id} />
      }}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now, whenever something is typed in the box, the search state will be updated. Now, let's write a useEffect hook to update the filteredPosts state whenever the search state changes.

const BlogIndex = ({ data, location }) => {

    const posts = data.allMdx.edges;
    const [filteredPosts, setFilteredPosts] = useState(posts);
  const [search, setSearch] = useState('');

  //highlight-start
  useEffect(() => {
    if (search) {
      // post filtering here
    }
  }
  // only update the filteredPosts state when the search state changes or the posts state changes
  , [search, posts]);
  ///highlight-end

  return (
    ... // rest of the code
  )
Enter fullscreen mode Exit fullscreen mode

And now let's write some very simple code to filter the posts.

...

  if (search) {
    const filteredPosts = posts.filter(post => {
        //highlight-start
        const title = post.title.toLowerCase();
        const description = post.description.toLowerCase();
        return title.match(search.toLowerCase()) || description.match(search.toLowerCase());
        //highlight-end
    }
  }
  setFilteredPosts(filteredPosts);
...
Enter fullscreen mode Exit fullscreen mode

Since my blog has tags and stuff like that, I added functionality to search and filter by tags, too

    if (search.startsWith("#")) {
      return tags.includes(search.replace("#", ""));
    }
...
Enter fullscreen mode Exit fullscreen mode

And that's it! But wait, there's more. This works, but you can't really share a search query to someone else, I can share google links - google.com/search?q=github
I think that's kinda important, like, for times when I have to share all my Rust blogs, it's just easier and convenient.

so well, let's update the URL to include the search query, in real time! I hadn't ever done this before, so it was great learning it. I got the inspiration from the IFTTT search engine

I found out about the window.history.pushState() method, which basically allows you to push a new URL without adding it to the browser history, or reloading the page. Read the documentation for the same over here -
History API | MDN

useEffect(() => {
        if (search) {
      //highlight-start
            if (window.history.pushState) {
        window.history.pushState(null, null, `/?q=${search}`);
      }
      //highlight-end
      const filteredPosts = posts.filter(post => {
        const title = post.title.toLowerCase();
        const description = post.description.toLowerCase();
        return title.match(search.toLowerCase()) || description.match(search.toLowerCase());
    }
  }
  setFilteredPosts(filteredPosts);
  }, [search]);
Enter fullscreen mode Exit fullscreen mode

And now, we also need to parse the original request, using the window location object, and make it default for the useState hook we made for search

                      // πŸ‘‡πŸ» converts the URL from HTML encoded to a string (%20 to space)
    const initialState = decodeURI(location.href? // Use window location
                      .split('/')               // Split the URL into an array
                      .pop()                    // Get the last element only
                      .split("=")               // at this point, it's q=search, so we only need the "Search" parth
                      .pop() );                 

                                        // πŸ‘‡πŸ» We're using the initialState to set the search query.
  const [search, setSearch] = useState(initialState); // Now, only the blogs that match the query will be displayed on first load 
Enter fullscreen mode Exit fullscreen mode

That's it!

The full implementation can be found in the source code of this blog on Github

You can try out the search yourself

Dhravya Shah

Feel free to visit the repository for this blog here
Dhravya Shah's blog

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .