Skip to main content
  1. All Posts/

pwa-nextjs-ecommerce-boirlerplate

eCommerce TypeScript

NextJs PWA E-commerce: Progressive web application

STACK: React | Typescript | NextJS | Mobx-State-Tree | SASS | BEM Syntax | Jest | React Testing Library | Cypress
This is a e-commerce app built with React with all the stack mentioned above. I used the best practices of the market. I’m being careful about performance and SEO all the time. So I suggest to check this documentation before start coding.

The PWA (Progressive Web Application)

As you can see on the name, it’s a web app that uses service workers, manifests, and other web-platform features combined with progressive enhancement to give users an experience on par with native apps.
In short words, the user can install the web application as an app on their computer or can access it offline, and some other advantages.
It looks complex but React make it simple, and if you use the next-pwa library, as this app is using, it will require almost zero-config to make it work well.

Running App Locally

  1. Clone repository
  2. Install packages
    $ yarn
    
  3. Run dev to develop and watch your changes. (It will auto compile when you save and you only need to refresh the browser)
    $ yarn dev
    
  4. Access http://localhost:3000/
  5. Let’s Rock!

P.S.: The PWA is disallowed for dev

Running Production

  1. Run build

    $ yarn build
    
  2. Run start

    $ yarn start
    
  3. Access http://localhost:3000/

TDD – Test-Driven Development

The development practice focused on creating test cases before developing the actual code, at really means developing using the baby steps technique and testing and “refactoring” every little progress.

  • WRITE a “single” test describing an aspect of the program.
  • RUN the test, which should fail because the program lacks that feature.
  • WRITE “just enough” code, the simplest possible, just to make the test pass.
  • INCREMENT / “refactor” the code keeping the simplicity criteria.
  • REPEAT it, “accumulating” unit tests, until you achieve the program goal.

Folders Structure

The component and function tests are located in their own directory to be found easily.

  • The extension .spec.ts|tsx means it’s a CYPRESS test.
  • The extension .test.ts|tsx means it’s a JEST test.

Testing PWA

  • Turn on/off the wifi and refresh the website page. You should be able to keep navigating on the pages you have already accessed.
  • On the right side of the URL input on the browser or in the setting menu, you can find a link to install the app. Do it and open the PWA on your computer.

IMPORTANT: Always test the PWA after any change in the code the be sure everything still working fine.

NextJS

NextJs has a file-system based router built on the concept of pages.
A page is a React Component in the src/pages directory. Each page is associated with a route based on its file name. So, when a file is added to the src/pages directory with the extension .page.tsx it’s automatically available as a route.
Always create a folder for each page and add an index.page.tsx because the SEO config file and styles also are stored together with the related page/component to make it easier to find it in the future.
Read more on NextJs official documentation:

Static Paths

If you create pages/about.page.tsx that exports a React component like below, it will be accessible at /about.
The router supports nested files. If you create a nested folder structure files will be automatically routed in the same way still.

import {AppLayout} from '@app/components/Layout/AppLayout';
import {ContactsSeo} from '@app/pages/contacts/_seo.config';
import React, {ReactElement} from 'react';

const ContactsPage = () => {
	return (
		<>
			<p>Welcome to the contact page!</p>
			<ContactsSeo />
		</>
	);
};

ContactsPage.getLayout = (page: ReactElement) => {
	return <AppLayout>{page}</AppLayout>;
};

export default ContactsPage;

getLayout is required for all pages. It’s responsible for adding the header and footer. Go to the src/layout to check what layouts are available.

Dynamic Paths

To match a dynamic segment you can use the bracket syntax. This allows you to match named parameters.
For example:
pages/blog/[slug].tsx/blog/:slug (/blog/hello-world)
pages/[username]/settings.tsx/:username/settings (/foo/settings)

