![article hero](https://react-japan.dev/assets/blog/eslint-foundation-640.webp)
![Steven Sacks](https://react-japan.dev/authors/steven.webp)
ESLint - Foundation
The following are my recommendations for ESLint, Prettier, Stylelint, and editorconfig.
ESLint v8.57.0
First and foremost, even though ESLint v9 is available, most plugins are incompatible with it. For this reason, I strongly suggest sticking with version 8.57.0 until more plugins are made compatible with v9.
Plugins
These are the plugins with rulesets (with some customization) that will form the foundation of your ESLint setup.
- Airbnb TypeScript
- Unicorn
- SonarJS
- Comments
- Import and Import Resolver TypeScript
- Config Prettier (not plugin prettier or eslint prettier, which you should not use)
- React and React Hooks
- jsx-a11y
Here is my recommended configuration in the .eslintrc.cjs
file. You can choose to modify the setting of the rules as you see fit, but I believe these rules should all be set to something. This setup supports js
and ts
files (as well as cjs
, mjs
etc.). I have added comments as notes for this article, which you can remove.
const shared = {
extends: [
'eslint:recommended',
'plugin:eslint-comments/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:sonarjs/recommended-legacy',
'plugin:tailwindcss/recommended',
'plugin:unicorn/recommended',
'prettier',
],
plugins: [
'react',
'sonarjs',
'unicorn',
'tailwindcss', // if you use tailwind
'you-dont-need-lodash-underscore', // if you use lodash
],
rules: [
'consistent-return': 'off',
curly: ['error', 'all'],
'eslint-comments/disable-enable-pair': 'off',
'eslint-comments/no-unused-disable': 'error',
'import/extensions': 'off',
'import/no-anonymous-default-export': [
'error',
{
allowArray: true,
allowLiteral: true,
allowObject: true,
},
],
'import/order': 'off', // handled by another plugin
'import/prefer-default-export': 'off',
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'], // if you use Remix, Next, etc.
specialLink: ['to'],
},
],
'max-params': ['error'], // default is 3
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
next: ['block-like', 'export', 'return'],
prev: '*',
},
],
quotes: [
'error',
'single',
{
allowTemplateLiterals: false,
avoidEscape: true,
},
],
'react/boolean-prop-naming': [
'error',
{
propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
rule: '^((is|has|can|show|hide|no)[A-Z]([A-Za-z0-9]?)+|(show|hide|disabled|required))',
},
],
'react/function-component-definition': 'off',
'react/jsx-boolean-value': ['error', 'always'],
'react/jsx-filename-extension': ['error', {extensions: ['.tsx']}],
'react/jsx-newline': ['error', {prevent: true}],
'react/jsx-props-no-spreading': 'off',
'react/prop-types': 'off', // use TypeScript for props
'react/require-default-props': 'off',
'spaced-comment': 'off', // good in theory, annoying in practice
// handled by prettier-plugin-tailwindcss
'tailwindcss/classnames-order': 'off',
'unicorn/consistent-destructuring': 'error',
// the following should be turned off for React compatability
// and a few are rules I believe are problematic
'unicorn/new-for-builtins': 'off',
'unicorn/no-array-callback-reference': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/no-null': 'off',
'unicorn/no-useless-undefined': 'off',
'unicorn/prefer-export-from': 'off',
'unicorn/prefer-set-has': 'off',
'unicorn/prefer-switch': 'off',
'unicorn/prefer-ternary': 'off',
'unicorn/prevent-abbreviations': [
'error',
{
// these are my preferences
ignore: [
'acc',
'ctx',
'e2e',
'env',
'obj',
'prev',
'req',
'res',
/args/i,
/fn/i,
/params/i,
/props/i,
/ref/i,
/utils/i,
],
},
],
'unicorn/text-encoding-identifier-case': 'off',
],
};
module.exports = {
root: true,
env: {
browser: true,
commonjs: true,
es2024: true,
node: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
ignorePatterns: ['node_modules', 'build'],
extends: ['airbnb', ...shared.extends],
plugins: shared.plugins,
rules: shared.rules,
overrides: [
{
files: ['**/*.ts?(x)'],
extends: [
'airbnb',
'airbnb-typescript',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
...shared.extends,
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
},
plugins: [
...shared.plugins,
'@typescript-eslint',
'import',
],
settings: {
'import/internal-regex': '^~/', // see below why tilde
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true,
},
},
react: {
pragma: 'React',
version: 'detect',
},
tailwindcss: {
callees: ['twJoin', 'twMerge'], // tailwind-merge
},
},
rules: {
...shared.rules,
'no-undef': 'off', // TypeScript handles this
'no-unused-vars': 'off', // TypeScript handles this
'@typescript-eslint/array-type': ['error', {default: 'generic'}],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/method-signature-style': 'error',
'@typescript-eslint/no-throw-literal': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': ['error'],
'@typescript-eslint/no-unused-vars': 'error',
},
},
{
files: ['*.tsx', '**/hooks/*.ts?(x)'],
rules: {
// too easy to violate in components and hooks
'sonarjs/cognitive-complexity': 'off',
},
},
{
files: ['src/?(components|pages|services|utils)/**/*.ts?(x)'],
rules: {
'max-lines': ['error', 200],
},
},
{
files: ['./*.ts'], // only project root files
rules: {
'global-require': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'error',
'import/prefer-default-export': 'off',
'no-void': 'off',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
},
},
{
files: ['**/*.d.ts'],
rules: {
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/method-signature-style': 'error',
'@typescript-eslint/no-unused-vars': 'off',
},
},
],
};
Details
You can lookup any of the rules yourself and decide if you want to change the setting. Here are my justifications for some of them.
- max-params - In functional programming, you should ideally only have 1 param per function. In practice, this can be a bit too restrictive, so having a limit of 3 params is sensible before converting to a single object with named params. Too many params requires developers to memorize the order and also makes multiple optional params a pain. It does not matter if you can look up the signature or TypeScript helps you. In practice, this limit is better. React components are a great example of this in practice because props is single object param.
- padding-line-between-statements - This rule ensures consistent code styling and readability. It is a “prettier”-type rule because if you just accept that it does what it does, it ensures that everyone’s code is formatted the same, and you get used to it quickly.
- quotes - This ensures optimal quotes for all 3 types (single, double, backtick).
- react/jsx-boolean-value - Being explicit is better, and it makes props look consistent. Even for people who don’t prefer this, since it fixes on save, they can write it without being explicit, and when they save, it makes it explicit, and thus consistent for everyone.
- import/internal-regex - Remix uses
~
tilde, and I believe this is superior because so many installed packages use@
. By using~
, there is a distinct difference between importing your code vs code fromnode_modules
. This needs to be configured inside yourtsconfig.json
, as well (paths). - @typescript-eslint/array-type - Pick the setting you prefer. There are good arguments for both array and generic. This enforces one or the other automatically and consistency is our goal. It automatically fixes to the preferred one on save, so developers can write whichever is more comfortable for them.
- max-lines - If your components are getting too large, it’s usually a sign that you should break them up into smaller ones for performance, readability, and separation of concerns. There are going to be exceptions here and there. In those cases, the developer can disable this rule in the file, and it can be discussed during a code review. In my experience, nearly all of my component files are less than 200 lines, but you can increase this to 250, or use the default of 300, if you prefer.
- @typescript-eslint/consistent-type-definitions - You should use
type
for functional programming and immutability.interface
is for OOP and mutability. This article has a good explanation why. No matter what, you want to consistently use one or the other. The rule is disabled for.d.ts
files because third party libraries may requireinterface
.
.prettierrc
These are my Prettier settings.
{
"bracketSpacing": false,
"experimentalTernaries": true,
"plugins": ["prettier-plugin-tailwindcss"], // if you use tailwind
"singleQuote": true,
"tailwindFunctions": ["twJoin", "twMerge"], // if you use tailwind
"trailingComma": "es5"
}
bracket-spacing
Some people like bracket-spacing. I think it makes code wider than it needs to be, and I don’t find that it improves readability. If you like it, stick with the default setting.
experimentalTernaries
You can read about them here. These will eventually become the default, so I enable them to get used to them sooner. I agree with the Prettier team that they are better.
plugins
If you use Tailwind, you should use the Tailwind Prettier Plugin.
single-quote
It is easier to type single quotes (does not require typing two keys at once), and whether it is more common to use apostrophes or double-quotes inside of strings, it does not outweigh how frequently you type single quotes while coding. Single quotes have been the standard for a very long time, and, in my experience, everyone sets this to true
.
tailwindFunctions
If you use Tailwind, you should use Tailwind Merge, instead of clsx or classnames.
trailingComma
Trailing commas are great! But, I don’t like them in function parameters. I think they look messy before the closing parentheses and arrow const foo = (a, b, c,) =>
. So, I set this to “es5” instead of “all”.
.stylelintrc.json
Here are my suggested settings for Stylelint.
{
"extends": [
"stylelint-config-standard",
"stylelint-config-idiomatic-order",
"stylelint-config-tailwindcss" // if you're using tailwind
],
"ignoreFiles": ["build/**/*.css", "public/build/**/*.css"],
"overrides": [
{
"files": ["**/*.module.css"],
"rules": {
// enforces camelCase class names for css modules
"selector-class-pattern": "^[a-z][a-zA-Z0-9]+$"
}
}
],
"plugins": ["stylelint-order"],
"rules": {
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "local"]
}
]
}
}
.editorconfig
IDEs will automatically follow your rules by adding this file to the root of your project. These are my suggested settings.
# editorconfig.org
root = true
# We recommend to keep these unchanged
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
Next article: ESLint - Sorting
Main article: Take your workflow to the next level with ESLint fix on save