Migrating From Headless v1 to v2

In this article

Headless v2 introduces changes that improve the library’s performance, make it easier to use, and let you scale projects by supporting additional use cases.

Important
The following are breaking changes from Headless v1 to v2

Renamed Variables

The following were renamed without changes to the underlying functionality.

  • RelevanceInspector.fetchFieldDescriptions renamed to RelevanceInspector.fetchFieldsDescription

    Headless Version 1

    <button onClick={() => controller.fetchFieldDescriptions()}>

    Headless Version 2

    <button onClick={() => controller.fetchFieldsDescription()}>

    Documentation: RelevanceInspector

  • RelevanceInspectorState.fieldDescriptions renamed to RelevanceInspectorState.fieldDescription

    Documentation: RelevanceInspector

  • ResultListState.searchUid renamed to ResultListState.searchResponseId

    Documentation: ResultList

  • searchAPIClient renamed to apiClient

    Documentation: SearchActions

Changes

CDN

https://static.cloud.coveo.com/headless/latest/* resources will no longer be updated (also, using this was not recommended). If you want to use the CDN rather than use npm to install Headless, specify a major version to follow updates, such as in https://static.cloud.coveo.com/headless/v2/*.

Headless Version 1

<script type="module" src="https://static.cloud.coveo.com/headless/latest/headless.esm.js"></script>

Or

<script type="module" src="https://static.cloud.coveo.com/headless/v1/headless.esm.js"></script>

Headless Version 2

<script type="module" src="https://static.cloud.coveo.com/headless/v2/headless.esm.js"></script>

FieldSuggestionsOptions

Except for the delimitingCharacter option, which had no effect and has been removed completely, the options exposed through FieldSuggestionsOptions have been removed and must now be set by passing a FacetOptions object.

Headless Version 1 Example

controller = buildFieldSuggestions(engine, {field: 'author', facetId: 'author-2'});

Headless Version 2 Example

controller = buildFieldSuggestions(engine, {facet: {field: 'author', facetId: 'author-2'}});

Similarly, the FieldSuggestionsFacetSearchOptions have been removed and you must use FacetSearchOptions instead. Because they expose the same attributes, this change should be transparent.

Headless Version 1 Example

controller = buildFieldSuggestions(engine, {field: 'author', facetId: 'author-2', facetSearch: {query: "herman"}});

Headless Version 2 Example

controller = buildFieldSuggestions(engine, {facet: {field: 'author', facetId: 'author-2', facetSearch: {query: "herman"}}});

CategoryFieldSuggestionsOptions

Except for the delimitingCharacter option, which had no effect and has been removed completely, the options exposed through CategoryFieldSuggestionsOptions have been removed and must now be set by passing a CategoryFacetOptions object.

Headless Version 1 Example

controller = buildCategoryFieldSuggestions(engine, {
  options: {
    field: 'geographicalhierarchy',
    facetId: 'geographicalhierarchy-3',
  }
});

Headless Version 2 Example

controller = buildCategoryFieldSuggestions(engine, {
  options: {
    facet: {
      field: 'geographicalhierarchy',
      facetId: 'geographicalhierarchy-3',
    }
  }
});

Similarly, the CategoryFieldSuggestionsFacetSearchOptions have been removed and you must use CategoryFacetSearchOptions instead. Because they expose the same attributes, this change should be transparent.

Headless Version 1 Example

controller = buildCategoryFieldSuggestions(engine, {
  options: {
    field: 'geographicalhierarchy',
    facetId: 'geographicalhierarchy-3',
    facetSearch: {query: "brazil"}
  }
});

Headless Version 2 Example

controller = buildCategoryFieldSuggestions(engine, {
  options: {
    facet: {
      field: 'geographicalhierarchy',
      facetId: 'geographicalhierarchy-3',
      facetSearch: {query: "brazil"}
    }
  }
});

UrlManager

The UrlManager now serializes facets with a dash instead of brackets.

For instance, f[my-facet]=valueA,valueB in v1 is serialized as f-my-facet=valueA,valueB in v2.

The UrlManager generally handles this part of the serialization and deserialization by itself, so the changes should be transparent, unless you were leveraging the URL directly somehow, for example by linking to your search page with preselected facets. In that case, adjust to the new serialization.

Headless Version 1 Example

<a href="https://mysite.com/search/#f[country]=canada"></a>

Headless Version 2 Example

<a href="https://mysite.com/search/#f-country=canada"></a>

deselectAllFacets has been removed. Instead, use deselectAllBreadcrumbs.

Headless Version 1 Example

