Skip to main content

A Journey of Building with Next.js

· 8 min read
3Alan
🤖WARNING
The English translation was done by AI.
🚧 🚧warning
Some information may be outdated. Please be aware of the latest references when reading this article.

Recently, I was required to migrate a Vue H5 activity page to Next.js. After some research and experimentation, I have summarized the following content.

What is Next.js

Next.js is a Server-Side Rendering (SSR) framework based on React.

SSR & CSR

Reference: The Benefits of Server-Side Rendering over Client-Side Rendering

The main difference is that for SSR your server’s response to the browser is the HTML of your page that is ready to be rendered, while for CSR the browser gets a pretty empty document with links to your JavaScript. That means your browser will start rendering the HTML from your server without having to wait for all the JavaScript to be downloaded and executed. In both cases, React will need to be downloaded and go through the same process of building a virtual DOM and attaching events to make the page interactive — but for SSR, the user can start viewing the page while all of that is happening. For the CSR world, you need to wait for all of the above to happen and then have the virtual DOM moved to the browser DOM for the page to be viewable.

Advantages of Next.js

  • Better SEO
  • Faster initial rendering speed

Basics of Next.js (Differences from React Development)

Next.js Basics

Route Mapping

In Next.js, a page is a React component exported from a .js, .jsx, .ts, or .tsx file in the pages directory. Each page is associated with a route based on its file name.

pages/about.js/jsx/ts/tsx → /about

pages/dashboard/settings/username.js/dashboard/settings/username

Similar to react-router, including programmatic navigation with router.push and component-based navigation with <Link href="/about"><a>click me</a></Link>.

import { useRouter } from 'next/router';

const router = useRouter();
router.push({
pathname: '/activities/experience-lesson/course-info',
query: { ...queryData, isFree: 0 }
});
router.push('/about');

Rendering Modes

Pre-rendering

  • Static Generation (HTML reuse, build-generated)
  • Server-Side Rendering (Different HTML generated for each request, generated on user's request)

Relevant APIs

  • Static Generation

    • getStaticProps(context)
    • getStaticPaths(context)
  • Server-Side Rendering

    • getServerSideProps(context)
  • Client-Side Data Fetching

    • SWR (official recommendation)

Note: In development environment, getStaticProps and getStaticPaths are called on every request.

Usage: Export in page file.

function Page({ data }) {
// Render data...
}

// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`);
const data = await res.json();

// Pass data to the page via props
return { props: { data } };
}

export default Page;

Project Structure and Engineering Setup

Eslint+Prettier Configuration

https://github.com/paulolramos/eslint-prettier-airbnb-react

https://dev.to/karlhadwen/setup-eslint-prettier-airbnb-style-guide-in-under-2-minutes-a27

https://dev.to/bybruno/configuring-absolute-paths-in-react-for-web-without-ejecting-en-us-52h6

Solution for eslint not recognizing dynamic import syntax import():

Related issue

// Eslint configuration
parserOptions: {
ecmaVersion: 2020, // Use the latest ECMAScript standard
sourceType: 'module', // Allows using import/export statements
ecmaFeatures: {
jsx: true // Enable JSX since we're using React
}
},

To ensure code consistency within the team, create a .vscode folder in the root directory and create a settings.json file inside it to automatically fix lint issues upon saving.

{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

After some time, it was found that some team members had enabled auto-save functionality in their editors, so this step could be skipped. Instead, husky (used to add git hooks) can be used in conjunction with lint-staged to automatically format the code before committing.

yarn add husky lint-staged prettier --dev

Write package.json:

"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js, jsx}": [
"npm run lint",
"git add"
]
}

Alias Configuration

next.config.js configuration

/* eslint-disable no-param-reassign */
const path = require('path');

module.exports = {
webpack: config => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
config.resolve.alias['@'] = path.resolve(__dirname, './src');
// Important: return the modified config
return config;
}
};

Eslint cannot recognize aliases, so create a jsconfig.json file in the root directory and configure settings in .eslintrc.js.

{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["./*"]
}
},
"exclude": ["node_modules", "**/node_modules/*"]
}

Axios Wrapper to Display Spin Component for Every Request

Note: Since the server-side does not have a document, it is necessary to check the current environment before executing the operation.

Detailed Code
import axios from 'axios';
import ReactDOM from 'react-dom';
import Spin from '../components/Spin/Spin';

