Set up a Next.js app with Typescript, ESLint & Prettier

Posted on: Saturday, 12 December 2020
← Back to the Blog

This post outlines the steps I take to bootstrap a new Next.js project and covers the following features:

  • TypeScript support
  • Prettier for formatting
  • Eslint for linting
  • VS Code settings

Getting Started

Bootstrap a Next.js project with TypeScript support:

npx create-next-app app-name
cd app-name
touch tsconfig.json
npm install --save-dev typescript @types/react @types/node
npm run dev

(Next.js will populate the empty tsconfig.json for you. The only additional change I make is to set "strict": true.)

Set up eslint and prettier:

npm i --save-dev @typescript-eslint/eslint-plugin \
  @typescript-eslint/parser \
  eslint \
  eslint-config-prettier \
  eslint-plugin-jsx-a11y \
  eslint-plugin-prettier \
  eslint-plugin-react-hooks \
  prettier

Configure eslint and prettier by adding the following to your package.json:

{
  "eslintConfig": {
    "ignorePatterns": ["**/*.js", ".next"],
    "extends": ["eslint:recommended", "plugin:prettier/recommended"],
    "env": {
      "browser": true,
      "node": true
    },
    "parserOptions": {
      "ecmaVersion": 6
    },
    "overrides": [
      {
        "files": ["*.{ts,tsx}"],
        "parser": "@typescript-eslint/parser",
        "plugins": ["@typescript-eslint"],
        "extends": [
          "plugin:jsx-a11y/recommended",
          "plugin:react-hooks/recommended",
          "plugin:@typescript-eslint/recommended",
          "prettier/@typescript-eslint"
        ],
        "parserOptions": {
          "project": "./tsconfig.json"
        }
      }
    ]
  },
  "prettier": {
    "trailingComma": "es5",
    "tabWidth": 2,
    "semi": true,
    "singleQuote": true,
    "overrides": [
      {
        "files": "*.svg",
        "options": {
          "parser": "html"
        }
      }
    ]
  }
}

Add linting scripts to package.json:

{
  "scripts": {
    "lint": "npm run lint:prettier && npm run lint:eslint",
    "lint:fix": "npm run lint:fix:prettier && npm run lint:eslint -- --fix",
    "lint:prettier": "prettier --check \"**/*.{ts,js,json,svg,md,yml}\"",
    "lint:fix:prettier": "prettier --write '**/*.{ts,js,json,svg,md,yml}'",
    "lint:eslint": "eslint . --ext .js,.ts"
  }
}

Add the following to .prettierignore:

.next
package-lock.json

Update vscode settings within .vscode/settings.json:

{
  "files.exclude": {
    ".next": false,
    "package-lock.json": false
  },
  "typescript.tsc.autoDetect": "off",
  "typescript.updateImportsOnFileMove.enabled": "always",
  "typescript.tsserver.experimental.enableProjectDiagnostics": false,
  "eslint.validate": ["javascript", "typescript"],
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.organizeImports": false
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[yaml]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "files.insertFinalNewline": true,
  "files.trimTrailingWhitespace": true,
  "cSpell.language": "en-GB"
}

Runtime helpers

Classnames

If you're using css modules then using a utility library like classnames can be handy.

npm i classnames --save
npm i @types/classnames --save-dev

Usage:

import classNames from 'classnames/bind';
import STYLES from './MyComponent.module.css';
const classes = classNames.bind(STYLES);

const MyComponent = ({ className }) => {
  return <div className={classes('root', className)}></div>;
};

The next.js link component requires you to omit the href property on the anchor tag and this conflicts with ally linting as well as just being a little weird. To workaround this it can be helpful to compose a new Link component to ignore the linting error in one place, as well as make the component easier to use by accepting HTML props.

// Default next.js Link usage, a little weird, and shows an ally linting error as href is missing
<Link href="/blog">
  <a>
    Blog
  </a>
</Link>

// New and improved composed Link component, nice and neat
<Link href="/blog">
  Blog
</Link>

Link.tsx:

import { default as NextLink, LinkProps as NextLinkProps } from 'next/link';

export type LinkProps = React.DetailedHTMLProps<
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
  HTMLAnchorElement
> &
  NextLinkProps;

export const Link: React.FunctionComponent<LinkProps> = ({
  href,
  ...props
}) => {
  return (
    <NextLink href={href}>
      <a {...props} />
    </NextLink>
  );
};

Project Organisation

I like to have my styles sitting next to my components, and organize my components into features. Page components are stored within a features directory, along with all the other app features.

Here's a typical layout for a multi-page next.js app:

├── features
│   ├── layout
│   │   ├── Header
│   │   │   ├── Header.module.css
│   │   │   └── Header.tsx
│   │   ├── Link
│   │   │   └── Link.tsx
│   │   └── PageShell
│   │       ├── PageShell.module.css
│   │       └── PageShell.tsx
│   └── pages
│       ├── BlogPage
│       │   └── BlogPage.tsx
│       └── HomePage
│           ├── HomePage.module.css
│           └── HomePage.tsx
├── pages
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── blog.tsx
│   └── index.tsx
├── public
│   ├── favicon.ico
├── styles
│   ├── globals.css
│   └── tokens.css
└── tsconfig.json

This is the contents of a route (eg pages/index.tsx):

export { HomePage as default } from '../features/pages/HomePage/HomePage';

Setting browserslist

Setting the supported browsers affects how CSS features and Polyfills are generated.

In package.json:

{
  "browserslist": {
    "production": [">0.2%", "not dead", "not op_mini all"],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

View the Next.js docs for more info.


Comments

No comments

Add a new comment

Markdown supported