Using React, Next.js, and Vercel to Build an E-Commerce Storefront

Our goal today is to create a new e-commerce storefront. We will use React, Next.js, and JamCart to build the website and then deploy it on Vercel.

In the course of this tutorial, a basic proficiency with these technologies will be helpful:

  • HTML, CSS, and JS
  • Local development using Node.js
  • git and hosting repositories on GitHub

Front-end development is a large, interconnected set of skills and we cannot cover everything in a single tutorial.

Final result: https://react-nextjs-vercel-demo.vercel.app/

GitHub: https://github.com/JamCart/react-nextjs-vercel-demo

A Quick Primer

Before we begin writing code, it would be useful to familiarize ourselves with JSX and CSS Modules (covered in the next section).

React and JSX

React takes a "JS First" view of development and philisophically prefers to do as much inside the JavaScript layer as possible. Creating new HTML nodes via vanilla JavaScript is tedious, difficult to read quickly, and easy to introduce mistakes. JSX allows us to embed what looks like HTML and then compiles that away to the equivalent JavaScript function calls.

const name = "world"
const element = <h1 className="greeting">Hello, { name }!</h1>

will compile to (roughly)

const name = "world"
const element = React.createElement(
  "h1",
  {className: "greeting"},
  `Hello, ${ name }!`
);

This is important to understand because it is a leaky abstraction. As you can see in the previous example, we used className instead of class. React uses names of the JavaScript properties and not the html attribute names. (htmlFor instead of for is the second most common mistake after classname)

The upside to this is that these html fragments can be treated analogously to string templates. They can be assigned to variables, returned from functions, and can have JavaScript expressions embedded inside of them. Sort of a super powered version of:

const name = "world"
const element = `<h1>Hello, ${ name }!</h1>`

(This is just for syntax demonstration. Do not actually try to write a application via string concatentation.)

Lastly, note that there has to be a single top level node. If you want to pass multiple nodes around together, you can use an empty "fragment" node:

function myFunc () {
  return <>
    <h1>Header</h1>
    <p>Body</p>
  </>
}

While technically a different format, Next.js automatically parses all .js files as though they were JSX so we don't need to distinguish clearly between the two for this tutorial.

CSS Modules

Anyone who has worked on even a medium scaled application can attest to how quickly global CSS can get out of hand. It becomes difficult to tell where styling a particular element is coming from and styling between components can come in to conflict. The React community has developed multiple patterns to combat this problem including strict naming conventions, style everything inline, or writing everything in JS.

Next.js includes support for a different option, which we will use today: CSS Modules. All files that end in .module.css will have any class names they contained modified to include a random hash. So CSS like this:

.author { ... }

will become

.author-xyz { ... }

If we import book.module.css in to a JS file, it returns an object that maps the name we gave it to the hashed class name. We can then apply the hashed class name to our elements.

import style from  './book.module.css'
const element = <div className={ style.author }>Steve</div>

We've now created a direct dependency from our JS to our CSS. The CSS can be chunked, code split side, and tree shaken side by side with our JS.

This also means the styling is unique to this module. We could use the class name .author in another file and, because the hashed class name would be different, we woudn't accidentally change the styling here.

Starting Our Next.js Project

Installing Next.js is simple. From the command line type:

npx create-next-app nextjs-ecommerce-demo
cd nextjs-ecommerce-demo
npm run dev

