Home

Awesome

@nosto/shopify-hydrogen

Welcome to the Nosto React Component Library for Shopify Hydrogen!

Our component library is designed to help you easily integrate Nosto features into your Hydrogen-based storefront.

The library includes a comprehensive set of reusable components, each designed to support a specific feature or functionality of Nosto. With our library, you can quickly and easily implement Nosto features into your storefront.

This README is designed to provide you with an overview of our component library, including instructions on how to install and use our components, as well as information on the features and functionalities that our library supports.

:warning: Please note that the information provided in this documentation is specific to Hydrogen 2, which is built on Remix. If your storefront is still running on Hydrogen 1, we recommend referring to our documentation specifically tailored for Hydrogen 1. This will ensure that you access the appropriate guidance and instructions for your specific version.

nosto-react

It's important to note that our React component library for Shopify Hydrogen is an extension of our nosto-react library, a powerful and flexible library that provides seamless integration with Nosto. Our Shopify Hydrogen-specific component library builds upon the core functionality of nosto-react, adding Hydrogen-specific hooks and logic to make integration even easier and more intuitive.

Demo store

To see our React component library in action in a Hydrogen project, we invite you to check out our Shopify Hydrogen demo store. The code for the demo store is available on our GitHub repository. We hope that our Hydrogen demo store will serve as a source of inspiration for your own storefront development, and we encourage you to explore our component library as well and customize it further to fit your unique needs.

Feature list

Our React component library for Shopify Hydrogen includes the following features:

*Note: Our React component library currently does not support advanced use cases of the debug toolbar, but we are constantly working to improve our library and provide you with the best possible integration options.

**Note: The search feature is available when implemented via our code editor.

Installation

To install the package into your project, run:

npm install @nosto/shopify-hydrogen

Or if you prefer using Yarn:

yarn add @nosto/shopify-hydrogen

Usage

Adding Nosto's fetcher function to the root loader

By following these steps, you enable Nosto components to seamlessly receive relevant server-based data through the root loader.

// app/root.jsx

import { getNostoData } from '@nosto/shopify-hydrogen'

export async function loader({ request, context }) {
  const cartId = getCartId(request);
  //...

  return defer({
    ...(await getNostoData({ context, cartId })),
  });
}

Adding components

The library uses @nosto/nosto-react under the hood combined with Hydrogen functionality. You can import the following components:

NostoProvider

// app/root.jsx

import { NostoProvider } from "@nosto/shopify-hydrogen";

export default function App() {
  const nonce = useNonce();
  const data = useLoaderData();
  
  return (
      ...
      <body>
        <NostoProvider account="shopify-11368366139" recommendationComponent={<NostoSlot />} nonce={nonce}>
          <Layout>
            <Outlet/>
          </Layout>
        </NostoProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
  );
}

export function ErrorBoundary() {

  const [root] = useMatches();

  return (
      ...
      <body>
        <NostoProvider account="shopify-11368366139" recommendationComponent={<NostoSlot />}>
          <Layout/>
        </NostoProvider>
        <Scripts />
      </body>
  );
}

Shopify Markets

// Enable with automatic market and language detection:
<NostoProvider shopifyMarkets={true} account="shopify-11368366139" nonce={nonce}/>

// Manually set only the language of the market:
<NostoProvider shopifyMarkets={{ language: "EN" }} account="shopify-11368366139" nonce={nonce}/>

// Manually set both the language and ID of the market:
<NostoProvider shopifyMarkets={{ language: "EN", marketId: '123456789' }} account="shopify-11368366139" nonce={nonce}/>

Client side rendering for recommendations

In order to implement client-side rendering, the <NostoProvider> requires a designated component to render the recommendations provided by Nosto. This component should be capable of processing the JSON response received from our backend. Notice the recommendationComponent={<NostoSlot />} prop passed to <NostoProvider> above.

We have included a set of basic components as examples, however, additional customizations can be made to suit specific requirements. It is important to note that these components serve as a starting point for implementation.

// app/components/nosto/NostoItem.jsx

export function NostoItem({product, onClick}) {
  return (
    <div className="nosto-item" onClick={onClick}>
      <a href={product.url}>
        <div className="nosto-image-container">
          <div className="nosto-image">
            <img src={product.thumb_url} alt={product.name} />
          </div>
          <div className="nosto-product-details">
            <div className="nosto-product-name">{product.name}</div>
            <div className="nosto-product-price">{product.price_text}</div>
          </div>
        </div>
      </a>
    </div>
  );
}

Additionally, we have a component that will use <NostoItem> in slots defined for recommendations.

// app/components/nosto/NostoSlot.jsx

import { NostoItem } from './NostoItem';

export function NostoSlot({nostoRecommendation}) {
  let {title, products, result_id} = nostoRecommendation;

  function reportClick(productId) {
    // To report attribution for product clicks towards segmentation & analytics
    window?.nostojs(function (api) {
      api.defaultSession().recordAttribution('vp', productId, result_id).done();
    });
  }

  return (
    <div className="nosto-container">
      <h2 className="nosto-title">{title}</h2>
      <div className="nosto-list">
        {products.map((product) => (
          <NostoItem
            product={product}
            key={product.product_id}
            onClick={() => {
              reportClick(product.product_id);
            }}
          />
        ))}
      </div>
    </div>
  );
}

