Base

Principles

Syntax

  1. Using Prettier for better formatting.
  2. Syntactic homogeneity. Semantically identical constructions must use the same syntax. To achieve that we fix spacings, paddings, quotes, shorthand techniques, etc. E.g., curly and dot-notation.
  3. Minimization of git diffs when code is being changed or refactored. Changing or extending code must change minimal count of symbols which aren't connected with the change in logic. By the most part this is achieved by using Prettier.
  4. Avoid disabling linting rules. We believe that linting rules shouldn't interfere with writing code. Disabling rules is allowed as an exception and a temporary measure. Because of this, we don't use frequently disabled rules like no-mixed-operators, prefer-arrow-callback, no-useless-return, etc.
  5. No warnings. All kinds of undesirable constructions must be fixed. Warning add noise to the result of project building and quickly become being ignored. As an exception warnings are allowed in a situation of a long refactoring but only as a temporary measure. [~]

Semantics

  1. Explicit is better than implicit. JS has many implicit defaults so to avoid confusion we prefer being explicit and not using ambiguous constructions. E.g., no-implicit-coercion.
  2. Minimization of errors. JavaScript is an interpreted language so all the incorrect or meaningless constructions can be detected only during execution. We try to minimize the amount of constructions that can lead to runtime errors. E.g., guard-for-in and eqeqeq.
  3. Make code readable without limiting developers too much. We don't want for linting to be in the way of writing logic so we don't use rules like complexity or max-depth. But we still try to restrict semantic constructions which make reading code difficult and use rules like max-params or no-nested-ternary.
  4. Make sure code is ready for production environment. Code shouldn't contain temporary or debug construction. E.g., no-eval, no-console, no-debugger, etc. [~]

Rules overview

[~]

Prettier

eslint-plugin-prettier is used for integration of Prettier and ESLint.

eslint-config-prettier turns off all the rules from ESLint and its plugins that might conflict with Prettier. [~]

ESLint rules

We are using eslint:recommended as starting point. You can see list of included rules here marked with a check mark. [~]

camelcase

// Fail
const my_var = { prop_name: 1 }

// Pass
const myVar = { propName: 1 }

Names starting with UNSAFE_ are allowed to support React's legacy lifecycle methods:

class MyComponent extends React.Component {
  UNSAFE_componentWillUpdate() {}
}

[~]

curly

// Fail
if (true) return null

// Pass
if (true) { return null }

[~]

default-param-last

// Fail
function f(foo = 0, bar) {}

// Pass
function f(bar, foo = 0) {}

[~]

dot-notation

// Fail
obj['param']

// Pass
obj.param

[~]

eqeqeq

Prefer strict equals to avoid implicit checks.

// Fail
if (x == null) {}

// Pass
const isNil = (v) => v === null || v === undefined

if (isNil(x)) {}

[~]

guard-for-in

For iterating over object keys use Object.keys instead of for ... in as it returns object's own property names and ignores inherited ones.

// Fail
for (key in obj) { }

// Pass
Object.keys(obj).forEach((key) => {})

[~]

max-params

Functions may have maximum 3 parameters:

// Fail
const myFn = (a, b, c, d) => {}

// Pass
const myFn = ({ a, b, c, d }) => {}

If you're using reselect and errors are reported for createSelector with too many params, switch to createStructuredSelector. [~]

no-bitwise

Quite often bitwise operator is just a typo in logical operator:

// Fail
const x = y | z

// Pass
const x = y || z

If you need to use bitwise operators, disable this rule for the lines containing them. [~]

no-console

Remove console.log from production code:

// Fail
console.log('Debug info')

// Pass
console.info('Info')
console.warn('Warning')
console.error('Error')

[~]

no-eval

Forbid usages of eval as potentially dangerous:

// Fail
eval("var a = 0")

[~]

no-implicit-coercion

All implicit coercions except !! are disallowed:

