Skip to content
Twilight React Playground
ثيم رائدaren

Wishlist

route-moduleBeginnerserverbrowser

The signed-in customer's saved products, starting from the ?page= in the address and loading further pages as they scroll.

import { Wishlist, WishlistPage, wishlistLoader, WishlistPageProps } from '@salla.sa/twilight-theme-engine/routes/account';

In plain words

/account/wishlist lists the products the customer saved with the heart button, each with its price, an add-to-cart button and a remove button. More products load as they scroll.

Signature

const Wishlist: {
  readonly id: 'customer.wishlist';
  readonly loader: (
    ctx: LoaderContext,   // { search?: { page?: string | number }; locale?: string }
    extend?: (data: WishlistPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
  ) => Promise<WishlistPageProps>;
  readonly head: (ctx: TwilightContext, data: WishlistPageProps) => HeadDescriptor;
  readonly Component: React.LazyExoticComponent<(props: WishlistPageProps) => JSX.Element>;
};

const WishlistPage: typeof Wishlist.Component;
function wishlistLoader(ctx: LoaderContext): Promise<WishlistPageProps>;

interface WishlistPageProps {
  page: Page;                   // { title, slug: 'wishlist' }
  query: { page: number };
  products: Product[];
  pagination: RoutePagination;  // only next is set
}

Example

app/routes/account.wishlist.tsx (generated)
import { createFileRoute } from '@tanstack/react-router';
import { Wishlist } from '@salla.sa/twilight-theme-engine/routes/account';
import type { WishlistPageProps } from '@salla.sa/twilight-theme-engine/routes/account';
import { CustomerPageSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/account/wishlist')({
  validateSearch: (search: Record<string, unknown>) => {
    const page = Number(search.page) || 1;
    return page > 1 ? { page } : {};
  },
  loaderDeps: ({ search }) => ({ page: search.page }),
  loader: ({ deps, params }): Promise<WishlistPageProps> =>
    Wishlist.loader({ search: { page: deps.page ?? 1 }, locale: params.locale }),
  head: withHead(Wishlist),
  pendingComponent: () => <CustomerPageSkeleton />,
  component: WishlistComponent,
});

function WishlistComponent() {
  const data: WishlistPageProps = Route.useLoaderData();
  return <Wishlist.Component {...data} />;
}

How it behaves

  • wishlistLoader reads Number(ctx.search?.page) || 1 and wraps the API call in orUnauthorized: a 401 or 403 shows the 401 page, which is what a guest gets (the wishlist API answers a request without a customer token with 401).

  • The generated route keeps ?page= in the address only above 1, so page 1 has a clean URL.

  • WishlistPage loads the following pages in the browser as the customer scrolls, starting at query.page + 1. Each card's remove button calls useWishlist().remove(id). Slots: customer:wishlist.items.start and customer:wishlist.items.end.

  • head sets the title and hreflang alternates only.

Gotchas

  • pagination holds only next. current and previous exist on RoutePagination and are shown filled in packages/theme-engine/docs/route-pagination.md, but no loader sets them.

Related

Source and docs