NostoSession

In Hydrogen the NostoSession component is rendered automatically, no need for manually adding it to your app.

NostoPlacement

import { NostoPlacement } from "@nosto/shopify-hydrogen";

<NostoPlacement id="frontpage-nosto-1" />;
<NostoPlacement id="frontpage-nosto-2” />;
:warning: Dynamic placements and Shopify Hydrogen

Please note that the concept of dynamic placements does not apply to Shopify Hydrogen headless environments, as they can interfere with React's DOM rendering process and adversely affect site navigation. As such, we have disabled Nosto's dynamic placement feature in our React component library for Shopify Hydrogen builds. Instead, all placements should be statically placed where needed with the NostoPlacement component described above. While dynamic placements may be a useful feature in other environments, we have found that they are not compatible with the unique architecture of Shopify Hydrogen, and can cause unexpected behavior in your storefront.

Optionally passing placement IDs to page type components

An optional prop called "placements" can be passed for all components in the library. This prop is a string array that accepts placement IDs. When used, it allows Nosto to render only the placements you specify, rather than all placements on the page (which is the default behavior). This can include placements that are 'visible' on the page, like ones in the background such as those on a product detail page or your homepage when your mini-cart is open. To use this prop, simply pass the placement IDs you wish to render as an array of strings. For example, if you only want to render placements "frontpage-nosto-1" and "frontpage-nosto-2" you would pass the following prop to the <NostoHome> component:

<NostoHome placements={['frontpage-nosto-1', 'frontpage-nosto-2']} />

NostoHome

// app/routes/($locale)._index.jsx

import { NostoHome, NostoPlacement } from "@nosto/shopify-hydrogen";

function Homepage() {
  return (
    <>
      ...
      <NostoPlacement id="frontpage-nosto-1" />
      <NostoPlacement id="frontpage-nosto-2" />
      <NostoHome />
    </>
  );
}

NostoProduct

// app/routes/($locale).products.$productHandle.jsx

import { NostoProduct, NostoPlacement } from '@nosto/shopify-hydrogen';

export default function Product() {

  ...
  const { product } = useLoaderData();

  let nostoProductId = product?.id?.split('/');
  nostoProductId && (nostoProductId = nostoProductId[nostoProductId.length - 1]);

  return (
      <>
        ...
        <NostoPlacement id="productpage-nosto-1" />
        <NostoPlacement id="productpage-nosto-2" />
        <NostoProduct product={nostoProductId} tagging={product} />        
      </>
  );
}

NostoCategory

// app/routes/($locale).collections.$collectionHandle.jsx

import { NostoCategory, NostoPlacement } from "@nosto/shopify-hydrogen";

export default function Collection() {

  const { collection } = useLoaderData();

  return (
    <>
      ...
      <NostoPlacement id="categorypage-nosto-1" />
      <NostoPlacement id="categorypage-nosto-2" />
      <NostoCategory category={collection.title} />
    </>
  );
}

NostoSearch

// app/routes/($locale).search.jsx

import { NostoSearch, NostoPlacement } from "@nosto/shopify-hydrogen";

export default function Search() {

  const { searchTerm } = useLoaderData();  

  return (
    <>
      ...
      <NostoPlacement id="searchpage-nosto-1" />
      <NostoPlacement id="searchpage-nosto-2" />
      <NostoSearch query={searchTerm ? decodeURI(searchTerm) : ""} />
    <>
  );
}

NostoOther

import { NostoOther, NostoPlacement } from "@nosto/shopify-hydrogen";

function OtherPage() {
  return (
    <>
      ...
      <NostoPlacement id="other-nosto-1" />
      <NostoPlacement id="other-nosto-2" />
      <NostoOther />
    </>
  );
}

NostoCheckout

// app/components/Cart.jsx

import { NostoCheckout, NostoPlacement } from "@nosto/shopify-hydrogen";

export function Cart({ layout, onClose, cart }) {

  const linesCount = Boolean(cart?.lines?.edges?.length || 0);

  return (
    <>
      <CartEmpty hidden={linesCount} onClose={onClose} layout={layout} />
      <CartDetails cart={cart} layout={layout} />

      {/* Render specific Nosto slot when there are items in cart: */}
      {linesCount && (<NostoPlacement id="cartpage-nosto-1" />)}

      <NostoCheckout />
    </>
  );
}

export function CartEmpty({ hidden = false}) {

  return (
    <div>
      ...

      {/* Render specific Nosto slot when cart is empty: */}
      {!hidden && (<NostoPlacement id="cartpage-nosto-2" />)}

    </div>
  );
}

Nosto404

// app/components/NotFound.jsx

import { Nosto404, NostoPlacement } from "@nosto/shopify-hydrogen";

export function NotFound() {
  return (
    <>
      ...
      <NostoPlacement id="notfound-nosto-1" />
      <NostoPlacement id="notfound-nosto-2" />
      <Nosto404 />
    </>
  );
}

Feedback

If you've found a feature missing or you would like to report an issue, simply open up an issue and let us know.

We're always collecting feedback and learning from your use-cases. If you find yourself customising widgets and forking the repo to make patches - do drop a message. We'd love to know more and understand how we can make this library even better for you.

Authors

See also the list of contributors who participated in this project.

License

MIT License © Nosto Solutions