// Fail
+foo
1 * foo
'' + foo
`${foo}`
~foo.indexOf(bar)

// Pass
!!foo

[~]

no-nested-ternary

// Fail
return a ? b ? c : d : e

// Pass
if (a) {
  return b ? c : d
} else {
  return e
}

[~]

no-new-wrappers

// Fail
const n = new Number(1)

// Pass
const n = 1

[~]

no-param-reassign

// Fail
function f(foo) {
  foo++
}

// Pass
function f(foo) {
  const bar = foo + 1
}

[~]

no-shadow

// Fail
rows.forEach((item) => {
  item.columns.forEach((item) => {
    // `item` is shadowed
  })
})

// Pass
rows.forEach((row) => {
  item.columns.forEach((column) => {
  })
})

[~]

no-template-curly-in-string

// Fail
"Hello, ${name}"

// Pass
`Hello, ${name}`

[~]

no-throw-literal

// Fail
throw 'Error'

// Pass
throw new Error('Error')

[~]

no-undef-init

// Fail
let foo = undefined

// Pass
let foo

[~]

no-underscore-dangle

// Fail
const _private = 1

// Pass
items.map((_item, idx) => idx)

[~]

no-unneeded-ternary

// Fail
const isYes = answer === 1 ? true : false

// Pass
const isYes = answer === 1

[~]

no-unused-expressions

// Fail
a || b
(function f() {})

// Pass
a && b()
a ? b() : c()

[~]

no-unused-vars

// Fail
const { foo, bar } = props
console.log(foo)

// Pass
const { foo } = props
console.log(foo)

[~]

Hanging unused parameters in functions are not allowed:

// Fail
function f(a, b, c, d) {
  console.log(b)
}

// Pass
function f(a, b) {
  console.log(b)
}

[~]

Omitted params in object destructuring can be left unused:

// Pass
const { foo, ...rest } = props
console.log(rest)

[~]

no-var

// Fail
var foo

// Pass
let foo

[~]

object-shorthand

// Fail
const a = { x : x }

// Pass
const a = { x }

[~]

one-var

// Fail
let foo, bar

// Pass
let foo
let bar

[~]

operator-assignment

// Fail
x = x + y

// Pass
x += y

[~]

padding-line-between-statements

We only configure paddings between exports and other statements to emphasize exported values. You may customize this rule in your project depending on your needs.

// Fail
const a = 1
export const b = 2

// Pass
const a = 1

export const b =2

[~]

prefer-const

// Fail
let a = 3

// Pass
const a = 3

[~]

radix

ES5 no longer treats strings with leading 0 as octal literal, so there's no need to pass the radix 10:

// Fail
const n = parseInt('071', 10)

// Pass
const n = parseInt('071')

[~]

Disabled rules

[~]

no-irregular-whitespace

We disable this rule as it can't properly allow using non-breaking whitespaces inside JSX tags. It forces to use   instead:

<>Push the&nbsp;button</>

Which leads to a problem when converting JSX text node to a string:

<>{'Push the&nbsp;button'}</>

If developer don't pay attention or text is too big to look through it properly, &nbsp; moves into string without any warning and shows up in UI as a group of symbols, not a whitespace. [~]

no-use-before-define

We don't enable this rule as it is often more practical to place usages before definitions. E.g., in React component it is better to place component code first and helper functions below. That way the most important thing in the file comes first. [~]

Unicorn

Additional rules from eslint-config-unicorn. [~]

unicorn/explicit-length-check

Checking for .length of an empty array can lead to rendering 0 instead of not rendering anything. This rule forces to always explicitly check for array length by comparing it:

// Fail
return items.length && <Items />

// Pass
return items.length > 0 && <Items />

[~]

unicorn/filename-case

// Fail
my-component.js
my_component.js

// Pass
myComponent.js
MyComponent.js
en-US.js

[~]

unicorn/no-abusive-eslint-disable

// Fail
// eslint-disable-next-line
console.log(message);

