search mobile facets autocomplete spellcheck crawler rankings weights synonyms analytics engage api customize documentation install setup technology content domains user history info home business cart chart contact email activate analyticsalt analytics autocomplete cart contact content crawling custom documentation domains email engage faceted history info install mobile person querybuilder search setup spellcheck synonyms weights engage_search_term engage_related_content engage_next_results engage_personalized_results engage_recent_results success add arrow-down arrow-left arrow-right arrow-up caret-down caret-left caret-right caret-up check close content conversions-small conversions details edit grid help small-info error live magento minus move photo pin plus preview refresh search settings small-home stat subtract text trash unpin wordpress x alert case_deflection advanced-permissions keyword-detection predictive-ai sso

Using App Search in React

You will need an App Search account to follow along. Sign up for a free trial.


This is an introduction to using Swiftype App Search within a React application. For this tutorial, we'll be building a simple search interface for packages available in the Node Package Manager. A more complete example with advanced usage can be found here. We will reference it throughout this tutorial and encourage you to check it out after completing this tutorial.

Setup

You will need to have the following installed:

  1. A recent version of Node.js.
  2. A recent version of npm.

We recommend installing and using Node Version Manager, although entirely optional.

Step 1: Set up an App Search Engine

We need to set up an Engine and index our npm package data before we can build an application to search it. After signing up for an Account, create an engine called "node-modules", and run these commands in a shell. Note that you will need to provide your Swiftype App Search API Credentials for REACT_APP_HOST_IDENTIFIER and REACT_APP_API_KEY.

$ git clone https://github.com/swiftype/app-search-demo-react.git
$ cd app-search-demo-react
$ npm install
$ REACT_APP_HOST_IDENTIFIER={Your Host Identifier} \
REACT_APP_API_KEY={Your Private API key} \
npm run index-data

You should now have a Dashboard for your newly created Engine with ~9,500 npm packages indexed as documents. Take some time to look at the data if you want to get a better feel for the dataset.

Your new node-modules Engine!
Your new node-modules Engine!

Step 2: Create a New React App

create-react-app is a command line interface for creating React applications. Let's use it now to create a new application called "node-module-search" and run it.

$ npm install -g create-react-app
$ create-react-app node-module-search
$ cd node-module-search/
$ npm start

create-react-app creates a bare-bones React application that this tutorial will use as a starting point. Your new application will be running at http://localhost:3000/ and will refresh automatically as you update your code. Keep this link open to see your code changes take form.

Create two new files in your project root: App.css and .env. These will provide all the CSS needed for this tutorial and environment variables that will be used later. Make sure to use your actual Account key and API key or else you will not have permission to search over your engine.

// App.css

.App {
  text-align: center;
}

.App-header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.App-title {
  font-size: 1.5em;
}

.App-search-box {
  height: 40px;
  width: 500px;
  font-size: 1em;
  margin-top: 10px;
}
// .env

REACT_APP_HOST_IDENTIFIER=<Account key>
REACT_APP_SEARCH_KEY=<read-only Search API key>

Step 3: Add the App Search JavaScript Client

Searching with the App Search API is abstracted through the swiftype-app-search-javascript client. Install it as a dependency by executing the following command in terminal so we can use it in our code.

$ npm install --save swiftype-app-search-javascript

Here is an example on how to search over the "node-modules" engine. The query will be hard-coded as "foo" for now, and we will make it configurable later.

import * as SwiftypeAppSearch from "swiftype-app-search-javascript";

const client = SwiftypeAppSearch.createClient({
  accountHostIdentifier: <Account key>,
  searchKey: <read-only Search API key>,
  engineName: "node-modules"
});

