React widgets

@cascade-commerce/widgets is the React package behind the storefront widgets. If your storefront is a React app, you can import the widgets directly instead of dropping in the embed script, and you can take any widget apart and lay its pieces out yourself.

This page covers the three depths you can work at, the parts each widget exposes, and the props each part takes. It is written to be precise enough to build from: if you are handing it to a code assistant, hand it the whole page.

Setup

npm install @cascade-commerce/widgets

Wrap the part of your tree that renders widgets in a CascadeProvider. Every connected widget reads the API base URL, the publishable key and the signed-in customer from it.

import { CascadeProvider } from "@cascade-commerce/widgets"
import "@cascade-commerce/widgets/theme.css"
;<CascadeProvider
  baseUrl="https://api.cascade.example.com"
  apiPublishableKey="pk_live_..."
  customerAuth={customer ? { email, timestamp, signature } : null}
>
  <App />
</CascadeProvider>

customerAuth is the same signed trio the embed script takes. See Identifying the customer for how to make it. Leave it null for a signed-out shopper.

Each widget's stylesheet is imported by the widget itself. The custom properties and class names in Appearance apply exactly as they do to the embed, so a theme set in the dashboard reaches a React storefront too, provided you link the served stylesheet from your page's <head>.

Two components per widget

Every widget ships as a pair:

ConnectedPresentationalWhat it does
ProductReviewsWidgetProductReviewsThe reviews list, with summary, sort, filter, votes and form.
ProductRatingWidgetProductRatingStars and a count.
ReviewsCarouselWidgetReviewsCarouselA sliding row of review cards.
MediaGridWidgetMediaGridA wall of shopper photos and videos with a lightbox.
CustomerWishlistsWidgetCustomerWishlistsThe shopper's lists and the products on them.
AddToWishlistWidgetAddToWishlistThe save button for a product page.
WishlistsModalWidgetWishlistsModalA button that opens the lists in a dialog.
LoyaltyWidgetLoyaltyPanelThe loyalty program, inline.
LoyaltyLauncherWidgetLoyaltyLauncherThe same panel behind a floating corner button.
LoyaltyPointsWidgetLoyaltyPoints"Earn 250 points with this purchase."

The connected widget talks to the API: it fetches, paginates, votes, submits, and handles the shopper's sign-in state. The presentational component takes everything as props and fetches nothing. You will mostly use the connected one; the presentational one is for when you already have the data, or for a preview against sample data.

Level one: drop it in

import { ProductReviewsWidget } from "@cascade-commerce/widgets"
;<ProductReviewsWidget productSlug="classic-tee" />

Every connected widget renders its default composition with no children. Its props have not changed; see Connected widget props.

Level two: lay the parts out yourself

Give the connected widget children and it renders them instead of the default composition, inside a root it has filled with what it fetched. Each part reads that data and draws its own internals. How the parts sit relative to each other, and any markup between them, is up to you.

import { ProductReviews, ProductReviewsWidget } from "@cascade-commerce/widgets"
;<ProductReviewsWidget productSlug="classic-tee">
  <div className="flex justify-between gap-8">
    <ProductReviews.RatingSummary />
    <ProductReviews.RatingBreakdown />
    <ProductReviews.WriteReview />
  </div>
  <ProductReviews.Toolbar />
  <ProductReviews.List />
  <ProductReviews.LoadMore />
</ProductReviewsWidget>

Four rules hold for every widget:

  • Parts are properties of the presentational component, ProductReviews.List, never separate imports. Import the presentational component to reach them.
  • A part takes no data props. It reads the root. Leaving a part out leaves that piece off the page; a part with nothing to show (no summary, no more pages, no form handler) renders nothing.
  • The root element and its class names are unchanged. The connected widget still renders .cascade.cascade-reviews around your children, so custom CSS written against the documented classes keeps applying, and anything with structured data still emits it.
  • With the presentational component, the root is explicit. Pass the same props you would pass to the component to <ProductReviews.Root> and put the parts inside it. The connected widget does exactly this for you, which is why you do not write Root inside it.
<ProductReviews.Root averageRating={4.5} totalReviews={2} ratingBreakdown={...} reviews={reviews} hasMore={false}>
  <ProductReviews.RatingSummary />
  <ProductReviews.List />
</ProductReviews.Root>

Level three: recompose a row

A list part renders its children once per item, inside a context holding that item. With no children it renders the default row. So a review row can be rebuilt from its own parts, or replaced with a component of yours:

