Eventbrite’s ESLint guidelines to ensure consistency in JavaScript code written in ES6 and later.
For browser, server, and compiler ES6 support, check out kangax's ES6 compatibility table.
Avoid using var for declaring local variables; instead use let
, which provides block scoping (eslint: no-var
):
// good
let x = 'y';
// bad (uses var)
var x = 'y'
Use const
for the following:
- Actual constants; i.e., variables that remain the same throughout the entire execution
- Arrow function references
// good
const DEFAULT_NAME = 'Eventbrite';
const generateGreeting = (name=DEFAULT_NAME) => {
let formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
// bad (uses `let` for a constant & arrow function reference)
let DEFAULT_NAME = 'Eventbrite';
let generateGreeting = (name=DEFAULT_NAME) => {
let formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
Name constants using UPPER_SNAKE_CASE
for easy identification:
// good
const MAX_ALLOWED = 7;
// bad (uses snake_case)
const max_allowed = 7;
// bad (uses PascalCase)
const MaxAllowed = 7;
// bad (uses normal camelCase)
const maxAllowed = 7;
If a given module has more than three constants, factor them out into a separate constants module, and then import that constants module as an object:
// good (imports constants as an object from
// constants module)
import * as constants from './constants';
// bad (has more than 3 constants w/in module)
const FIRST_CONSTANT = 'foo';
const SECOND_CONSTANT = 'bar';
const THIRD_CONSTANT = 'baz';
const FOURTH_CONSTANT = 'qux';
const FIFTH_CONSTANT = 'quux';
const SIXTH_CONSTANT = 'corge';
However, if a given module uses three or fewer constants, use individual named imports instead:
import {FIRST_CONSTANT, FIFTH_CONSTANT} from './constants';
Avoid using const
for local variables (eslint: prefer-const
):
// good
const generateGreeting = (name=DEFAULT_NAME) => {
let formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
// bad (uses `const` for `formattedNow` local variable)
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
There is a pattern in the industry to use const
if a variable is never going to be reassigned within a block (see eslint prefer-const
). However, this is an abuse of const
as it is intended for variables that are truly constant. The motivation for this practice is that it can help enforce "immutability" since a const
variable cannot be reassigned. But immutability and const
bindings are two separate things. For instance, an object declared const
can still have its properties mutated.
For more on constants, read Learning ES6: Block-level scoping with let
and const
.
When building up a string, use a template literal instead of string concatenation (eslint: prefer-template
):
// good
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
// bad (uses string concatenation)
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return 'Hi, ' + name + ' on ' + formattedNow;
}
// bad (uses array join)
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return ['Hi, ', name, ' on ', formattedNow].join();
}
When using template literals, tokens should not be padded by spaces (eslint: template-curly-spacing
):
// good
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return `Hi, ${name} on ${formattedNow}`;
}
// bad (has extra padding around the curlies)
const generateGreeting = (name=DEFAULT_NAME) => {
const formattedNow = new Date();
return `Hi, ${ name } on ${ formattedNow }`;
}
Don't use template literals when there is nothing to interpolate (eslint: no-useless-escape
):
// good
const COMPANY_NAME = 'Eventbrite';
// bad (uses template literal unnecessarily)
const COMPANY_NAME = `Eventbrite`;
For more on template literals, read Learning ES6: Template literals & tagged templates.
Use the spread operator (...
) to create a shallow copy of an array:
// good
let list = [1, 2, 3];
let listCopy = [...list];
// bad (uses `concat` to copy)
let list = [1, 2, 3];
let listCopy = list.concat();
// bad (uses a map to create a copy)
let list = [1, 2, 3];
let listCopy = list.map((item) => item);
Use the spread operator (...
) to join multiple arrays together:
// good
let start = ['do', 're', 'mi'];
let end = ['la', 'ti'];
let scale = [...start, 'fa', 'so', ...end];
// bad
let start = ['do', 're', 'mi'];
let end = ['la', 'ti'];
let scale = start.concat(['fa', 'so']).concat(end);
Use the spread operator (...
) to convert an array-like object into an Array
:
// good
// NodeList object
let nodeList = document.querySelectorAll('p');
// Array
let nodes = [...nodeList];
// bad (uses a loop convert to an array)
// NodeList object
let nodeList = document.querySelectorAll('p');
// Array
let nodes = [];
for (let i = 0; i < nodeList.length; i++) {
nodes.push(nodeList[i]);
}
For more on the spread operator, read Learning ES6: Rest & Spread Operators.
When a variable name matches the name of the object key in an object literal, use property value shorthand (eslint: object-shorthand
):
let name = 'Eventbrite';
// good
let data = {
name
};
// bad (duplicates key and variable name)
let data = {
name: name
};
Group any object literal property value shorthands at the beginning of the object literal so that it's easier to see which properties are using the shorthand:
let name = 'Eventbrite';
let location = 'San Francisco, CA';
// good
let data = {
name,
location,
ceo: 'Julia Hartz',
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage']
};
// bad (shorthands aren't at the top)
let data = {
name,
ceo: 'Julia Hartz',
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage'],
location
};
When creating object literals with dynamic property names, use computed property keys (eslint: object-shorthand
):
let name = 'Eventbrite';
let location = 'San Francisco, CA';
let leaderName = 'ceo';
// good
let data = {
name,
location,
[leaderName]: 'Julia Hartz',
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage']
};
// bad (doesn't leverage computed property keys)
let data = {
name,
location,
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage']
};
data[leaderName] = 'Julia Hartz';
When defining methods on an object literal, use method definition shorthand (eslint: object-shorthand
):
let name = 'Eventbrite';
let location = 'San Francisco, CA';
let leaderName = 'ceo';
// good
let data = {
name,
location,
[leaderName]: 'Julia Hartz',
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage'],
getDisplay() {
return `${this.name} in ${this.location}`;
}
};
// bad (doesn't leverage method definition shorthand)
let data = {
name,
location,
[leaderName]: 'Julia Hartz',
founders: ['Julia Hartz', 'Kevin Hartz', 'Renaud Visage'],
getDisplay: function() {
return `${this.name} in ${this.location}`;
}
};
Use the object spread operator instead of Object.assign
to create a shallow copy with source object properties merged in:
// good
let warriors = {Steph: 95, Klay: 82, Draymond: 79};
let newWarriors = {
...warriors,
Kevin: 97
};
// bad (uses Object.assign instead)
let warriors = {Steph: 95, Klay: 82, Draymond: 79};
let newWarriors = Object.assign({}, warriors, {
Kevin: 97
});
// terrible (mutates `warriors` variable)
let warriors = {Steph: 95, Klay: 82, Draymond: 79};
let newWarriors = Object.assign(warriors, {
Kevin: 97
});
The object spread operator (as well as Object.assign
) only make shallow copies. As such they only work on a single level of nesting at a time. However, you need to merge into a level deeper than the top level, you can still make use of the object spread operator:
let teams = {
warriors: {Steph: 95, Klay: 82, Draymond: 79},
cavs: {Lebron: 98, Kyrie: 87, Kevin: 80}
};
let updatedTeams = {
...teams,
warriors: {
...updatedTeams.warriors,
Kevin: 97
}
};
For more on enhanced object literals, read Learning ES6: Enhanced object literals.
When an arrow function expression is needed, use an arrow function in place of an anonymous function (eslint: prefer-arrow-callback
):
// good
[1, 2, 3].map((x) => x * x);
// bad (uses anonymous function)
[1, 2, 3].map(function(x) {
return x * x;
})
Include a single space around the arrow (=>
) in an arrow function (eslint: arrow-spacing
):
// good
[1, 2, 3].map((x) => x * x);
// bad (missing spaces around arrow)
[1, 2, 3].map((x)=>x * x);
Always surround the parameters of an arrow function with parentheses (eslint: arrow-parens
):
// good
[1, 2, 3].map((x) => x * x);
// bad (missing parentheses surrounding parameters)
[1, 2, 3].map(x => x * x);
When the function body is a single expression, omit the curly braces and use the implicit return syntax (eslint: arrow-body-style
):
// good (uses implicit return for single expression)
[1, 2, 3].map((x) => x * x);
// bad (doesn't use implicit return for single expression)
[1, 2, 3].map((x) => {
return x * x;
});
When the function body is a single expression, but spans multiple lines, surround the function body in parentheses:
// good
eventIds.forEach((eventId) => (
fetch(`EVENT_SAVE_URL/${eventId}`, {
method: 'POST'
})
));
// bad (missing parentheses surrounding function body)
eventIds.forEach((eventId) => fetch(`EVENT_SAVE_URL/${eventId}`, {
method: 'POST'
}));
For more on arrow functions, read Learning ES6: Arrow Functions.
Use the rest operator (...
) instead of the arguments
object to handle an arbitrary number of function parameters (eslint prefer-rest-params
):
// good
const join = (separator, ...values) => (
values.join(separator);
);
// bad (uses arguments object)
function join(separator) {
var values = [];
for (var argNo = 1; argNo < arguments.length; argNo++) {
values.push(arguments[argNo]);
}
return values.join(separator);
};
The arguments
object is problematic for many reasons. It's not an actual Array
object, so methods like slice
are unavailable to use. Because we have the separator
parameter, we have to start at index 1
of arguments
, which is pretty annoying. Also, just looking at our join
function, it's not immediately discoverable that it actually takes more than one parameter, let alone that it supports an infinite number of them. Lastly arguments
doesn't work with arrow functions.
There should be no spacing between the rest operator and its parameter (eslint: rest-spread-spacing
):
// good
const join = (separator, ...values) => (
values.join(separator);
);
// bad (space between rest operator and param)
const join = (separator, ... values) => (
values.join(separator);
);
For more on rest parameters, read Learning ES6: Rest & Spread Operators.
Use default parameters in the function header instead of mutating parameters in the function body:
// good
const getData = (options, useCache = true) => {
let data;
// get data based on whether we're using the
// cache or not
return data;
}
// bad (defaults the parameter in function body)
const getData = (options, useCache) => {
let data;
if (useCache === undefined) {
useCache = true;
}
// get data based on whether we're using the
// cache or not
return data;
}
Put all default parameters at the end of the function header:
// good
const getData = (options, useCache = true) => {
let data;
// get data based on whether we're using the
// cache or not
return data;
}
// bad (default parameter isn't at the end)
const getData = (useCache = true, options) => {
let data;
// get data based on whether we're using the
// cache or not
return data;
}
For more on default parameters, read Learning ES6: Default parameters.
Use the spread operator (...
) instead of Function.prototype.apply
when needing to pass elements of an array as arguments to a function call (eslint: prefer-spread
):
// good
let maxValue = Math.max(...[3, 41, 17]);
let today = new Date(...[2016, 11, 16]);
// bad (uses `apply`)
let maxValue = Math.max.apply(null, [3, 41, 17]);
let today = new (Function.prototype.bind.apply(Date, [null, 2016, 11, 16]));
Using the spread operator is cleaner because you don't have to specify a context (first example). Furthermore you cannot easily combine new
with apply
(second example).
There should be no spacing between the spread operator and its expression (eslint: rest-spread-spacing
):
// good
let maxValue = Math.max(...[3, 41, 17]);
// bad (space between spread operator and
// array literal)
let maxValue = Math.max(... [3, 41, 17]);
For more on the spread operator, read Learning ES6: Rest & Spread Operators.
Avoid classes with an empty constructor because classes have a default constructor when one isn't specified (eslint: no-useless-constructor
):
// good
class Person {
speak(phrase) {
}
}
class Child extends Person {
speak(phrase) {
}
}
// bad (has empty/unnecessary constructors)
class Person {
constructor() {
}
speak(phrase) {
}
}
class Child extends Person {
constructor() {
super();
}
speak(phrase) {
}
}
Avoid duplicate class members because the interpreter will (silently) use the last one (eslint no-dupe-class-members
):
// good
class Person {
speak(phrase) {
}
}
// bad (has duplicate methods)
class Person {
speak(phrase) {
}
speak(phrase, lang) {
}
}
Set default values for class properties using declarative syntax instead of defaulting within the constructor
so that it's clear which properties the class supports instead of being hidden in code:
// good
class TextInput extends React.Component {
state = {
value: ''
}
render() {
return (<div />);
}
}
// bad (defaults `state` within constructor instead
// of using declarative syntax)
class TextInput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
render() {
return (<div />);
}
}
Initialize static class properties using declarative syntax instead of assigning to the class after the class declaration in order to keep everything within the class declaration:
// good
class Button extends React.Component {
static propTypes = {
type: React.PropTypes.string.isRequired
}
render() {
return (<button />);
}
}
// bad (assigns static properties on the class declaration)
class Button extends React.Component {
render() {
return (<button />);
}
}
Button.propTypes = {
type: React.PropTypes.string.isRequired
};
Both declarative property syntaxes are not a part of the ES2015 specification and are a in the midst of the ECMAScript proposal approval process. Currently they are sitting in Stage 3. For details, check out ECMAScript Class Fields and Static Properties.
For more on classes, read Learning ES6: Classes.
Avoid importing from the same module in separate statements for better maintainability (eslint: no-duplicate-imports
):
// good
import React, {Component, PropTypes} from 'react';
// bad (imports from same module are in
// separate statements)
import React from 'react';
import {Component, PropTypes} from 'react';
Only export constants (eslint: import/no-mutable-exports
):
// good
export const DEFAULT_LENGTH = 10;
// bad (exports a `let` variable)
export let DEFAULT_LENGTH = 10;
Place all import
statements at the beginning of a module (eslint: import/first
):
// good
import React from 'react';
import classNames from 'classnames';
import {connect} form 'react-redux';
const DEFAULT_LENGTH = 10;
// bad (imports aren't all at the top)
import React from 'react';
const DEFAULT_LENGTH = 10;
import classNames from 'classnames';
import {connect} from 'react-redux';
Avoid using webpack loader syntax in module import
statements in order to decouple code from the module bundler (eslint: import/no-webpack-loader-syntax
):
// good
import someCss from 'some.css';
import otherSass from 'other.scss';
// bad (uses webpack loader syntax)
import someCss from 'style!css!some.css';
import otherSass from 'css!sass!other.scss';
In order to avoid the webpack loader syntax in import
statements, configure the loaders in webpack.config.js
.
For more on modules, read ES6 Modules (Final).