In a rush? Skip to the tutorial or live example
Like many web developers, I have been doing quite a lot of React stuff recently. The last few years have seen its usage in the industry grow in a trajectory commensurate to its parent company.
These days, there isn’t much you can’t do with React, whether you’re a seasoned developer or a complete beginner.
This is mostly due to the creation of tools such as Next.js that have successfully simplified React frontend development.
So, today, we'll explore how to craft a Next.js e-commerce single-page application quickly.
In the technical tutorial below, I’ll show you how to:
- Set up a Next.js development environment
- Create new pages & components
- Fetch data & import components
- Create serverless API routes in Next
- Add a shopping cart to a Next.js app
- Style the app
But before we go through this, let’s make sure we understand what Next.js is and how it can improve your next e-commerce projects.
What's Next.js?
In a nutshell, Next.js is a lightweight framework for React applications that allows you to build server-side rendering and static applications in React easily.
It takes all the good parts of React and makes it even easier to get an app running with optimized rendering performance. Next.js does this thanks to multiple built-in configurations—automatic code-splitting, file-system routing, server-side rendering, static files exporting, and styling solutions.
Trust me when I say that you can build a LOT of different things with Next.js:
- Static websites—we’ve listed it as one of the top static site generators for 2021.
- Progressive Web Apps (PWAs)
- Server-rendered applications
- SEO-friendly websites—as we’ve demonstrated here.
- Mobile apps
It was built by Zeit (now Vercel) back in 2016 and has quickly gained traction to the point of becoming one of the most popular tools of its kind. I mean, it’s used by Marvel, Netflix, Uber, Nike… and the list goes on.
Okay, this is all great, and I’m genuinely excited to play with Next.js here. But is it any good for e-commerce?
Next.js & e-commerce: a good fit?
Like any static site generator or JavaScript framework out there, one of its most significant advantages, vs. more traditional e-commerce platforms, is the options it gives to developers to create a kickass shopping UX while removing the burden of many implementation details required for building a web app.
Depending on your need, you can easily build a server-side or statically rendering app with Next, which implements those for you while also abstracting other details such as app-bundling and transcompilation.
The power of the Jamstack right here!
We’ve covered the general React e-commerce ecosystem and its benefits in an earlier post. I would strongly suggest reading it to understand further why it’s a great fit.
But on the probable chance that you're pressed for time, here’s a TL;DR:
→ The use of components for flexibility.
Component-based development enables easy code reuse through your app but also the writing of small features. Or, in our case, small e-commerce functionalities. This comes in handy once you start scaling and expanding your shopping cart integration.
→ Virtual DOM (document object model) for performance.
React’s virtual DOM provides a more efficient way of updating the view in a web application. Performance is Everything in e-commerce; all milli-seconds count.
→ Popularity & vast community.
Any issue has probably been documented already, so you're likely to find a solution to any potential pitfalls in your way.
Next.js features like server-side rendering and static exporting push these React benefits even further by guaranteeing that your website/app will be SEO-friendly. This is something vital to any e-commerce business.
Still need social proof? Here are some Next.js e-commerce site examples.
Technical tutorial: a Next.js e-commerce SPA
Okay, time to jump into code and create our own handcrafted Next.js e-commerce app with the help of Snipcart. For you fish aficionados — or really anyone waiting for the beta of any cool software library — rest assured because we will make a betta fish store today.
Pre-requisites
- Basic understanding of single-page applications (SPAs)
- A Snipcart account (forever free in test mode)
- Npm and node.js installed
Basic knowledge of React & TypeScript will also help you here, but it is not mandatory to follow along.
1. Setting up the development environment
First, let set up our environment so that we can start building.
Open a terminal and type the following command:
npx create-next-app --typescript
A prompt will appear asking you for the project's name. It will then install all of the project dependencies for you and create files and folders. We'll look into these further in this tutorial.
Beginner note: The command's
--typescript
options will set up a Typescript project, which I often favor. The typing system helps prevent many runtime errors and perky bugs. It also allows for better refactoring in the long run!
Then, run npm run dev
. Your app should now be served at localhost:3000
.
2. Defining a layout
With our environment ready, let's create a layout for our store. It will include a Header and a Footer with links to our cart and contact information.
We will add this layout to the app's main entry point. In Next
, this entry point is located at pages/_app.tsx
. You can see that the MyApp
function returns the pageProps
. We will use this function to create our app's layout:
At the project's root, create a components
directory in which — you guessed it — we will create our components.
1. Creating components
Now let's create the components we need.
In the components
directory, create a Header.tsx
file with the following content:
// components/Header.tsx
import Link from "next/link";
export default function Header() {
return (
<header >
<Link href="/">
<img src="/static/logo.svg" alt="" >
</Link>
<Link href="/">
<h1 >FishCastle</h1>
</Link>
<a href="#" style={{textDecoration: "none"}}>
<svg width="31" height="27" viewBox="0 0 31 27" fill="none" xmlns="<http://www.w3.org/2000/svg>">
<path d="" fill="#9094FF"/>
</svg>
<span></span>
</a>
</header>
)
}
The Link
component from Next.js allows us to convert most HTML elements into in-website links.
Still in the components
directory, create a Footer.tsx
file with the following content:
// components/Footer.tsx
export default function Footer(){
return (
<footer>
<p>
Next.js app with a <a href="<https://snipcart.com>">Snipcar t</a> - powered store
<div >
<a href="<https://github.com/snipcart/snipcart-nextjs-spa>">Github</a>
</div>
</p>
</footer>
)
}
2. Integrating components
Let's now integrate those components into our app. First, create a Layout
component and put the Header
and Footer
in it:
import Header from "./Header";
import Footer from "./Footer";
import {PropsWithChildren} from "react";
export default function Layout({ children }: PropsWithChildren<any>) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}
With your layout components created, all that's left to do is to add it to the _app.tsx
file:
// _app.tsx
function MyApp({ Component, pageProps }: AppProps) {
return <>
<Layout>
<Component {...pageProps} />
</Layout>
</>
}
If you run your app's dev mode and go to your localhost page, you should now see your app's layout created. Later in this tutorial, we will see how to add style to it and give it that drip.
But first things first, let's give our homepage the content it deserves.
3. Customizing your homepage
As we need to display both information about our store and the products we will sell, we will create a few different components to keep things modular and maintainable. Then, we will look at how to assemble them:
1. Creating required components
The product component
As this is a Next.js tutorial for an e-commerce app, you will need a Product
component to show on your homepage.
The component will output whatever information you need to display about our particular product. You can create an IProduct
interface that matches Snipcart's product definition and an IProductProps
interface to define the types of our props, which will be pass as a parameter to the function.
// components/Product.tsx
export interface IProduct {
id: string
name: string
price: number
url: string
description: string
image: StaticImageData
}
Underneath the interface, add this component:
// components/Product.tsx
interface IProductProps {
product: IProduct
}
const Product = (props: IProductProps) => {
return (
<div className={styles.product}>
<h2 className={styles.product__title}>{props.product.name}</h2>
<p className={styles.product__description}>{props.product.description}</p>
<div className={styles.product__image}>
<Image src={props.product.image} alt={props.product.image.src} />
</div>
<div className="product__price-button-container">
<div className={styles.product__price}>${props.product.price.toFixed(2)}</div>
<button
className={`snipcart-add-item ${styles.product__button}`}
data-item-id={props.product.id}
data-item-name={props.product.name}
data-item-price={props.product.price}
data-item-url={props.product.url}
data-item-image={props.product.image.src}>
Add to cart
</button>
</div>
</div>
)
}
A note on the Image component
Notice in the block below we are using Next.js's Image component instead of a good ol' img
tag. The former is actually an extension of the latter. It allows automatic image optimization, lazy loading by default, and providing images in WebP when the browser allows it, which optimizes images to the client device. Moreover, the component optimizes image on requests, which saves you build time. This helps to reduce your website loading time and thus keep your users' interest!
2. The product list component
We will integrate this product component into a ProductList
component, whose name is quite self-explanatory. The ProductList.tsx
component will be used to display our list of products on the homepage. Therefore, you can create an IProductListProps
interface that describes an array of IProduct
, which is eventually going to be passed by our website:
import Product, {IProduct} from "./Product";
interface IProductListProps {
products: IProduct[]
}
const ProductList = (props: IProductListProps) => {
return (
<div className="product-list">
{props.products.map((product, index) => <Product product={product} key={index}/>)}
</div>
)
}
export default ProductList
4. Pre-rendering data and importing components
At this stage, you'll probably want to populate your products to the ProductList component. In pure React, you could use React's useEffect lifecycle inside the ProductList
to fill the data. However, this method won't get called on the server during a static or server-side rendering.
Thankfully Next.js adds two ways to pre-render the data: getStaticProps
, which fetches data at build time, and getServerSideProps
, which fetches data on each request. The latter may be useful, for example, for an auction store where the price may be rapidly fluctuating. In our use case, since the product doesn't change often, we will use the former as the pre-rendering will decrease loading time by saving the user a request.
<main className="main">
<Jumbotron />
<ProductList products={products}/>
<Contact/>
</main>
export const products: IProduct[] = [
{
id: "halfmoon",
name: "Halfmoon Betta",
price: 25.00,
image: halfmoonBettaPicture,
description: "The Halfmoon betta is arguably one of the prettiest betta species. It is recognized by its large tail that can flare up to 180 degrees.",
url: '/api/products/halfmoon'
},
(...)
{
id: "veiltail",
name: "Veiltail Betta",
price: 5.00,
image: veiltailBettaPicture,
description: "By far the most common betta fish. You can recognize it by its long tail aiming downwards.",
url: '/api/products/veiltail'
}
]
export const getStaticProps: GetStaticProps = async (context) => {
return {
props: {
products
}
}
}
5. Importing Snipcart
Now, let's install Snipcart into our website. First, you'll need to import the Head
component from next/head inside your index.tsx page, which will allow you to add HTML inside the <Head>
element.
You can do so by adding the following code inside the Index function return clause:
// pages/index.tsx
<Head>
<title>My awesome store</title>
<link rel="preconnect" href="<https://app.snipcart.com>"/>
<link rel="preconnect" href="<https://cdn.snipcart.com>"/>
<link rel="stylesheet" href="<https://cdn.snipcart.com/themes/v3.2.0/default/snipcart.css>"/>
<link rel="shortcut icon" href="../public/favicon.ico" />
</Head>
For more information on how to use the
Head
component to optimize for SEO, have a look at our article regarding SEO using Next.js.
We now need to load Snipcart's script content. Next.js offers a Script component in the next/script
, module to do so. It allows for performance optimization by offering different loading strategies.
// pages/index.tsx
<script src="https://cdn.snipcart.com/themes/v3.2.0/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="YOUR_PUBLIC_API_KEY"></div>
Don't forget to swap the data-api-key
attribute with your own API key ;)
6. Product validation
Now that Snipcart is installed, the last step before completing orders is to validate your products.
1. HTML validation
The first way to do that is by simply changing the URL in your product list to /
for each product to the homepage for Snipcart's HTML validation. It will read the /
on our homepage and crawl it in order to validate the products if you want. You can do just that and skip to the next section, and you will have a working e-commerce site!
If you are curious, let's take the opportunity to check out a neat Next.js feature:
serverless API routes combined with Snipcart's JSON validation.
2. JSON validation using Next.js serverless API
For more complex scenarios, it might be useful to have an API returning our products information in JSON format. To do so, we will need to have a unique URL for each product that will return its information in a JSON file.
- Configuring static API routes
While technically we only need a dynamic API route returning each product, let's make this API RESTful and have a route returning the whole product list.
You may have noticed that an API folder was created with the project. In this folder, create another one called products
and add an index.ts
file to it with the following content:
// In pages/api/products/index.ts
import {NextApiRequest, NextApiResponse} from "next";
import {products} from "../../index";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json(products);
}
If you now go to https://localhost:3000/${YOUR_PORT}
, you will get a JSON file containing your product list.
- Configuring dynamic API routes
In the products
folder, add the following to the [productId].ts
file. Notice the brackets. This special syntax tells Next.js that [productid] is a dynamic parameter. Hence if you go to /api/products/ONE_OF_YOUR_PRODUCTS_ID
, you should get the JSON information of one of our products.
import {NextApiRequest, NextApiResponse} from "next";
import {products} from "../../index";
import {IProduct} from "../../../components/Product";
export interface ISnipcartProduct {
id: string
name: string
price: number
url: string
description: string
image: string // Hack to pass the image URL instead of the StaticImage object we required
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const {productId} = req.query;
const product: IProduct | undefined = products.find(p => p.id === productId);
if (!product) {
res.status(404).json({});
return ;
}
const snipcartProduct: ISnipcartProduct = {...product, image: product?.image.src ?? ""}
res.status(200).json(snipcartProduct);
}
You should now be able to complete a test order!
It's time to style our website, so it's more appealing to our future customers.
7. Styling your Next.js SPA
If you paid attention, you saw that most of the components in this tutorial already had classnames. We will now look at 2 different ways to apply them:
1. Setting up a global stylesheet
In the style
s folder, create a global.scss
style sheet. Afterwards, simply import it to pages/_app.tsx
:
// in pages/_app.tsx
import "../styles/globals.scss";
Global stylesheets can only be imported in the _app.tsx
file.
I used SCSS, which does not come built-in with Next.js, but can be easily integrated simply by running npm install sass
.
2. Setting up CSS modules for our components
Next.js also supports CSS modules, which can become quite handy if your CSS file gets larger. To use it, simply create a file respecting the [name].module.css
convention, for example, Product.module.css
or Product.module.scss
.
Afterward, you can import it as a styles
object in the component's file and access the styles with it:
import styles from '../styles/Product.module.scss';
(...)
const Product = (props: IProductProps) => {
return (
<div className={styles.product}>
<h2 className={styles.product__title}>{props.product.name}</h2>
<p className={styles.product__description}>{props.product.description}</p>
(...)
)
}
For further examples of how these styles are applied, have a look at the project:
Allow me a shout-out to Michael from Snipcart, who came up with the neat UI design!
And voilà! You're server-side rendered Next.js e-commerce store should be ready to go.
Closing thoughts
I liked how easy it was to create a static website with great performance using Next.js. I did notice some parts of the Next.js documentation could be more up-to-date.
We could have explored Image optimization on mobile using the Image
component or Next's dynamic imports to push this demo further.
Are you up to it? If so, let us know how it goes in the comments below!
Liked this article? Hit the share buttons below.
Top comments (7)
Great Article. 👏🏼
One question on the
getStaticProps
andgetServerSideProps
.getStaticProps
would be called only at build time, since we are using that here, if we update our database with new products it would be fetched only on fresh build.If we have used
getServerSideProps
with API routes, for the above use case, the new products would be reflected on a page reload/new request. Is my understanding is correct?@payapula much thanks! 🙇
Indeed
getStaticProps
will be called at build time, independently of database updates. Although you can use therevalidate
option to refresh the page after a certain delay.getServerSideProps
will pre-render the page on each request as you said, which is most often a good approach if your use case requires very frequent database updates.So your understanding was indeed correct! 🙂
how can we persist state when while client side routing
Hi Mohd! You can setup a custom
App
component just like we did in Step 2 of this tutorial (integrating components), except you can add your state hooks to it.Other solutions you might want to explore include shallow routing that enables you to change the URL without fetching data again! Some npm modules I haven't had the chance to try are also starting to be developed if you want to look into this.
If you try any of these solutions, please let me know how it goes!
I found it, it is already present in next js. It does not work in dev environment where getStaticProps runs on each request
I recently worked on a similar stuff but struggled with header. How do we make header dynamic? I wanted to display different options to logged in user but NEXT won't rerender the layout again.
Hi Usman,
Depending on your use case, you could use hooks in your custom
App
component and then pass them to your header, or shallow routing to change your route's query parameter and then, here also, pass them to your header.Let me know how it goes!