<ProductReviewsWidget productSlug="classic-tee">
  <ProductReviews.RatingSummary />
  <ProductReviews.List>
    <ProductReviews.Review.Root>
      <ProductReviews.Review.Rating />
      <ProductReviews.Review.Author />
      <ProductReviews.Review.Body />
    </ProductReviews.Review.Root>
  </ProductReviews.List>
</ProductReviewsWidget>
function Excerpt() {
  const review = ProductReviews.Review.useReview()
  return <blockquote>{review.body}</blockquote>
}

;<ProductReviews.List>
  <Excerpt />
</ProductReviews.List>

Every widget with a list exposes the same shape: a list part that takes children, a default row, and a hook that returns the current item. The hooks are listed per widget below.

Writing a part of your own

Every presentational component exposes useData(), which returns what its Root was given with defaults applied, plus any state the root keeps for its parts (which tile is open, whether the popover is showing). A custom part is an ordinary component that calls it:

function ReviewCount() {
  const { totalReviews } = ProductReviews.useData()
  return <span>{totalReviews} reviews</span>
}

A hook used outside its root throws with the name of the root it needs, for example This part has to be rendered inside <ProductReviews.Root>.

The parts, widget by widget

Each table lists a part, what it renders, and when it renders nothing. A part's props are listed where it takes any; most take none. children on a list part are rendered once per item; empty replaces the default empty state.

ProductReviews

Root element: div.cascade.cascade-reviews. Default order: Summary, QuestionSummary, WriteReview, AiSummary, AskBox, Toolbar, List, LoadMore, Questions.

PartRendersNothing when
RootThe provider and root element. Props: every ProductReviews prop, children.
SummaryRatingSummary and RatingBreakdown side by side in .cascade-reviews-summary.
RatingSummaryThe average as a number, its stars and the review count.
RatingBreakdownOne bar per star rating; filter buttons when onRatingChange is set.
QuestionSummaryWhat published reviews answered to your review questions.questionSummary is empty.
WriteReviewThe "Write a review" toggle and its inline form.No onSubmitReview.
AiSummaryThe generated "What reviewers say" block.No summary.
AskBoxThe ask box for instant answers and questions.No onAsk, or neither feature enabled.
ToolbarThe sort control and the "Showing 5 star reviews only" line.No onSortChange and no active rating.
ListThe reviews. Props: children, empty.Shows the empty state instead when no reviews.
ReviewOne default row. Also a namespace, below.
LoadMoreThe "Load more reviews" button.hasMore is false or no onLoadMore.
QuestionsThe published Q&A section.Questions not enabled.
useData()Hook: the root's props with defaults applied.

ProductReviews.Review parts, valid inside List:

PartRendersNothing when
Rootdiv.cascade-reviews-item. Props: children.
HeaderAuthor, Verified, the country and Date on one line.
AuthorThe reviewer's first name and last initial.
VerifiedThe "Verified buyer" badge.Not a verified buyer.
DateThe review date.
RatingThe review's stars.
TitleThe title.No title.
BodyThe body.No body.
AnswersThis reviewer's answers to your review questions.No answers.
MediaThe attached photos and videos, opening a lightbox.No files.
ResponseThe store's reply.No reply.
VotesThe helpful and not helpful buttons.No onVote on the root.
useReview()Hook: the current review.

ProductRating

Root element: div.cascade.cascade-rating. Default: Stars, then Count unless hideCount.

PartRenders
RootThe provider and root element. Props: every ProductRating prop, children.
StarsFive stars filled to the average.
CountThe review count.
useData()Hook: averageRating, totalReviews, starSize, filledColor, emptyColor.

ReviewsCarousel

Root element: section.cascade.cascade-reviews-carousel. Default: Header, Track. With no reviews the root renders the empty state (or its empty prop) whatever children it was given.

PartRendersNothing when
RootThe provider and root element. Props: every ReviewsCarousel prop, empty, children.
HeaderHeading and Nav on one line.
HeadingThe heading.heading is "".
NavThe previous and next arrows, each disabled at its end of the track.
TrackThe sliding row. Props: children.
CardOne default card. Also a namespace, below.
useData()Hook: the props plus atStart, atEnd, slide(direction).

ReviewsCarousel.Card parts, valid inside Track: Root (li.cascade-reviews-carousel-card, takes children), Rating, Title, Body, Image (the first photo), Footer (name and verified badge), Product (nothing unless showProduct and the review names one), and useCard() for the current review.

MediaGrid

Root element: section.cascade.cascade-media-grid. Default: Heading, Tiles, LoadMore, Lightbox. With no items the root renders the empty state (or its empty prop) whatever children it was given.

