# react-stripe-js: Checkout Sessions behind a React provider

> react-stripe-js gives React teams typed wrappers around Stripe.js Elements, and its Checkout Sessions flow renders inside your own layout while Stripe holds the checkout state. Results that name their own state, the two export entries, and the gap between its stated React floor and its own sample are what to read before adopting it.

**stripe/react-stripe-js** — React components for Stripe.js and Stripe Elements

- Repository: https://github.com/stripe/react-stripe-js
- Website: https://stripe.com/docs/stripe-js/react
- Stars: 2,046 · Forks: 346
- Language: TypeScript
- License: MIT
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/stripe-react-stripe-js

## Stripe.js loads behind a promise the provider owns

Wrapping Stripe.js for React is the job here, not replacing it. In the client sample the loader runs once, `loadStripe('pk_test_...')` produces a promise, and that promise is handed to `CheckoutElementsProvider` as its `stripe` prop; everything below the provider reads Elements state through `useCheckoutElements`. No component in the tree touches the Stripe.js script tag, so page layout stays in your own React code while the payment field itself comes from the `PaymentElement` component.

Its audience is narrow and identifiable: teams that already run a server able to call Stripe and want to render a custom checkout page in React, not teams looking for a hosted prebuilt page. TypeScript is the primary language here, the package ships its own declarations, and some of those types are pulled from `@stripe/stripe-js`, which the README tells you to add as a dependency for full TypeScript support. Typings in both packages follow the same versioning policy, so the two move together instead of being pinned apart.

## The install line pulls two packages, not one

Only one install line is given, and it names two packages because the wrapper is not usable without the loader it wraps.

```bash
npm install @stripe/react-stripe-js @stripe/stripe-js
```

The first real step happens on your server, before any React code runs. Create the Checkout Session with trusted product and pricing data, then return only its client secret. The sample below is the body of the `/create-checkout-session` route the client later posts to:

```js
const session = await stripe.checkout.sessions.create({
  ui_mode: 'elements',
  mode: 'payment',
  return_url: 'https://example.com/order/123/complete',
  line_items: [
    {
      price_data: {
        currency: 'usd',
        product_data: {name: 'T-shirt'},
        unit_amount: 1099,
      },
      quantity: 1,
    },
  ],
});
```

Prices come from your database and never from the browser, and the sample throws when `session.client_secret` is missing rather than pushing an undefined value into the client. `return_url` decides where a finished payment lands.

## Every checkout read names its own state, never null

`useCheckoutElements()` returns a value that names its own state, which removes the class of bug where a child renders before Elements is ready. In the sample, `result.type === 'loading'` draws a placeholder, `result.type === 'error'` draws a provider level failure, and only in the success branch does it destructure `checkout`. Submit-time guards repeat that discipline before the handler touches Stripe at all:

```js
if (result.type !== 'success' || !result.checkout.canConfirm) {
  return;
}

setIsSubmitting(true);
setErrorMessage(null);

try {
  const confirmResult = await result.checkout.confirm({
    returnUrl: 'https://example.com/order/123/complete',
  });

  if (confirmResult.type === 'error') {
    setErrorMessage(confirmResult.error.message);
  }
```

A rejected confirm is a value rather than an exception, so the message reaches component state without a thrown error; the `catch` only fires on a real throw, where `error instanceof Error` chooses between the actual message and the fixed `'An unexpected error occurred.'` fallback. A `finally` clause always clears `isSubmitting`, and the submit button is disabled on `!checkout.canConfirm || isSubmitting`, so an incomplete checkout cannot be submitted twice.

## A React v16.8 floor its own example steps over

The README sets the minimum supported version of React at v16.8 and tells readers on anything older to upgrade React or move to the legacy `react-stripe-elements` package. Then the only complete client sample in the repository imports `createRoot` from `react-dom/client` and calls it at the end of the file to mount the tree. `react-dom/client` did not exist at v16.8, so the documented setup cannot run at the documented floor.

Read those two claims as different things and the conflict dissolves: v16.8 is what the components still support, while the tutorial targets a React new enough to have the modern root API. Anyone copying the sample end to end is therefore on a newer React than the requirements line permits, and nothing in the documentation reconciles the pair. No second tier of sample code exists to fall back on either, since `examples/class-components/` and `examples/hooks/` split by component style rather than by React version. Reading 7.0.0 in the version field of package.json, this gap is current rather than historical.

## Two export entries split Elements from checkout

Published resolution runs through two entry points rather than one, and each maps to its own build:

```json
"exports": {
  ".": {
    "require": "./dist/react-stripe.js",
    "import": "./dist/react-stripe.esm.mjs",
    "default": "./dist/react-stripe.esm.mjs",
    "types": "./dist/react-stripe.d.ts"
  },
  "./checkout": {
    "require": "./dist/checkout.js",
    "import": "./dist/checkout.esm.mjs",
    "default": "./dist/checkout.esm.mjs",
    "types": "./dist/checkout.d.ts"
  }
}
```

Underneath sit the older bundler fields (`main`, `module`, `jsnext:main`, `browser`, `browser:min`, `types`) so tools that predate the `exports` map still resolve something. Three details reward a closer look. The `types` condition is written last in both entries, behind `default`, and a separate `typesVersions` block maps `checkout` to `dist/checkout.d.ts` for resolvers that read that older field instead. In the source tree, `checkout.js` and `checkout.d.ts` sit at the repository root next to `rollup.config.js`, so the subpath is visible before a build has run. A `releaseCandidate` flag set to false sits in the same block, and the README does not explain what sets it.