const Axios = axios.create({
timeout: 20000
});

const csr = process.browser;

// Number of ongoing requests
let requestCount = 0;

function showLoading() {
if (requestCount === 0) {
var dom = document.createElement('div');
dom.setAttribute('id', 'loading');
document.body.appendChild(dom);
ReactDOM.render(<Spin />, dom);
}
requestCount++;
console.log('showLoading', requestCount);
}

function hideLoading() {
requestCount--;
if (requestCount === 0) {
document.body.removeChild(document.getElementById('loading'));
}
console.log('hideLoading', requestCount);
}

Axios.interceptors.request.use(
config => {
csr && showLoading();
return config;
},
err => {
csr && hideLoading();
return Promise.reject(err);
}
);

Axios.interceptors.response.use(
res => {
csr && hideLoading();
return res;
},
err => {
csr && hideLoading();
return Promise.reject(err);
}
);

export default Axios;

Custom Input Hook

Using this hook eliminates the need to set onChange for every form component.

import { useState } from 'react';

export function useInput(initialValue) {
const [value, setValue] = useState(initialValue);

return {
value,
setValue,
reset: () => setValue(''),
bind: {
value,
onChange: e => {
setValue(e.target.value);
}
}
};
}

Usage:

// Before
const [phone, setPhone] = useState('');

<input
name="phone"
type="number"
placeholder="Please enter your phone number (required)"
className={`${styles['cell-content']} ${styles['cell-content-right']}`}
value={phone}
onChange={() => setPhone(e.target.value)}
/>;

// After
const { value: phone, bind: bindPhone } = useInput('');

<input
name="phone"
type="number"
placeholder="Please enter your phone number (required)"
className={`${styles['cell-content']} ${styles['cell-content-right']}`}
{...bindPhone}
/>;
import { createPortal } from 'react-dom';
import styles from './Modal.module.css';

export default function Modal({ content, show, onOk }) {
const modal = show && (
<div className={styles['overlay']}>
<div className={styles['modal']}>
{/* Prevent propagation to close the window */}
<div className={styles['wrapper']} onClick={e => e.stopPropagation()}>
<div className={styles['content']}>{content}</div>
<div className={styles['readed_btn']} onClick={() => onOk()}>
OK
</div>
</div>
</div>
</div>
);

const PortalContent = () => {
// Handle the absence of a document on the server-side
try {
// Mount the modal on the body
return document && createPortal(modal, document.body);
} catch (error) {
return null;
}
};

// Dynamically import component
// import dynamic from 'next/dynamic';
// const Modal = dynamic(() => import('./components/Modal/Modal'), { ssr: false });

return (
<>
<PortalContent />
</>
);
}

Mobile Adaptation

Using the postcss-px-to-viewport plugin

Create a postcss.config.js file in the root directory

module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
unitPrecision: 3,
viewportUnit: 'vw',
selectorBlackList: ['.ignore'],
minPixelValue: 1,
mediaQuery: false
}
}
};

Automated Deployment with Docker + Coding

Common Docker Commands

  • docker image ls
  • docker image build -t [imageName] . The . represents the path to the Dockerfile.
  • docker container ls List all running containers. --all , -a can be used to list all containers.
  • docker container run -p [appPort:dockerPort] [imageName] Create a container instance.
  • docker container kill [containID]

Dockerfile

FROM node:12-alpine

ARG API_ENV

RUN echo ${API_ENV}

ENV NEXT_PUBLIC_API_ENV=${API_ENV}

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY package*.json /usr/src/app/
RUN npm install

COPY . /usr/src/app

RUN npm run build
EXPOSE 3000

CMD [ "npm", "run", "start" ]

Set up a code push trigger rule in Coding to trigger the generation of artifacts.

Using Redux

https://github.com/vercel/next.js/tree/canary/examples/with-redux

https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk

It uses a new JavaScript feature called Nullish coalescing operator.

Pitfalls of Next.js

Environment Configuration

Environment variables cannot be accessed by the client-side. Background: I needed to use different API domain names based on the environment variable in my project.

Solution: The official documentation provides environment variables starting with NEXT_PUBLIC_, which allows the environment variables to be accessed by both the client-side and server-side.

Environment Detection

process.browser === true ? 'client' : 'server';

This article is licensed under the CC BY-NC-SA 4.0.