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

Rename StatefulCore to AnswersHeadless #45

Merged
merged 4 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The following NPM packages may be included in this product:

- @yext/answers-core@1.3.2
- @yext/answers-headless-react@0.3.0-beta.0
- @yext/answers-headless@0.0.5
- @yext/answers-headless@0.1.0-beta.0

These packages each contain the following license and notice below:

Expand Down
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();
});
2 changes: 1 addition & 1 deletion sample-app/src/components/Facet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function Facet(props: FacetProps): JSX.Element {
});

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

return (
Expand Down
4 changes: 2 additions & 2 deletions src/AnswersActionsContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StatefulCore } from '@yext/answers-headless';
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 AnswersActionsContext = createContext<StatefulCore>({} as StatefulCore);
export const AnswersActionsContext = createContext<AnswersHeadless>({} as AnswersHeadless);
8 changes: 4 additions & 4 deletions src/AnswersActionsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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';

Expand All @@ -10,10 +10,10 @@ 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}>
<AnswersActionsContext.Provider value={answers}>
{children}
</AnswersActionsContext.Provider>
);
Expand Down
12 changes: 6 additions & 6 deletions src/subscribeToStateUpdates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(AnswersActionsContext);
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
4 changes: 2 additions & 2 deletions src/useAnswersActions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { StatefulCore } from '@yext/answers-headless';
import { AnswersHeadless } from '@yext/answers-headless';
import { useContext } from 'react';
import { AnswersActionsContext } from './AnswersActionsContext';

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

export function useAnswersActions(): AnswersActions {
return useContext(AnswersActionsContext);
Expand Down
16 changes: 8 additions & 8 deletions src/useAnswersState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(AnswersActionsContext);

// 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;
}
40 changes: 20 additions & 20 deletions tests/useAnswersState.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Result } from '@yext/answers-core';
import { provideStatefulCore } from '@yext/answers-headless';
import { provideAnswersHeadless } from '@yext/answers-headless';
import { State } from '@yext/answers-headless/lib/esm/models/state';
import React, { useCallback, useReducer } from 'react';
import { AnswersActionsContext, useAnswersActions, useAnswersState } from '../src';
Expand Down Expand Up @@ -43,13 +43,13 @@ it('does not perform extra renders/listener registrations for nested components'
);
}

const statefulCore = createStatefulCore();
const addListenerSpy = jest.spyOn(statefulCore, 'addListener');
const answers = createAnswersHeadless();
const addListenerSpy = jest.spyOn(answers, 'addListener');
expect(addListenerSpy).toHaveBeenCalledTimes(0);
expect(parentStateUpdates).toHaveLength(0);
expect(childStateUpdates).toHaveLength(0);
render(
<AnswersActionsContext.Provider value={statefulCore}>
<AnswersActionsContext.Provider value={answers}>
<Test />
</AnswersActionsContext.Provider>
);
Expand Down Expand Up @@ -88,16 +88,16 @@ it('does not trigger render on unmounted component', async () => {
return <div>child component</div>;
}

const statefulCore = createStatefulCore();
const answers = createAnswersHeadless();
render(
<AnswersActionsContext.Provider value={statefulCore}>
<AnswersActionsContext.Provider value={answers}>
<ParentComponent/>
</AnswersActionsContext.Provider>
);
act(() => statefulCore.setQuery('resultsWithFilter'));
await act( () => statefulCore.executeUniversalQuery());
act(() => statefulCore.setQuery('default'));
await act( () => statefulCore.executeUniversalQuery());
act(() => answers.setQuery('resultsWithFilter'));
await act( () => answers.executeUniversalQuery());
act(() => answers.setQuery('default'));
await act( () => answers.executeUniversalQuery());
expect(consoleSpy).not.toHaveBeenCalledWith(
expect.stringMatching('Can\'t perform a React state update on an unmounted component'),
expect.anything(),
Expand All @@ -120,9 +120,9 @@ describe('uses the most recent selector',() => {
);
}

const statefulCore = createStatefulCore();
const answers = createAnswersHeadless();
render(
<AnswersActionsContext.Provider value={statefulCore}>
<AnswersActionsContext.Provider value={answers}>
<Test />
</AnswersActionsContext.Provider>
);
Expand Down Expand Up @@ -153,11 +153,11 @@ describe('uses the most recent selector',() => {
);
}

const statefulCore = createStatefulCore();
statefulCore.setQuery('initial value');
const answers = createAnswersHeadless();
answers.setQuery('initial value');
expect(stateUpdates).toHaveLength(0);
render(
<AnswersActionsContext.Provider value={statefulCore}>
<AnswersActionsContext.Provider value={answers}>
<Test />
</AnswersActionsContext.Provider>
);
Expand All @@ -173,18 +173,18 @@ describe('uses the most recent selector',() => {
expect(stateUpdates).toEqual(['initial value', 1]);

act(() => {
statefulCore.setContext('trigger a state update that would not update the initial selector');
answers.setContext('trigger a state update that would not update the initial selector');
});
expect(stateUpdates).toEqual(['initial value', 1, 3]);
});
});

function createStatefulCore() {
const statefulCore = provideStatefulCore({
function createAnswersHeadless() {
const answers = provideAnswersHeadless({
apiKey: 'fake api key',
experienceKey: 'fake exp key',
locale: 'en',
});
statefulCore.setVerticalKey('fakeVerticalKey');
return statefulCore;
answers.setVerticalKey('fakeVerticalKey');
return answers;
}