DEV Community

Cover image for VS Code + React + Typescript code quality setup 2020
Zikitel22
Zikitel22

Posted on • Edited on

VS Code + React + Typescript code quality setup 2020

So here's the thing.
I started several projects combining React and Typescript lately and found myself repeating same setup over and over again.
Usually on project's first day i would do only this chore and esentially waste one day.

Dont get me wrong create-react-app offers nice start but gives you almost nothing in terms of code quality.

As my teams usually consist of non trivial percentage of junior developers i want to make sure common mistakes are caught early, code is formatted well and commit messages make sense.

If you are experiencing same problems keep reading
We will be using yarn as our package manager throughout this post.
If you dont have it installed yet do it via npm install -g yarn in your terminal

1. Lets start with creating our project

npx create-react-app dev-experience-boilerplate --template typescript
Enter fullscreen mode Exit fullscreen mode

We are using npx which is npm package runner and executes create-react-app package without installing it globally

Above code is equivalent to

npm install -g create-react-app
create-react-app dev-experience-boilerplate --template typescript
Enter fullscreen mode Exit fullscreen mode

3. Linting (Eslint)

Linting is by definition tool that analyzes source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. We will be using eslint - linting tool for javascript.

First lets integrate eslint in our IDE by installing VS Code extension from marketplace

In Next step will be installling all needed dependencies

yarn add eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
@typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-header eslint-plugin-import eslint-config-prettier --dev
Enter fullscreen mode Exit fullscreen mode

That is lot of dependencies.
What we did here? Well we installed bunch of plugins
Lets look at them one by one

  • eslint - Tool itself
  • eslint-config-airbnb - Good guys in airbnb made their eslint configuration public. Everyone can use extend and override defined rules
  • eslint-plugin-react - React specific linting rules for ESLint
  • eslint-plugin-jsx-a11y - Set of accessibility rules on JSX elements. We want to be proper developers and deliver best possible experience also for impaired visitors of our application. One of such rules can be that <img> tags should have alt attribute so screen reader knows what is on image. If you forget to add alt wslint will yell at you
  • eslint-plugin-react-hooks - Since react version 16.8.0 we are writing majority of our components in hooks. We want them write right.
  • @typescript-eslint/parser - Sice our project uses typescript we need to tell eslint that our code is not vanilla javascript and needs to be parsed
  • @typescript-eslint/eslint-plugin - Set of rules for typescript
  • eslint-config-prettier - Eslint plugin that removes all rules that can possibly conflict with prettier which we will install in next step
  • eslint-plugin-header - EsLint plugin to ensure that files begin with given comment. I personally like when every file starts with header with basic info like Author and Date. When you work in larger team its a nice way to see ownership of file and when something is not clear or right you know who you should bother with questions
  • eslint-plugin-import - Linting of ES6 import/export syntax

Now when everything is installed lets define our rules

This is very opinionated but heres what works for me.

