React app integration
Add the Donations Embed to any React 18 / 19 application using the typed wrapper component.
React app integration
This guide adds the Donations Embed to a React 18 or 19 application using the typed wrapper component, <FlockDonate>. It works with Vite, Create React App, Remix client routes, Astro's React island, and any other React tooling that builds for the browser.
For Next.js (App Router or Pages Router), see the dedicated Next.js guide — it covers the SSR-vs-CSR considerations specific to that framework.
Step 1: Generate a sandbox publishable key
In the 1st Flock Hub:
- Open Donations Embed → Keys.
- Click Create key pair.
- Set environment to Sandbox and name it (e.g.
My React app - sandbox). - Copy the publishable key (starts with
pk_test_).
Step 2: Install the wrapper
npm install @1st-flock/donations-embed @1st-flock/donations-embed-react
# or
pnpm add @1st-flock/donations-embed @1st-flock/donations-embed-react
# or
yarn add @1st-flock/donations-embed @1st-flock/donations-embed-react
The wrapper's peer dependencies are React 18 or 19 and @1st-flock/donations-embed.
Step 3: Drop in the component
import { FlockDonate } from "@1st-flock/donations-embed-react";
// Replace pk_test_REPLACE_ME with your publishable key.
const PUBLISHABLE_KEY = "pk_test_REPLACE_ME";
export function GivePage() {
return (
<main className="give-page">
<h1>Give to Example Church</h1>
<p>Thank you for partnering with us. Every gift goes directly toward our mission.</p>
<FlockDonate
publishableKey={PUBLISHABLE_KEY}
mode="inline"
/>
</main>
);
}
Importing FlockDonate automatically registers the underlying <flock-donate> web component on window.customElements. There is no separate setup step.
In production, read the publishable key from your environment variables instead of hard-coding it (it is safe to expose to the browser, but checking it into source control is bad hygiene):
const PUBLISHABLE_KEY = import.meta.env.VITE_FLOCK_PUBLISHABLE_KEY!;
// or for Create React App:
const PUBLISHABLE_KEY = process.env.REACT_APP_FLOCK_PUBLISHABLE_KEY!;
Step 4: Test the donation flow (sandbox)
- Run your dev server:
npm run dev(ornpm start). - Navigate to the page where you placed
<FlockDonate>. - Walk through the donor flow.
- At the payment step, use these sandbox card details:
See Getting Started → Sandbox testing for the canonical sandbox card details (4111 1111 1111 1111, exp 10/29, CVV 123).
- Submit. You should see a confirmation screen with an ID like
don_8a1b9c4d. - Open the Hub's Donations tab — your test donation appears within a few seconds, marked with a TEST badge.
Step 5: Switch to live
When you are ready to take real donations:
- Generate a live key pair in the Hub (environment set to Live).
- Update the environment variable holding the publishable key.
- Re-deploy.
Customisation
React props
Every web-component attribute is exposed as a React prop. Comma-list attributes accept arrays or strings.
<FlockDonate
publishableKey={PUBLISHABLE_KEY}
mode="modal"
triggerLabel="Give"
funds={["general", "building", "missions"]}
defaultFund="general"
coverFees
theme="modern"
successMode="callback"
suggestedAmounts={[25, 50, 100, 500]}
minAmount={5}
maxAmount={5000}
language="en"
/>
React event handlers
The wrapper translates the five CustomEvents into typed on* props:
import { FlockDonate } from "@1st-flock/donations-embed-react";
export function GivePageWithAnalytics() {
return (
<FlockDonate
publishableKey={PUBLISHABLE_KEY}
mode="inline"
onOpened={(detail) => {
analytics.track("donation_started", { source: detail.source });
}}
onStep={(detail) => {
analytics.track("donation_step", { step: detail.step, total: detail.total });
}}
onComplete={(detail) => {
analytics.track("donation_completed", {
confirmationId: detail.confirmationId,
amount: detail.amount,
fund: detail.fund.name,
frequency: detail.frequency,
});
}}
onFailed={(detail) => {
analytics.track("donation_failed", { code: detail.code });
console.error("Donation failed:", detail.userMessage);
}}
onClosed={(detail) => {
analytics.track("donation_closed", { stepReached: detail.stepReached });
}}
/>
);
}
Imperative open / close
For modal and drawer modes, the wrapper forwards ref to the underlying <flock-donate> HTMLElement so you can imperatively open or close the flow:
import { useRef } from "react";
import { FlockDonate, type FlockDonateElement } from "@1st-flock/donations-embed-react";
export function CustomLauncher() {
const ref = useRef<FlockDonateElement>(null);
return (
<>
<button onClick={() => ref.current?.open()}>Open custom launcher</button>
<FlockDonate
ref={ref}
publishableKey={PUBLISHABLE_KEY}
mode="modal"
// The default trigger button is hidden because we are launching imperatively.
triggerLabel=""
/>
</>
);
}
Per-fund pages
A common pattern: a "Give" landing page that links to per-fund pages with the embed pre-configured for each fund.
function FundDonationPage({ fundId, fundName }: { fundId: string; fundName: string }) {
return (
<main>
<h1>Give to {fundName}</h1>
<FlockDonate
publishableKey={PUBLISHABLE_KEY}
funds={[fundId]}
defaultFund={fundId}
/>
</main>
);
}
The full attribute reference is in Browser Embed.
Common pitfalls
Note: TypeScript complains that
FlockDonateis not a valid JSX element. Make sure you imported it from@1st-flock/donations-embed-react, not from@1st-flock/donations-embed. The latter exports the underlying web component module, not a React component.
Note: The embed does not render and I see "publishable-key required" in the console. You either passed an empty string, or you are reading from an environment variable that is not set. Vite environment variables must be prefixed
VITE_to be exposed to the client; CRA variables must be prefixedREACT_APP_. Confirm the variable is exposed before debugging anything else.
Note: My UI library's button styles override the launcher button. The launcher renders inside Shadow DOM and is isolated from your CSS. If your launcher button looks unstyled, you are looking at a different button —
<FlockDonate mode="modal">renders the launcher itself. To wrap it in your own button, use the imperativeref.current.open()pattern shown above and put your custom button outside the embed.
Note:
onCompletefires twice in development. That is React 18+ Strict Mode replaying effects. The web component itself only fires the event once; React's Strict Mode mounts twice in dev. The duplicated call goes away in production builds.
Note: The component re-mounts every time I change a prop. That is normal. The web component handles attribute changes internally without remounting; if you see DOM remounting, you have a
keyprop bound to the changing value, or the parent component is unmounting and remounting on its own (often caused by inline arrow functions inuseEffectdeps).
Next steps
- For Next.js specifically, see the dedicated Next.js guide — it covers SSR considerations.
- Read the full Browser Embed reference for every attribute and event.
- Receive donation events on your server — see Webhooks.