Lesson 20-Next.js Build Process and Tool Integration

Build Process

Creating a Next.js Project

First, ensure you have Node.js and npm installed. Then, create a new Next.js project using the following command:

npx create-next-app@latest my-app
cd my-app

Build Command

To build a Next.js application, run:

npm run build

Build Process Overview

The build process consists of the following key stages:

  • Static Assets and Dependencies Bundling: Packages static assets (e.g., CSS and JavaScript) and dependencies into browser-compatible files.
  • Page Compilation: Compiles Next.js pages into formats usable by both client and server.
  • Static Generation: Generates static HTML files for statically generated pages.
  • Server-Side Rendering Preparation: Prepares necessary files and configurations for server-side rendering.
  • Final Packaging: Bundles all generated files into a deployable production version.

Detailed Build Process

Below is a detailed analysis of each step in the build process.

Configuration File

Next.js 14 uses the next.config.js file to configure the build process. By default, Next.js looks for this file and applies its settings.

// next.config.js
module.exports = {
  reactStrictMode: true,
  swcMinify: true,
  // Other configurations...
};

Bundling Static Assets

Next.js bundles static assets (e.g., CSS and JavaScript) into files that browsers can load.

// pages/_app.js
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

Page Compilation

Next.js compiles each page, generating code usable by both the client and server.

// pages/index.js
export default function Home() {
  return <h1>Hello, world!</h1>;
}

Static Generation

For statically generated pages, Next.js generates static HTML files during the build process.

// pages/posts/[id].js
export async function getStaticPaths() {
  const posts = await fetch('https://my-api/posts').then(res => res.json());
  const paths = posts.map(post => ({
    params: { id: post.id.toString() },
  }));
  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  const post = await fetch(`https://my-api/posts/${params.id}`).then(res => res.json());
  return { props: { post } };
}

function Post({ post }) {
  return <div>{post.title}</div>;
}

export default Post;

Server-Side Rendering Preparation

For server-side rendered pages, Next.js generates the necessary files and configurations.

// pages/_app.js
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

MyApp.getInitialProps = async ({ Component, ctx }) => {
  let pageProps = {};

  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  return { pageProps };
};

export default MyApp;

Final Packaging

After the build is complete, Next.js packages all generated files into a final deployable version.

npm run build

In-Depth Build Process Analysis

Key steps and technical details of the build process include:

SWC Compiler

Next.js 14 uses the SWC compiler to compile JavaScript code. SWC is a high-performance JavaScript compiler that transforms modern JavaScript into browser-compatible versions.

Webpack Configuration

Next.js uses Webpack as its module bundler. The Webpack configuration is defined in the .next/webpack.config.js file, which specifies how to bundle various parts of the application.

React Server Components

React Server Components are a new feature that enables rendering components on the server. Next.js 14 supports React Server Components, improving application performance and maintainability.

// pages/index.js
export default function Home() {
  return (
    <div>
      <h1>Hello, world!</h1>
      <p>This is a server component.</p>
    </div>
  );
}

Incremental Static Regeneration (ISR)

Incremental Static Regeneration (ISR) is a hybrid approach combining the benefits of static generation and server-side rendering. ISR allows generating static HTML files during the build and re-generating them on-demand after deployment.

// pages/posts/[id].js
export async function getStaticProps({ params }) {
  const post = await fetch(`https://my-api/posts/${params.id}`).then(res => res.json());
  return { props: { post }, revalidate: 60 }; // Regenerate every minute
}

function Post({ post }) {
  return <div>{post.title}</div>;
}

export default Post;

export async function getStaticPaths() {
  // Fetch the list of post IDs
  const posts = await fetch('https://my-api/posts').then(res => res.json());
  const paths = posts.map(post => ({
    params: { id: post.id.toString() },
  }));
  return { paths, fallback: 'blocking' }; // Blocking mode, wait for new page generation
}

Deployment

After building, you can deploy the application to various platforms.