In root of your project create file named .eslintrc and paste following code snippet inside

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "react-app",
    "prettier",
    "prettier/@typescript-eslint"
  ],
  "plugins": ["@typescript-eslint", "react-hooks", "header"],
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "rules": {
    "header/header": [2, "block", [{ "pattern": " \\* Author : .*" }]],
    "@typescript-eslint/consistent-type-definitions": ["warn", "type"],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-member-accessibility": "off",
    "@typescript-eslint/no-angle-bracket-type-assertion": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    "@typescript-eslint/no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "ignoreRestSiblings": true
      }
    ],
    "@typescript-eslint/no-use-before-define": [
      "warn",
      {
        "functions": false,
        "classes": false,
        "variables": true
      }
    ],
    "import/no-extraneous-dependencies": "warn",
    "import/order": [
      "warn",
      {
        "newlines-between": "always"
      }
    ],
    "no-case-declarations": "warn",
    "no-console": "warn",
    "no-debugger": "warn",
    "no-else-return": "warn",
    "no-param-reassign": "warn",
    "no-undef": "off",
    "no-unused-vars": "off",
    "no-var": "warn",
    "object-shorthand": "warn",
    "padding-line-between-statements": [
      "warn",
      {
        "blankLine": "always",
        "prev": "*",
        "next": "class"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "for"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "function"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "if"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "return"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "switch"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "try"
      },
      {
        "blankLine": "always",
        "prev": "*",
        "next": "while"
      },
      {
        "blankLine": "always",
        "prev": "block-like",
        "next": ["let", "const"]
      }
    ],
    "prefer-const": "warn",
    "react/jsx-boolean-value": "warn",
    "react/jsx-curly-brace-presence": "warn",
    "react/jsx-key": "warn",
    "react/jsx-sort-props": [
      "warn",
      {
        "callbacksLast": true,
        "reservedFirst": true,
        "shorthandLast": true
      }
    ],
    "react/no-array-index-key": "warn",
    "react/prefer-stateless-function": "warn",
    "react/self-closing-comp": "warn",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "off",
    "yoda": "warn"
  }
}

Enter fullscreen mode Exit fullscreen mode

I dont want to go into much details here but i encourage you to sit with your team go through all of them, and discuss what works and what doesn't for you. There is no single right answer to how .eslintrc should look like

Last thing we need to do is to set up linting command in package.json

Into section scripts add following snippet

 "lint": "eslint \"src/**/*.{ts,tsx}\"",
 "lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\""
Enter fullscreen mode Exit fullscreen mode

Now when you run yarn lint in project root
You should see output similar to this
alt text

Ok so we have 14 warnings. Lets try to fix them by running yarn lint:fix in project root
alt text

Awesome down to 6 with no effort. Eslint sorted props added blank lines for better readability and more for us for free.

There are some console.log statements in serviceWorker.ts
For some reason i want to leave service worker as is and not to lint this partiular file.
Eslint comes with solution for that.

Lets create .eslintignore file in project root and add following content inside

src/serviceWorker.ts
Enter fullscreen mode Exit fullscreen mode

Now after running yarn lint there should be no errors. Life is beautiful again 🦄

2. Prettier

Prettier is code formatter that supports number of languages and can be easily integrated into VS Code.

Similar to eslint we first need to install VS code extension

Add dependencies

yarn add prettier --dev
Enter fullscreen mode Exit fullscreen mode

And create configuration file

Lets create file .prettierrc in project root and paste following content inside

{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100
}

Enter fullscreen mode Exit fullscreen mode

Thats everything for prettier now your code will look nice and consistent across all files

If for some reason you dont want to prettify some of your files you can create .prettierignore file in your project root

3. Precommit hook

Now. You can run eslint and prettify every time you are about to commit your changes but lets be honest . We all forget.
You cannot forgot if husky barks at you though.
Husky is handy tool that prevents you from accidentaly pushing changes that are well.... not ideal into repository.

Lets see it in action.

First install dependencies

yarn add husky lint-staged --dev
Enter fullscreen mode Exit fullscreen mode

Add following into your package.json's script section

"precommit": "lint-staged"
Enter fullscreen mode Exit fullscreen mode

And following to the end of package.json

"husky": {
    "hooks": {
        "pre-commit": "lint-staged"
    }
},
"lint-staged": {
    "src/**/*.{js,ts,tsx}": [
      "prettier --config .prettierrc --write",
      "eslint --fix \"src/**/*.{ts,tsx}\"",
      "eslint \"src/**/*.{ts,tsx}\"",
      "git add"
    ]
  }
Enter fullscreen mode Exit fullscreen mode

To see if our setup works lets create unused variable in App.tsx. And try to commit our changes via git add . and git commit -m "This shouldnt work"
alt text

And indeed husky barked and we have to fix our code in order to be able to push it into repository.

4. Commit message hook

