Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ To see this in action:
export const termsWithImagesAndCountsDescription = `Pass boolean flags for \`displaySearchSuggestionImages\` and \`displaySearchSuggestionResultCounts\` fields to display images and counts for search suggestions. These fields need to be made displayable before they can be used. Please contact your Constructor Integration Engineer for details.`;

export const debounceDescription = `Pass an integer to \`debounce\` to override the recommended, default delay employed for debouncing autocomplete network requests between keystrokes as your users type into the text input field. The default value is 250, which results in a debounce delay of 250 milliseconds.`;
export const fetchZeroStateOnFocusDescription = `Pass a boolean to \`fetchZeroStateOnFocus\` to override the zero state fetching behavior from initial render to input focus.`;

export const translationsDescription = `Pass a \`translations\` object to display translatable words in your preferred language.

Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useCioAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {

// Get autocomplete sections (autocomplete + recommendations + custom)
const {
fetchRecommendationResults,
activeSections,
activeSectionsWithData,
zeroStateActiveSections,
Expand Down Expand Up @@ -178,6 +179,9 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
openMenu();
}
try {
if (advancedParameters?.fetchZeroStateOnFocus) {
fetchRecommendationResults();
}
cioClient?.tracker?.trackInputFocus();
} catch (error) {
// eslint-disable-next-line no-console
Expand Down
78 changes: 41 additions & 37 deletions src/hooks/useFetchRecommendationPod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,58 @@ import { Item, SectionsData, RecommendationsSectionConfiguration, PodData } from

const useFetchRecommendationPod = (
cioClient: Nullable<ConstructorIOClient>,
recommendationPods: RecommendationsSectionConfiguration[]
recommendationPods: RecommendationsSectionConfiguration[],
fetchZeroStateOnFocus: boolean = false
) => {
const [recommendationsResults, setRecommendationsResults] = useState<SectionsData>({});
const [podsData, setPodsData] = useState<Record<string, PodData>>({});

useEffect(() => {
const fetchRecommendationResults = async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving function outside useEffect so it can be exported

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this! What do you think about moving it outside of useFetchRecommendationPod into utils file. So it can be imported in useCioAutocomplete without having to keep exposing it up across different hooks?

Copy link
Contributor

@mocca102 mocca102 May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lordvkrum I checked your POC branch, and I thought of not calling fetchRecommendationResults inside the onFocus callback because of missing dependencies. And depend on the useEffect here to run an effect when the input is focused but there was still complexity with the dependencies. Because of that it's better that we move forward with your current implementation for now.

if (!cioClient || !Array.isArray(recommendationPods) || recommendationPods.length === 0) return;
const fetchRecommendationResults = async () => {
const responses = await Promise.all(
recommendationPods.map(({ podId, indexSectionName, ...parameters }) =>
cioClient.recommendations.getRecommendations(podId, {
...parameters,
section: indexSectionName,
})
)
);
const recommendationsPodResults = {};
const recommendationsPodsData = {};
responses.forEach(({ response, request }, index) => {
const { pod, results } = response;
if (pod?.id) {
recommendationsPodResults[pod.id] = results?.map((item: Item) => ({
...item,
id: item?.data?.id,
section: recommendationPods[index]?.indexSectionName,
podId: pod.id,
}));
recommendationsPodsData[pod.id] = {
displayName: pod.display_name,
podId: pod.id,
request,
};
}
});

try {
setRecommendationsResults(recommendationsPodResults);
setPodsData(recommendationsPodsData);
} catch (error: any) {
// eslint-disable-next-line no-console
console.log(error);
const responses = await Promise.all(
recommendationPods.map(({ podId, indexSectionName, ...parameters }) =>
cioClient.recommendations.getRecommendations(podId, {
...parameters,
section: indexSectionName,
})
)
);
const recommendationsPodResults = {};
const recommendationsPodsData = {};
responses.forEach(({ response, request }, index) => {
const { pod, results } = response;
if (pod?.id) {
recommendationsPodResults[pod.id] = results?.map((item: Item) => ({
...item,
id: item?.data?.id,
section: recommendationPods[index]?.indexSectionName,
podId: pod.id,
}));
recommendationsPodsData[pod.id] = {
displayName: pod.display_name,
podId: pod.id,
request,
};
}
};
});

try {
setRecommendationsResults(recommendationsPodResults);
setPodsData(recommendationsPodsData);
} catch (error: any) {
// eslint-disable-next-line no-console
console.log(error);
}
};

useEffect(() => {
if (fetchZeroStateOnFocus) return;
fetchRecommendationResults();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cioClient]);

return { recommendationsResults, podsData };
return { fetchRecommendationResults, recommendationsResults, podsData };
};

export default useFetchRecommendationPod;
12 changes: 11 additions & 1 deletion src/hooks/useRecommendationsObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import ConstructorIO from '@constructor-io/constructorio-client-javascript';
import { Section } from '../types';
import { isRecommendationsSection } from '../typeGuards';

const viewedRecommendations = new Set();

/**
* Custom hook that observes the visibility of recommendation sections and calls trackRecommendationView event.
* This is done by using the IntersectionObserver API to observe the visibility of each recommendation section.
Expand Down Expand Up @@ -41,8 +43,9 @@ function useRecommendationsObserver(
const observer = new IntersectionObserver((entries) => {
// For each section, check if it's intersecting
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.isIntersecting && !viewedRecommendations.has(entry.target)) {
trackRecommendationView(entry.target as HTMLElement, sections, constructorIO);
viewedRecommendations.add(entry.target);
}
});
}, intersectionObserverOptions);
Expand All @@ -63,6 +66,13 @@ function useRecommendationsObserver(
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [menuIsOpen, sections]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a race condition where menuIsOpen=true but the recommendations element was not rendered and the functiontrackRecommendationView was not being called for the first focus but gets fired for subsequent focuses

Adding sections as dependency fixed that but caused multiple trackRecommendationView hence the addition if viewedRecommendations to keep track of when the recommendation has been viewed as long as the menu is open. Once the menu is closed it restrarts


// clear viewed recommendations when menu is closed
useEffect(() => {
if (!menuIsOpen) {
viewedRecommendations.clear();
}
}, [menuIsOpen]);
}

Expand Down
11 changes: 7 additions & 4 deletions src/hooks/useSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export default function useSections(
} = useDebouncedFetchSection(query, cioClient, autocompleteSections, advancedParameters);

// Fetch Recommendations Results
const { recommendationsResults, podsData } = useFetchRecommendationPod(
cioClient,
recommendationsSections
);
const { fetchRecommendationResults, recommendationsResults, podsData } =
useFetchRecommendationPod(
cioClient,
recommendationsSections,
advancedParameters?.fetchZeroStateOnFocus
);

// Remove sections if necessary
useEffect(() => {
Expand Down Expand Up @@ -101,6 +103,7 @@ export default function useSections(
}, [autocompleteResults, recommendationsResults, activeSectionConfigs, podsData]);

return {
fetchRecommendationResults,
activeSections,
activeSectionsWithData,
zeroStateActiveSections,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
termsWithImagesAndCountsDescription,
debounceDescription,
translationsDescription,
fetchZeroStateOnFocusDescription,
} from '../../../constants';

export default {
Expand Down Expand Up @@ -117,3 +118,22 @@ addComponentStoryDescription(
`const args = ${stringifyWithDefaults(Translations.args)}`,
translationsDescription
);

export const FetchZeroStateOnFocus = ComponentTemplate.bind({});
FetchZeroStateOnFocus.args = {
apiKey,
onSubmit,
advancedParameters: { fetchZeroStateOnFocus: true },
zeroStateSections: [
{
podId: 'bestsellers',
type: 'recommendations',
numResults: 3,
},
],
};
addComponentStoryDescription(
FetchZeroStateOnFocus,
`const args = ${stringifyWithDefaults(FetchZeroStateOnFocus.args)}`,
fetchZeroStateOnFocusDescription
);
20 changes: 20 additions & 0 deletions src/stories/Autocomplete/Hook/AdvancedParameters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
onSubmitDefault as onSubmit,
termsWithImagesAndCountsDescription,
debounceDescription,
fetchZeroStateOnFocusDescription,
} from '../../../constants';

export default {
Expand Down Expand Up @@ -97,3 +98,22 @@ addHookStoryCode(
`const args = ${stringifyWithDefaults(Debounce.args)}`,
debounceDescription
);

export const FetchZeroStateOnFocus = HooksTemplate.bind({});
FetchZeroStateOnFocus.args = {
apiKey,
onSubmit,
advancedParameters: { fetchZeroStateOnFocus: true },
zeroStateSections: [
{
podId: 'bestsellers',
type: 'recommendations',
numResults: 3,
},
],
};
addHookStoryCode(
FetchZeroStateOnFocus,
`const args = ${stringifyWithDefaults(FetchZeroStateOnFocus.args)}`,
fetchZeroStateOnFocusDescription
);
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface AdvancedParametersBase {
displaySearchSuggestionResultCounts?: boolean;
debounce?: number;
translations?: Translations;
fetchZeroStateOnFocus?: boolean;
}

export type AdvancedParameters = AdvancedParametersBase &
Expand Down