## Five checks stand between an edit and a tarball

The `test` script is not a single runner. It chains five checks, in this order:

```json
"test": "yarn run lint && yarn run lint:prettier && yarn run test:unit && yarn test:package-types && yarn run typecheck",
```

Linting runs as `eslint --max-warnings=0 '{src,examples}/**/*.{ts,tsx,js}'`, so a warning inside an example directory fails the run instead of sliding through. Prettier lists differing files, `jest` covers the units, `yarn run typecheck` hands the job to the compiler, and `test:package-types` runs `attw --pack .` against the packed tarball to inspect the types a consumer resolves. Building then clears `dist` with `rimraf dist`, bundles with `rollup -c --bundleConfigAsCjs`, and finishes with `checkimport`, which points at `scripts/check-imports`. Example apps run locally through Storybook on port 6006, started by `storybook dev -p 6006`, with a committed `.storybook/` directory beside it. Worth knowing before reproducing any of it: every script shells out to yarn and a `yarn.lock` is committed, yet the install line in the README is npm.

## Payment Intents and the legacy package are the two exits

Two other paths are named, and each costs something different. If you want to own every part of the checkout, the lower-level Payment Intents API gives finer-grained control, and the README says plainly that it requires significantly more code and ongoing maintenance. Nothing in this package changes that trade, since a new custom checkout page is steered toward Checkout Sessions with `ui_mode: 'elements'`, which lets you combine Elements with your own React layout while Stripe manages checkout state. If you cannot move to React v16.8 or later, the same page points at the legacy `react-stripe-elements` package and keeps its documentation, with a migration file at `docs/migrating.md`.

The clearest wrong-tool case here is documentation rather than architecture. The README's only complete sample imports from the `@stripe/react-stripe-js/checkout` subpath, and no sample in it exercises the main entry, so a team that needs the package's other components has to read the reference documentation instead of copying a working page. The README also does not document a webhook flow, a retry path or a rollback for a failed session creation; `return_url` is the only completion route the sample shows.

## Issues, not pull requests, decide what changes

Contributions run one way: the contributing note says the project is maintained by Stripe and does not accept external pull requests, and it asks for feedback and ideas through the issue tracker, which leaves a fork or an internal patch as the realistic route when you need behaviour the maintainers will not take. Two facts frame the upgrade cost. Typings follow the same versioning policy as `@stripe/stripe-js`, so a bump in one package pulls a bump in the other, and the package does not give you full TypeScript support without that second dependency present. The code sits under an MIT licence, which asks that the notice travel with copies and imposes no conditions on your own source.

The last push landed on 2026-10-01, and the version field in package.json reads 7.0.0, with v6.12.0 dated 2026-09-23 and v6.11.0 dated 2026-09-21 behind it, so three published versions arrived inside ten days. For a library that others re-export into their own bundles, that cadence matters more than any single version, because the major in that window is already the one a fresh install resolves to.

## Conclusion

Adopt it when your checkout page is React, your server can create a Checkout Session with `ui_mode: 'elements'`, and you are willing to let Stripe hold the checkout state. Leave it when you need to own every payment step, since the Payment Intents path costs more code and upkeep by the README's own account. First check the React version in your app against the sample, because its `createRoot` import needs a newer React than the stated v16.8 floor. Everything the README leaves open, including rollback and webhooks, you will be writing yourself.

## FAQ

### What React version does react-stripe-js require?

The README sets the minimum supported React version at v16.8 and tells readers on older versions to upgrade React, or to use the legacy `react-stripe-elements` package instead. The complete client sample in the same README imports `createRoot` from `react-dom/client`, an entry point that did not exist at that floor.

### How do I get a Checkout Session into a react-stripe-js page?

Create the session on your server with `ui_mode: 'elements'` and `mode: 'payment'`, then return `session.client_secret` to the browser as `clientSecret`. Pass that promise as the `clientSecret` option to `CheckoutElementsProvider` and read the checkout object through `useCheckoutElements`.

### Which npm packages do I install for react-stripe-js?

The README's install line installs `@stripe/react-stripe-js` and `@stripe/stripe-js` together, and it names the second one again as required for full TypeScript support because some types are pulled from it. Typings in both packages follow the same versioning policy.

### When should a checkout page use the Payment Intents API instead of react-stripe-js Checkout Sessions?

The README steers new custom checkout pages to Checkout Sessions with `ui_mode: 'elements'`, which lets you combine Elements with your own React layout while Checkout Sessions manages checkout state. The lower-level Payment Intents API offers finer-grained control but needs significantly more code and ongoing maintenance.

### Does react-stripe-js accept outside contributions?

No. The contributing note says the project is maintained by Stripe and does not accept external pull requests, so feedback and ideas go to the issue tracker instead of a patch.

## Sources

- [License: MIT](https://github.com/stripe/react-stripe-js/blob/master/LICENSE)
- [Project website](https://stripe.com/docs/stripe-js/react)
- [README](https://github.com/stripe/react-stripe-js/blob/master/README.md)
- [Releases](https://github.com/stripe/react-stripe-js/releases)
- [stripe/react-stripe-js on GitHub](https://github.com/stripe/react-stripe-js)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/stripe-react-stripe-js