import {GetStaticPaths, GetStaticProps} from 'next';
import {ParsedUrlQuery} from 'querystring';
import {useRouter} from 'next/router';
import LoadingCmsPage from '@app/components/Loading/LoadingCmsPage';
import React, {ReactElement} from 'react';
import {AppLayout} from '@app/components/Layout/AppLayout';
import {CmsPagesSeo} from '@app/pages/cms/_seo.config';

export interface CmsPageParams extends ParsedUrlQuery {...}
export interface CmsPagePaths {...}

const topCmsUrlKey: string[] = ['testing', 'bacon'];

export const getStaticPaths: GetStaticPaths = async () => {...};

export const getStaticProps: GetStaticProps = async (context) => {
	const {cmsUrlKey} = context.params as CmsPageParams;
	const revalidationTime: number = Number(process.env.REACT_APP_DEFAULT_DATA_REVALIDATION_TIME);
	const cmsPath = topCmsUrlKey.includes(cmsUrlKey); // get from API

	if (!cmsPath) return {notFound: true};

	return {
		props: {
			cmsUrlKey,
		},
		revalidate: revalidationTime,
	};
};

const CmsPage = ({cmsUrlKey}: CmsPageParams) => {
	const {isFallback} = useRouter();
	return isFallback ? (
		<LoadingCmsPage />
	) : (
		<>
			<p>CMS URL Key: {cmsUrlKey}</p>
			<CmsPagesSeo />
		</>
	);
};

CmsPage.getLayout = (page: ReactElement) => {
	return <AppLayout>{page}</AppLayout>;
};

export default CmsPage;

Catch-All Routes: Multiple Nested Dynamic Paths

Dynamic routes can be extended to catch all paths by adding three dots (…) inside the brackets.
For example:
pages/post/[...slug].tsx matches /post/a, but also /post/a/b, /post/a/b/c and so on.

import {GetStaticPaths, GetStaticProps} from 'next';
import {ParsedUrlQuery} from 'querystring';
import {useRouter} from 'next/router';
import LoadingCatalogCategoryPage from '@app/components/Loading/LoadingCatalogCategoryPage';
import React, {ReactElement} from 'react';
import {AppLayout} from '@app/components/Layout/AppLayout';
import {CatalogSeo} from '@app/pages/catalog/_seo.config';

export interface CatalogCategoryPageProps {...}
export interface CatalogCategoryPageParams extends ParsedUrlQuery {...}
export interface CatalogCategoryPagePaths {...}

const topCategoryUrlKey: string[] = ['testing', 'testing/bacon', 'testing/bacon/american'];

export const getStaticPaths: GetStaticPaths = async () => {...};

export const getStaticProps: GetStaticProps = async (context) => {
	const {urlKey} = context.params as CatalogCategoryPageParams;
	const revalidationTime: number = Number(process.env.REACT_APP_DATA_REVALIDATION_TIME);
	const categoryPath = urlKey.toString().split(',').join('/');
	const category = topCategoryUrlKey.includes(categoryPath); // get from API

	if (!category) return {notFound: true};

	return {
		props: {
			category: urlKey[0],
			...(urlKey[1] && {subcategory: urlKey[1]}),
			...(urlKey[2] && {subsubcategory: urlKey[2]}),
		},
		revalidate: revalidationTime,
	};
};

const CatalogCategoryPage = ({category, subcategory, subsubcategory}: CatalogCategoryPageProps) => {
	const {isFallback} = useRouter();
	return isFallback ? (
		<LoadingCatalogCategoryPage />
	) : (
		<>
			<div>
				<p>Category URL Key: {category}</p>
				{subcategory && <p>Subcategory URL Key: {`${category}/${subcategory}`}</p>}
				{subsubcategory && (
					<p>Sub-subcategory URL Key: {`${category}/${subcategory}/${subsubcategory}`}</p>
				)}
			</div>
			<CatalogSeo />
		</>
	);
};

CatalogCategoryPage.getLayout = (page: ReactElement) => {
	return <AppLayout>{page}</AppLayout>;
};