create-next-app automatically installed all of our dependencies and even made our first git commit. If you open a browser to the indicated port (default is http://localhost:3000) you should see a short intro page.

Looking inside the directory, there are two folders with special status:

  • pages: Every js file that lives inside here will become a new page on our website.
  • public: These files are copied directly to our build. Static files (like images) should go in here.

styles was also created but doesn't have any special behaviour attached to it. It could have been named anything and we'll be removing it shortly.

Cleaning up

Now that we've verified our install is working, it's time to remove the default demo page. Delete everything inside of pages, the entire styles directory, and everything inside of public, except public/favicon.ico. Leave that alone or replace it with your own file if you don't want random 404s in the developer tools' network tab.

Create a file named next.config.js and include this content:

module.exports = {
  i18n: {
    locales: ['en-US'],
    defaultLocale: 'en-US'
  }
}

This just sets the default language (<html lang="en-US">) for our application. It's not critical but it is polite.

Create API

We'll just be mocking out an API today. Mock APIs are great for prototyping although you should probably replace this with a real API you wrote or, better yet, a headless CMS such as Forestry, Contentful, or Sanity before rolling out to production.

Let's create lib/api.js and declare our mock data. We'll just keep it in a single array to make things simple.

const data = [{
  id: "diamond-bracelet",
  image: "/images/sabrinna-ringquist-2z7MxnXQs3k-unsplash.jpg",
  name: "Diamond Bracelet",
  category: "bracelet",
  price: 140,
  }, {
    ...
  }]

After that, we create the dummy functions to access this data:

export async function getProduct (id) {
  return data.find(product => product.id === id)
}

export async function getProducts () {
  return data
}

While these don't have to be declared as async, doing so now means we can swap out all of these implementations for something that hits a database or remote service without changing any of the dependant code.

Example Repo

Instead of typing out all of your own products and finding matching pictures, it may be easier to simply checkout out our project up to this point:

https://github.com/JamCart/react-nextjs-vercel-demo/releases/tag/mock-api

I've also included all the CSS files I used since those will not be covered in this tutorial.

Main Layout

Let's create pages/_app.js. This is a special file and does not map to a page on our website. Instead, it is used to render the main layout hosting the content on every page. Because this layout persists across pages, everything we don't want to be rerendered when switching between pages should live here.

import Head from 'next/head'
import Link from 'next/link'

import './_app.css'
import style from './_app.module.css'

export default function App({ Component, pageProps }) {
  return <>
    <Head>
      <link rel="icon" href="/favicon.ico" />
      <meta name="description" content="A demo e-commerce storefront" />
    </Head>

    <nav className={ style.nav }>
      <h1 className={ style.logo }>
        <Link href="/">
          <a>Jewels {/* Our Demo Website's Title */}</a>
        </Link>
      </h1>
    </nav>

    <Component {...pageProps} />
  </>
}

Breaking this file down a little bit:

import './_app.css'

This is the one time we will include a regular CSS file instead of a CSS module in our project. _app.js is also the only file permitted to do so in our project: Next.js will throw an error if you try otherwise. Importing global CSS on individual pages would result in undefined behaviour so you're prevented from doing it. Styling for individual pages should live in .module.css file.

<Component />

export default function App({ Component, pageProps }) {
  ...
  <Component {...pageProps} />

JSX allows us to use function calls inside of our templates as tags. <Component {...pageProps } /> just calls the function Component with a single parameter: an object containing the attributes attached to <Component>. You could also write this as { Component({ ...pageProps }) } with the same result.

The Component passed to this function is the component we will write for each our pages, which ever is currently being viewed. Fail to include it in our main layout and all of our pages will be identical!

import Head from 'next/head'
...
<Head>
  <link rel="icon" href="/favicon.ico" />
  <meta name="description" content="A demo e-commerce storefront" />
</Head>

next/head is a special component supplied by the Next.js framework. All tags contained inside of it are appended to the head of the current html document, instead of the current position on the page. It will also automatically take care of removing those tags from the document head when no longer needed. We didn't include a <title> tag because that needs to different on each page.

import Link from 'next/link'
...
<Link href="/">
  <a>Jewels {/* Our Demo Website's Title */}</a>
</Link>

next/link is required for the Next.js router to work. This allows us to navigate to other pages without having to do a full page reload and will automatically preload linked content for us. It needs to always wrap an <a> element but the Link tag itself will modify this <a>, so we don't have to double up on the href attribute.

Home Page

It's now time to create our first page: pages/index.js. All of the following blocks should live inside of there, I've simply broken it up to talk about individual sections.

import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'

import style from './index.module.css'

import { getProducts } from '/lib/api.js'

In a regular Node application, beginning an import with / would make it an absolute path from the root of our hard drive. This is seldom the behaviour we actually want and Next.js has kindly rewritten these imports to instead use our project directory as root. This is a lot easier to read and mentally process than dealing with relative imports across multiple directory levels.

export async function getStaticProps () {
  return {
    props: {
      products: await getProducts()
    }
  }
}

By exporting a function named getStaticProps, we have asked Next.js to run this code during build time. The resulting build will include the result of our function call which means any code here, and its dependencies, will be tree shaken out of the final build. getProducts and any of its dependencies won't be present in the client bundle because of this.

export default function Home({ products }) {
  return (
    <>
      <Head>
        <title>Next.js E-Commerce Demo</title>
      </Head>

      <main>
        <ul className={ style['product-grid'] }>
          { products.map(product => ProductView({ product })) }
        </ul>
      </main>
    </>
  )
}

The default export is what Next.js calls to get the page contents and is the function passed to our App function in /pages/_app.js as Component. The first parameter is our props we defined in getStaticProps just above.

Inside our view, we iterate across all of our products to render a view for each. for loops can not be written inside of our HTML fragments, so you'll get plenty of practice with map, filter, and other functional patterns when working inside of JSX. ProductView we haven't defined yet, so we should do that:

function ProductView ({ product }) {
  return (
    <li key={ product.id }>
      <Link href={ `/product/${ product.id }` } prefetch={ false }>
        <a className={ style.product }>
          <div className={ style['product-image'] }>
            <Image alt="" height="427" width="640" src={ product.image } />
          </div>
          <div className={ style['product-description'] }>
            { product.name }
          </div>
          <div className={ style['product-price'] }>
            { product.price }
          </div>
        </a>
      </Link>
    </li>
  )
}

Note that JSX does not support interpolation inside of attributes. It's all-or-nothing, so we can't just write <Link href="/product/{ product.id }">. We need to disable prefetching on this Link or risk kicking off prefetch requests for our entire product catalog.

The real killer feature of Next.js is <Image /> (imported from next/image). This will automatically handle lazy loading, image optimization, resizing, and serving alternative formats. Clients that support new formats will get small .webp files and everyone else will be stuck with legacy .jpg. Images are a large part of any e-commerce store and having them optimized correctly dramatically improves site speed.

As a requirement, <Image /> needs the image dimensions up front. We've cheated a bit for this demo: all of our images are the same size. You may need to include this data in your own API if that's not the case for you. On the plus side, this does mean that Cumulative Layout Shift won't be a problem. Each of the <Image /> tags will reserve their dimensions even before the image is loaded.

Also remember that this is just a wrapper around the HTML <img /> tag, so alt is also a required attribute.

Product Page

With the home page taken care of, it's time to write the product pages. We'll create a file named pages/product/[id].js. The square brackets in the file name create a parameter so this will match all paths that look like /product/some-product-id and then pass that id to our code. We do need to tell Next.js what the possible valid values for id are.

import Image from 'next/image'
import Head from 'next/head'
import { getProductById, getProducts } from '/lib/api.js'
import style from './product.module.css'

export async function getStaticPaths () {
  const products = await getProducts()
  const paths = products.map(product => ({
    params: {
      id: product.id
    }
  }))

  return {
    paths,
    fallback: false
  }
}

That's what exporting getStaticPaths here does. In paths on the return object, we have an array with one item per valid page. Note that id is not special: it is the property name used because we named our file with [id].

fallback: false informs Next.js that this is the complete list. The pages could be dynamically generated on the fly but this introduces more complexity. It's simplest to just pregenerate everything you hit the size and scale where build times become a problem.

export async function getStaticProps ({ params }) {
  return {
    props: {
      product: await getProductById(params.id)
    }
  }
}

getStaticProps plays the same role as it did in pages/index.js. The id from our filename parameter gets passed here, so we need to make sure we fetch the right product.

export default function ProductPage ({ product }) {
  return <>
    <Head>
      <title>{ product.name }</title>
    </Head>

    <div className={ style.product }>
      <div className={ style.image }>
        <Image alt="" height="427" width="640" src={ product.image } />
      </div>

      <div className={ style.header }>
        <h2>{ product.name }</h2>
        <p className={ style.price }>{ product.price }</p>
      </div>
    </div>
  </>
}

Using that data, we can render our page. There's nothing new we haven't seen yet here.

Integrating Checkout

It's not much of an e-commerce site without a shopping cart and checkout. Let's integrate JamCart into our demo.

Installing the Script

We need to install JamCart on each page of our website. The global layout file pages/_app.js would be perfect for that.

First, we'll need to grab our install code from the JamCart dashboard. It will look something like this:

<script type="module" data-currency="USD" data-id="KbTF8dXGss" src="https://api.jamcart.io/v1/jamcart.js"></script>

We'll just add this line right next to our declartion for favicon.


 




<Head>
  <script type="module" data-id="KbTF8dXGss" src="https://api.jamcart.dev/v1/jamcart.js"></script>
  <link rel="icon" href="/favicon.ico" />
  <meta name="description" content="A demo e-commerce storefront" />
</Head>

Make sure you use your own install code and not the one from this demo!

Opening the Cart

Next up, we'll add a link to open our shopping cart to each page. We already have pages/_app.js open, so let's add a button right next to our site title.







 


<nav className={ style.nav }>
  <h1 className={ style.logo }>
    <Link href="/">
      <a>Jewels {/* Our Demo Website's Title */}</a>
    </Link>
  </h1>
  <jamcart-open class={ style.open } />
</nav>

For esoteric reasons, React requires we use class instead of className for Custom Elements.

Add Items to Cart

To add items to our cart, we need to add a button to pages/product/[id].js. We'll just add a button right after the product header:






 
 
 
 
 
 
 
 
 
 
 

<div className={ style.header }>
  <h2>{ product.name }</h2>
  <p className={ style.price }>{ product.price }</p>
</div>

<p className={ style.add }>
  <jamcart-add
    id={ product.id }
    image={ product.image }
    name={ product.name }
    price={ product.price }
    open-cart
    >
    Add to shopping bag
  </jamcart-add>
</p>

We choose to override the default template provided by JamCart because the button's default color scheme doesn't really line up with the rest of the website.

Configuration

That's it for code changes. Before your code goes live, there's a few more steps to double check in the JamCart dashboard:

  1. Domain Whitelist – You must explicitly whitelist each domain to allow JamCart to run for security reasons. This may need to wait until you know where your website will be hosted.
  2. Shipping Configuration – Unless all items are marked as intangible, you must create at least one shipping method before users can checkout.
  3. Payment Gateway – You must connect a payment gateway in order to collect payment from customers.

You can also see which of these steps still need to be completed from Jamcart's install page.

Hosting

To host our code on Vercel, you could simplely type npx vercel from the command line. That will get your site hosted and a new deployment available immediately. However, any time you make changes, you will have to re run this command.

A better approach would be to commit our code and host it on GitHub, GitLab, or BitBucket. From Vercel's dashboard, we can hit "New Project", choose our repository, and click "Import". Vercel will automatically detect that we built this project using Next.js and set all of the configuration for us.

Vercel will now automatically redeploy our website any time we push changes to the repository.

Congratulations

That's the end of our tutorial. We now have a fully functional storefront capable of accepting and processing payments.