Last thing i want to cover is consistent naming of commit messages. This is common mistake in lots of repositories. You can of course create your own git hook but i recently fell in love with git-cz which is tool for interactively commiting changes via your favourite terminal.

Installation

yarn add git-cz @commitlint/cli @commitlint/config-conventional --dev
Enter fullscreen mode Exit fullscreen mode

Add following into your package.json's script section

 "commit": "clear && git-cz"
Enter fullscreen mode Exit fullscreen mode

Add following to the end of package.json

 "commitlint": {
    "extends": [
      "@commitlint/config-conventional"
    ]
  }
Enter fullscreen mode Exit fullscreen mode

And last thing is to tell husky to run our new commit-msg hook
We do this by changing husky section in package.json

"husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
      "pre-commit": "lint-staged"
    }
  },
Enter fullscreen mode Exit fullscreen mode

We can test our new git hook by running yarn commit
alt text

You can see this awesome cli tool that lets you choose type of change you are about to commit and more. This can be all configured
In default config you will fill in folllwing:

  • Type of change (test, feature, fix, chore, docs, refactor, style, ci/cd and performance)
  • Commit message
  • Longer description (optional)
  • List of breaking changes (optional)
  • Referenced issue (i.e JIRA task number)

And commit messages are now consistent across whole team
Plus you get neat commit message icons like this
alt text

You can find whole working solution on github
If you liked this article you can follow me on twitter

Top comments (1)

Collapse
 
kiranbansode profile image
kiranbansode • Edited

Hey, I setup my project using above config. But I'm stuck with incorrect header warning. Would you please share your header file

Update[9,Nov 2020]
Hey everyone, those who are having issues with author's above .eslintrc rules you can use mine. Check if it's work for you

{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
// force to use airbnb styleguide
"airbnb",
"prettier",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "react-hooks", "header"],
"settings": {
// solves import problems with tsx,ts,jsx,js files
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
},
"react": {
"version": "detect"
}
},
"rules": {
// force to type header comment on each file
"header/header": [2, "config/header.js"], // folder/filename in project directory
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
// solves import problems with tsx,ts,jsx,js files
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"@typescript-eslint/consistent-type-definitions": ["warn", "type"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-angle-bracket-type-assertion": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
// solve React defined problem
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [
"warn",
{
"functions": false,
"classes": false,
"variables": true
}
],
"import/no-extraneous-dependencies": "warn",
"import/order": [
"warn",
{
"newlines-between": "always"
}
],
"no-case-declarations": "warn",
"no-console": "warn",
"no-debugger": "warn",
"no-else-return": "warn",
"no-param-reassign": "warn",
"no-undef": "off",
"no-unused-vars": "off",
"no-var": "warn",
"object-shorthand": "warn",
"padding-line-between-statements": [
"warn",
{
"blankLine": "always",
"prev": "*",
"next": "class"
},
{
"blankLine": "always",
"prev": "*",
"next": "for"
},
{
"blankLine": "always",
"prev": "*",
"next": "function"
},
{
"blankLine": "always",
"prev": "*",
"next": "if"
},
{
"blankLine": "always",
"prev": "*",
"next": "return"
},
{
"blankLine": "always",
"prev": "*",
"next": "switch"
},
{
"blankLine": "always",
"prev": "*",
"next": "try"
},
{
"blankLine": "always",
"prev": "*",
"next": "while"
},
{
"blankLine": "always",
"prev": "block-like",
"next": ["let", "const"]
}
],
"prefer-const": "warn",
"react/jsx-boolean-value": "warn",
"react/jsx-curly-brace-presence": "warn",
"react/jsx-key": "warn",
"react/jsx-sort-props": [
"warn",
{
"callbacksLast": true,
"reservedFirst": true,
"shorthandLast": true
}
],
"react/no-array-index-key": "warn",
"react/prefer-stateless-function": "warn",
"react/self-closing-comp": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "off",
"yoda": "warn"
}
}