# Hooks: useProductBundles to useSubmitReview (/themes/sdk/reference/product/hooks-2)

```ts
import { … } from '@poltura/theme-sdk/product';
```

## `useProductBundles` [#useproductbundles]

The bundles that include the given products, by product id.

```ts
useProductBundles(by: {
    readonly productIds: readonly string[];
}, options?: Omit<QueryOptions, 'suspense'>): QueryResult<ReadonlyMap<string, readonly Bundle[]>>
```

**Parameters**

- `by: { readonly productIds: readonly string[]; }`: The products to find bundles for.
- `options?: Omit<QueryOptions, 'suspense'>`: Whether the read runs, and whether it keeps the previous answer while loading.

**Returns:** A map from each product id to its bundles; a product without bundles is absent.

**Example**

```tsx
import { useProductBundles } from '@poltura/theme-sdk/product';

export function BundleCount({ productId }: { productId: string }) {
  const { data } = useProductBundles({ productIds: [productId] });
  return <span>{data?.get(productId)?.length ?? 0}</span>;
}
```

## `useRelatedProducts` [#userelatedproducts]

Related product cards: the merchant's picks, then automatic recommendations when switched on, then popular products of the category and the store.

```ts
useRelatedProducts(query: {
    readonly limit?: number;
} | undefined, options: SuspenseQueryOptions): SuspenseQueryResult<readonly ProductCard[]>
useRelatedProducts(query?: {
    readonly limit?: number;
}, options?: QueryOptions): QueryResult<readonly ProductCard[]>
```

The store's most popular products fill what the picks, the automatic recommendations and the category leave free, and outside a product. The picks are read from the nearest product, so place the code inside the product page or a `ProductScope`. `useProducts` is unchanged: a section that still reads it lists exactly what it asks for.

**Parameters**

- `query: { readonly limit?: number; } | undefined`: How many cards to return: `limit`, at most this many, 8 when omitted.
- `options: SuspenseQueryOptions`: Suspense, idling and keeping the previous answer while new inputs load.

**Returns:** The cards under "You may also like" for the nearest product, each product once; the product itself is never among them, and a pick that is hidden, unlisted or unpublished is left out.

**Example**

```tsx
import { useRelatedProducts } from '@poltura/theme-sdk/product';

export function Related() {
  const { data } = useRelatedProducts({ limit: 4 }, { suspense: true });
  return <ul>{data.map((card) => <li key={card.id}>{card.title}</li>)}</ul>;
}
```

## `useWishlist` [#usewishlist]

The shopper's favourites: how many, which products, and a toggle. A signed-in shopper's list lives in their account and a guest's in the browser.

```ts
useWishlist(): WishlistState
```

