Comments (6)
Hey there!
Yes. That is very tricky thing to do. I'm not 100% sure that this will work, but let's try :)
What I assume is that you this kind of case: You have two server components: nav and main. These two are server components that are rendering on server. And then after the client has been hydrated then client main should react to changes made in client nav.
To transfer the request from server to client I created these two functions: KlevuPackFetchResult
and KlevuHydratePackedFetchResult
. You should pack results in the server side and hydrate in the frontend side. In your case I would try hydrate in both: nav and in main. But you would need to create separate client only FilterManager that is imported to both of these (nav and main) client components.
Now you can use that one client FilterManager instance to change facets to different values in nav component and in main component it is only used as parameter to client side KlevuFetch()
(You need to do separate client only fetching after hydration that is separate from server side fetching).
And as last bit you need to listen Dom events in main component. Listen for FilterSelectionUpdate
and in callback run client side KlevuFetch()
again. Using same FilterManager between those two client components should do the trick of updating facets for query.
Here is my example code that does the same, but with just one client component: https://github.com/klevultd/frontend-sdk/blob/master/examples/hydrogen/src/routes/search.server.tsx
And here is client component: https://github.com/klevultd/frontend-sdk/blob/master/examples/hydrogen/src/components/searchResultPage.client.tsx#L15
There in line 15 is the FilterManager. If it would be moved to separate file where it is created and instance is exported to both of your client components.
from frontend-sdk.
So this kind of hits the nail on the head in what I am trying to do @rallu.
The only difference is that there are lots of other deep nested components within that Header and Main that control lots of complex functionality so it isn't a a simple one and they are both client because of what they need to do with state management and hooks in react. This means that the filters are deeply nested inside all of this as a client component. eg below:
import {Suspense, useEffect} from 'react';
import {
useLocalization,
useShopQuery,
CacheLong,
gql,
useServerProps,
useServerState,
useRouteParams,
useUrl,
} from '@shopify/hydrogen';
import {useContentfulQuery as useContentfulQuery} from '~/services/contentful';
import {Header, MainContent} from '~/components';
import {parseMenu} from '~/lib/utils';
import {PAGINATION_SIZE} from '~/lib/const';
const HEADER_MENU_HANDLE = 'main-menu';
const FOOTER_MENU_HANDLE = 'footer';
const SHOP_NAME_FALLBACK = 'l';
/**
* A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
*/
export function Layout({
children,
pageTransition,
title,
description,
mainClassName,
product,
variant,
}) {
const {pathname} = useUrl();
// activeLayer
const {
language: {isoCode: languageCode},
country: {isoCode: countryCode},
} = useLocalization();
const {category, slug} = useRouteParams();
const {data} = useShopQuery({
query: TYPES_QUERY,
variables: {
country: countryCode,
language: languageCode,
pageBy: PAGINATION_SIZE,
},
preload: true,
});
const cats = data.collections.nodes.filter((v) => v.node != '');
// get last segment of a url
const lastSegment = pathname.split('/').pop();
console.log(lastSegment);
const catsTransform = cats.map((v) => {
return {
name: v.title,
slug: '/collections/' + v.handle.toLowerCase().replace(/ /g, '-'),
};
});
const {data: useConData} = useContentfulQuery({
query: CONTENTFUL_HIGHLIGHTS_AND_INFO_QUERY,
key: pathname,
});
const {items} = useConData.highlightCollection;
const highlightItems = items.filter(
(item) => item && item.type.includes('Men'),
);
const infoData = useConData.informationCollection?.items[0] || {};
return (
<>
<div className="flex flex-col md:flex-row justify-between items-center min-h-screen bg-lav_white">
<div className="">
<a href="#mainContent" className="sr-only">
Skip to content
</a>
</div>
<Suspense fallback={<Header title={SHOP_NAME_FALLBACK} />}>
<Header
title={title}
description={description}
information={infoData}
highlights={highlightItems}
categories={catsTransform}
product={product}
/>
</Suspense>
<MainContent children={children}></MainContent>
</div>
</>
);
}
function HeaderWithMenu({pageTitle}) {
const {shopName} = useLayoutQuery();
return <Header title={pageTitle} />;
}
function useLayoutQuery() {
const {
language: {isoCode: languageCode},
} = useLocalization();
const {data} = useShopQuery({
query: SHOP_QUERY,
variables: {
language: languageCode,
headerMenuHandle: HEADER_MENU_HANDLE,
footerMenuHandle: FOOTER_MENU_HANDLE,
},
cache: CacheLong(),
preload: '*',
});
const shopName = data ? data.shop.name : SHOP_NAME_FALLBACK;
/*
Modify specific links/routes (optional)
@see: https://shopify.dev/api/storefront/unstable/enums/MenuItemType
e.g here we map:
- /blogs/news -> /news
- /blog/news/blog-post -> /news/blog-post
- /collections/all -> /products
*/
const customPrefixes = {BLOG: '', CATALOG: 'products'};
const headerMenu = data?.headerMenu
? parseMenu(data.headerMenu, customPrefixes)
: undefined;
return {headerMenu, shopName};
}
const TYPES_QUERY = gql`
query ProductTypes($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
collections(first: 10) {
nodes {
handle
title
}
}
}
`;
const SHOP_QUERY = gql`
fragment MenuItem on MenuItem {
id
resourceId
tags
title
type
url
}
query layoutMenus($language: LanguageCode, $headerMenuHandle: String!)
@inContext(language: $language) {
shop {
name
}
headerMenu: menu(handle: $headerMenuHandle) {
id
items {
...MenuItem
items {
...MenuItem
}
}
}
}
`;
const CONTENTFUL_HIGHLIGHTS_AND_INFO_QUERY = gql`
query {
highlightCollection {
items {
title
slug
collection
image {
url
width
height
title
}
type
}
}
informationCollection {
items {
instagramLink
facebookLink
emailContact
pageLinksCollection {
items {
title
slug
}
}
}
}
}
`;
from frontend-sdk.
@rallu how are you?
I have got the above working as you wrote above.
My only query is the hydrate function that I wrote gets the correct filters back but for some reason products do not seem to be updating. When I check the network the search from klevu servers suggests otherwise.
import {
useState,
useRef,
forwardRef,
useImperativeHandle,
useEffect,
useCallback,
} from 'react';
import {useDebounce} from 'react-use';
import {Link, flattenConnection, useServerProps} from '@shopify/hydrogen';
import {
listFilters,
applyFilterWithManager,
KlevuFetch,
FilterManager,
KlevuDomEvents,
KlevuListenDomEvent,
KlevuPackFetchResult,
KlevuHydratePackedFetchResult,
search,
sendSearchEvent,
} from '@klevu/core';
import {Button, Grid, ProductCard} from '~/components';
import {getImageLoadingPriority} from '~/lib/const';
import {searchQuery} from '~/services/klevu';
import {manager} from '~/components/global/FilterManagerClient.client';
let currentResult = {};
let clickEvent = null;
export function ProductGrid({url, collection, description, baseKlevuQuery}) {
const nextButtonRef = useRef(null);
const initialProducts = collection?.products?.nodes || [];
const {hasNextPage, endCursor} = collection?.products?.pageInfo ?? {};
const [products, setProducts] = useState(initialProducts);
const [klevuProducts, setKlevuProducts] = useState(null);
const [cursor, setCursor] = useState(endCursor ?? '');
const [nextPage, setNextPage] = useState(hasNextPage);
const [pending, setPending] = useState(false);
const haveProducts = initialProducts.length > 0;
const {serverProps, setServerProps} = useServerProps();
const [options, setOptions] = useState(manager.options);
const [sliders, setSliders] = useState(manager.sliders);
const [searchResultData, setSearchResultData] = useState([]);
const hydrate = async () => {
currentResult = await KlevuHydratePackedFetchResult(
baseKlevuQuery,
searchQuery('*', manager),
);
console.log(currentResult);
const search = currentResult.queriesById('search');
if (search) {
setKlevuProducts(search.records);
if (search.getSearchClickSendEvent) {
clickEvent = search.getSearchClickSendEvent();
}
}
};
const handleFilterUpdate = (e) => {
hydrate();
};
useEffect(() => {
const stop = KlevuListenDomEvent(
KlevuDomEvents.FilterSelectionUpdate,
handleFilterUpdate,
);
// cleanup this component
return () => {
stop();
};
}, []);
const fetchProducts = useCallback(async () => {
setPending(true);
const postUrl = new URL(window.location.origin + url);
postUrl.searchParams.set('cursor', cursor);
const response = await fetch(postUrl, {
method: 'POST',
});
const {data} = await response.json();
// ProductGrid can paginate collection, products and search routes
// @ts-ignore TODO: Fix types
const newProducts = flattenConnection(
data?.collection?.products || data?.products || [],
);
const {endCursor, hasNextPage} = data?.collection?.products?.pageInfo ||
data?.products?.pageInfo || {endCursor: '', hasNextPage: false};
// this was changed from {...newProducts} to {...products} because sorting was causing issues.
// If we get issues with pagination then we need to revisit this more
setProducts([...products, ...newProducts]);
setCursor(endCursor);
setNextPage(hasNextPage);
setPending(false);
}, [cursor, url, products]);
const handleIntersect = useCallback(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fetchProducts();
}
});
},
[fetchProducts],
);
useEffect(() => {
const observer = new IntersectionObserver(handleIntersect, {
rootMargin: '100%',
});
setServerProps({description: description});
const nextButton = nextButtonRef.current;
if (nextButton) observer.observe(nextButton);
return () => {
if (nextButton) observer.unobserve(nextButton);
};
}, [nextButtonRef, cursor, handleIntersect]);
if (!haveProducts) {
return (
<>
<p>No products found on this collection</p>
<Link to="/products">
<p className="underline">Browse catalog</p>
</Link>
</>
);
}
return (
<>
<div className="product-fixed-container">
<div className="snap-container grid grid-cols-2 gap-x-2 md:gap-x-6 px-2 md:px-0">
{klevuProducts && klevuProducts.length > 0
? klevuProducts.map((product) => <p>{product.name}</p>)
: products.map((product) => (
<section key={product.id} className="scroll-snap-top_section">
<ProductCard
key={product.id}
product={product}
loadingPriority={getImageLoadingPriority(product)}
className="product-card_sm-width"
/>
</section>
))}
</div>
</div>
{nextPage && (
<div
className="flex items-center justify-center mt-6"
ref={nextButtonRef}
>
<Button
variant="secondary"
disabled={pending}
onClick={fetchProducts}
width="full"
>
{pending ? 'Loading...' : 'Load more products'}
</Button>
</div>
)}
</>
);
}
If you check my hydrate function it seems to not give the correct product records or it never updates.
from frontend-sdk.
I think this
const handleFilterUpdate = (e) => {
hydrate();
};
should be replaced with
const handleFilterUpdate = (e) => {
fetchProducts();
};
from frontend-sdk.
@rallu yeah I spotted that yesterday. Still getting my head around all this ;)
from frontend-sdk.
Closing as inactive. Please comment if more information is required.
from frontend-sdk.
Related Issues (20)
- Morland UK: URL Redirects not working HOT 1
- Collection handles with special characters HOT 11
- Create a example how to create URL redirects HOT 1
- Create an example how to use Banners.
- Customers Also Bought HOT 2
- Exclude items with a itemGroupId HOT 3
- customANDQuery is misspelled in the query settings HOT 1
- Analytics Events HOT 1
- Load more functionality stops sending analytical events with helpers from previous pages
- Filters applied for the category collection erases the filtering per category HOT 1
- Persist selected filters in URL HOT 5
- Not possible to use a custom method for the products filtering HOT 4
- kmcRecommendation doesn't support BOUGHT_TOGETHER_PDP logic HOT 3
- Promo banner in Quick Search HOT 1
- SDK support for Klevu search dropdown HOT 4
- FilterManager.readFromURLParams should take care of multiple values in the URL HOT 2
- proposal: add selectedfilters getter to the FilterManager HOT 1
- 'itemGroupdId' is required for Product recommendation HOT 2
- "currentProductId" should be in "recentObjects" instead of "sourceObjects" for PDP recommendations HOT 2
- Integration issue | https://eselo-qbj-1618476066-staging-dal.readymage.com/ HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from frontend-sdk.