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

Add useAnswersUtilities #46

Merged
merged 9 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dependencies": {
"@reduxjs/toolkit": "^1.6.2",
"@types/react": "^17.0.15",
"@yext/answers-headless": "^0.0.5",
"@yext/answers-headless": "^0.1.0-beta.0",
"typescript": "^4.3.5"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions sample-app/package-lock.json

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

2 changes: 1 addition & 1 deletion sample-app/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);

expect(getByText(/Stateful Core React Test/i)).toBeInTheDocument();
expect(getByText(/Answers Headless React Test/i)).toBeInTheDocument();
});
6 changes: 3 additions & 3 deletions sample-app/src/components/Facet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAnswersActions } from '@yext/answers-headless-react'
import { useAnswersUtilities } from '@yext/answers-headless-react'
import { DisplayableFacet, DisplayableFacetOption } from '@yext/answers-core';
import { useState } from 'react';
import useCollapse from 'react-collapsed';
Expand All @@ -21,15 +21,15 @@ interface FacetProps extends FacetTextConfig {

export default function Facet(props: FacetProps): JSX.Element {
const { facet, onToggle, searchable, collapsible, defaultExpanded, placeholderText, label } = props;
const answersActions = useAnswersActions();
const answersUtilities = useAnswersUtilities();
const hasSelectedFacet = !!facet.options.find(o => o.selected);
const [ filterValue, setFilterValue ] = useState('');
const { getCollapseProps, getToggleProps } = useCollapse({
defaultExpanded: hasSelectedFacet || defaultExpanded
});

const facetOptions = searchable
? answersActions.searchThroughFacet(facet, filterValue).options
? answersUtilities.searchThroughFacet(facet, filterValue).options
: facet.options;

return (
Expand Down
4 changes: 2 additions & 2 deletions sample-app/src/components/StaticFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import { Filter, CombinedFilter, FilterCombinator, Matcher } from '@yext/answers-core';
import { AnswersActionsContext } from '@yext/answers-headless-react';
import { AnswersHeadlessContext } from '@yext/answers-headless-react';

interface CheckBoxProps {
fieldId: string,
Expand Down Expand Up @@ -115,4 +115,4 @@ function formatOrFilters(filters: Filter[]) {
}
}

StaticFilters.contextType = AnswersActionsContext;
StaticFilters.contextType = AnswersHeadlessContext;
5 changes: 0 additions & 5 deletions src/AnswersActionsContext.ts

This file was deleted.

12 changes: 6 additions & 6 deletions src/AnswersActionsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactChild, ReactChildren } from 'react';
import { provideStatefulCore, StatefulCore } from '@yext/answers-headless';
import { provideAnswersHeadless, AnswersHeadless } from '@yext/answers-headless';
import { AnswersConfig } from '@yext/answers-core';
import { AnswersActionsContext } from './AnswersActionsContext';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';

interface Props extends AnswersConfig {
children?: ReactChildren | ReactChild | (ReactChildren | ReactChild)[],
Expand All @@ -10,11 +10,11 @@ interface Props extends AnswersConfig {

export function AnswersActionsProvider(props: Props): JSX.Element {
const { children, verticalKey, ...answersConfig } = props;
const statefulCore: StatefulCore = provideStatefulCore(answersConfig);
verticalKey && statefulCore.setVerticalKey(verticalKey);
const answers: AnswersHeadless = provideAnswersHeadless(answersConfig);
verticalKey && answers.setVerticalKey(verticalKey);
return (
<AnswersActionsContext.Provider value={statefulCore}>
<AnswersHeadlessContext.Provider value={answers}>
{children}
</AnswersActionsContext.Provider>
</AnswersHeadlessContext.Provider>
);
}
5 changes: 5 additions & 0 deletions src/AnswersHeadlessContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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);
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useAnswersActions, AnswersActions } from './useAnswersActions';
import { useAnswersState, StateSelector } from './useAnswersState';
import { useAnswersUtilities, AnswersUtilities } from './useAnswersUtilities';
import { subscribeToStateUpdates } from './subscribeToStateUpdates';
import { AnswersActionsProvider } from './AnswersActionsProvider';
import { AnswersActionsContext } from './AnswersActionsContext';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';

export {
AnswersActionsContext,
AnswersHeadlessContext,
subscribeToStateUpdates,
useAnswersActions,
useAnswersState,
useAnswersUtilities,
AnswersActionsProvider,
AnswersActions,
AnswersUtilities,
StateSelector
};
14 changes: 7 additions & 7 deletions src/subscribeToStateUpdates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ComponentType, useReducer, useEffect, useContext } from 'react';
import { State } from '@yext/answers-headless/lib/esm/models/state';
import { AnswersActionsContext } from './AnswersActionsContext';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';
import isShallowEqual from './utils/isShallowEqual';

type SubscriberGenerator = (WrappedComponent: ComponentType<any>) => (props: any) => JSX.Element;
Expand All @@ -20,22 +20,22 @@ export function subscribeToStateUpdates(
const generateSubscriberHOC: SubscriberGenerator = WrappedComponent => {
/**
* Keep manual track of the props mapped from state instead of storing
* it in the StatefulCoreSubscriber's state. This avoids react's batching
* it in the AnswersHeadlessSubscriber'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 stateful-core listener invocations.
* This can, in turn, result in extra answers-headless listener invocations.
*/
let previousPropsFromState = {};
return function StatefulCoreSubscriber(props: Record<string, unknown>) {
const statefulCore = useContext(AnswersActionsContext);
return function AnswersHeadlessSubscriber(props: Record<string, unknown>) {
const answers = useContext(AnswersHeadlessContext);
const [mergedProps, dispatch] = useReducer(() => {
return {
...props,
...previousPropsFromState
};
}, { ...props, ...mapStateToProps(statefulCore.state) });
}, { ...props, ...mapStateToProps(answers.state) });

useEffect(() => {
return statefulCore.addListener({
return answers.addListener({
valueAccessor: (state: State) => mapStateToProps(state),
callback: newPropsFromState => {
if (!isShallowEqual(previousPropsFromState, newPropsFromState)) {
Expand Down
8 changes: 4 additions & 4 deletions src/useAnswersActions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { StatefulCore } from '@yext/answers-headless';
import { AnswersHeadless } from '@yext/answers-headless';
import { useContext } from 'react';
import { AnswersActionsContext } from './AnswersActionsContext';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';

export type AnswersActions = StatefulCore;
export type AnswersActions = AnswersHeadless;

export function useAnswersActions(): AnswersActions {
return useContext(AnswersActionsContext);
return useContext(AnswersHeadlessContext);
}
18 changes: 9 additions & 9 deletions src/useAnswersState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext, useLayoutEffect, useRef, useState } from 'react';
import { State } from '@yext/answers-headless/lib/esm/models/state';
import { AnswersActionsContext } from './AnswersActionsContext';
import { AnswersHeadlessContext } from './AnswersHeadlessContext';

export type StateSelector<T> = (s: State) => T;

Expand All @@ -9,41 +9,41 @@ export type StateSelector<T> = (s: State) => T;
* Very similar to useSelector in react-redux.
*/
export function useAnswersState<T>(stateSelector: StateSelector<T>): T {
const statefulCore = useContext(AnswersActionsContext);
const answers = useContext(AnswersHeadlessContext);

// useRef stores values across renders without triggering additional ones
const storedStoreState = useRef<State>(statefulCore.state);
const storedStoreState = useRef<State>(answers.state);
const storedSelector = useRef<StateSelector<T>>(stateSelector);
const storedSelectedState = useRef<T>();
/**
* Guard execution of {@link stateSelector} for initializing storedSelectedState.
* Otherwise it's run an additional time every render, even when storedSelectedState is already initialized.
*/
if (storedSelectedState.current === undefined) {
storedSelectedState.current = stateSelector(statefulCore.state);
storedSelectedState.current = stateSelector(answers.state);
}

/**
* The currently selected state - this is the value returned by the hook.
* Tries to use {@link storedSelectedState} when possible.
*/
const selectedStateToReturn: T = (() => {
if (storedStoreState.current !== statefulCore.state || storedSelector.current !== stateSelector) {
return stateSelector(statefulCore.state);
if (storedStoreState.current !== answers.state || storedSelector.current !== stateSelector) {
return stateSelector(answers.state);
}
return storedSelectedState.current;
})();

const [, triggerRender] = useState<T>(storedSelectedState.current);
useLayoutEffect(() => {
storedSelector.current = stateSelector;
storedStoreState.current = statefulCore.state;
storedStoreState.current = answers.state;
storedSelectedState.current = selectedStateToReturn;
});

useLayoutEffect(() => {
let unsubscribed = false;
const unsubscribe = statefulCore.addListener({
const unsubscribe = answers.addListener({
valueAccessor: state => state,
callback: (state: State) => {
// prevent React state update on an unmounted component
Expand All @@ -61,7 +61,7 @@ export function useAnswersState<T>(stateSelector: StateSelector<T>): T {
unsubscribed = true;
unsubscribe();
};
}, [statefulCore]);
}, [answers]);

return selectedStateToReturn;
}
9 changes: 9 additions & 0 deletions src/useAnswersUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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;
}
Loading