Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Answers -> Search 1: Customer facing renaming #141

Merged
merged 16 commits into from
Jul 14, 2022
88 changes: 44 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,114 +1,114 @@
# 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.

<div>
<a href="https://npmjs.org/package/@yext/answers-headless-react">
<img src="https://img.shields.io/npm/v/@yext/answers-headless-react" alt="NPM version"/>
<a href="https://npmjs.org/package/@yext/search-headless-react">
<img src="https://img.shields.io/npm/v/@yext/search-headless-react" alt="NPM version"/>
</a>
<a href="./LICENSE">
<img src="https://img.shields.io/badge/License-BSD%203--Clause-blue.svg" alt="License"/>
</a>
<a href='https://coveralls.io/github/yext/answers-headless-react?branch=main'>
<img src='https://coveralls.io/repos/github/yext/answers-headless-react/badge.svg?branch=main' alt='Coverage Status' />
<a href='https://coveralls.io/github/yext/search-headless-react?branch=main'>
<img src='https://coveralls.io/repos/github/yext/search-headless-react/badge.svg?branch=main' alt='Coverage Status' />
</a>
</div>
<br>

## 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 `<AnswersHeadlessProvider />` component, which instantiates an AnswersHeadless instance and makes it available to the rest of your app.
Search Headless React includes an `<SearchHeadlessProvider />` 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 (
<AnswersHeadlessProvider
<SearchHeadlessProvider
apiKey='your api key'
experienceKey='your experience key'
locale='en'
>
{/* Add components that use Answers as children */}
{/* Add components that use Search as children */}
<SearchBar/>
<MostRecentSearch/>
<UniversalResults/>
</AnswersHeadlessProvider>
</SearchHeadlessProvider>
);
}
```

## 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);
default function MostRecentSearch() {
const mostRecentSearch = useSearchState(state => state.query.mostRecentSearch);
return <div>Showing results for {mostRecentSearch}</div>;
}
```

## 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<HTMLInputElement>) => {
answers.setQuery(e.target.value);
}, [answers]);
search.setQuery(e.target.value);
}, [search]);

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

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

## `AnswersHeadlessContext`
### Class Components
## `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 the `subscribeToStateUpdates` HOC to 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 })
Expand All @@ -121,15 +121,15 @@ export default class Searcher extends Component {
}

render() {
const answers: AnswersHeadless = this.context;
const search: SearchHeadless = this.context;
return (
<div>
<p>Query: {this.state.query}</p>
<input
onChange={evt => answers.setQuery(evt.target.value)}
onChange={evt => search.setQuery(evt.target.value)}
onKeyDown={evt => {
if (evt.key === 'Enter') {
answers.executeUniversalQuery();
search.executeUniversalQuery();
}
}}
/>
Expand All @@ -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;
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@yext/answers-headless-react",
"name": "@yext/search-headless-react",
"version": "1.2.0",
"description": "The official React UI Bindings layer for Answers Headless",
"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",
Expand Down Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ 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<AnswersHeadless>({} as AnswersHeadless);
export const SearchHeadlessContext = createContext<AnswersHeadless>({} as AnswersHeadless);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PropsWithChildren } from 'react';
import { provideAnswersHeadless, AnswersHeadless, HeadlessConfig } from '@yext/answers-headless';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';
import { SearchHeadlessContext } from './SearchHeadlessContext';
import acquireSessionId from './utils/acquireSessionId';
import packageJson from '../package.json';

Expand All @@ -11,7 +11,7 @@ type Props = HeadlessConfig & {
sessionTrackingEnabled?: boolean
};

export function AnswersHeadlessProvider(props: PropsWithChildren<Props>): JSX.Element {
export function SearchHeadlessProvider(props: PropsWithChildren<Props>): JSX.Element {
const { children, verticalKey, sessionTrackingEnabled=true, ...answersConfig } = props;
const additionalHttpHeaders = {
'Client-SDK': {
Expand All @@ -27,8 +27,8 @@ export function AnswersHeadlessProvider(props: PropsWithChildren<Props>): JSX.El
sessionId && answers.setSessionId(sessionId);
}
return (
<AnswersHeadlessContext.Provider value={answers}>
<SearchHeadlessContext.Provider value={answers}>
{children}
</AnswersHeadlessContext.Provider>
</SearchHeadlessContext.Provider>
);
}
}
51 changes: 51 additions & 0 deletions src/deprecated.ts
Original file line number Diff line number Diff line change
@@ -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/answers-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<T>(stateSelector: StateSelector<T>): T {
return useSearchState(stateSelector);
}

/**
* @deprecated useAnswersUtilities has been deprecated and repalced by useSearchUtilities
*/
export function useAnswersUtilities(): SearchUtilities { return useSearchUtilities(); }

/**
* @deprecated AnswersHeadlessProvider has been deprecated and replaced by SearfchHeadlessProvider
*/
export function AnswersHeadlessProvider(props: PropsWithChildren<Props>): JSX.Element {
return SearchHeadlessProvider(props);
}
27 changes: 14 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 './deprecated';
export {
AnswersHeadlessContext,
SearchHeadlessContext,
subscribeToStateUpdates,
useAnswersActions,
useAnswersState,
useAnswersUtilities,
AnswersHeadlessProvider,
AnswersActions,
AnswersUtilities,
useSearchActions,
useSearchState,
useSearchUtilities,
SearchHeadlessProvider,
SearchActions,
SearchUtilities,
StateSelector
};
};
14 changes: 7 additions & 7 deletions src/subscribeToStateUpdates.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
/* 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 { SearchHeadlessContext } from './SearchHeadlessContext';
import isShallowEqual from './utils/isShallowEqual';

type SubscriberGenerator = (WrappedComponent: ComponentType<any>) => (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 component, use `SearchHeadlessContext` directly to dispatch actions and receive state updates.
* For functional component, use `useSearchActions` and `useSearchState` instead.
*/
export function subscribeToStateUpdates(
mapStateToProps: (s: State) => Record<string, unknown>
): SubscriberGenerator {
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<string, unknown>) {
const answers = useContext(AnswersHeadlessContext);
const answers = useContext(SearchHeadlessContext);
const [mergedProps, dispatch] = useReducer(() => {
return {
...props,
Expand Down
Loading