import { engine } from './engine';
import { loadBreadcrumbActions } from '@coveo/headless';

const breadcrumbActionCreators = loadBreadcrumbActions(headlessEngine);
const action = breadcrumbActionCreators.deselectAllFacets();

headlessEngine.dispatch(action);

Headless Version 2 Example

import { engine } from './engine';
import { loadBreadcrumbActions } from '@coveo/headless';

const breadcrumbActionCreators = loadBreadcrumbActions(headlessEngine);
const action = breadcrumbActionCreators.deselectAllBreadcrumbs();

headlessEngine.dispatch(action);

DateRangeOptions

The useLocalTime option was removed.

In v2, if you don’t want to use the local time, use SearchConfigurationOptions.timezone instead. By default, the user local time is used. To change it, specify the target timezone value when initializing your search interface.

Headless Version 1 Example

const controller = buildDateFacet(engine, {
  options: {
    field: 'created',
    generateAutomaticRanges: false,
    currentValues: [
      buildDateRange({
        start: {period: 'past', unit: 'day', amount: 1},
        end: {period: 'now'},
        useLocalTime: false,
      }),
      buildDateRange({
        start: {period: 'past', unit: 'week', amount: 1},
        end: {period: 'now'},
        useLocalTime: false,
      }),
    ],
  },
});

Headless Version 2 Example

import { buildSearchEngine } from '@coveo/headless';

export const headlessEngine = buildSearchEngine({
  configuration: {
    // ...
    search: {
        timezone: 'Etc/UTC'
    }
  }
});

SmartSnippetRelatedQuestion

The documentId property has been removed. Use questionAnswerId instead.

In particular, you now need to use questionAnswerId rather than documentId when using the following methods:

The same applies when using the following actions:

Headless Version 1 Example

// ...

export const SmartSnippetQuestionsList: FunctionComponent<
  SmartSnippetQuestionsListProps
> = (props) => {
  const {controller} = props;
  const [state, setState] = useState(controller.state);

  const {questions} = state;

  // ...

  return (
    <div style={{textAlign: 'left'}}>
      People also ask:
      <dl>
        {questions.map((question) => {
          return (
            <>
              <dt>{question.question}</dt>
              <dd>
                <!--- ... --->
                <button
                  style={{display: question.expanded ? 'none' : 'block'}}
                  onClick={() => controller.expand(question.documentId)}
                >
                  Show answer
                </button>
                <button
                  style={{display: question.expanded ? 'block' : 'none'}}
                  onClick={() => controller.collapse(question.documentId)}
                >
                  Hide answer
                </button>
              </dd>
            </>
          );
        })}
      </dl>
    </div>
  );
};

Headless Version 2 Example

// ...

export const SmartSnippetQuestionsList: FunctionComponent<
  SmartSnippetQuestionsListProps
> = (props) => {
  const {controller} = props;
  const [state, setState] = useState(controller.state);

  const {questions} = state;

  // ...

  return (
    <div style={{textAlign: 'left'}}>
      People also ask:
      <dl>
        {questions.map((question) => {
          return (
            <>
              <dt>{question.question}</dt>
              <dd>
                <!--- ... --->
                  <button
                    style={{display: question.expanded ? 'none' : 'block'}}
                    onClick={() =>
                      this.controller.expand(question.questionAnswerId)}
                  >
                    Show answer
                  </button>
                  <button
                    style={{display: question.expanded ? 'block' : 'none'}}
                    onClick={() =>
                      this.controller.collapse(question.questionAnswerId)}
                  >
                    Hide answer
                  </button>
              </dd>
            </>
          );
        })}
      </dl>
    </div>
  );
};

logOpenSmartSnippetSource

This action no longer requires the source parameter to be specified. Headless infers it automatically.

Headless Version 1

// ...
engine.dispatch(logOpenSmartSnippetSource(result));

Headless Version 2

// ...
engine.dispatch(logOpenSmartSnippetSource());

NotifyTrigger

The controller state notification property has been replaced by the notifications property, which is an array that could contain multiple notifications.

Headless Version 1 Example

const notify = () => {
  if (state.notification) {
    alert('Notification: ' + state.notification);
  }
};

Headless Version 2 Example

const notify = () => {
  state.notifications.forEach((notification) => {
    alert('Notification: ' + notification);
  });
};

ExecuteTrigger

engine.state.triggers.executed has been replaced by engine.state.triggers.executions, which is an array that could contain multiple executions. Also, functionName and params have been removed from engine.state.triggers and moved to the executions in the engine.state.triggers.executions array.

Headless Version 1 Example

// ...