Start the Server Locally

npm start

Deploy to Vercel

Use the Vercel CLI or Vercel website for deployment.

Other Cloud Providers

Deploy the built files to AWS Lambda, Google Cloud Functions, or similar services.

Tool Integration

Creating a Next.js Project

Ensure Node.js and npm are installed, then create a new Next.js project:

npx create-next-app@latest my-app
cd my-app

Integrating ESLint

ESLint is a static code analysis tool that helps developers identify errors and inconsistencies in their code.

Install ESLint

npm install eslint --save-dev

Configure ESLint

Create an .eslintrc.json file to configure ESLint.

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": ["react"],
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
  }
}

Add ESLint to the Build Process

Add a script to package.json to run ESLint.

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

Integrating Prettier

Prettier is a code formatter that ensures consistent code style.

Install Prettier

npm install prettier --save-dev

Configure Prettier

Create a .prettierrc.json file to configure Prettier.

// .prettierrc.json
{
  "semi": true,
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "trailingComma": "all"
}

Integrate ESLint and Prettier

Install additional dependencies to make ESLint and Prettier work together.

npm install eslint-config-prettier eslint-plugin-prettier --save-dev

Update the .eslintrc.json file to use Prettier.

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": ["react"],
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
  }
}

Integrating TypeScript

TypeScript is a superset of JavaScript that adds a type system.

Install TypeScript

npm install typescript @types/react @types/node --save-dev

Configure TypeScript

Create a tsconfig.json file to configure TypeScript.

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Update Next.js Configuration

Configure TypeScript in the next.config.js file.

// next.config.js
const withTypescript = require('@zeit/next-typescript');

module.exports = withTypescript({
  // Other configurations...
});

Integrating Storybook

Storybook is a tool for developing UI component libraries.

Install Storybook

npx sb init

Configure Storybook

Storybook automatically creates a .storybook directory with configuration files.

// .storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions'
  ],
  framework: '@storybook/nextjs',
  core: {
    builder: '@storybook/builder-webpack5'
  }
};

Create Component Stories

Create a .stories.js file in the src directory to write component stories.

