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.
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.
The carousel¶
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.