Skip to content

React SDK

@withfeedback/react is a real React package: components, hooks and server-side rendering. It talks to the same public endpoint the script tag does, and it exists for one reason the script tag cannot serve.

npm install @withfeedback/react

React 18.2 or 19, no runtime dependencies, no CSS import.

Why not the script tag

The script tag renders in the browser, into a Shadow DOM. That is the right choice for a site nobody controls, and the wrong one for two things:

  • Machines that read your page. Search engines run JavaScript slowly and not always. Most AI crawlers do not run it at all. Testimonials that only exist after JavaScript are close to invisible to both.
  • The first paint. An empty space, then a jump.

This package fixes both — but only through @withfeedback/react/server. The client entry point is a nicer script tag; the server entry point is the reason the package exists.

Three entry points

Import Runs where For
@withfeedback/react/server Server Component, getServerSideProps, a route handler, a build script Testimonials in the served HTML
@withfeedback/react The browser Pagination, the carousel
@withfeedback/react/collect The browser Feedback, NPS and survey forms

They are separate so importing display code on a server does not drag hooks and browser code in with it — and so a page that only shows testimonials never pays for a camera, a form and an upload.

Testimonials in your HTML

// app/page.tsx — a React Server Component
import { fetchTestimonials, TestimonialWall } from "@withfeedback/react/server";

export default async function Page() {
  const payload = await fetchTestimonials({ widgetId: "YOUR_WIDGET_ID" });
  return <TestimonialWall payload={payload} />;
}

TestimonialWall and TestimonialCard are pure and hook-free, so they render in a Server Component as happily as in the browser.

The Pages Router works the same way from getServerSideProps or getStaticProps.

TestimonialCarousel holds the current slide in state, so it is a Client Component and is not exported from /server. A Server Component still fetches and passes the payload down, and every slide is prerendered into the HTML:

import { fetchTestimonials } from "@withfeedback/react/server";
import { TestimonialCarousel } from "@withfeedback/react";

const payload = await fetchTestimonials({ widgetId: "YOUR_WIDGET_ID" });
return <TestimonialCarousel payload={payload} label="What people say" />;

Autoplay comes from the widget's own configuration. It pauses while the pointer is over the carousel or focus is inside it, stops entirely for prefers-reduced-motion: reduce, and off-screen slides are inert so a video's controls are never reachable by keyboard while invisible.

Pagination in the browser

"use client";
import { useTestimonials } from "@withfeedback/react";

const { items, loading, error, hasMore, loadMore } = useTestimonials({
  widgetId: "YOUR_WIDGET_ID",
});

Pass a server payload as initialPayload and the hook renders it immediately, hydration matches, and no second request goes out.

Collecting

"use client";
import { CollectWidget } from "@withfeedback/react/collect";

<CollectWidget payload={payload} publicKey="YOUR_PUBLIC_KEY" />

One component draws all three collect widgets — feedback, survey and NPS. Which one you get comes from the widget you point it at.

The collect panel is styleable, on purpose

It renders into the normal DOM, not a Shadow DOM. The script tag isolates itself because it lands on sites nobody controls; you installed a package and own your page. Every part carries a stable wf- class name, and unstyled drops the inline defaults entirely so the panel can be made to look like the rest of your product.

The video recorder, the honeypot, the minimum-fill timer, the consent wording and the three-step upload are the same code the script tag runs, not a second implementation. Both behave identically.

Styling

No CSS import, and nothing to configure. Defaults are inline, every part has a stable class name (wf-card, wf-stars, wf-text, wf-person, wf-panel, wf-submit, …), and unstyled removes the defaults so your own CSS is the only thing in play.

<TestimonialWall payload={payload} />                        // inline defaults
<TestimonialWall payload={payload} unstyled className="…" /> // your CSS only

Types

The published types are generated from this API's OpenAPI schema, not written by hand, so a payload field cannot drift from what the server sends. They resolve under node16 module resolution with skipLibCheck off.

Two behaviours to know about

A server-rendered wall records no impressions

The impression beacon needs a view token. The server mints a fresh one per response and it expires in ten minutes, so fetchTestimonials strips it — its return type does not even have the field. Putting a token into HTML would hand every visitor the same dead token. An uncounted impression is better than a wrong one.

Use the client entry point on pages where the count matters.

Paginate by de-duplicating on id

This API pages by offset. Approving one newer testimonial between two requests shifts every row by one, so page 2 can repeat a row page 1 already showed. useTestimonials handles it; fetchTestimonials hands you the raw page and leaves it to you.

Branding

Whether the "Powered by withfeedback" line appears is computed by the server from your plan and travels in the payload. There is no prop that turns it off.