PartRendersNothing when
RootThe provider and root element. Props: every MediaGrid prop, empty, children.
HeadingThe heading.heading is "".
TilesThe grid. Props: children.
TileOne default tile: the photo or video frame as a button.
LoadMoreThe "Load more photos" button.hasMore is false or no onLoadMore.
LightboxThe lightbox with the review behind the open photo.No tile is open.
useData()Hook: the props plus openIndex and setOpenIndex.
useTile()Hook, inside Tiles: { item, index, open() }.

A custom tile should call open() from useTile() to show the lightbox, and the composition should include <MediaGrid.Lightbox /> somewhere for it to open into.

CustomerWishlists

Root element: div.cascade.cascade-wishlists. Default: ErrorNotice, Overview, Detail, ShareDialog. The Overview parts render only while no list is open and the Detail parts only while one is, so a composition can include both.

PartRendersNothing when
RootThe provider and root element. Props: every CustomerWishlists prop, children.
ErrorNoticeThe error message.No error.
OverviewOverview.Header, Overview.CreateForm, Overview.Rows. Props: children.A list is open.
Overview.HeaderThe "Your wishlists" title.A list is open.
Overview.CreateFormThe new wishlist form.A list is open, or single-list mode.
Overview.RowsOne row per list. Props: children, empty.A list is open.
Overview.RowOne default row: name, count, View, Make default, Share, Delete.
DetailDetail.Header, Detail.Items. Props: children.No list is open.
Detail.HeaderBack, the name or rename form, Share, Rename, Delete.No list is open.
Detail.ItemsOne card per product. Props: children, empty.No list is open.
Detail.ItemOne default card with its edit, move and remove controls.
ShareDialogThe share dialog.Share has not been pressed.
useData()Hook: the props plus standalone, shareTarget, openShare(wishlist), closeShare().
useWishlist()Hook, inside Overview.Rows: the current list.
useItem()Hook, inside Detail.Items: the current product.

AddToWishlist

Root element: div.cascade.cascade-add-to-wishlist. Default: Controls, Popover, Toast, ErrorNotice. The root closes the popover on a click anywhere outside it.

PartRendersNothing when
RootThe provider and root element. Props: every AddToWishlist prop, children.
ControlsButton and PickerButton grouped.
ButtonThe save button, toggling the product on the default list.
PickerButtonThe chevron that opens the list picker.Single-list mode with one list.
PopoverThe list picker with "Manage wishlists" at the bottom.Closed.
ToastThe "Added to ..." message.Nothing saved in the last 5 seconds.
ErrorNoticeThe error message.No error.
useData()Hook: the props plus inDefault, showPicker, isBusy, popoverOpen, toast, toggleDefault(), togglePopover(), toggleList(wishlist), manage().

WishlistsModal

Root element: dialog.cascade.cascade-wishlists-modal. This root is the exception: its children are the dialog's content, not a default composition, because a modal has nothing to draw until it is told what goes in it. <WishlistsModal> itself renders Header then Body around its children.

PartRenders
RootThe dialog and its content box. Props: open, onClose, title, children.
HeaderThe title and the close button.
BodyThe scrolling body. Props: children.
useData()Hook: open, title, onClose.

WishlistsModalWidget has two parts of its own, since it owns the open state: WishlistsModalWidget.Trigger (the button) and WishlistsModalWidget.Dialog (the modal, holding <CustomerWishlistsWidget /> unless given children). Pass children to the widget to place them apart.

LoyaltyPanel

Root element: div.cascade.cascade-loyalty. Default: Header, Notices, ProgramTabs, ErrorNotice, HeldRewards, Sections, Birthday, Referral. Without a program the root renders the "no program yet" line (or the error) whatever children it was given.

PartRendersNothing when
RootThe provider and root element. Props: every LoyaltyPanel prop, children.
HeaderProgram name, balance, tier, and the join button or sign-in hint.
NoticesThe draft and paused notices.The program is live.
ProgramTabsThe switcher between programs.One program, or no onSelectProgram.
ErrorNoticeThe error message.No error.
HeldRewards"Your rewards", one row per held reward. Props: children.Not a member, or none held.
HeldRewardRowOne default held reward: name, code, expiry, Cancel.
SectionsTiers, Rules, Offers, StampCards, Rewards in the program's order.
TiersThe tier ladder and progress to the next tier.Section off or no tiers.
Rules"Ways to earn", one row per rule. Props: children.Section off or no rules.
Offers"Offers", one row per rule. Props: children.Section off or no offers.
StampCardsThe stamp cards and the member's progress on each.Section off or no cards.
Rewards"Redeem your points", one row per reward. Props: children.Section off or no rewards.
RuleRowOne default earning rule or offer.
RewardRowOne default redeemable reward, with the confirm step.
BirthdayThe birthday block.Not a member, or no onSaveBirthday.
ReferralThe share link and referral counts.Not a member.
useData()Hook: the props with the program resolved, plus names, member, enabled.
useRule()Hook, inside Rules, Offers or Rewards: the current rule.
useHeldReward()Hook, inside HeldRewards: the current held reward.

