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

Testimonials

route-moduleBeginnerserverbrowser

The store reviews page, whose loader only sets a title while Salla's comments web component loads and sorts the reviews in the browser.

import { Testimonials, TestimonialsPage, testimonialsLoader, TestimonialsPageProps } from '@salla.sa/twilight-theme-engine/routes/testimonials';

In plain words

Shoppers review the store itself, not only its products. /testimonials lists those reviews with a sort menu (latest, oldest, top rated, low rated). The list is Salla's ready-made comments web component, a custom HTML element that fetches and draws its own content, so the server sends only the page title. See it on Built-in pages.

Signature

const Testimonials: {
  readonly id: 'testimonials';
  readonly loader: (
    ctx: { locale?: string },
    extend?: (data: TestimonialsPageProps, ctx: { params: {} }) => Record<string, unknown> | Promise<Record<string, unknown>>
  ) => Promise<TestimonialsPageProps>;
  readonly head: (ctx: TwilightContext, data: TestimonialsPageProps) => HeadDescriptor;
  readonly Component: React.LazyExoticComponent<(props: TestimonialsPageProps) => JSX.Element>;
};

const TestimonialsPage: typeof Testimonials.Component;
function testimonialsLoader(ctx: { locale: string }): Promise<TestimonialsPageProps>;

interface TestimonialsPageProps {
  page: Page;   // title: the blocks.home.testimonials translation, or 'Customer Reviews'
}

Example

app/routes/testimonials.tsx (generated)
import { createFileRoute } from '@tanstack/react-router';
import { Testimonials } from '@salla.sa/twilight-theme-engine/routes/testimonials';
import type { TestimonialsPageProps } from '@salla.sa/twilight-theme-engine/routes/testimonials';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/testimonials')({
  loader: ({ params }): Promise<TestimonialsPageProps> =>
    Testimonials.loader({ locale: params.locale }),
  head: withHead(Testimonials),
  component: TestimonialsComponent,
});

function TestimonialsComponent() {
  const data: TestimonialsPageProps = Route.useLoaderData();
  return <Testimonials.Component {...data} />;
}

How it behaves

  • testimonialsLoader makes no request and ignores locale. Testimonials.loader requires its context object ({} is enough) and passes locale ?? 'ar' on.

  • head sets the title and hreflang alternates only.

  • The sort menu starts from ?sort= in the address (default latest). An effect writes the current value back with history.replaceState, on mount and on every change, so opening the page adds ?sort=latest to the address.

  • useComments remounts the comments web component when a review is added. The page has no hook slots.

  • Lazy: outside a route, wrap TestimonialsPage in <Suspense>.

Gotchas

  • TestimonialsPage reads window.location.search while it renders (in a useState initializer, packages/theme-engine/src/routes/testimonials/TestimonialsPage.tsx). The server has no window, so the server render of the page throws window is not defined. In the generated route the router's Suspense boundary catches that: React sends the pending skeleton and renders the page in the browser, logging the error. Anywhere else, render the component only in the browser, for example behind useIsClient().

  • It writes ?sort= with history.replaceState directly, outside the router, on mount and on every change. The router's own location.search never contains that sort, and a component that reuses TestimonialsPage rewrites the address of whatever page it is on.

Related

Source and docs