export default CatalogCategoryPage;

Linking Pages

The Next.js router allows you to do client-side route transitions between pages, similar to a single-page application.
A React component called Link is provided to do this client-side route transition.

import Link from 'next/link'
<Link href="https://github.com/about">...</Link>

Linking to Dynamic Paths

<Link href={`/blog/${encodeURIComponent(post.slug)}`}>...</Link>

Linking to Nested Dynamic Paths

<Link href={`/catalog/${encodeURIComponent(category.slug)}/${encodeURIComponent(subcategory.slug)}`}>...</Link>

Testing Pages

It’s easy to messy with pages file and break the urls of que ecommerce, so you can test all the url (valid and invalid) using cypress.

yarn test:cypress

Alternatively you can use yarn test:cypress-open to watch the test running in the browser.

Mobile First

It’s a mobile-first app. Please, develop for mobile and adapt to bigger screens.
How to develop for mobile-first: Why you’ve got to start practicing mobile-first development

Styles

The app uses SASS superset of CSS to style the components and also uses BEM Syntax to keep the code clean and easy to read and maintain.
You have some default classes that you should use to set the font styles like size, weight, decoration and position. Check the default classes section to learn more about it.
Don’t hesitate to ask for help if you have any questions about BEM Syntax. Finding the right name for the class could look tricky, but using it wrong could be worst than not using it.

Folder Structure

Path: src/styles

  • base: Where we keep the core configurations of styles and default classes.
    Example: variables, breakpoints, colors, typography, containers, icons, alignments, browsers and containers.
  • exports: Where we keep the CSS Modules. It’s used to export the SASS variables and use it on the JS, but as the BEM Syntax is a good alternative for it, you will rarely use it.
  • helpers: Where we keep the SASS functions and mixins created to help with development and reduce the number of lines you need to write. You can find all the instructions about how to use it in the files.
  • pages: DON’T USE IT
    Where we keep the homepage style as NextJs don’t allow to relocate the index folder.
  • theme: Where we keep the resets and styles specifics for the theme.

Pages & Components

To make it easier to find the styles for each component we are keeping the styles related to every component or page in the related folder(outside the style folder).
_For example: When you create a new component in the src/components folder you should create your component folder, the index.tsx (your react component) and the component-name.scss.

src
|- components
| |- YourComponent
| | |- index.tsx
| | |- _your-component.scss

“_” is used to denote partials. Underscore in front of the file name won’t be generated into the compiled CSS unless you import it into another sass file.

Default Classes

  • txt-left | txt-center | txt-right
  • theme-container
  • txt-light | txt-regular | txt-bold
  • txt-italic
  • txt-uppercase | txt-underline | txt-decoration-none
  • txt-xxs | txt-xs | txt-s | txt-m | txt-l | txt-xl | txt-xxl: The font-size starting on 10px increasing two-by-two until 32px.

Breakpoints

They are mixins that you can easily re-use.

  • small-screens: max-width 480px
  • mobile: min-width 480px
  • mobile-tablet: min-width 480px and max-width 768px
  • tablet: min-width 768px
  • tablet-laptop: min-width 768px and max-width 992px
  • laptop: min-width 992px
  • laptop-desktop: min-width 992px and max-width 1200px
  • desktop: min-width 1200px
  • desktop-big-screens: min-width 1200px and max-width 1600px
  • big-screens: min-width 1600px

Browser Mixins

  • IE: Internet Explorer
  • MS: Microsoft Edge
  • IOS: Any iOS device with touchable screen

Helpers

Some mixins and functions to save time and reduce number of line to write.

  • rem($pixels): convert pixel to rem
  • flex($direction, $justify, $align, $wrap): If you would not like to set a property you can use “unset” as a value.
  • grid($row, $column, $gap): You MUST need to inform all values. The autoprefixer will automatically convert it to be frendly to you browsers list.
  • grid-child($row, $column): This mixin must be called only on elements that the parent has display: grid. The…