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
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
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
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 havealt
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 withprettier
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"
}
}
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}\""
Now when you run yarn lint
in project root
You should see output similar to this
Ok so we have 14 warnings. Lets try to fix them by running yarn lint:fix
in project root
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
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
And create configuration file
Lets create file .prettierrc
in project root and paste following content inside
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}
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
Add following into your package.json
's script section
"precommit": "lint-staged"
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"
]
}
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"
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
Add following into your package.json
's script section
"commit": "clear && git-cz"
Add following to the end of package.json
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
}
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"
}
},
We can test our new git hook by running yarn commit
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
You can find whole working solution on github
If you liked this article you can follow me on twitter
Top comments (1)
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"
}
}