UI Libraries / Autocomplete / Reshaping Sources

When you’re browsing a website that fetches content from a database, the UI isn’t fully representative of how that data is structured on the back-end. This allows more human-friendly experiences and interactions. A search UI doesn’t have to be a one-to-one mapping with your search engine either.

The Autocomplete Reshape API lets you transform static, dynamic and asynchronous sources into friendlier search UIs.

Here are some examples of what you can do with the Reshape API:

  • Apply a limit of items for a group of sources
  • Remove duplicates in a group of sources
  • Group sources by an attribute
  • Sort sources

In this guide, you’ll learn how to use multiple reshape functions to remove duplicates and create a shared limit for your autocomplete suggestions.

Creating a reshape function

To remove duplicates between sources, you can start by creating a uniqBy function. You can later apply this function to your Recent Searches and Query Suggestions sources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import R from 'ramda';

function uniqBy(predicate) {
  return function runUniqBy(...rawSources) {
    const sources = rawSources.flat().filter(Boolean);

    return sources.map(source => {
      const items = R.uniqBy(
        item => predicate({ source, item }),
        source.getItems()
      );
      return {
        ...source,
        getItems() {
          return items;
        },
      };
    });
  };
}

Autocomplete supports conditional sources, meaning that sources sometimes exist and sometimes don’t. This can happen when you display a source on empty query, but not when the user starts typing (for example, with popular queries). Your function can support this by removing any nonexistent sources with filter(Boolean).

Using the reshape function

Now that you’ve created the uniqBy function, you can specify the implementation for your sources and use it in the reshape option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const removeDuplicates = uniqBy(({ source, item }) =>
  source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
);

autocomplete({
  container: '#autocomplete',
  plugins: [recentSearchesPlugin, querySuggestionsPlugin],
  reshape({ sourcesBySourceId }) {
    const {
      recentSearchesPlugin,
      querySuggestionsPlugin,
      ...rest
    } = sourcesBySourceId;

    return [
      removeDuplicates(recentSearchesPlugin, querySuggestionsPlugin),
      Object.values(rest),
    ];
  },
});

The reshape option provides three arguments:

  • sources: the resolved sources provided by getSources
  • sourcesBySourceId: the resolved sources grouped by sourceIds
  • state: the Autocomplete state

The uniqBy function uses item.query as identifier for the Query Suggestions plugin and item.label for the Recent Searches plugin. These are the shape of the items that the plugins return. If you use custom sources, you can use a switch statement based on source.sourceId.

Sources are retrieved from sourcesBySourceId with their sourceId via object destructuring. You can return sources that you didn’t reshape with Object.values(rest).

Combining reshape functions

You can create a function that balances results from two different sources. It can be used with uniqBy so that there are no duplicates and there’s a fixed number of combined items.

Combine suggestions

Notice how there are always four results showing, and that the number of Query Suggestions varies depending on the number of recent searches.

Here’s a limit function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function limit(value) {
  return function runLimit(...rawSources) {
    const sources = rawSources.flat().filter(Boolean);
    const limitPerSource = Math.ceil(value / sources.length);
    let sharedLimitRemaining = value;

    return sources.map((source, index) => {
      const isLastSource = index === sources.length - 1;
      const sourceLimit = isLastSource
        ? sharedLimitRemaining
        : Math.min(limitPerSource, sharedLimitRemaining);
      const items = source.getItems().slice(0, sourceLimit);
      sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0);

      return {
        ...source,
        getItems() {
          return items;
        },
      };
    });
  };
}

You can then combine the uniqBy and limit functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const removeDuplicates = uniqBy(({ source, item }) =>
  source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
);
const limitSuggestions = limit(4);

autocomplete({
  container: '#autocomplete',
  plugins: [recentSearchesPlugin, querySuggestionsPlugin],
  reshape({ sourcesBySourceId }) {
    const {
      recentSearchesPlugin,
      querySuggestionsPlugin,
      ...rest
    } = sourcesBySourceId;

    return [
      limitSuggestions(
        removeDuplicates(recentSearchesPlugin, querySuggestionsPlugin)
      ),
      Object.values(rest),
    ];
  },
});

Piping reshape functions

Nested function calls can become cumbersome, so you can use functional libraries like Ramda to pipe reshape functions instead.

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
import { pipe } from 'ramda';

const combineSuggestions = pipe(
  uniqBy(({ source, item }) =>
    source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
  ),
  limit(4)
);

autocomplete({
  container: '#autocomplete',
  plugins: [recentSearchesPlugin, querySuggestionsPlugin],
  reshape({ sourcesBySourceId }) {
    const {
      recentSearchesPlugin,
      querySuggestionsPlugin,
      ...rest
    } = sourcesBySourceId;

    return [
      combineSuggestions(recentSearchesPlugin, querySuggestionsPlugin),
      Object.values(rest),
    ];
  },
});

For your reshape functions to support pipe, you need to make sure your sources are one level deep, for example using Array.flat.

You can find an example that groups products by category with the Reshape API in the sandboxes.

Did you find this page helpful?