InstantSearch / React / V6 / Guides / Injecting content between hits

Inject Third-Party Content

This version of React InstantSearch has been deprecated in favor of the latest version of React InstantSearch.

Sometimes you may need to inject content that’s not in Algolia. This could be static data or even data coming from a third-party API.

In those cases, you can provide the data to inject using slotComponent and omit getHits.

Screenshot showing editorial content injected between regular hits

Requirements

React InstantSearch exposes a connector API that lets you reuse existing logic and plug your own. With it, you can build a custom React InstantSearch widget to mix regular Algolia results with injected content. Make sure you’re familiar with this concept before continuing to the next section.

Inject static data

To inject static data, such as JSON from a file, or hard coded information, you can provide it as a component to slotComponent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react';
import {
  InstantSearch,
  Configure,
  Index,
  SearchBox,
} from 'react-instantsearch-dom';

import { InjectedHits } from './InjectedHits';

function App() {
  return (
    <InstantSearch searchClient={searchClient} indexName="ingredients">
      <Configure hitsPerPage={8} />
      <SearchBox />
      <InjectedHits
        slots={() => [
          {
            injectAt: 3,
            slotComponent: BannerHit,
          },
        ]}
        hitComponent={IngredientHit}
      />
    </InstantSearch>
  );
}

function BannerHit() {
  return (
    <a href="https://example.org/">
      <img src="./path/to/image.jpg" alt="My banner" />
    </a>
  )
}

// ...

Inject remote data

If your data comes from a remote source, like a third-party API, you can fetch it from the component directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React from 'react';
import {
  InstantSearch,
  Configure,
  SearchBox,
} from 'react-instantsearch-dom';

import { InjectedHits } from './InjectedHits';

function App() {
  return (
    <InstantSearch searchClient={searchClient} indexName="ingredients">
      <Configure hitsPerPage={8} />
      <SearchBox />
      <InjectedHits
        slots={() => [
          {
            injectAt: 3,
            slotComponent: BannerHit,
          },
        ]}
        hitComponent={IngredientHit}
      />
    </InstantSearch>
  );
}

function BannerHit({ resultsByIndex }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (resultsByIndex.products?.query) {
      fetch(`https://example.org/api/q=${resultsByIndex.products?.query}`)
        .then((r) => r.json())
        .then(setData);
    }
  }, [resultsByIndex.products]);

  if (!data) {
    return <div>Loading...</div>;
  }

  return (
    <a href={data.url}>
      <img src={data.image} alt={data.name} />
    </a>
  );
}

// ...

The widget re-renders once the data comes in. This can result in layout shift, where the banner pushes hits that the user was looking at, or about to click.

To avoid it, make sure to always provide fallback content of the same size as the target one.

Avoid layout shift

Third-party content is fetched on render (or during render if you’re using Suspense), meaning your Algolia hits always come first. To avoid layout shift, make sure to always provide fallback content with the same size as the target one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...

function BannerHit({ resultsByIndex }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (resultsByIndex.products?.query) {
      fetch(`https://example.org/api/q=${resultsByIndex.products?.query}`)
        .then((r) => r.json())
        .then(setData);
    }
  }, [resultsByIndex.products]);

  if (!data) {
    // Render a fallback right away at the right dimensions
    // to avoid Cumulative Layout Shift.
    return <div style={{ width: '100%', height: '200px' }}>Loading...</div>;
  }

  return (
    <a href={data.url} style={{ width: '100%', height: '200px' }}>
      <img src={data.image} alt={data.name} />
    </a>
  );
}

Handle network errors

Network requests can fail: the API can be down, the user can block requests to advertising services with a browser extension, the network can be slow or unstable. Not being able to render injected hits shouldn’t have an impact on the search experience. Errors should be handled locally, in their component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...

function BannerHit({ resultsByIndex }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (resultsByIndex.products?.query) {
      fetch(`https://example.org/api/q=${resultsByIndex.products?.query}`)
        .then((r) => r.json())
        .then((data) => {
          setData(data);
          setError(null);
        })
        .catch((err) => {
          setError(err);
          setData(null);
        });
    }
  }, [resultsByIndex.products]);

  if (error) {
    return null;
  }

  // ...
}

To avoid layout shift when you get an error, you can provide alternative content instead of removing the block. For example, you could display static data or data coming from Algolia.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...

function BannerHit({ hit }) {
  // ...

  if (error) {
    return (
      <div style={{ width: '100%', height: '200px' }}>
        {/* Some error fallback content */}
      </div>
    );
  }

  return (
    <a href={data.url} style={{ width: '100%', height: '200px' }}>
      <img src={data.image} alt={data.name} />
    </a>
  );
}
Did you find this page helpful?