diff --git a/LICENSE b/LICENSE index 6e1dc0b4..1e04e29a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Contains information from the language-subtag-registry JSON Database (https://github.com/mattcg/language-subtag-registry/tree/master/data/json) which is made available under the ODC Attribution License (https://github.com/mattcg/language-subtag-registry/blob/master/LICENSE.md). -The Answers Headless React files listed in this repository are licensed under the below license. All other features and products are subject to +The Search Headless React files listed in this repository are licensed under the below license. All other features and products are subject to separate agreements and certain functionality requires paid subscriptions to Yext products. BSD 3-Clause License diff --git a/README.md b/README.md index a17a7107..8ccabd09 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# Answers Headless React +# Search Headless React -Answers Headless React is the official React UI Bindings layer for [Answers Headless](https://www.npmjs.com/package/@yext/answers-headless). +Search Headless React is the official React UI Bindings layer for [Search Headless](https://www.npmjs.com/package/@yext/search-headless). Written in 100% TypeScript.
- - NPM version + + NPM version License - - Coverage Status + + Coverage Status

@@ -20,95 +20,95 @@ Written in 100% TypeScript. ## Installation ```shell -npm install @yext/answers-headless-react +npm install @yext/search-headless-react ``` -## Getting Started - `AnswersHeadlessProvider` +## Getting Started - `SearchHeadlessProvider` -Answers Headless React includes an `` component, which instantiates an AnswersHeadless instance and makes it available to the rest of your app. +Search Headless React includes an `` component, which instantiates an SearchHeadless instance and makes it available to the rest of your app. ```tsx -import { AnswersHeadlessProvider } from '@yext/answers-headless-react'; +import { SearchHeadlessProvider } from '@yext/search-headless-react'; import SearchBar from './SearchBar'; import MostRecentSearch from './MostRecentSearch'; import UniversalResults from './UniversalResults'; function MyApp() { return ( - - {/* Add components that use Answers as children */} + {/* Add components that use Search as children */} - + ); } ``` -## Respond to State Updates with `useAnswersState` +## Respond to State Updates with `useSearchState` -`useAnswersState` reads a value from the `AnswersHeadless` state and subscribes to updates. +`useSearchState` reads a value from the `SearchHeadless` state and subscribes to updates. ```tsx -import { useAnswersState } from '@yext/answers-headless-react'; +import { useSearchState } from '@yext/search-headless-react'; export default function MostRecentSearch() { - const mostRecentSearch = useAnswersState(state => state.query.mostRecentSearch); + const mostRecentSearch = useSearchState(state => state.query.mostRecentSearch); return
Showing results for {mostRecentSearch}
; } ``` -## Dispatch Actions with `useAnswersActions` +## Dispatch Actions with `useSearchActions` -`useAnswersActions` allows you to dispatch actions using the `AnswersHeadless` instance. +`useSearchActions` allows you to dispatch actions using the `SearchHeadless` instance. These include performing searches, getting autocomplete suggestions, and adding filters. -For a full list of capabilities see [the answers-headless docs](https://www.npmjs.com/package/@yext/answers-headless). +For a full list of capabilities see [the search-headless docs](https://www.npmjs.com/package/@yext/search-headless). ```tsx -import { useAnswersActions } from '@yext/answers-headless-react'; +import { useSearchActions } from '@yext/search-headless-react'; import { ChangeEvent, KeyboardEvent, useCallback } from 'react'; function SearchBar() { - const answers = useAnswersActions(); + const search = useSearchActions(); const handleTyping = useCallback((e: ChangeEvent) => { - answers.setQuery(e.target.value); - }, [answers]); + search.setQuery(e.target.value); + }, [search]); const handleKeyDown = useCallback((evt: KeyboardEvent) => { if (evt.key === 'Enter' ) { - answers.executeUniversalQuery(); + search.executeUniversalQuery(); } - }, [answers]); + }, [search]); return ; } ``` -## `AnswersHeadlessContext` +## `SearchHeadlessContext` ### Class Components -For users that want to use class components instead of functional components, you can use the `AnswersHeadlessContext` directly to dispatch actions and receive updates from state. +For users that want to use class components instead of functional components, you can use the `SearchHeadlessContext` directly to dispatch actions and receive updates from state. -As an example, here is our simple SearchBar again, rewritten as a class using `AnswersHeadlessContext`. +As an example, here is our simple SearchBar again, rewritten as a class using `SearchHeadlessContext`. ```tsx -import { AnswersHeadlessContext, AnswersHeadless, State } from '@yext/answers-headless-react'; +import { SearchHeadlessContext, SearchHeadless, State } from '@yext/search-headless-react'; import { Component } from 'react'; export default class Searcher extends Component { - static contextType = AnswersHeadlessContext; + static contextType = SearchHeadlessContext; unsubscribeQueryListener: any; state = { query: "" }; componentDidMount() { - const answers: AnswersHeadless = this.context; - this.unsubscribeQueryListener = answers.addListener({ + const search: SearchHeadless = this.context; + this.unsubscribeQueryListener = search.addListener({ valueAccessor: (state: State) => state.query.mostRecentSearch, callback: newPropsFromState => { this.setState({ query: newPropsFromState }) @@ -121,15 +121,15 @@ export default class Searcher extends Component { } render() { - const answers: AnswersHeadless = this.context; + const search: SearchHeadless = this.context; return (

Query: {this.state.query}

answers.setQuery(evt.target.value)} + onChange={evt => search.setQuery(evt.target.value)} onKeyDown={evt => { if (evt.key === 'Enter') { - answers.executeUniversalQuery(); + search.executeUniversalQuery(); } }} /> @@ -139,14 +139,14 @@ export default class Searcher extends Component { } ``` -## `useAnswersUtilities` +## `useSearchUtilities` -We offer a `useAnswersUtilities` convenience hook for accessing `AnswersHeadless.utilities`, which offers a number of stateless utility methods. -The `answersUtilities` and `answersUtilitiesFromActions` variables below are equivalent. +We offer a `useSearchUtilities` convenience hook for accessing `SearchHeadless.utilities`, which offers a number of stateless utility methods. +The `searchUtilities` and `searchUtilitiesFromActions` variables below are equivalent. -For class components, you can access `AnswersUtilities` through `AnswersHeadlessContext`. +For class components, you can access `SearchUtilities` through `SearchHeadlessContext`. ```ts -const answersUtilities = useAnswersUtilities(); -const answersUtilitiesFromActions = useAnswersActions().utilities; +const searchUtilities = useSearchUtilities(); +const searchUtilitiesFromActions = useSearchActions().utilities; ``` diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index ddaebc26..4790f3f4 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -62,11 +62,11 @@ SOFTWARE. The following NPM package may be included in this product: - - @yext/answers-core@1.7.0 + - @yext/search-core@1.8.0 This package contains the following license and notice below: -The Answers Core files listed in this repository are licensed under the below license.  All other features and products are subject to separate agreements +The Search Core files listed in this repository are licensed under the below license.  All other features and products are subject to separate agreements and certain functionality requires paid subscriptions to Yext products. Contains information from the language-subtag-registry JSON Database (https://github.com/mattcg/language-subtag-registry/tree/master/data/json) @@ -106,11 +106,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following NPM package may be included in this product: - - @yext/answers-headless@1.2.0 + - @yext/search-headless@1.3.0 This package contains the following license and notice below: -The Answers Headless files listed in this repository are licensed under the below license.  All other features and products are subject to separate agreements +The Search Headless files listed in this repository are licensed under the below license.  All other features and products are subject to separate agreements and certain functionality requires paid subscriptions to Yext products. Contains information from the language-subtag-registry JSON Database (https://github.com/mattcg/language-subtag-registry/tree/master/data/json) @@ -118,7 +118,7 @@ which is made available under the ODC Attribution License (https://github.com/ma BSD 3-Clause License -Copyright (c) 2021, Yext +Copyright (c) 2022, Yext All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/package-lock.json b/package-lock.json index b43f4106..52b71bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { - "name": "@yext/answers-headless-react", - "version": "1.2.0", + "name": "@yext/search-headless-react", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@yext/answers-headless-react", - "version": "1.2.0", + "name": "@yext/search-headless-react", + "version": "1.3.0", "license": "BSD-3-Clause", "dependencies": { - "@yext/answers-headless": "^1.2.0", + "@yext/search-headless": "^1.3.0", "use-sync-external-store": "^1.1.0" }, "devDependencies": { @@ -4214,29 +4214,6 @@ "node": ">=10.0.0" } }, - "node_modules/@yext/answers-core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@yext/answers-core/-/answers-core-1.7.0.tgz", - "integrity": "sha512-wOG9TAj3fVUXVJvS5HO+S54O6Ehyd32FfRUtZ4j1kE8ES5T9bhJ8xALos4NC+gCuXIMmV1kHXzsKLafeuSrpAg==", - "dependencies": { - "@babel/runtime-corejs3": "^7.12.5", - "cross-fetch": "^3.1.5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@yext/answers-headless": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@yext/answers-headless/-/answers-headless-1.2.0.tgz", - "integrity": "sha512-i/TFOfM8izoYu7hala6JRqh6vdhf6gCcIy9Ggh5YIwqkqlUJNQKEyr+O/ZE5tdvOddDizFxTxZvd5yizQC/cSw==", - "dependencies": { - "@reduxjs/toolkit": "^1.8.1", - "@yext/answers-core": "^1.7.0", - "js-levenshtein": "^1.1.6", - "lodash": "^4.17.21" - } - }, "node_modules/@yext/eslint-config-slapshot": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@yext/eslint-config-slapshot/-/eslint-config-slapshot-0.4.0.tgz", @@ -4266,6 +4243,29 @@ "eslint-module-utils": "^2.7.1" } }, + "node_modules/@yext/search-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-1.8.0.tgz", + "integrity": "sha512-t7rg9JZ9K6pIt2nBZ1O+xhzG+zMFVAu9Vgwh8cTey8NA7878cQ6n6BauTwimVdbrNGHq3D2dPUno/kVtKAgPiQ==", + "dependencies": { + "@babel/runtime-corejs3": "^7.12.5", + "cross-fetch": "^3.1.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@yext/search-headless": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-1.3.0.tgz", + "integrity": "sha512-T5Gvb7MObWTtBPiTIs2WuZgxF44Ro89qiGabl2S1PcN4eQBV3nVVVgFNN1cqCN9K2m4gcm1U9ctAbJyme83vRA==", + "dependencies": { + "@reduxjs/toolkit": "^1.8.1", + "@yext/search-core": "^1.8.0", + "js-levenshtein": "^1.1.6", + "lodash": "^4.17.21" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -15686,26 +15686,6 @@ "version": "0.7.5", "dev": true }, - "@yext/answers-core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@yext/answers-core/-/answers-core-1.7.0.tgz", - "integrity": "sha512-wOG9TAj3fVUXVJvS5HO+S54O6Ehyd32FfRUtZ4j1kE8ES5T9bhJ8xALos4NC+gCuXIMmV1kHXzsKLafeuSrpAg==", - "requires": { - "@babel/runtime-corejs3": "^7.12.5", - "cross-fetch": "^3.1.5" - } - }, - "@yext/answers-headless": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@yext/answers-headless/-/answers-headless-1.2.0.tgz", - "integrity": "sha512-i/TFOfM8izoYu7hala6JRqh6vdhf6gCcIy9Ggh5YIwqkqlUJNQKEyr+O/ZE5tdvOddDizFxTxZvd5yizQC/cSw==", - "requires": { - "@reduxjs/toolkit": "^1.8.1", - "@yext/answers-core": "^1.7.0", - "js-levenshtein": "^1.1.6", - "lodash": "^4.17.21" - } - }, "@yext/eslint-config-slapshot": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@yext/eslint-config-slapshot/-/eslint-config-slapshot-0.4.0.tgz", @@ -15725,6 +15705,26 @@ "eslint-module-utils": "^2.7.1" } }, + "@yext/search-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@yext/search-core/-/search-core-1.8.0.tgz", + "integrity": "sha512-t7rg9JZ9K6pIt2nBZ1O+xhzG+zMFVAu9Vgwh8cTey8NA7878cQ6n6BauTwimVdbrNGHq3D2dPUno/kVtKAgPiQ==", + "requires": { + "@babel/runtime-corejs3": "^7.12.5", + "cross-fetch": "^3.1.5" + } + }, + "@yext/search-headless": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@yext/search-headless/-/search-headless-1.3.0.tgz", + "integrity": "sha512-T5Gvb7MObWTtBPiTIs2WuZgxF44Ro89qiGabl2S1PcN4eQBV3nVVVgFNN1cqCN9K2m4gcm1U9ctAbJyme83vRA==", + "requires": { + "@reduxjs/toolkit": "^1.8.1", + "@yext/search-core": "^1.8.0", + "js-levenshtein": "^1.1.6", + "lodash": "^4.17.21" + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", diff --git a/package.json b/package.json index cf273a74..5b1007f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@yext/answers-headless-react", - "version": "1.2.0", - "description": "The official React UI Bindings layer for Answers Headless", + "name": "@yext/search-headless-react", + "version": "1.3.0", + "description": "The official React UI Bindings layer for Search Headless", "main": "./lib/esm/src/index.js", "license": "BSD-3-Clause", "types": "./lib/esm/src/index.d.ts", @@ -32,7 +32,7 @@ "generate-notices": "generate-license-file --input package.json --output THIRD-PARTY-NOTICES --overwrite" }, "dependencies": { - "@yext/answers-headless": "^1.2.0", + "@yext/search-headless": "^1.3.0", "use-sync-external-store": "^1.1.0" }, "devDependencies": { @@ -84,7 +84,7 @@ "/tests/setup/setup-env.ts" ], "transformIgnorePatterns": [ - "/node_modules/(?!@yext/answers-headless)" + "/node_modules/(?!@yext/search-headless)" ], "transform": { "^.+\\.tsx?$": "ts-jest", @@ -106,6 +106,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/yext/answers-headless-react.git" + "url": "https://github.com/yext/search-headless-react.git" } } diff --git a/src/AnswersHeadlessContext.ts b/src/AnswersHeadlessContext.ts deleted file mode 100644 index c2d3504d..00000000 --- a/src/AnswersHeadlessContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AnswersHeadless } from '@yext/answers-headless'; -import { createContext } from 'react'; - -// The default is empty because we don't know the user's config yet -export const AnswersHeadlessContext = createContext({} as AnswersHeadless); diff --git a/src/SearchHeadlessContext.ts b/src/SearchHeadlessContext.ts new file mode 100644 index 00000000..0eed077c --- /dev/null +++ b/src/SearchHeadlessContext.ts @@ -0,0 +1,5 @@ +import { SearchHeadless } from '@yext/search-headless'; +import { createContext } from 'react'; + +// The default is empty because we don't know the user's config yet +export const SearchHeadlessContext = createContext({} as SearchHeadless); \ No newline at end of file diff --git a/src/AnswersHeadlessProvider.tsx b/src/SearchHeadlessProvider.tsx similarity index 62% rename from src/AnswersHeadlessProvider.tsx rename to src/SearchHeadlessProvider.tsx index 1a33d740..6348446f 100644 --- a/src/AnswersHeadlessProvider.tsx +++ b/src/SearchHeadlessProvider.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from 'react'; -import { provideAnswersHeadless, AnswersHeadless, HeadlessConfig } from '@yext/answers-headless'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; +import { provideHeadless, SearchHeadless, HeadlessConfig } from '@yext/search-headless'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; import acquireSessionId from './utils/acquireSessionId'; import packageJson from '../package.json'; @@ -11,14 +11,14 @@ type Props = HeadlessConfig & { sessionTrackingEnabled?: boolean }; -export function AnswersHeadlessProvider(props: PropsWithChildren): JSX.Element { +export function SearchHeadlessProvider(props: PropsWithChildren): JSX.Element { const { children, verticalKey, sessionTrackingEnabled=true, ...answersConfig } = props; const additionalHttpHeaders = { 'Client-SDK': { ANSWERS_HEADLESS_REACT: version } }; - const answers: AnswersHeadless = provideAnswersHeadless(answersConfig, additionalHttpHeaders); + const answers: SearchHeadless = provideHeadless(answersConfig, additionalHttpHeaders); verticalKey && answers.setVertical(verticalKey); answers.setSessionTrackingEnabled(sessionTrackingEnabled); @@ -27,8 +27,8 @@ export function AnswersHeadlessProvider(props: PropsWithChildren): JSX.El sessionId && answers.setSessionId(sessionId); } return ( - + {children} - + ); -} +} \ No newline at end of file diff --git a/src/deprecated.ts b/src/deprecated.ts new file mode 100644 index 00000000..0661235c --- /dev/null +++ b/src/deprecated.ts @@ -0,0 +1,51 @@ +import { useSearchActions, SearchActions } from './useSearchActions'; +import { StateSelector, useSearchState } from './useSearchState'; +import { useSearchUtilities, SearchUtilities } from './useSearchUtilities'; +import { SearchHeadlessProvider } from './SearchHeadlessProvider'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; +import { PropsWithChildren } from 'react'; +import { HeadlessConfig } from '@yext/search-headless'; + +type Props = HeadlessConfig & { + verticalKey?: string, + sessionTrackingEnabled?: boolean +}; + +/** + * @deprecated AnswersHeadlessContext has been deprecated and replaced by SearchHeadlessContext + */ +export const AnswersHeadlessContext = SearchHeadlessContext; + +/** + * @deprecated AnswersActions has been deprecated and replaced by SearchActions + */ +export type AnswersActions = SearchActions; + +/** + * @deprecated AnswersUtilities has been deprecated and replaced by SearchUtilities + */ +export type AnswersUtilities = SearchUtilities; + +/** + * @deprecated useAnswersActions has been deprecated and replaced by useSearchActions + */ +export function useAnswersActions(): SearchActions { return useSearchActions(); } + +/** + * @deprecated useAnswersState has been deprecated and replaced by useSearchState + */ +export function useAnswersState(stateSelector: StateSelector): T { + return useSearchState(stateSelector); +} + +/** + * @deprecated useAnswersUtilities has been deprecated and replaced by useSearchUtilities + */ +export function useAnswersUtilities(): SearchUtilities { return useSearchUtilities(); } + +/** + * @deprecated AnswersHeadlessProvider has been deprecated and replaced by SearchHeadlessProvider + */ +export function AnswersHeadlessProvider(props: PropsWithChildren): JSX.Element { + return SearchHeadlessProvider(props); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c7e32c3a..bbc95ac1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,20 @@ -import { useAnswersActions, AnswersActions } from './useAnswersActions'; -import { useAnswersState, StateSelector } from './useAnswersState'; -import { useAnswersUtilities, AnswersUtilities } from './useAnswersUtilities'; +import { useSearchActions, SearchActions } from './useSearchActions'; +import { useSearchState, StateSelector } from './useSearchState'; +import { useSearchUtilities, SearchUtilities } from './useSearchUtilities'; import { subscribeToStateUpdates } from './subscribeToStateUpdates'; -import { AnswersHeadlessProvider } from './AnswersHeadlessProvider'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; +import { SearchHeadlessProvider } from './SearchHeadlessProvider'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; -export * from '@yext/answers-headless'; +export * from '@yext/search-headless'; +export * from './deprecated'; export { - AnswersHeadlessContext, + SearchHeadlessContext, subscribeToStateUpdates, - useAnswersActions, - useAnswersState, - useAnswersUtilities, - AnswersHeadlessProvider, - AnswersActions, - AnswersUtilities, + useSearchActions, + useSearchState, + useSearchUtilities, + SearchHeadlessProvider, + SearchActions, + SearchUtilities, StateSelector -}; +}; \ No newline at end of file diff --git a/src/subscribeToStateUpdates.tsx b/src/subscribeToStateUpdates.tsx index e4c86dc9..5681e953 100644 --- a/src/subscribeToStateUpdates.tsx +++ b/src/subscribeToStateUpdates.tsx @@ -1,18 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentType, useReducer, useEffect, useContext } from 'react'; -import { State } from '@yext/answers-headless'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; +import { State } from '@yext/search-headless'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; import isShallowEqual from './utils/isShallowEqual'; type SubscriberGenerator = (WrappedComponent: ComponentType) => (props: any) => JSX.Element; /** * Generates a HOC that updates a given Component's props based on the current - * answers-headless state and a given mapping function. + * search-headless state and a given mapping function. * * @deprecated - * For class component, use `AnswersHeadlessContext` directly to dispatch actions and receive state updates. - * For functional component, use `useAnswersActions` and `useAnswersState` instead. + * For class components, use `SearchHeadlessContext` directly to dispatch actions and receive state updates. + * For functional components, use `useSearchActions` and `useSearchState` instead. */ export function subscribeToStateUpdates( mapStateToProps: (s: State) => Record @@ -20,13 +20,13 @@ export function subscribeToStateUpdates( const generateSubscriberHOC: SubscriberGenerator = WrappedComponent => { /** * Keep manual track of the props mapped from state instead of storing - * it in the AnswersHeadlessSubscriber's state. This avoids react's batching + * it in the SearchHeadlessSubscriber's state. This avoids react's batching * of state updates, which can result in mappedState not updating immediately. - * This can, in turn, result in extra answers-headless listener invocations. + * This can, in turn, result in extra search-headless listener invocations. */ let previousPropsFromState = {}; return function AnswersHeadlessSubscriber(props: Record) { - const answers = useContext(AnswersHeadlessContext); + const answers = useContext(SearchHeadlessContext); const [mergedProps, dispatch] = useReducer(() => { return { ...props, diff --git a/src/useAnswersActions.ts b/src/useAnswersActions.ts deleted file mode 100644 index 3283dbe5..00000000 --- a/src/useAnswersActions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AnswersHeadless } from '@yext/answers-headless'; -import { useContext } from 'react'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; - -export type AnswersActions = AnswersHeadless; - -export function useAnswersActions(): AnswersActions { - const answersHeadless = useContext(AnswersHeadlessContext); - if (answersHeadless.state === undefined) { - throw new Error('Attempted to call useAnswersActions() outside of AnswersHeadlessProvider.' - + ' Please ensure that \'useAnswersActions()\' is called within an AnswersHeadlessProvider component.'); - } - return answersHeadless; -} \ No newline at end of file diff --git a/src/useAnswersUtilities.ts b/src/useAnswersUtilities.ts deleted file mode 100644 index f1a77f5b..00000000 --- a/src/useAnswersUtilities.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AnswersHeadless } from '@yext/answers-headless'; -import { useContext } from 'react'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; - -export type AnswersUtilities = AnswersHeadless['utilities']; - -export function useAnswersUtilities(): AnswersUtilities { - return useContext(AnswersHeadlessContext).utilities; -} \ No newline at end of file diff --git a/src/useSearchActions.ts b/src/useSearchActions.ts new file mode 100644 index 00000000..303c43de --- /dev/null +++ b/src/useSearchActions.ts @@ -0,0 +1,14 @@ +import { SearchHeadless } from '@yext/search-headless'; +import { useContext } from 'react'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; + +export type SearchActions = SearchHeadless; + +export function useSearchActions(): SearchActions { + const searchHeadless = useContext(SearchHeadlessContext); + if (searchHeadless.state === undefined) { + throw new Error('Attempted to call useSearchActions() outside of SearchHeadlessProvider.' + + ' Please ensure that \'useSearchActions()\' is called within an SearchHeadlessProvider component.'); + } + return searchHeadless; +} \ No newline at end of file diff --git a/src/useAnswersState.tsx b/src/useSearchState.tsx similarity index 58% rename from src/useAnswersState.tsx rename to src/useSearchState.tsx index 05678541..a0501fa1 100644 --- a/src/useAnswersState.tsx +++ b/src/useSearchState.tsx @@ -1,24 +1,24 @@ import { useCallback, useContext, useEffect, useRef } from 'react'; -import { State } from '@yext/answers-headless'; -import { AnswersHeadlessContext } from './AnswersHeadlessContext'; +import { State } from '@yext/search-headless'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'; export type StateSelector = (s: State) => T; /** - * Returns the Answers State returned by the map function. + * Returns the Search State returned by the map function. * Uses "use-sync-external-store/shim" to handle reading * and subscribing from external store in React version * pre-18 and 18. */ -export function useAnswersState(stateSelector: StateSelector): T { - const answers = useContext(AnswersHeadlessContext); - if (answers.state === undefined) { - throw new Error('Attempted to call useAnswersState() outside of AnswersHeadlessProvider.' - + ' Please ensure that \'useAnswersState()\' is called within an AnswersHeadlessProvider component.'); +export function useSearchState(stateSelector: StateSelector): T { + const search = useContext(SearchHeadlessContext); + if (search.state === undefined) { + throw new Error('Attempted to call useSearchState() outside of SearchHeadlessProvider.' + + ' Please ensure that \'useSearchState()\' is called within an SearchHeadlessProvider component.'); } - const getSnapshot = useCallback(() => answers.state, [answers.state]); + const getSnapshot = useCallback(() => search.state, [search.state]); const isMountedRef = useRef(false); useEffect(() => { isMountedRef.current = true; @@ -28,7 +28,7 @@ export function useAnswersState(stateSelector: StateSelector): T { }, []); const subscribe = useCallback(cb => - answers.addListener({ + search.addListener({ valueAccessor: state => state, callback: () => { // prevent React state update on an unmounted component @@ -37,7 +37,7 @@ export function useAnswersState(stateSelector: StateSelector): T { } cb(); } - }), [answers]); + }), [search]); const selectedState = useSyncExternalStoreWithSelector( subscribe, @@ -46,4 +46,4 @@ export function useAnswersState(stateSelector: StateSelector): T { stateSelector ); return selectedState; -} +} \ No newline at end of file diff --git a/src/useSearchUtilities.ts b/src/useSearchUtilities.ts new file mode 100644 index 00000000..eb9d51ad --- /dev/null +++ b/src/useSearchUtilities.ts @@ -0,0 +1,9 @@ +import { SearchHeadless } from '@yext/search-headless'; +import { useContext } from 'react'; +import { SearchHeadlessContext } from './SearchHeadlessContext'; + +export type SearchUtilities = SearchHeadless['utilities']; + +export function useSearchUtilities(): SearchUtilities { + return useContext(SearchHeadlessContext).utilities; +} \ No newline at end of file diff --git a/tests/AnswersHeadlessProvider.test.tsx b/tests/AnswersHeadlessProvider.test.tsx deleted file mode 100644 index 56cd9070..00000000 --- a/tests/AnswersHeadlessProvider.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { AnswersHeadlessProvider, SandboxEndpoints } from '../src'; -import { render } from '@testing-library/react'; -import { provideAnswersHeadless } from '@yext/answers-headless'; - -jest.mock('@yext/answers-headless', () => ({ - provideAnswersHeadless: jest.fn(() => ({ - setSessionTrackingEnabled: jest.fn(), - setSessionId: jest.fn() - })) -})); - -it('correctly passes through an answers config with sandbox endpoints', () => { - const config = { - apiKey: '', - experienceKey: '', - locale: 'en', - endpoints: SandboxEndpoints - }; - - render(); - expect(provideAnswersHeadless).toHaveBeenCalledTimes(1); - expect(provideAnswersHeadless).toHaveBeenCalledWith(config, expect.anything()); -}); \ No newline at end of file diff --git a/tests/SearchHeadlessProvider.test.tsx b/tests/SearchHeadlessProvider.test.tsx new file mode 100644 index 00000000..234a5f1a --- /dev/null +++ b/tests/SearchHeadlessProvider.test.tsx @@ -0,0 +1,23 @@ +import { SearchHeadlessProvider, SandboxEndpoints } from '../src'; +import { render } from '@testing-library/react'; +import { provideHeadless } from '@yext/search-headless'; + +jest.mock('@yext/search-headless', () => ({ + provideHeadless: jest.fn(() => ({ + setSessionTrackingEnabled: jest.fn(), + setSessionId: jest.fn() + })) +})); + +it('correctly passes through an answers config with sandbox endpoints', () => { + const config = { + apiKey: '', + experienceKey: '', + locale: 'en', + endpoints: SandboxEndpoints + }; + + render(); + expect(provideHeadless).toHaveBeenCalledTimes(1); + expect(provideHeadless).toHaveBeenCalledWith(config, expect.anything()); +}); \ No newline at end of file diff --git a/tests/useAnswersActions.test.tsx b/tests/useAnswersActions.test.tsx deleted file mode 100644 index 87b0ee37..00000000 --- a/tests/useAnswersActions.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useAnswersActions } from '../src'; -import { render } from '@testing-library/react'; - -it('invoke useAnswersActions outside of AnswersHeadlessProvider', () => { - function Test(): JSX.Element { - const answersActions = useAnswersActions(); - answersActions.setQuery(''); - return
Test
; - } - jest.spyOn(global.console, 'error').mockImplementation(); - const expectedError = new Error( - 'Attempted to call useAnswersActions() outside of AnswersHeadlessProvider.' + - ' Please ensure that \'useAnswersActions()\' is called within an AnswersHeadlessProvider component.'); - expect(() => render()).toThrow(expectedError); - jest.clearAllMocks(); -}); \ No newline at end of file diff --git a/tests/useSearchActions.test.tsx b/tests/useSearchActions.test.tsx new file mode 100644 index 00000000..bc5b604d --- /dev/null +++ b/tests/useSearchActions.test.tsx @@ -0,0 +1,16 @@ +import { useSearchActions } from '../src'; +import { render } from '@testing-library/react'; + +it('invoke useSearchActions outside of SearchHeadlessProvider', () => { + function Test(): JSX.Element { + const searchActions = useSearchActions(); + searchActions.setQuery(''); + return
Test
; + } + jest.spyOn(global.console, 'error').mockImplementation(); + const expectedError = new Error( + 'Attempted to call useSearchActions() outside of SearchHeadlessProvider.' + + ' Please ensure that \'useSearchActions()\' is called within an SearchHeadlessProvider component.'); + expect(() => render()).toThrow(expectedError); + jest.clearAllMocks(); +}); \ No newline at end of file diff --git a/tests/useAnswersState.test.tsx b/tests/useSearchState.test.tsx similarity index 81% rename from tests/useAnswersState.test.tsx rename to tests/useSearchState.test.tsx index 5af5b341..5e82d49e 100644 --- a/tests/useAnswersState.test.tsx +++ b/tests/useSearchState.test.tsx @@ -1,19 +1,19 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { provideAnswersHeadless, Result, State } from '@yext/answers-headless'; +import { provideHeadless, Result, State } from '@yext/search-headless'; import { useCallback, useReducer } from 'react'; -import { AnswersHeadlessContext, useAnswersActions, useAnswersState } from '../src'; +import { SearchHeadlessContext, useSearchActions, useSearchState } from '../src'; import { renderToString } from 'react-dom/server'; -it('invoke useAnswersState outside of AnswersHeadlessProvider', () => { +it('invoke useSearchState outside of SearchHeadlessProvider', () => { function Test(): JSX.Element { - const query = useAnswersState(state => state.query.input); + const query = useSearchState(state => state.query.input); return
{query}
; } jest.spyOn(global.console, 'error').mockImplementation(); const expectedError = new Error( - 'Attempted to call useAnswersState() outside of AnswersHeadlessProvider.' + - ' Please ensure that \'useAnswersState()\' is called within an AnswersHeadlessProvider component.'); + 'Attempted to call useSearchState() outside of SearchHeadlessProvider.' + + ' Please ensure that \'useSearchState()\' is called within an SearchHeadlessProvider component.'); expect(() => render()).toThrow(expectedError); jest.clearAllMocks(); }); @@ -24,14 +24,14 @@ it('Retrieves state snapshot during server side rendering and hydration process' answers.setVertical('anotherFakeKey'); }); function Test(): JSX.Element { - const verticalKey = useAnswersState(state => state.vertical.verticalKey); + const verticalKey = useSearchState(state => state.vertical.verticalKey); return ; } function App(): JSX.Element { return ( - + - + ); } const renderOnServer = () => renderToString(); @@ -54,8 +54,8 @@ it('does not perform extra renders/listener registrations for nested components' const childStateUpdates: string[] = []; let pendingVerticalQuery; function Test() { - const actions = useAnswersActions(); - const results = useAnswersState(state => { + const actions = useSearchActions(); + const results = useSearchState(state => { return state?.vertical?.results; }) || []; parentStateUpdates.push(results); @@ -73,7 +73,7 @@ it('does not perform extra renders/listener registrations for nested components' } function Child({ results }: { results: Result[] }) { - const queryId = useAnswersState(state => { + const queryId = useSearchState(state => { return state.query.queryId; }) || ''; childStateUpdates.push(queryId); @@ -92,9 +92,9 @@ it('does not perform extra renders/listener registrations for nested components' expect(parentStateUpdates).toHaveLength(0); expect(childStateUpdates).toHaveLength(0); render( - + - + ); expect(addListenerSpy).toHaveBeenCalledTimes(1); expect(parentStateUpdates).toHaveLength(1); @@ -120,20 +120,20 @@ it('does not perform extra renders/listener registrations for nested components' it('does not trigger render on unmounted component', async () => { const consoleSpy = jest.spyOn(console, 'error'); function ParentComponent() { - const results = useAnswersState(state => state.universal?.verticals) || []; + const results = useSearchState(state => state.universal?.verticals) || []; return
{results.map((_, index) => )}
; } function ChildComponent() { - useAnswersState(state => state); + useSearchState(state => state); return
child component
; } const answers = createAnswersHeadless(); render( - + - + ); act(() => answers.setQuery('resultsWithFilter')); await act( async () => { await answers.executeUniversalQuery(); }); @@ -150,7 +150,7 @@ describe('uses the most recent selector', () => { let selector = () => 'initial selector'; function Test() { - const selectedState: string = useAnswersState(selector); + const selectedState: string = useSearchState(selector); const [, triggerRender] = useReducer(s => s + 1, 0); return ( @@ -163,9 +163,9 @@ describe('uses the most recent selector', () => { const answers = createAnswersHeadless(); render( - + - + ); expect(screen.getByTestId('selected-state')).toHaveTextContent('initial selector'); @@ -182,7 +182,7 @@ describe('uses the most recent selector', () => { const stateUpdates: (string | undefined | number)[] = []; function Test() { - const selectedState: string | undefined | number = useAnswersState(selector); + const selectedState: string | undefined | number = useSearchState(selector); const [, triggerRender] = useReducer(s => s + 1, 0); stateUpdates.push(selectedState); @@ -198,9 +198,9 @@ describe('uses the most recent selector', () => { answers.setQuery('initial value'); expect(stateUpdates).toHaveLength(0); render( - + - + ); expect(stateUpdates).toEqual(['initial value']); @@ -220,7 +220,7 @@ describe('uses the most recent selector', () => { }); function createAnswersHeadless() { - return provideAnswersHeadless({ + return provideHeadless({ apiKey: 'fake api key', experienceKey: 'fake exp key', locale: 'en', diff --git a/tsconfig.base.json b/tsconfig.base.json index 53cc339e..a9831fa1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@yext/answers-headless-react": ["src"] + "@yext/search-headless-react": ["src"] }, "target": "es2015", "esModuleInterop": true,