A custom row inside any of the list parts should render its own <li>; the list part renders the <ul>.

LoyaltyLauncher

Root element: div.cascade.cascade-loyalty-launcher. Default: Panel, Button. <LoyaltyLauncher> itself puts its children inside Panel.

PartRendersNothing when
RootThe provider and root element. Props: open, onToggle, label, title, position, children.
PanelThe panel with its title bar. Props: children.Closed.
ButtonThe floating pill.
useData()Hook: open, onToggle, label, title, position.

LoyaltyPoints

Root element: a.cascade.cascade-loyalty-points when href is set, otherwise p.cascade.cascade-loyalty-points. Default: Label. The root renders nothing at all when points is zero or less, whatever children it was given.

PartRenders
RootThe link or paragraph. Props: every LoyaltyPoints prop, children.
Label"Earn 250 points with this purchase." or the join variant.
useData()Hook: points, pointsNames, member, href, amount (formatted, such as "250 points"), label.

Connected widget props

The connected widgets take the props below, plus children on every one of them. A connected widget renders nothing until its first response has arrived, so a part never sees a half-loaded root.

WidgetProps
ProductReviewsWidgetproductSlug, pageSize (10), structuredData (true), reviewForm (true), questionSummary (true), productUrl, starSize, filledColor, emptyColor.
ProductRatingWidgetproductSlug, structuredData (false), productUrl, className, starSize, filledColor, emptyColor, hideCount.
ReviewsCarouselWidgetproductSlug, minRating (4 store-wide), maxReviews (10), heading, starSize, filledColor, emptyColor.
MediaGridWidgetproductSlug, minRating, maxItems (12), heading, starSize, filledColor, emptyColor.
CustomerWishlistsWidgetNone.
AddToWishlistWidgetproductSlug, productSnapshot (what a guest save remembers: title, imageUrl, price, productUrl).
WishlistsModalWidgettriggerLabel, title.
LoyaltyWidgetcurrencyCode (USD), siteSlug, emitVisit (true).
LoyaltyLauncherWidgetAs LoyaltyWidget, plus label, title, position (bottom-right or bottom-left).
LoyaltyPointsWidgetprice (whole currency units), currencyCode, siteSlug, className.

What children do not change: ProductReviewsWidget and ProductRatingWidget still emit their structured data, AddToWishlistWidget still renders the manage dialog beside the button, and CustomerWishlistsWidget still switches between the signed-in and guest lists.

Presentational component props

The presentational components' props are the data the connected widget would fetch plus the handlers it would wire up. They are typed in the package; the shapes worth knowing:

  • ProductReviews: averageRating, totalReviews, ratingBreakdown, reviews, hasMore, and optionally summary, reviewAnswers, questions, sort, activeRating, loading, signedInEmail, the star props, and the handlers onLoadMore, onSortChange, onRatingChange, onVote, onSubmitReview, onUploadReviewFile, onAsk, onLoadMoreQuestions, onAnswerQuestion.
  • ReviewsCarousel: reviews, heading, showProduct, the star props.
  • MediaGrid: items, heading, showProduct, hasMore, onLoadMore, loading, the star props.
  • CustomerWishlists: wishlists, activeWishlist, error, loading, sharingEnabled, multipleWishlists, guest, and one handler per action.
  • AddToWishlist: wishlists, loading, pending, error, disabled, multipleWishlists, onToggleDefault, onAddToWishlist, onRemoveFromWishlist, onManageWishlists.
  • LoyaltyPanel: program, programs, loading, error, canJoin, currencyCode, onJoin, onClaim, onCancelReward, onSelectProgram, onSaveBirthday.
  • LoyaltyPoints: points, pointsNames, member, href, className.

Every type is exported from the package root: Review, ReviewSummary, CarouselReview, MediaGridItem, WishlistSummary, WishlistDetail, WishlistItem, AddToWishlistOption, LoyaltyProgram, LoyaltyMember, LoyaltyRule, LoyaltyReward and the rest.

A worked example

packages/example-custom-store in the Cascade repository is a React storefront that uses the connected widgets on every page it belongs on. Its product page lays the reviews widget out from parts: the rating summary, the breakdown and the write button in one row, then the rest in the default order. See Custom integration for the store as a whole.