Use [`WishlistButton`](/themes/sdk/reference/product/parts-2#wishlistbutton) for a plain heart; this hook is for a theme that draws its own.

**Returns:** The favourites, up to date as products are saved and removed anywhere on the page.

**Example**

```tsx
import { useWishlist } from '@poltura/theme-sdk/product';

export function SavedCount() {
  const { enabled, count } = useWishlist();
  return enabled ? <span>{count}</span> : null;
}
```

## `useOptionalProductPage` [#useoptionalproductpage]

The product page's view, or null where no product page is drawn, such as a product shown inside another page.

```ts
useOptionalProductPage(): ProductPageView | null
```

**Returns:** The page's product, trail, highlights, specifications, guides, warranty, safety, coverage and reviews, or null.

**Example**

```tsx
import { useOptionalProductPage } from '@poltura/theme-sdk/product';

export function Highlights() {
  const page = useOptionalProductPage();
  if (page === null || page.highlights.length === 0) return null;
  return <p>{page.highlights.join(', ')}</p>;
}
```

## `useProductPage` [#useproductpage]

The product page with everything it shows beyond the product: the trail, highlights, specifications, guides, warranty, safety, coverage and the first reviews.

```ts
useProductPage(): ProductPageView
```

Throws outside the product page; place it under the product page's main section.

**Returns:** The page's view.

**Example**

```tsx
import { useProductPage } from '@poltura/theme-sdk/product';

export function Highlights() {
  const { highlights } = useProductPage();
  return (
    <ul>
      {highlights.map((line) => (
        <li key={line}>{line}</li>
      ))}
    </ul>
  );
}
```

## `useProductTabs` [#useproducttabs]

The tabs the nearest product page shows, in the merchant's order.

```ts
useProductTabs(): readonly ProductTab[]
```

A built-in tab with nothing to show for the product, a text tab without words and a table tab whose table the product does not have are left out, so every tab has something to draw. A store that never arranged its tabs gets the four built-in ones.

**Returns:** The tabs; empty outside a product page or when there is nothing to show.

**Example**

```tsx
import { ProductTabContent, useProductTabs } from '@poltura/theme-sdk/product';

export function AllTabs() {
  return useProductTabs().map((tab) => (
    <section key={tab.id}>
      <h3>{tab.label}</h3>
      <ProductTabContent tab={tab} />
    </section>
  ));
}
```

## `useProductTableLinks` [#useproducttablelinks]

The nearest product's link tables, sorted onto its variant picker options.

```ts
useProductTableLinks(): ProductTableLinks
```

**Returns:** The tables per option and the ones left over.

**Example**

```tsx
import { ProductTableLink, useProductTableLinks } from '@poltura/theme-sdk/product';

export function SizeGuides() {
  const links = useProductTableLinks();
  return links.standalone.map((table) => <ProductTableLink key={table.id} table={table} />);
}
```

## `useProductTables` [#useproducttables]

The tables attached to the nearest product, optionally only those the merchant placed one way.

```ts
useProductTables(placement?: ProductTablePlacement): readonly ProductTable[]
```

**Parameters**

- `placement?: ProductTablePlacement`: Keeps the tables placed as a link, a tab or a section; every table when omitted.

**Returns:** The tables, strongest first; empty when the product has none.

**Example**

```tsx
import { ProductTableView, useProductTables } from '@poltura/theme-sdk/product';

export function SizeTables() {
  return useProductTables('section').map((table) => (
    <ProductTableView key={table.id} table={table} />
  ));
}
```

## `useReviews` [#usereviews]

The reviews of one or several products, one page at a time, with the review score over all of them.

```ts
useReviews(by: ReviewsBy, page?: PageRequest, options?: Omit<QueryOptions, 'suspense'>): QueryResult<ReviewPage>
```

**Parameters**

- `by: ReviewsBy`: Which products' reviews to read.
- `page?: PageRequest`: Which page to read and how many reviews it holds; the first three reviews by default.
- `options?: Omit<QueryOptions, 'suspense'>`: Whether the read runs, and whether it keeps the previous page while the next loads.

**Returns:** The page of reviews, newest first, once it loads.

**Example**

```tsx
import { useReviews } from '@poltura/theme-sdk/product';

export function LatestReviews({ productId }: { productId: string }) {
  const { data } = useReviews({ productId }, { limit: 5 });
  return (
    <ul>
      {data?.reviews.map((review) => (
        <li key={review.id}>
          {review.title} {review.sourceLabel}
        </li>
      ))}
    </ul>
  );
}
```

## `useSubmitReview` [#usesubmitreview]

Sending a shopper's review of a product, with the bot check and the refresh of the shown reviews.

```ts
useSubmitReview(): SubmitReviewState
```

The bot check and the moderation are host-owned: a review lands in the merchant's approval queue unless it carries an invitation code.

**Returns:** The `submit` action and where the last send stands.

**Example**

```tsx
import { useSubmitReview } from '@poltura/theme-sdk/product';

export function QuickReview({ productId, productHandle }: { productId: string; productHandle: string }) {
  const review = useSubmitReview();
  return (
    <button
      type='button'
      disabled={review.status === 'submitting'}
      onClick={() =>
        void review.submit({
          productId,
          productHandle,
          author: 'Anna',
          email: 'anna@example.com',
          rating: 5,
          title: 'Lovely',
          body: 'Exactly as described, fast delivery.',
        })
      }
    >
      Send
    </button>
  );
}
```
