Integrating Stripe to Your React Application for Seamless Payments

Integrating Stripe to Your React Application for Seamless Payments

Discover the process of incorporating Stripe into a React app for creating an online store without needing a backend

Β·

18 min read

Introduction

When you know how to code, you can create anything you desire. What if you have something to offer for sale, be it services, products, or anything else? To sell whatever you can provide, you have decided to build your own online shop as a programmer. However, when it comes to selling online, there's one minor challenge - how do we process payments? πŸ’Έ

That's why I decided to write this blog, where I guide you through the minimum setup necessary to process payments using Stripe, requiring only basic knowledge of frontend to start selling.

If you've never heard of it before, Stripe is a payment processing platform that enables any business to effectively accept payments. What's really nice for us developers is that they offer comprehensive documentation on how to use Stripe in any project.

Do you want your application to have a smooth payment flow and a polished checkout page, just like the one shown in the video above?

Today, I'm excited to show you how to easily set up payment processing using Stripe in your React project, without even needing a backend! I hope this guide gives you a great starting point to build upon in your own application. Let's have some fun and dive right in! 🀿

Prerequisites

As previously mentioned, this article doesn't require any backend knowledge from you, but I do expect some familiarity with React. Moreover, you should have a basic understanding of React Router, and I won't delve too much into the topic of styling. I'll leave that entirely up to you, so you can build your online shop in whatever style you want. With that being said, let's get started! πŸš€

Setting Up the Frontend

Alright, let's begin by creating a basic Vite project for the React app. To do so, run the following command in the terminal and choose the appropriate options:

npm create vite@latest

βœ” Project name: react-stripe-shop
βœ” Select a framework: β€Ί React
βœ” Select a variant: β€Ί JavaScript

Great! Now, delete everything inside the src directory, as we will be starting from scratch.

Configuring Stripe

Now that we've got our frontend all set up, let's hop on over to the Stripe website and create a new account.

After creating the account, you will be presented with a comprehensive dashboard. Here, we will need to create the products that we are going to sell. To do so, select the "Products" option on the dashboard panel.

To create a new product, click "Add product," and a panel for creating the new product will appear. To keep it simple, let's enter a name with description, add an image, and provide pricing information:

That's all! Now, click "Save Product," and you will be redirected to your new product. Notice that Stripe provides us with two IDs: the price ID and the product ID. We will need them later to integrate the created products into our React application.

Alright, now that we've got the basics down, let's go ahead and create a few more products to fill up our brand new online store. πŸ›’

Displaying Stripe Products on the Frontend

Alright, we've completed the basic Stripe setup, but keep the Stripe tab open, as we'll need it later. For now, let's focus on our frontend. Our shop will consist of two pages: one where customers can view the list of products, and another where customers can proceed to checkout the products, i.e., purchase them.

To manage multiple pages, let's install React Router and define the necessary routes in advance. To install React Router, run the following command:

npm install react-router-dom

Next, create the main.jsx file inside the src directory, with BrowserRouter wrapping our main App component:

// main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import { BrowserRouter } from "react-router-dom";

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>,
);

The App.jsx component will contain all the defined routes in our application. However, we haven't created this component yet. First, we will create two new components inside a new folder called pages, where we will store Home.jsx and Cart.jsx. Then, we can create the App.jsx file where we will define the aforementioned routes:

// App.jsx

import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Cart from "./pages/Cart";

const App = () => {
    return (
        <>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/cart" element={<Cart />} />
            </Routes>
        </>
    );
};

export default App;

Before we begin working on displaying the product list for our users, we need to create a basic navigation bar to allow users to navigate between pages. To do this, we will create a folder called components with the Appbar.jsx file inside of it. This component will contain the following basic code:

// components/Appbar.jsx

import { Link } from "react-router-dom";

const Appbar = () => {
  return (
    <nav>
      <div>
        <Link to="/">Shop</Link>
      </div>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/cart">Cart</Link>
          </li>
        </ul>
      </div>
    </nav>
  );
};

export default Appbar;

As mentioned earlier, my primary focus is not on styling for this project, so I will leave that up to you. Now, we can add the navigation bar to the App component to make it accessible on both pages of our website:

// App.jsx

import Appbar from "./components/Appbar";

const App = () => {
    return (
        <>
            <Appbar />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/cart" element={<Cart />} />
            </Routes>
        </>
    );
};

Great! Now we can move forward to display the list of products on our page. There are different approaches to handling this. If we wanted a more complicated application, we would probably create the products on the backend, store them in the database, and create the corresponding product at Stripe.

