Search Headless| Hitchhikers Platform

Yext has developed a series of tools designed to make UI development with the Search API easier. This includes a series of components that can be added to any React application.

Under the hood, the components are powered by Search Headless React; a library that manages data returned by the Search API in a global SearchHeadless instance. This is implemented with Redux, “a predictable state container for JavaScript apps”. There’s a lot of boilerplate code that comes with setting up Redux for calling APIs based on user-triggered events and modifying the state accordingly.

Search Headless React removes the complexity of having to write all the boilerplate for Redux + Search API. It also comes with a series of custom hooks for rendering Search state data in the UI and changing the Search state based on events triggered by the components.

This article introduces Search Headless React, the hooks that come with it, and how the Search UI React components use headless.

Check out the appendix section if you want to learn more about Redux.

Search Headless React: State and Hooks

Search Headless React

If you were to develop a Redux state management solution for the Search API you have to do something resembling the following steps:

  1. Design the shape of your Redux store that will need to accommodate the search state of your application.
  2. Write API client functions for calling the Search APIs that you want to use in your app which could include search queries, autocomplete, and filter search.
  3. Create the action functions that will call the API client functions before dispatching actions.
  4. Create the reducer functions responsible for modifying and returning a new copy of the redux store

Fortunately, Search Headless React takes care of all of this setup for you. It also offers it’s own provider component and hooks that are conceptually similar to useSelector and useDispatch.

SearchHeadlessProvider and provideHeadless

To use the Search Headless React hooks, components need to be children of a <SearchHeadlessProvider />. The provider requires a prop called searcher which is an instance of the SearchHeadless state. To instantiate a new SearchHeadless instance, you need to use the provideHeadless hook.

The searchHeadless requires a HeadlessConfig argument with has a few mandatory fields:

  • apiKey → the unique Search API key for your Yext account
  • experienceKey → the unique key for a specific Search experience
  • locale → locale (language) of the Search experience

Under the hood, these props are for authenticating the API calls made by Search Headless to the Search API. Any nested component within the <SearchHeadlessProvider /> can use the Search Headless hooks.

import {
  SearchHeadlessProvider,
  provideHeadless,
} from "@yext/search-headless-react";
// ...other imports

const searcher = provideHeadless({
  apiKey: "YOUR_API_KEY",
  experienceKey: "YOUR_EXPERIENCE_KEY",
  locale: "en",
});

ReactDOM.render(
  <React.StrictMode>
    <SearchHeadlessProvider searcher={searcher}>
      <App />
    </SearchHeadlessProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

useSearchState

Similar to React Redux’s useSelector hook, the useSearchState hook is designed to extract data from the search store for usage by the component. The Search store is passed as an argument and a specified part of the store is returned. Just like the useSelector hook, useSearchState will run when the component is rendered or when a Search action is dispatched.

This example shows how you could use the hook to read a value from the state and render it in a component:

import { useSearchState } from "@yext/search-headless-react";

export default function MostRecentSearch() {
  const mostRecentSearch = useSearchState(
    (state) => state.query.mostRecentSearch
  );
  return <div>Showing results for {mostRecentSearch}</div>;
}

You can find all the Search properties exposed by the state object here.

useSearchActions

The useSearchActions hook allows for your components to dispatch actions like searching, autocomplete, and applying facets. Just like useDispatch, you can use functions from this hook to trigger search actions from events triggered by your components.

The example below is an input element that calls the useSearchActions setQuery function to update the search query in the store each time the user enters a key stroke and calls the executeUniversalQuery function when the enter key is pressed.

import { useSearchActions } from "@yext/search-headless-react";
import { ChangeEvent, KeyboardEvent, useCallback } from "react";

function SearchBar() {
  const search = useSearchActions();
  const handleTyping = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      search.setQuery(e.target.value);
    },
    [search]
  );

  const handleKeyDown = useCallback(
    (evt: KeyboardEvent<HTMLInputElement>) => {
      if (evt.key === "Enter") {
        search.executeUniversalQuery();
      }
    },
    [search]
  );

  return <input onChange={handleTyping} onKeyDown={handleKeyDown} />;
}

You can find all the Search actions functions here.

Developing Components with Headless React

Search Headless React has everything you need to create your own custom UI search components.

Building some things like search bars with autocomplete are complex and time consuming. You need to consider:

  • Calling the executeAutocompleteSearch action on every keystroke
  • Re-rendering the autocomplete dropdown each time the autocomplete state changes
  • Event handling for clicks on the autocomplete results, search icon, and X icon

You can to develop this by yourself, but we recommend importing the <SearchBar /> component from @yext/search-ui-react. Then, import the <VerticalResults /> component to render the vertical search results.

Take a look at the source code for <VerticalResults/> and <SearchBar /> to see how they are using the Search Headless React hooks.

Appendix

A Brief Introduction to Redux

When developing React applications, your components often have their own state for managing data that will be rendered in the UI. As your app gets bigger, components often need to share data between one another. However, passing data up and down the component tree can become unwieldy and have unintended side-effects. Introducing Redux to your application can solve this problem by serving as a global state that is shared by all the components in your application.

The state is a plain object that contains the global state of the application that the developer defines. You might have a button in a component that triggers a search API call somewhere in your application. When the response from the API is returned, rather than passing the search results across components, the component that renders the search results can pull them from the state.

But how did the search button trigger a search API call in the first place? It called a function that fetched data from the API and then dispatched an action. The Redux documentation says that you can think of dispatching actions as “triggering an event…something happened, and we want the store to know about it”.

Reducer functions are responsible for listening for these events and adjusting the state if need be. According to the Redux documentation, reducers functions listen to 1 of 2 things:

  1. If the reducer cares about the action, make a copy of the state, update the copy with new values, and return it.
  2. Return the existing state unchanged.

The Redux documentation has some examples of what Redux looks like in React applications.

React + Redux

React Redux is the official React bindings library for Redux. It provides a series of components and hooks so that your React components can interact with the Redux store.

Provider

In order for your components to get access to the Redux store, the Provider component is used at the top level of the application.

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import { App } from './App'
import createStore from './createReduxStore'

const store = createStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  document.getElementById("root")
);

useSelector

The useSelector hook is used to extract data from the Redux store for usage by a component. It takes the entire Redux store as an argument and returns the piece of state that you want. It runs when the component is first rendered and when any actions are dispatched so that it’s always up-to-date with the latest changes to the store.

import React from "react";
import { useSelector } from "react-redux";

export const CounterComponent = () => {
  const counter = useSelector((state) => state.counter);
  return <div>{counter}</div>;
};

useDispatch

The useDispatch hook is used to dispatch actions that are picked up by reducers to change the state. You can use this hook in your component to trigger state changes from user actions in your component.

import React from "react";
import { useDispatch } from "react-redux";

export const CounterComponent = ({ value }) => {
  const counter = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  return (
    <div>
      <span>{counter}</span>
      <button onClick={() => dispatch({ type: "increment-counter" })}>
        Increment counter
      </button>
    </div>
  );
};
Feedback