Accept a payment with Vite, React Stripe, and the Payment Element
Tags: stripe, react, webdev, tutorial
Published: 2022-07-04 09:56:03

## Introduction

Recently, I’ve been learning the basics of React since it’s a central part of Stripe Apps. After taking Brian Holt’s excellent course on frontendmasters [“Complete intro to React V7”](https://frontendmasters.com/courses/complete-react-v7/) I decided to see if I could use those fundamentals to build a site to accept a payment using React, [React Stripe](https://stripe.com/docs/stripe-js/react), and the [Payment Element](https://stripe.com/docs/payments/payment-element). In order to try to learn as much as I could about the tooling, I opted to not use anything other than [Vite’s](https://vitejs.dev/) (a frontend development and build tool built by the creator of Vue) scaffolding tool to create a basic project and go from there.

## Follow along

The completed demo is [available on GitHub](https://github.com/matthewling-stripe/vite-react-stripe) if you would like to clone the project.

## What you'll learn

In this post you’ll learn how to use the Payment Element with React to accept payments. The Payment Element is an embeddable UI component that lets you accept more than 18 (and growing!) payment methods with a single integration.  To achieve this we’ll leverage Vite, Fastify, and React Stripe.

## High level overview

In this end to end integration we’ll:

1. Start a brand new Vite project
1. Create a Checkout component to initialize a payment flow
1. Create a simple Node backend to return a publishable key and create a Payment Intent
1. Run both the Vite server and the Node server concurrently
1. Create a Checkout Form component to render the Payment Element
1. Confirm the Payment Intent

## Versioning

The versions of all dependencies at the time of writing can be seen in the [`package.json`](https://github.com/matthewling-stripe/vite-react-stripe/blob/main/package.json) file in the repo. Since I’m a beginner with React, I took the chance to install the most recent versions and everything worked fine, but I understand that getting version compatibility right can be a challenge.

## Vite

[Vite](https://vitejs.dev/) is a development server and build tool that supports different frontend frameworks like React, Vue and Svelte. It supports hot reloading code while developing and can also build your code for production. I’ll just be using Vite to stand up a development project. I used Parcel (which just works out of the box) during my first forays into React, but Vite is an alternative that works very well and is also used on Glitch where I’ll host my final project.

## Prerequisites

For this demo, we’ll use Node version `16.10.0`, and npm version `7.24.0`. You also need a basic understanding of React components, useState, useEffect, and a Stripe account which you can [sign up for here](https://dashboard.stripe.com/register).

### Starting a new project

```bash
npm create vite@latest
```

When prompted, I selected the default project name of `vite-project` and used the standard React framework and variant.

Now we’ll `cd` into the project and we’ll specify that we don’t want to use React 18, but rather 17. At the time of writing, React 18 hasn’t been fully GA’d and also there are some new changes with `useEffect` and `StrictMode` that I’ll avoid for this demo.

In `package.json` change `react` `react-dom` `@types/react` and `@types/react-dom` packages to `^17.0.2`.

```json
"react": "^17.0.2",
"react-dom": "^17.0.2"

"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2"
```

Now we’ll install dependencies and run the dev server.

```shell
npm install
npm run dev
```

At this point, the code will actually not fully work because the boilerplate code that Vite generated is for React 18 and not React 17 which we just specified. If you navigate to http://localhost:3000/ (the standard port for Vite), in fact we'll see this error:

```text
[plugin:vite:import-analysis] Failed to resolve import "react-dom/client" from "src/main.jsx". Does the file exist?
```

The file that we need to fix is `main.jsx`. Running this command will nevertheless start  a local development server on port 3000, but again we need to make some fixes before we’ll see anything.

We’ll replace the code in `main.jsx` with this variant:

```javascript
import React from "react";
import { render } from "react-dom";
import App from "./App.jsx";

const container = document.getElementById("root");
render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  container
);
```

Not a huge amount has changed, but let’s review the differences. Firstly, on line two we import the `render` function from `react-dom` instead of importing `ReactDOM` from `react-dom/client`. Secondly, we use that `render` function to render the App component rather than using `createRoot` from the new React 18 root API.

The site should now hot reload and we see our friendly React page with the counter. If not, restart the server and reload the page.

![React screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/828vk6u4l79phpt6mrog.png)
 

### Adding a Checkout component

Let’s jump into the `App` component and start building our own checkout. Our `App` will render our Checkout component, so we’ll remove the boilerplate code and replace it with this:

```javascript
import Checkout from "./Checkout.jsx";

function App() {
  return <Checkout />;
}

export default App;
```

But, we’ll receive an error since we haven’t created the Checkout component yet.

So, let’s create that! Create `Checkout.jsx` in the `src` folder. Before we write our imports, let’s install the required Stripe dependencies:

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

We’ll also install `axios` to help with making calls to a backend server:


```bash
npm install --save axios
```

Now let’s import the things that we need in the Checkout component:

```javascript
import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
```

Let’s discuss these imports and their uses:

* We will need `useEffect` when the component first renders, to fetch data from a backend API with axios, specifically to create a Payment Intent
* We’ll leverage `useState` to set a client secret from the Payment Intent and a boolean `loading` state
* We’ll use the Elements provider to render the Payment Element on our CheckoutForm (we’ll code this later)
* And we’ll import `loadStripe` to actually load Stripe.js on our page

Let’s start with a React function component that just renders a `h1` in a `div`.

```javascript
import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

const Checkout = () => {
  return (
    <div>
      <h1>Checkout</h1>
    </div>
  );
};

export default Checkout;
```

![Checkout heading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nmswwdgknn802kt5pfk9.png)

Next, we’ll set up our state handling for a client secret and a `loading` boolean value using `useState`:

```javascript
import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

const Checkout = () => {
  const [clientSecretSettings, setClientSecretSettings] = useState({
    clientSecret: "",
    loading: true,
  });

  return (
    <div>
      <h1>Checkout</h1>
    </div>
  );
};

export default Checkout;
```

### Setting up a backend

To setup a simple backend to interact with the Stripe API we’ll perform the following:

1. Install the require dependencies, in this case `dotenv`, `fastify` and `stripe`
1. Setup our keys in a `.env` file (used by dotenv)
1. Create a `server.js` for two backend routes
1. Configure Vite to proxy calls to the backend
1. Run both the Vite development server and the Node backend at the same time using the `concurrently` package

We’ll need to create a simple backend that will return the Stripe publishable key to the frontend and call the Stripe API to create a Payment Intent. For this demo, we’ll use Fastify as a lightweight server and configure our Stripe keys using `dotenv`. Let’s install those dependencies:


```bash
npm install --save dotenv fastify stripe
```

In the root of the project, we’’ll create a file named `.env` and configure the Stripe test secret key and test publishable key. Your test keys can be found in the dashboard in the Developers section under API keys. They begin with `sk_test` and `pk_test` respectively.

```text
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
```

Also in the root of the project we’ll create a `server.js` file for our backend code.

```javascript
require("dotenv").config();

// Require the framework and instantiate it
const fastify = require("fastify")({ logger: true });
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

// Fetch the publishable key to initialize Stripe.js
fastify.get("/publishable-key", () => {
  return { publishable_key: process.env.STRIPE_PUBLISHABLE_KEY };
});

// Create a payment intent and return its client secret
fastify.post("/create-payment-intent", async () => {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1099,
    currency: "eur",
    payment_method_types: ["bancontact", "card"],
  });

  return { client_secret: paymentIntent.client_secret };
});

// Run the server
const start = async () => {
  try {
    await fastify.listen(5252);
    console.log("Server listening ... ");
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();
```

Let’s dissect this backend code. First, we use `dotenv` to configure the Stripe API keys that we included in the `.env` file earlier. Then we instantiate both Fastify and Stripe. We need two routes for this demo, one GET route to send the publishable key to the frontend for Stripe.js, and one POST route to create a Payment Intent, and return the client secret to the frontend for the Payment Element. Our Payment Intent will be created to allow payment with cards and Bancontact. Finally, we start the server listening on port 5252.

### Configuring Vite to proxy calls to our backend

When starting Vite using the `npm run dev` script, it listens on port `3000` by default to serve the frontend. When developing, we’ll want our React code to make API calls to the Node server running on port `5252` as described above. Vite allows us to proxy those calls using some simple configuration. In this case, when making calls to our backend, we’ll prefix the paths with `/api`. Then we’ll configure Vite to proxy any calls that begin with `/api` to our backend server. Change the `vite.config.js` with this configuration:

```javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 4242,
    proxy: {
      // string shorthand
      // with options
      "/api": {
        target: "http://localhost:5252",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});
```

We’ve also changed the Vite development server port from `3000` to `4242`, so we’ll need to restart the server and  load [http://localhost:4242](http://localhost:4242) in the browser.

### Running both the Vite server and the node server

In development, we can run both the Vite server and the node server by installing the `concurrently` package, we’ll install this as a dev dependency:

```bash
npm install -D concurrently
```

Next we’ll update our `package.json` to start both the Vite and Node servers with some custom scripts. Update the scripts block in `package.json` with the following:

```json
  "scripts": {
    "start": "npm run development",
    "development": "NODE_ENV=development concurrently --kill-others \"npm run client\" \"npm run server\"",
    "client": "vite",
    "server": "node server.js",
```

Note that we’ve renamed the script that starts Vite from `dev` to `client`. The new scripts are `server`, to start the node server, `development`, which runs both the `client` and `server` scripts concurrently, and then finally `start`, which runs the development script. If we run `npm run start` we should see both the Vite server and node server boot up.

```bash
vite-project matthewling$ npm run start

> vite-project@0.0.0 start
> npm run development


> vite-project@0.0.0 development
> NODE_ENV=development concurrently --kill-others "npm run client" "npm run server"

^[[B[1]
[1] > vite-project@0.0.0 server
[1] > node server.js
[1]
[0]
[0] > vite-project@0.0.0 client
[0] > vite
[0]
[0]
[0]   vite v2.9.12 dev server running at:
[0]
[0]   > Local: http://localhost:4242/
[0]   > Network: use `--host` to expose
[0]
[0]   ready in 304ms.
[0]
[1] (Use `node --trace-warnings ...` to show where the warning was created)
[1] {"level":30,"time":1655285637895,"pid":93847,"hostname":"matthewling","msg":"Server listening at http://127.0.0.1:5252"}
[1] {"level":30,"time":1655285637898,"pid":93847,"hostname":"matthewling","msg":"Server listening at http://[::1]:5252"}
[1] Server listening ...
```

We can run two simple tests now to make sure that our proxying is working properly. This cURL call should return the publishable key directly from the backend:

```bash
curl http://localhost:5252/publishable-key
```

And this call should return the publishable key, proxied through the Vite development server to the backend:

```bash
curl http://localhost:4242/api/publishable-key
```

### Initializing Stripe.js

Now that we have a backend running, we can jump back to our Checkout component. After the imports, we’ll write an async function called `initStripe` that will initialize Stripe.js by using the `loadStripe` function that we imported earlier. This async function will call our backend to retrieve the publishable key and then will load Stripe.js returning a promise that will be passed to the Elements provider later.

```javascript
import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import CheckoutForm from './CheckoutForm.jsx';

const initStripe = async () => {
  const res = await axios.get("/api/publishable-key");
  const publishableKey = await res.data.publishable_key;

  return loadStripe(publishableKey);
};
```

We’ll add the call to `initStripe` at the top of the declaration to create the Checkout component:

```javascript
const Checkout = () => {
  const stripePromise = initStripe();
```

Don’t forget that our Vite server is now running on `4242`, not `3000` so we’ll need to navigate to http://localhost:4242 instead.

### Creating a Payment Intent and saving the client secret

Next we’ll use `useEffect` to create a Payment Intent. Here we’ll create an async function to create the Payment Intent and then use `setState` to set the `clientSecretSettings` object that we created earlier. Don’t forget to include an empty dependency array to instruct `useEffect` to run only once when the component is loaded. Note that when we used `useState` earlier, that the default value for `loading` was `true`, we’ll set that to false when setting the `clientSecretSettings`. We’ll use that loading state in the JSX HTML next to signify two states when rendering the component, a loading state and a loaded state.

```javascript
  useEffect(() => {
    async function createPaymentIntent() {
      const response = await axios.post("/api/create-payment-intent", {});

      setClientSecretSettings({
        clientSecret: response.data.client_secret,
        loading: false,
      });
    }
    createPaymentIntent();
  }, []);
```

### Creating a CheckoutForm component

We’ll create one more component which will be a form to render the Payment Element. Then we’ll wrap that form in the Elements provider later. In the `src` folder, create a `CheckoutForm.jsx` file:

```javascript
import { PaymentElement } from "@stripe/react-stripe-js";

const CheckoutForm = () => {
  return (
    <form>
      <PaymentElement />
      <button>Submit</button>
    </form>
  );
};

export default CheckoutForm;
```

### Using the Elements provider

Back in our `Checkout` component, let’s import that `CheckoutForm` component:

```javascript
import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import CheckoutForm from './CheckoutForm.jsx';
```

Next we’ll modify the JSX in the `Checkout` component use our `loading` state, but more importantly, we need to wrap the `CheckoutForm` component with the `Elements` provider passing the `stripePromise` which was loaded earlier:

```javascript
  return (
    <div>
      {clientSecretSettings.loading ? (
        <h1>Loading ...</h1>
      ) : (
        <Elements
          stripe={stripePromise}
          options={{
            clientSecret: clientSecretSettings.clientSecret,
            appearance: { theme: "stripe" },
          }}
        >
          <CheckoutForm />
        </Elements>
      )}
    </div>
  );
```

Now we should see the Payment Element rendering in the browser.

![Payment Element](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/slw12bws4e7xp1j45onb.png)
 

### Confirming the payment

To recap, we’ve completed the following steps:

* Created a `Checkout` component
* Set Up a backend that can return a publishable key and create a Payment Intent
* Used the `Checkout` component to load Stripe.js and to create a Payment Intent and save a client secret
* Created a `CheckoutForm` component that can render a Payment Element
* Used the `Elements` provider to wrap the CheckoutForm to provide the stripe object in nested components

Finally, we’ll confirm the payment when the checkout form is submitted using Stripe.js in the `CheckoutForm`. In `CheckoutForm.jsx`:

```javascript
import React, { useState } from 'react';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';

const CheckoutForm = () => {
  const stripe = useStripe();
  const elements = useElements();

  const [errorMessage, setErrorMessage] = useState(null);

  const handleSubmit = async (event) => {
    // We don't want to let default form submission happen here,
    // which would refresh the page.
    event.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js has not yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    const {error} = await stripe.confirmPayment({
      //`Elements` instance that was used to create the Payment Element
      elements,
      confirmParams: {
        return_url: 'http://localhost:4242/success.html',
      },
    });

    if (error) {
      // This point will only be reached if there is an immediate error when
      // confirming the payment. Show error to your customer (for example, payment
      // details incomplete)
      setErrorMessage(error.message);
    } else {
      // Your customer will be redirected to your `return_url`. For some payment
      // methods like iDEAL, your customer will be redirected to an intermediate
      // site first to authorize the payment, then redirected to the `return_url`.
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button disabled={!stripe}>Submit</button>
      {/* Show error message to your customers */}
      {errorMessage && <div>{errorMessage}</div>}
    </form>
  )
};

export default CheckoutForm;
```

Let’s walk through this code.

* We’ll import `useStripe` and `useElements` from react stripe
* We’ll then use the `useStripe` and `useElements` hooks to access the `stripe` and `elements` objects
* We’ll setup error message state using `useState`
* When the form is submitted, we’ll prevent the default action which is the form submission
* We use a guard conditional statement to simply return if either `stripe` or `elements` is not loaded
* Finally we’ll call `confirmPayment` passing the elements instance and the required confirmParams which is a return url. We’ll return to an empty `success.html` page.
* In the root of the project, let's create an empty `success.html` file to redirect to
* If an error occurs, this will be returned immediately which we’ll handle by using the `setErrorMessage` state.
* The form tag is also augmented to handle the form submission and disabling the button should `stripe` not be loaded.

### Testing

You can use any of the standard [Stripe test cards](https://stripe.com/docs/testing) to test the Payment Element. On successful payment we’ll be redirected to the `success.html` page. Note that the query parameters passed to this page are the Payment Intent ID, client secret and redirect status. These can be used to retrieve the Payment Intent from the API to report on the status of the payment. For payment methods like Bancontact, that must redirect to an intermediary bank, we’ll be redirected to a Stripe hosted test page — where we can authorize or fail the payment — and then back to `success.html` page.

### Conclusion

Being able to support the Payment Element using modern frontend technologies is essential to maintaining and increasing payments conversion. With React Stripe and the Payment Element, you can simply offer many different methods of payment using the same integration.

You can see the [finished repo](https://github.com/matthewling-stripe/vite-react-stripe) for this post on the main branch here. We’d love to hear any feedback on what you’ve learned and built along the way! Keep your eyes peeled for new posts on this series where we’ll modify this code to add new payment methods and features.

### About the author

![Matthew Ling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c8rpgkqr91ivs8sxr73g.png) 

Matthew Ling ([@mattling_dev](@mattling_dev)) is a Developer Advocate at Stripe. Matt loves to tinker with new technology, adores Ruby and coffee and also moonlighted as a pro music photographer. His photo site is at [matthewling.com](https://matthewling.com) and developer site is at [mattling.dev](https://mattling.dev).

### Stay connected

In addition, you can stay up to date with Stripe in a few ways:

📣 Follow us on [Twitter](https://twitter.com/StripeDev)
💬 Join the official [Discord server](https://stripe.com/go/developer-chat)
📺 Subscribe to our [Youtube channel](https://www.youtube.com/StripeDevelopers)
📧 Sign up for the [Dev Digest](https://go.stripe.global/dev-digest)