// Pass
// eslint-disable-next-line no-console
console.log(message);

[~]

unicorn/throw-new-error

By design Error(msg) and new Error(msg) can be used interchangeably but for the sake of homogeneity it is better to always use only one form.

// Fail
throw Error()

// Pass
throw new Error()

We prefer new Error for consistency with custom subclassed errors which cannot be created without new:

class MyError extends Error {}
throw MyError() // Uncaught TypeError
throw new MyError() // OK

[~]

Rules for asynchronous code

Using eslint-config-promise. [~]

prefer-promise-reject-errors

// Fail
Promise.reject('Error')

// Pass
Promise.reject(new Error('Error'))

[~]

require-await

// Fail
async function f() {
  doSomethingSynchronously()
}

// Pass
async function f() {
  await doSomethingAsynchronously()
}

[~]

promise/catch-or-return

// Fail
myPromise.then(doSomething)

// Pass
myPromise.then(doSomething).catch(handleError)

[~]

promise/no-return-in-finally

// Fail
myPromise.finally(() => {
  return 'done'
})

// Pass
myPromise.finally(() => {
  alert('done')
})

[~]

promise/no-return-wrap

// Fail
myPromise.then((val) => Promise.resolve(val * 2))
myPromise.then(() => Promise.reject(new Error()))

// Pass
myPromise.then((val) => val * 2)
myPromise.then(() => {
  throw new Error()
})

[~]

promise/param-names

// Fail
new Promise((reject, resolve) => {})

// Pass
new Promise((resolve, reject) => {})

[~]

Import rules

Using eslint-config-import and eslint-config-unicorn. [~]

Extending plugin:import/errors. [~]

import/no-duplicates

// Fail
import { App } from './App'
import { SOME_CONST } from './App'

// Pass
import { App, SOME_CONST } from './App'

[~]

import/first

// Fail
init()
import { App } from './App'

// Pass
import { App } from './App'
init()

[~]

import/newline-after-import

// Fail
import { App } from './App'
render(App)

// Pass
import { App } from './App'

render(App)

[~]

import/no-cycle

// Fail

 // a.js
import { b } from './b'
export const a = 1

// b.js
import { a } from './a'
export const b = 2

[~]

import/no-default-export

// Fail
export default function foo() {}

// Pass
export function foo() {}

[~]

import/order

Group imports by module type and sort them alphabetically inside a group.

Order of import groups: [~]

  1. builtin
import * from 'path'

[~]

  1. external
import * from 'lodash'

[~]

  1. internal
import * from 'src/utils'
import * from '@/utils'
import * from '~/utils'

[~]

  1. parent
import * from '../'

[~]

  1. sibling
import * from './sibling'

[~]

  1. index
import * from '.'

[~]

sort-imports

Sort import members:

// Fail
import { c, a, b } from 'module'

// Pass
import { a, b, c } from 'module'

[~]

unicorn/import-index

// Fail
import * from './index'

// Pass
import * from '.'

[~]

Rules for regular expressions

[~]

prefer-named-capture-group

We use named capture groups as they are more explicit while matching or replacing:

// Fail
const regexp = /(ba[rz])/
const groupResult = regexp.exec('bar')[1] // 'bar'
'foobar'.replace(regexp, '($1)') // 'foo(bar)'

// Pass
const regexp = /(?<named>ba[rz])/
const groupResult = regexp.exec('bar').groups.named // 'bar'
'foobar'.replace(regexp, '($<named>)') // 'foo(bar)'

If you don't need the result of capturing, mark group as a non-capturing:

// Pass
const regexp = /(?:ba[rz])/

[~]

prefer-regex-literals

// Fail
new RegExp("abc")

// Pass
/abc/
new RegExp(someVariable)
new RegExp(`${prefix}abc`)

[~]

unicorn/better-regex

// Fail
/[0-9]/

// Pass
/\d/

[~]