Searching with React

RelativeCI - In-depth bundle stats analysis and monitoring - Interview with Viorel Cojocaru

Let's say you want to implement a rough little search for an application without a proper backend. You could do it through lunr and generate a static search index to serve.

The problem is that the index can be sizable depending on the amount of the content. The good thing is that you don't need the search index straight from the start. You can do something smarter instead. You can start loading the index when the user selects a search field.

Doing this defers the loading and moves it to a place where it's more acceptable for performance. The initial search is going to be slower than the subsequent ones, and you should display a loading indicator. But that's fine from the user's point of view. Webpack's Code Splitting feature allows doing this.

Implementing search with code splitting#

To implement code splitting, you need to decide where to put the split point, put it there, and then handle the Promise:

import("./asset").then(asset => ...).catch(err => ...)

The beautiful thing is that this gives error handling in case something goes wrong (network is down, etc.) and gives a chance to recover. You can also use Promise based utilities like Promise.all for composing more complicated queries.

In this case, you need to detect when the user selects the search element, load the data unless it has been loaded already, and then execute search logic against it. Consider the React implementation below:

App.js

import React from "react";

const App = () => {
  const [index, setIndex] = React.useState(null);
  const [value, setValue] = React.useState("");
  const [lines, setLines] = React.useState([]);
  const [results, setResults] = React.useState([]);

  const search = (lines, index, query) =>
    index.search(query.trim()).map((match) => lines[match.ref]);

  const onChange = ({ target: { value } }) => {
    setValue(value);

    // Search against lines and index if they exist
    if (lines && index) {
      setResults(search(lines, index, value));

      return;
    }

    // If the index doesn't exist, it has to be set it up.
    // You could show loading indicator here as loading might
    // take a while depending on the size of the index.
    loadIndex()
      .then(({ index, lines }) => {
        setIndex(index);
        setLines(lines);
        setResults(search(lines, index, value));
      })
      .catch((err) => console.error(err));
  };

  return (
    <div className="app-container">
      <div className="search-container">
        <label>Search against README:</label>
        <input type="text" value={value} onChange={onChange} />
      </div>
      <div className="results-container">
        <Results results={results} />
      </div>
    </div>
  );
};

const Results = ({ results }) => {
  if (results.length) {
    return (
      <ul>
        {results.map((result, i) => (
          <li key={i}>{result}</li>
        ))}
      </ul>
    );
  }

  return <span>No results</span>;
};

function loadIndex() {
  // Here's the magic. Set up `import` to tell Webpack
  // to split here and load our search index dynamically.
  //
  // Shim Promise.all for older browsers and Internet Explorer!
  return Promise.all([
    import("lunr"),
    import("../search_index.json"),
  ]).then(([{ Index }, { index, lines }]) => ({
    index: Index.load(index),
    lines,
  }));
}

In the example, webpack detects the import statically. It can generate a separate bundle based on this split point. Given it relies on static analysis, you cannot generalize loadIndex in this case and pass the search index path as a parameter.

Conclusion#

Beyond search, the approach can be used with routers too. As the user enters a route, you can load the dependencies the resulting view needs. Alternately, you can start loading dependencies as the user scrolls a page and gets adjacent parts with actual functionality. import provides a lot of power and allows you to keep your application lean.

You can find the full example showing how it all goes together with lunr, React, and webpack. The basic idea is the same, but there's more setup in place.

To recap:

  • If your dataset is small and static, client-side search is a good option.
  • You can index your content using a solution like lunr and then perform a search against it.
  • Webpack's code splitting feature is ideal for loading a search index on demand.
  • Code splitting can be combined with a UI solution like React to implement the whole user interface.
Previous chapter
CSS Modules
Next chapter
Troubleshooting

This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.

Need help?