const query = "foo";
const options = {};
client.search(query, options)
  .then(resultList => console.log(resultList)
  .catch(error => console.log(error))
options is used to configure your query or request other response formats from the API. You can read more about that in the Search Guide.


After executing a successful search request, the client will return an object of type ResultList, which has the following structure:

ResultList {
  "rawResults": [
    {
      "field": {
        "raw": "value"
      }
    }
  ],
  "rawInfo": {
    "meta": {
      "page": {...},
      "request_id": "feece605c3302bd7ba0ea538bab6fb8c",
      "warnings":[]
    }
  },
  "results": [
    ResultItem {
      "data": {
        "field": {
          "raw": "value"
        }
      }
    }
  ],
  "info": {
    "meta": {
      "page": {...},
      "request_id": "feece605c3302bd7ba0ea538bab6fb8c",
      "warnings":[]
    }
  }
}

Here is a quick explanation of these fields:

  • results is the array of ResultItems that match the query issued by the search client. You can retrieve field values from each ResultItem by using the getRaw method.
  • info contains all of the metadata for the query.
  • rawResults and rawInfo contain the raw JSON response.

Using the pattern above, add a hard-coded query for the term "node" that runs when the application mounts. To do this, add the following:

  1. A response property in state, that we'll use to store the response from the query.
  2. A performQuery method, which queries App Search using client.search and stores the response in our new response property.
  3. A componentDidMount life-cycle hook, which will run just once when the application loads.
  4. A Results section that displays the total numbers of results matching the user query and lists out details for each individual result.
// src/App.js

import React, { Component } from "react";
// Import classes from the Swiftype App Search Package
import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
import "./App.css";

// Instantiate a new instance of the Swiftype App Search JavaScript Client
const client = SwiftypeAppSearch.createClient({
  accountHostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
  searchKey: process.env.REACT_APP_SEARCH_KEY,
  engineName: "node-modules"
});

class App extends Component {
  state = {
    // A new state property, which holds the most recent query response
    response: null
  };

  componentDidMount() {
    /* Calling this in componentDidMount ensures that results are displayed on
    the screen when the app first loads */
    this.performQuery("node");
  }

  // Method to perform a query and store the response
  performQuery = queryString => {
    client.search(queryString, {}).then(
      response => {
        // Add this for now so you can inspect the full response
        console.log(response);
        this.setState({ response });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  };

  render() {
    const {response} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        {/* Show the total count of results for this query */}
        <h2>{response.info.meta.page.total_results} Results</h2>
        {/* Iterate over results, and show their Name and Description */}
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

export default App;

After you put that all together, head over to your browser (you might need to refresh) and you should see search results on your page, nice work!

Search results for the hard coded query string `"node"`
Your new node-modules Engine!

If this doesn't work for you or if you want to see the response from the App Search API, open up your browser's console and browse the log. There are two console.log calls in the code that will print out both the response from the API and any errors that may be returned from the server.

We hard-coded "node" as the query string, but users will probably want to search for other modules too. We can provide a search box so users can change the query string and find their new favorite package.

To do this, let's add a property to state called queryString and an update method for queryString called updateQuery. updateQuery will be an onChange handler that updates queryString in our state and triggers a new search every time a user changes the text in the search box.

// src/App.js

import React, { Component } from "react";
import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
import "./App.css";

const client = SwiftypeAppSearch.createClient({
  accountHostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
  searchKey: process.env.REACT_APP_SEARCH_KEY,
  engineName: "node-modules"
});

class App extends Component {
  state = {
    // A new state property, which tracks value from the search box
    queryString: "",
    response: null
  };

  componentDidMount() {
    // Remove hard-coded search for "node"
    this.performQuery(this.state.queryString);
  }

  // Handles the `onChange` event every time the user types in the search box.
  updateQuery = e => {
    const queryString = e.target.value;
    this.setState(
      {
        queryString // Save the user entered query string
      },
      () => {
        this.performQuery(queryString); // Trigger a new search
      }
    );
  };

  performQuery = queryString => {
    client.search(queryString, {}).then(
      response => {
        this.setState({
          response
        });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  };

  render() {
    const {response, queryString} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        {/* A search box, connected to our query string value and onChange
         handler */}
        <input
          className="App-search-box"
          type="text"
          placeholder="Enter a search term here"
          value={queryString}
          onChange={this.updateQuery}
        />
        <h2>{response.info.meta.page.total_results} Results</h2>
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

export default App;

You should now have an interactive search interface!

Your new search box in action
Your new node-modules Engine!

Step 6: Optimize your Search Box

Before we send you off into the world with your wonderful new search app, let's talk a little bit about the way we're handling these API requests.

Reducing API requests

The current implementation can result in a large number of requests. If the user is typing quickly, the majority of these results are ephemeral and attempting to update them as they are returned from the API in rapid succession will just look like screen stutter. They're simply unnecessary and are not very conscientious about rate limiting.

To fix it, we can just pull in a debounce function from lodash and wrap our performQuery method with debounce. debounce is a tool to delay calling a function until a certain amount of time has passed. This is perfect if we want to start limiting calls to performQuery in a reasonable way.

Install lodash:

npm install --save lodash

Wrap performQuery in debounce. 200ms seems to be a reasonable value for this.

// src/App.js

import React, { Component } from "react";
import { debounce } from "lodash"; // Import debounce
import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
import "./App.css";

const client = SwiftypeAppSearch.createClient({
  accountHostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
  searchKey: process.env.REACT_APP_SEARCH_KEY,
  engineName: "node-modules"
});

class App extends Component {
  state = {
    queryString: "",
    response: null
  };

  componentDidMount() {
    this.performQuery(this.state.queryString);
  }

  updateQuery = e => {
    const queryString = e.target.value;
    this.setState(
      {
        queryString
      },
      () => {
        this.performQuery(queryString);
      }
    );
  };

  // Wrapped performQuery in debounce
  performQuery = debounce(queryString => {
    client.search(queryString, {}).then(
      response => {
        this.setState({
          response
        });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  }, 200);

  render() {
    const {response, queryString} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        <input
          className="App-search-box"
          type="text"
          placeholder="Enter a search term here"
          value={queryString}
          onChange={this.updateQuery}
        />
        <h2>{response.info.meta.page.total_results} Results</h2>
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

export default App;

After you have this implemented, your search should now be a bit smoother. There will now be a delay of 200ms from the time the last keystroke in a quick sequence ends, and your request begins. However, we've also significantly reduced the amount of requests sent to your App Search Engine. So there is a tradeoff here between responsiveness and request reduction. You may need to adjust the 200ms value to meet your specific needs.

Worried about increasing your bundle size by pulling in lodash? Don't sweat it. Webpack's Tree Shaking will make sure the only thing that gets bundled into your build is debounce.

Dealing with async responses

Another thing we'll have to think about is this: when a lot of requests are firing very close to one another and are running in parallel, what happens if they return out of order? I think we have a race condition here.

For example, if I type the term "react" in the search box, two queries may be initiated, in parallel.

  1. query for "reac"
  2. query for "react"

If the query for "react" actually returns before the query for "reac", we could end up looking at the results for "reac", despite having typed "react" in the search box.

To solve this, let's assign every request a unique "id" from a sequence. We'll then keep track of which "id" the results in state belong to. Since the "id"s are sequential, if we receive a response from a request "id" that is LESS than the "id" we have in state, we'll know to ignore it, because it's outdated.

  1. Create a requestSequence property on the App component, which we'll use to generate sequential requestIds. This doesn't need to be part of our component's state, so we'll assign it as a simple property on the class.
  2. Add a property to state called lastCompleted. This will hold the requestId of the request that corresponds to the current response in state.
  3. Update performQuery to assign a requestId to each request, and record the requestId as lastCompleted when a request completes, as long as the request is not an old request.
// src/App.js

import React, { Component } from "react";
import { debounce } from "lodash";
import * as SwiftypeAppSearch from "swiftype-app-search-javascript";
import "./App.css";

const client = SwiftypeAppSearch.createClient({
  accountHostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
  searchKey: process.env.REACT_APP_SEARCH_KEY,
  engineName: "node-modules"
});

class App extends Component {
  // requestSequence is used to generate sequential requestIds
  requestSequence = 0;

  state = {
    lastCompleted: 0, // A new state property to track the last completed requestId
    queryString: "",
    response: null
  };

  componentDidMount() {
    this.performQuery(this.state.queryString);
  }

  updateQuery = e => {
    const queryString = e.target.value;
    this.setState(
      {
        queryString
      },
      () => {
        this.performQuery(queryString);
      }
    );
  };

  performQuery = debounce(queryString => {
    // Assigning a requestId tp each new request
    const requestId = ++this.requestSequence;

    client.search(queryString, {}).then(
      response => {
        // Early exit check to avoid rendering old responses
        if (requestId < this.state.lastCompleted) return;
        this.setState({
          // Storing the last completed requestId
          lastCompleted: requestId,
          response
        });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  }, 200);

  render() {
    const {response, queryString} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        <input
          className="App-search-box"
          type="text"
          placeholder="Enter a search term here"
          value={queryString}
          onChange={this.updateQuery}
        />
        <h2>{response.info.meta.page.total_results} Results</h2>
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

export default App;

Run your app, make sure it works, and you're done! You should have a really solid foundation here to create your own App Search app. This is a minimal solution, but it is a great foundation you can use to build out a more complete search solution like this.

What is next?

We've only scratched the surface of what Swiftype App Search can do. If you want to go above and beyond this tutorial, check out a topic below and try to integrate it into your newly created npm module search application.