Moving our search drop down from Algolia to Alpine.js

Published under JavaScript

So for the search drop down you see on this website, I initially went with Algolia's InstantSearch.js. This was my implementation:

<div class="relative">
  <div id="#search-box"></div>
  <div id="#hits"></div>
</div>

// search.js 
import algoliasearch from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';
import { searchBox, hits } from 'instantsearch.js/es/widgets';
import 'instantsearch.css/themes/reset.css';

const searchClient = algoliasearch(window.algoliaAppId, window.algoliaApiKey);

export default function () {
  const search = instantsearch({
    searchClient,
    indexName: 'default',
    searchFunction(helper) {
      const container = document.querySelector('#hits');
      container.style.display = helper.state.query === '' ? 'none' : '';

      if (helper.state.query) {
        helper.search();
      }
    }
  });
  
  search.addWidgets([
    searchBox({
      container: '#search-box',
      placeholder: 'Search...',
    }),
    hits({
      container: '#hits',
      templates: {
        empty: '<p class="p-2 m-0">No results</p>',
        item: `<a href="{{ url }}" class="block px-2 py-3 bg-white text-sm text-left border-b border-gray-300 hover:bg-gray-200 hover:no-underline">
          {{ title }}
        </a>`
      }
    })
  ]);
  
  search.start();
}

So this isn't too bad, easy enough to understand and manage. However, my production bundle was 227kb (this includes highlight.js, which I am using for code syntax highlighting).

Seeing this, I decided to try out Alpine.js by Caleb Porzio to see if I could reduce my bundle size without sacrificing functionality. Here's the Alpine.js implementation:

<div class="relative w-64" x-data="initSearch()">
  <input
    type="search"
    x-model="query"
    x-on:keyup="search()"
    class="bg-white border border-gray-300 p-2 rounded w-full"
    placeholder="Search..."
  />
  <div x-show="query" class="absolute bg-white shadow rounded w-full max-w-full" style="display: none;">
    <p x-show="loading" class="py-2 m-0">Loading</p>
    <p x-show="!loading && !resultsHtml" class="py-2 m-0">No results.</p>
    <div x-show="!loading && resultsHtml" x-html="resultsHtml"></div>
  </div>
</div>

function initSearch() {
  return {
    query: '',
    loading: true,
    resultsHtml: null,
    search: debounce(function() {
      if (!this.query) {
        return;
      }

      this.loading = true;

      const host = `https://${window.algoliaAppId}-dsn.algolia.net`;
      const url = `${host}/1/indexes/default?query=${encodeURIComponent(this.query)}`;

      fetch(url, {
        headers: {
          'X-Algolia-Application-Id': window.algoliaAppId,
          'X-Algolia-API-Key': window.algoliaApiKey,
        }
      })
        .then((response) => response.json())
        .then((data) => {
          this.resultsHtml = null;

          if (data.hits.length) {
            this.resultsHtml = data.hits.map(({ url, title }) => {
              return `<a
                href="${url}"
                class="block px-2 py-3 bg-white text-sm text-left border-b border-gray-300 hover:bg-gray-200 hover:no-underline"
              >${title}</a>`;
            }).join('');
          }

          this.loading = false;
        });
    }, 250)
  };
}

A couple things to note:

And to top it off, the production build JS is down to 27kb: /js/app.js 27.5 kB

If you want to try a demo of the search component, feel free to check out the search component on this website!