export class ExecuteTrigger extends Component<{}, ExecuteTriggerState> {
  static contextType = AppContext;
  context!: ContextType<typeof AppContext>;

  private controller!: HeadlessExecuteTrigger;
  private unsubscribe: Unsubscribe = () => {};

  // ...

  componentDidMount() {
    this.controller = buildExecuteTrigger(this.context.engine!);
    this.unsubscribe = this.controller.subscribe(() => this.executeFunction());
  }

  private executeFunction = () => {
    const {functionName, params} = this.controller.state;
    // ...
  };

  // ...
}

Headless Version 2 Example

// ...

export class ExecuteTrigger extends Component<{}, ExecuteTriggerState> {
  static contextType = AppContext;
  context!: ContextType<typeof AppContext>;

  private controller!: HeadlessExecuteTrigger;
  private unsubscribe: Unsubscribe = () => {};

  // ...

  componentDidMount() {
    this.controller = buildExecuteTrigger(this.context.engine!);
    this.unsubscribe = this.controller.subscribe(() =>
      this.controller.state.executions.forEach((execution) =>
        this.executeFunction(execution)
      )
    );
  }

  private executeFunction = (execution: FunctionExecutionTrigger) => {
    const {functionName, params} = execution;
    // ...
  };

  // ...
}

querySuggest

engine.state.querySuggest.q has been removed. Instead, use the more general engine.state.querySet, which is also a set of queries (strings) available using the id of the target search box (see QuerySetActions).

Headless Version 1 Example

lastQuery = engine.state.querySuggest.q
// ...

Headless Version 2 Example

this.headlessSearchBox = buildSearchBox(headlessEngine, {
  options: {
    id: '123',
    // ...
  }
  // ...
})
// ...
lastQuery = engine.state.querySet['123']
// ...

facetSet and NumericFacetSet

The type of engine.state.facetSet has changed from { [facetId: string]: FacetRequest } to { [facetId: string]: FacetSlice }.

The type of engine.state.numericFacetSet has changed from { [facetId: string]: NumericFacetRequest } to { [facetId: string]: NumericFacetSlice }.

facetRequest, which used to be accessible in facetSet[<FACET_ID>], where <FACET_ID> is the ID of the target facet, is now accessible in facetSet[<FACET_ID>].request.

Headless Version 1

lastRequest = engine.state.facetSet[this.headlessFacet.state.facetId]
// ...

Headless Version 2

lastRequest = engine.state.facetSet[this.headlessFacet.state.facetId].request
// ...

hasBreadcrumbs, which used to be accessible in facetRequest, is now accessible in FacetSlice. Since the type of engine.state.facetSet has changed accordingly, the behavior should remain unchanged. I.e., the following are equivalent:

Headless Version 1

engine.state.facetSet[<FACET_ID>].hasBreadcrumbs

Headless Version 2

engine.state.facetSet[<FACET_ID>].hasBreadcrumbs

Internal Controllers Removal

The following internal controllers have been removed:

  • InteractiveResultCore

  • CoreQuerySummary

  • CoreResultList

  • CoreFacetManager

  • CoreStatus

  • DocumentSuggestion (from the Case Assist engine)

  • QuickviewCore

Redirection

The engine.state.redirection reducer (not documented) and the related actions (not documented either) have been removed. If you were using them previously, see StandaloneSearchBox and StandaloneSearchBoxSetActions instead.

FacetOptions

The delimitingCharacter option was removed. It wasn’t doing anything, since delimiting characters are only relevant in category facets.

Result Template Manager

Registering an invalid template now throws an error rather than just logging it. Previously, the invalid template wouldn’t work, but it wouldn’t throw an error. So, this change will only throw an error if you already had a result template issue.

The error could look like the following examples:

Each result template conditions should be a function that takes a result as an argument and returns a boolean
The following properties are invalid:
        content: value is required.
        conditions: value is required.

clientOrigin

The clientOrigin has been changed for some Search API requests (see Modify Requests and Responses). This change should be transparent, unless you were modifying the clientOrigin yourself.

Request clientOrigin value

Product listing

commerceApiFetch

Case assist

caseAssistApiFetch

Insight

insightApiFetch

Search Analytics Actions

All of the log* search action creators now return dispatchable actions of type PreparableAnalyticsAction rather than AsyncThunkAction<{ analyticsType: AnalyticsType.Search; }, void, AsyncThunkAnalyticsOptions<StateNeededBySearchAnalyticsProvider>>. Search Actions now require this new type of dispatchable analytics actions to trigger.

The change should be transparent, because only the types used are different, not the use of the functions involved themselves.