// src/components/Button/Button.stories.js
import Button from './Button';
import { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof Button> = {
  title: 'Example/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

export const Secondary: Story = {
  args: {
    primary: false,
    label: 'Button',
  },
};

Integrating React Query

React Query is a library for managing data state.

Install React Query

npm install react-query

Use React Query

Introduce QueryClient and QueryClientProvider in your application.

// pages/_app.js
import { QueryClient, QueryClientProvider } from 'react-query';
import '../styles/globals.css';

const queryClient = new QueryClient();

function MyApp({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

Use the useQuery Hook

Use the useQuery hook in components to fetch data.

// pages/index.js
import { useQuery } from 'react-query';

export default function Home() {
  const { data, isLoading, isError } = useQuery('repoData', () =>
    fetch('https://api.github.com/repos/vercel/next.js').then(res => res.json())
  );

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  );
}

Integrating Tailwind CSS

Tailwind CSS is a utility-first CSS framework for rapidly building beautiful user interfaces.

Install Tailwind CSS

npm install tailwindcss postcss autoprefixer --save-dev

Configure Tailwind CSS

Create a tailwind.config.js file to configure Tailwind CSS.

// tailwind.config.js
module.exports = {
  content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

Create PostCSS Configuration

Create a postcss.config.js file to configure PostCSS.

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

Update Next.js Configuration

Configure Tailwind CSS in the next.config.js file.

// next.config.js
const withCSS = require('@zeit/next-css');
const withPlugins = require('next-compose-plugins');
const withTM = require('next-transpile-modules')(['react', 'react-dom']);

module.exports = withPlugins(
  [
    [withCSS],
    withTM,
  ],
  {
    // Other configurations...
  }
);

Integrating Jest

Jest is a popular JavaScript testing framework for writing and running tests.

Install Jest

npm install jest @testing-library/react @testing-library/jest-dom --save-dev

Configure Jest

Create a jest.config.js file in the project root to configure Jest.

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
  moduleNameMapper: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/__mocks__/fileMock.js',
    '\\.(css|less|scss)$': '<rootDir>/__mocks__/styleMock.js',
  },
};

Create a __mocks__ directory to store mock files.

// __mocks__/fileMock.js
module.exports = '';

// __mocks__/styleMock.js
module.exports = {};

Write Tests

Create a __tests__ directory in the src folder to write tests.

// src/__tests__/Button.test.js
import { render, screen } from '@testing-library/react';
import Button from '../components/Button';

test('renders a button', () => {
  render(<Button />);
  const buttonElement = screen.getByText(/Button/i);
  expect(buttonElement).toBeInTheDocument();
});

Integrating Husky and Lint-Staged

Husky is a tool for managing Git hooks, and Lint-Staged runs linting and formatting tools before commits.

Install Husky and Lint-Staged

npm install husky lint-staged --save-dev

Configure Husky

Create a .huskyrc.js file to configure Husky.

// .huskyrc.js
module.exports = {
  hooks: {
    'pre-commit': 'lint-staged',
  },
};

Configure Lint-Staged

Create a .lintstagedrc.json file to configure Lint-Staged.

// .lintstagedrc.json
{
  "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
    "prettier --write",
    "git add"
  ]
}

Integrating Vercel

Vercel is a platform for deploying Next.js applications.

Install Vercel CLI

npm install -g vercel

Configure Vercel

Create a vercel.json file in the project root to configure Vercel.

// vercel.json
{
  "buildCommand": "next build",
  "devCommand": "next dev",
  "routes": [
    { "src": "/(.*)", "dest": "/_next/static/development/_buildManifest" }
  ]
}

Deploy to Vercel

Deploy the application using the Vercel CLI.

vercel

Integrating React Router

Although Next.js has built-in routing, you may need React Router for more complex routing logic in certain scenarios.

Install React Router

npm install react-router-dom

Configure React Router

Introduce React Router in _app.js and set up routes.

// pages/_app.js
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import '../styles/globals.css';
import Home from '../pages/index';
import About from '../pages/about';

function MyApp({ Component, pageProps }) {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route component={Component} />
      </Switch>
    </Router>
  );
}

export default MyApp;

Integrating Redux

Redux is a library for managing application state.

Install Redux

npm install redux react-redux

Create Redux Store

Create a store.js file to initialize the Redux store.

// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

export default store;

Create Reducers

Create a reducers.js file to define the application’s state logic.

// reducers.js
const initialState = {
  count: 0,
};

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

export default counterReducer;

Use Provider

Wrap the application in _app.js to make the Redux store accessible.

// pages/_app.js
import { Provider } from 'react-redux';
import store from '../store/store';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;

Create Actions

Create an actions.js file to define application actions.

// actions.js
export const increment = () => ({
  type: 'INCREMENT',
});

export const decrement = () => ({
  type: 'DECREMENT',
});

Use useSelector and useDispatch

Use the useSelector and useDispatch hooks in components to access state and dispatch actions.

// pages/index.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../store/actions';

export default function Home() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

Integrating MobX

MobX is a state management library with a simpler API.

Install MobX

npm install mobx mobx-react-lite

Create MobX Store

Create a store.js file to initialize the MobX store.

// store.js
import { makeAutoObservable } from 'mobx';

class CounterStore {
  count = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}

const store = new CounterStore();

export default store;

Use useObserver

Use the useObserver hook in components to observe state changes.

// pages/index.js
import { useObserver } from 'mobx-react-lite';
import store from '../store/store';

export default function Home() {
  return useObserver(() => (
    <div>
      <h1>Count: {store.count}</h1>
      <button onClick={() => store.increment()}>+</button>
      <button onClick={() => store.decrement()}>-</button>
    </div>
  ));
}
Share your love