However, as promised, we're doing the minimum work possible to create the online platform. Therefore, we will hard-code the list of products, which we previously created on Stripe. This approach will still work effectively in a real online store, especially if there is a limited number of products.

To do so, let's create a folder called data with a file named products.js inside of it. This file will serve as a simulation of the data stored in the database for the scope of this project.

Remember when I mentioned that we would need the product ID and price ID for each product? Well, now is their time to shine. Head over to Stripe and gather all the information you need to create a similar "database" for your own products. This is the data for all products that I previously created within the dashboard:

// data/products.js

export const productsData = [
    {
        id: "prod_Obn9XJgX9HOIoS",
        priceId: "price_1NoZZOKmUelmvh9tA6SGKZFS",
        name: "MacBook M1",
        description: "Incredibly portable laptop β€” it is nimble and quick, with a silent, fanless design and a beautiful Retina display.",
        price: 1000.00,
        image: "https://images.unsplash.com/photo-1611186871348-b1ce696e52c9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    },
    {
        id: "prod_ObnOYzCbtpl1on",
        priceId: "price_1NoZnQKmUelmvh9tdXTLLDDM",
        name: "Headphones",
        description: "You'll hear every note of your favorite tunes as clear as a bell.",
        price: 45.00,
        image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    },
    { 
        id: "prod_ObnMZ6810cy8HD",
        priceId: "price_1NoZm0KmUelmvh9tPpgtVAHp",
        name: "Sneakers Nike",
        description: "Whether you are logging long marathon miles, squeezing in a speed session before the sun goes down or hopping into a spontaneous group jaunt, it is still the established road runner you can put your faith in",
        price: 25.00,
        image: "https://images.unsplash.com/photo-1491553895911-0055eca6402d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    },
    {
        id: "prod_ObnWZ4bJZjefxS",
        priceId: "price_1NoZvMKmUelmvh9tovQQCvRy",
        name: "Shirt",
        description: "Crafted from pure cotton for breathability and is as easy to care for as it is to wear.",
        price: 27.00,
        image: "https://images.unsplash.com/photo-1626497764746-6dc36546b388?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    },
    {
        id: "prod_ObnbLgNV4xJEi1",
        priceId: "price_1NoZzqKmUelmvh9tjSAD5JWN",
        name: "Sunglasses",
        description: "The perfect pair of sunglasses can elevate your look and keep you looking cool all day long.",
        price: 10.00,
        image: "https://images.unsplash.com/photo-1572635196237-14b3f281503f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    }, 
    {
        id: "prod_Obndm21s7bLiUG",
        priceId: "price_1Noa2ZKmUelmvh9tJYhJrqts",
        name: "Camera Olympus",
        description: "Olympus mirrorless cameras are known for their stylish design, compact size and portability.",
        price: 395.00,
        image: "https://images.unsplash.com/photo-1591892014172-8a9b511165b8?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
    }
];

By the way, I used publicly accessible images from Unsplash as product images for my examples. However, you can certainly use your own product images from a local folder. You can create a folder, such as "assets," to store your product images.

Now that we have our product data prepared, let's define the appearance of each product. We'll create a product card containing all the relevant product information. To accomplish this, let's create a new component called Product.jsx inside the components folder:

// components/Product.jsx

const Product = (props) => {
  const addToCart = () => {};

  return (
    <div>
      <img src={props.image} alt={props.name} />
      <div className="card-body">
        <h5 className="card-title">{props.name}</h5>
        <p className="card-text">{props.description}</p>
        <p className="card-text">$ {props.price.toFixed(2)}</p>
        <button onClick={addToCart}>
          Add to the cart
        </button>
      </div>
    </div>
  );
};

Inside the component, we receive the product information passed as props and display it in the form of a card with an image. Note that I've also included a button to add the product to the cart, but we'll handle this slightly later.

Alright, let's head back to the Home component and begin showing our list of products. We'll import our data and loop through each component, passing the information to the Product component:

// pages/Home.jsx

import Product from "../components/Product";
import { productsData } from "../data/products";

const Home = () => {
    return (
        <div>
            <h2>Products List</h2>
            <div>
                {productsData.map((product) => (
                    <Product key={product.id} {...product} />
                ))}
            </div>
        </div>
    );
};

export default Home;

Now, it should resemble something like this (depending on your styling choices, of course):

Implementing the Shopping Cart

Next, let's manage adding products to the cart. There are multiple options we can use to tackle the issue of storing objects in the cart.

One of the most viable options is Redux storage, which allows for easy management of the application state. However, integrating Redux here is an entirely different topic, and I really want to focus on one thing: payment processing. That's why, in this project, we will use a still quite reasonable option - local storage.

If you want to learn more about Redux, make sure to check out one of my earlier articles where I did a deep dive into Redux and how it works under the hood while building an application to manage a list of favorite movies.

Within the local storage, we'll store the array of objects called "cart", in which each object corresponds to the product in the cart. Therefore, Product component will look the following way:

// components/Product.jsx

const Product = (props) => {
    const addToCart = () => {
        // Getting the cart contents from local storage 
        const cart = JSON.parse(localStorage.getItem("cart")) || [];
        // Verifying whether the element hasn't 
        // been added to the cart already
        const isCart = cart.find(product => product.id === props.id);
        if (!isCart) {
            // Updating the local storage with a new product
            localStorage.setItem("cart", JSON.stringify([...cart, props]));
        }
    };

    return (
        <div>
            <img src={props.image} alt={props.name} />
            <div className="card-body">
                <h5 className="card-title">{props.name}</h5>
                <p className="card-text">{props.description}</p>
                <p className="card-text">$ {props.price.toFixed(2)}</p>
                <button onClick={addToCart}>
                  Add to the cart
                </button>
            </div>
        </div>
    );
};

You may notice that I checked if a product with the given ID has already been present in the cart to avoid adding duplicate items. Now, I'd like to add a small feature that provides users with feedback to inform them whether the desired product has been successfully added to the cart. To do this, we'll use toastified messages from React Toastify. Run this command to get the package:

npm install react-toastify

Next, you need to import the provided styling into the App.jsx component and import the specialized ToastContainer that configures the appearance of the toastified messages. You can learn more about this in the documentation.

// App.jsx

import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Cart from "./pages/Cart";
import Appbar from "./components/Appbar";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";

const App = () => {
    return (
        <>
            <ToastContainer 
                position="top-left"
                autoClose={3000}
                pauseOnFocusLoss={false}
            />
            <Appbar />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/cart" element={<Cart />} />
            </Routes>
        </>
    );
};

export default App;

Finally, we need to display the toast notifications with the relevant message whenever the user wishes to add products to the cart.

// components/Product.jsx

const Product = (props) => {
    const addToCart = () => {
        const cart = JSON.parse(localStorage.getItem("cart")) || [];
        const isCart = cart.find(product => product.id === props.id);
        if (isCart) {
            toast.error("This product is already in the cart!");
        } else {
            localStorage.setItem("cart", JSON.stringify([...cart, props]));
            toast.success("Product added to the cart!");
        }
    };

    return (
        <div>
            <img src={props.image} alt={props.name} />
            <div className="card-body">
                <h5 className="card-title">{props.name}</h5>
                <p className="card-text">{props.description}</p>
                <p className="card-text">$ {props.price.toFixed(2)}</p>
                <button onClick={addToCart}>
                  Add to the cart
                </button>
            </div>
        </div>
    );
};

Alright, now that we've finished adding products to the cart, let's move on to the Cart.jsx component to manage the products we've added. Within this component, we'll get the product array from local storage and iterate over it to display each product. Note that I store the products in the cart as the component's state to reflect any changes made in the cart:

// pages/Cart.jsx

import { useState } from "react";

const Cart = () => {
    const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem("cart")) || []);

    return (
        <div>
            <div>
                <div>
                    <h2>Cart</h2>
                </div>
                <button>
                    Checkout
                </button>
            </div>
            <div>
                {cart.map(product => (
                    <div key={product.id}>
                        <img src={product.image} alt={product.name} />
                        <div>
                            <h5>{product.name}</h5>
                            <p>$ {product.price.toFixed(2)}</p>
                            <button>
                                Remove
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};

export default Cart;

In the Cart.jsx component, we need to allow users to remove a product from their cart. To do this, we will place a button next to each product for this purpose. Next, we can create an action handler to manage the removal process, where we pass the ID of the product we want to delete:

// pages/Cart.jsx

import { useState } from "react";

const Cart = () => {
    const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem("cart")) || []);

    const removeFromCart = (idToRemove) => {
        // Generating a copy of the cart array, 
        // excluding the specified product.
        const newCart = cart.filter(product => product.id !== idToRemove);
        localStorage.setItem("cart", JSON.stringify(newCart));
        setCart(newCart);
    };

    return (
        <div>
            <div>
                <div>
                    <h2>Cart</h2>
                </div>
                <button>
                    Checkout
                </button>
            </div>
            <div>
                {cart.map(product => (
                    <div key={product.id}>
                        <img src={product.image} alt={product.name} />
                        <div>
                            <h5>{product.name}</h5>
                            <p>$ {product.price.toFixed(2)}</p>
                            <button onClick={() => removeFromCart(product.id)}>
                                Remove
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};

export default Cart;

Additionally, it is essential to display the total price of all products in the cart, giving customers a clear understanding of the overall amount they need to pay (assuming that each product is only purchased in a single quantity for simplicity):

// pages/Cart.jsx

import { useState, useEffect } from "react";

const Cart = () => {
    const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem("cart")) || []);
    const [totalPrice, setTotalPrice] = useState(0);

    const removeFromCart = (idToRemove) => {
        const newCart = cart.filter(product => product.id !== idToRemove);
        localStorage.setItem("cart", JSON.stringify(newCart));
        setCart(newCart);
    };

    useEffect(() => {
        // Updating the total price with each change inside the cart
        const newTotalPrice = cart.reduce((accumulator, product) => accumulator + product.price, 0);
        setTotalPrice(newTotalPrice);
    }, [cart]);

    return (
        <div>
            <div>
                <div>
                    <h2>Cart</h2>
                    <h4>Total price: ${totalPrice.toFixed(2)}</h4>
                </div>
                <button>
                    Checkout
                </button>
            </div>
            <div>
                {cart.map(product => (
                    <div key={product.id}>
                        <img src={product.image} alt={product.name} />
                        <div>
                            <h5>{product.name}</h5>
                            <p>$ {product.price.toFixed(2)}</p>
                            <button onClick={() => removeFromCart(product.id)}>
                                Remove
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};

export default Cart;

We have created an additional component state that stores the total price of the products in the cart and updates whenever the cart changes. To calculate the current total price, we iterate over the entire array and calculate the price using the reducer() function. Basically, we are simply looking for the sum of the prices of all products in the array.

Great, now at this point, we have a well-designed application that displays the products we previously created on Stripe's dashboard and allows users to manage them in their cart.

It's time to create and process payments to collect money for our products πŸ’°. For this, we'll use Stripe Checkout, a pre-built, hosted payment page that eliminates the hassle of creating our own checkout page. All we need to do is connect it correctly, and Stripe will display a user-friendly page allowing customers to pay for their products. If you want to learn more about Stripe Checkout, check out this link.

Processing Payments with Stripe Checkout

Alright, to process payments using Stripe Checkout, we need to do two things: first, inform Stripe of the list of products the user wants to buy. It's crucial that these products have already been created within Stripe, as there's no other way for Stripe to identify them. Then, after passing the list of products, Stripe will generate a checkout link to which we need to redirect the user.

Luckily for us, we can handle everything on the client side! Since we've already set up the products we want to sell, there's no need for a backend at all. That's what's so awesome about Stripe - if you just need basic payment processing, it's got you covered without making you stress over building a backend.

First, we need to install the following package to communicate with Stripe:

npm install @stripe/stripe-js

Next, we need to create a new folder named lib and within it, a new file called getStripe.js. In this file, we will handle loading the Stripe client. The code will look as follows:

// lib/getStripe.js

import { loadStripe } from "@stripe/stripe-js";

let stripePromise;
const getStripe = () => {
    if (!stripePromise) {
        stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLIC_KEY);
    }
    return stripePromise;
};

export default getStripe;

You can see that within this function, we ensure that the Stripe client is loaded only once using loadStripe(). Within the function, I pass the Stripe publishable API key as an environment variable.

To obtain the Stripe API key, navigate to the Stripe Developers dashboard, where you will find the "API keys" option. For the frontend, you need to copy only your "Publishable key."

Then, within the project directory, create a .env file and insert the Stripe publishable key into it:

VITE_STRIPE_PUBLIC_KEY=pk_test_YOUR_KEY

IMPORTANT! If you create an application using Vite, as I do, Vite has specific requirements for naming your environment variables. You can read more about that in the Vite documentation. In summary, simply make sure to include "VITE_" at the beginning of any environment variable you want to access within your application.

Great! Now we can use the loaded Stripe client to process the payment. We will proceed to Cart.jsx and create a function to handle the checkout:

// pages/Cart.jsx

import { useState, useEffect } from "react";
import getStripe from "../lib/getStripe";

const Cart = () => {
    const [cart, setCart] = useState(() => JSON.parse(localStorage.getItem("cart")) || []);
    const [totalPrice, setTotalPrice] = useState(0);

    const removeFromCart = (idToRemove) => {
        const newCart = cart.filter(product => product.id !== idToRemove);
        localStorage.setItem("cart", JSON.stringify(newCart));
        setCart(newCart);
    };

    useEffect(() => {
        const newTotalPrice = cart.reduce((accumulator, product) => accumulator + product.price, 0);
        setTotalPrice(newTotalPrice);
    }, [cart]);

    const proccessCheckout = async () => {
        // Extracting the price ID from the products in the cart
        const lineItems = cart.map(product => ({
            price: product.priceId,
            quantity: 1,
        }));
        // Loading the Stripe client
        const stripe = await getStripe();
        // Initiating the checkout process with given products
        await stripe.redirectToCheckout({
            mode: "payment",
            lineItems,
            successUrl: `${window.location.origin}/success`,
            cancelUrl: `${window.location.origin}/cart`
        });
    };

    return (
        <div>
            <div>
                <div>
                    <h2>Cart</h2>
                    <h4>Total price: ${totalPrice.toFixed(2)}</h4>
                </div>
                <button onClick={processCheckout}>
                    Checkout
                </button>
            </div>
            <div>
                {cart.map(product => (
                    <div key={product.id}>
                        <img src={product.image} alt={product.name} />
                        <div>
                            <h5>{product.name}</h5>
                            <p>$ {product.price.toFixed(2)}</p>
                            <button onClick={() => removeFromCart(product.id)}>
                                Remove
                            </button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
};

export default Cart;

In this function, we access the previously loaded client using the getStripe() function. Once the client is loaded, we redirect the user to a specially created checkout page.

Within the redirectCheckout() function, we pass the following information:

  • lineItems: an array of products the customer wants to purchase. Stripe requires only two parameters: the quantity of the object and the price ID, which we store within the object. As mentioned earlier, we will have each product with a quantity of 1.

  • mode: to inform Stripe whether our transaction is recurring, such as a subscription, or a one-time payment.

  • successUrl: the page the user will be directed to upon a successful transaction.

  • cancelUrl: the page the user will be directed to upon a failed transaction.

Now, we have everything prepared. Let's add a few products to the cart and test our application by clicking the "Checkout" button. We click, and oops, nothing happens. Upon looking at the console, we can notice the following error:

It is easy to fix, as we simply need to follow Stripe's instructions. Navigate to the provided link. Here, we must complete two tasks:

  • Create an account name to enable checkout. Select "Add an account name" and enter any name you want.

  • Enable client-side integration by selecting "Enable client-only integration" at the bottom of the page.

Alright, now we're fully set up to process our payments! If you click the button, you will be redirected to this nicely polished checkout page.

Everything seems so easy with Stripe, right? Stripe allows testing the payment process by entering special dummy card information. Here is an excerpt from their documentation section about testing:

When testing interactively, use a card number, such as 4242 4242 4242 4242.

  • Use a valid future date, such as 12/34.

  • Use any three-digit CVC (four digits for American Express cards).

  • Use any value you like for other form fields.

If we input the information in the format they ask for, our payment will be processed successfully! Note that Stripe also permits clients to pay using Google Pay and Link.

Awesome! Our payment has been processed successfully. πŸš€ If you navigate to the Stripe dashboard and select "Payments," you can view the details of this payment. There is plenty of information, making Stripe incredibly convenient for any online business owner.

By the way, after checking out, we're sent to the "/success" route, but there's no page for it yet. Let's create one to let our users know their transaction was successful. We will create the file Success.jsx within the pages folder, representing a very basic page:

// pages/Success.jsx

const Success = () => {
    return (
        <div>
            <h2>Success</h2>
            <p>Thanks for your purchase at our store!</p>
        </div>
    );
};

export default Success;

Don't forget to add this page to the routes in App.jsx:

// App.jsx

import Home from "./pages/Home";
import Cart from "./pages/Cart";
import Success from "./pages/Success";
import Appbar from "./components/Appbar";

const App = () => {
    return (
        <>
            <ToastContainer 
                position="top-left"
                autoClose={3000}
                pauseOnFocusLoss={false}
            />
            <Appbar />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/cart" element={<Cart />} />
                <Route path="/success" element={<Success />} />
            </Routes>
        </>
    );
};

export default App;

Conclusion

Congratulations on making it this far! πŸŽ‰ I hope this article has helped you grasp the basics of managing online payments in your app, and shown you that it is possible to do it even without a backend!

Of course, most real-world complex e-commerce applications require some backend integration, database management, and so on. This can make the process of integrating Stripe seem more overwhelming. Therefore, if you enjoyed this article and would like me to write more about Stripe, let me know, and I'll be more than happy to help you explore its advanced features! 😊

If you have any questions or need assistance, don't hesitate to reach out to me. Feel free to contact me, even if it's not related to this project.

If you enjoyed this article, remember to follow me on Twitter/X, where I share daily updates. And feel free to connect with me on LinkedIn.

Β