GithubHelp home page GithubHelp logo

How can I listen for filterUpdate events within a Hydrogen app away from the client file that the update happens? about frontend-sdk HOT 6 CLOSED

mdunbavan avatar mdunbavan commented on June 9, 2024
How can I listen for filterUpdate events within a Hydrogen app away from the client file that the update happens?

from frontend-sdk.

Comments (6)

rallu avatar rallu commented on June 9, 2024

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.

mdunbavan avatar mdunbavan commented on June 9, 2024

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.

mdunbavan avatar mdunbavan commented on June 9, 2024

@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.

rallu avatar rallu commented on June 9, 2024

I think this

const handleFilterUpdate = (e) => {
  hydrate();
};

should be replaced with

const handleFilterUpdate = (e) => {
  fetchProducts();
};

from frontend-sdk.

mdunbavan avatar mdunbavan commented on June 9, 2024

@rallu yeah I spotted that yesterday. Still getting my head around all this ;)

from frontend-sdk.

rallu avatar rallu commented on June 9, 2024

Closing as inactive. Please comment if more information is required.

from frontend-sdk.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.