We follow Airbnb JavaScript Style Guide (https://github.com/airbnb/javascript) and Airbnb React/JSX Style Guide (https://github.com/airbnb/javascript/tree/master/react). This document extends and/or overrides those guides, so it take precedence. We also define some basic rules for Redux and CSS stylings.
Our linter packages, their naming style copies the one used by airbnb:
- @slidoapp/eslint-config
- typescript + react, mostly used for client-side code
- @slidoapp/eslint-config-base
- typescript, mostly used for server-side code
If a property or variable is a boolean, or function returns boolean, use is
, has
, can
or should
prefix. (Accessors - Booleans)
// bad
if (!dragon.age()) {
return false;
}
let good = false;
let sign = false;
let closeDocument = true;
export const updateQuery = function doSomething(createVersion) {};
// good
if (!dragon.hasAge()) {
return false;
}
let isGood = false;
let canSign = false;
let shouldCloseDocument = true;
export const updateQuery = function doSomething(hasToOverwriteVersion) {};
To consistently write certain variable names, we use these rules:
- if the name is an acronym, like
URL
comes fromUniform Resource Locator
, we write it lowercase when it is alone, and all uppercase when part of a longer name - if the name is an abbreviation of a longer word, like
id
(fromidentifier
) orsrc
(fromsource
), we write it lowercase when it is alone, and camel case when part of a longer name
examples:
const url = 'x'
const imageURL = 'x'
const id = 'x'
const imageId = 'x'
const src = 'x'
const imgSrc = 'x'
We’ve seen way too many bugs, where (not only) TypeScript inferred a type, but developer’s intention was different. He just didn’t notice that the automatically inferred type was different.
We have decided, that we want to always annotate our functions (input params and return values also). It serves both documentation purposes and developers intent.
// BAD
function isEven(value) {
return value % 2 === 0;
}
// BAD
function isEven(value: number) {
return value % 2 === 0;
}
// GOOD
function isEven(value: number): boolean {
return value % 2 === 0;
}
Images are in a /img
subfolder, has size suffix in its name, and imported into React component as a constant with image type suffix (Png
, Svg
, Jpg
, ...).
// bad
import organization from "./organization.png";
import phone from "../../common/phone.svg";
// good
import organizationPng from "./img/organization-24x24.png";
import phoneSvg from "./img/phone.svg";
Correctly setup ID's are essential for proper QA/testing. IDs consist from two parts, {LEFT}-{RIGHT}
, where {LEFT}
part is the name of the component, and {RIGHT}
is any string (words separated with dashes) that makes the whole ID unique. Only {LEFT}
part is mandatory.
So for example in our-example.component.jsx
file, there is a OurExample
React component and every single ID will start with our-example-
prefix.
const OurExample = (props) => {
const id = "our-example";
return <h1 id={`${id}-heading`}>{props.text}</h1>;
};
Redux containers (those React components which use Redux connect()
to access state) has Container
postfix in its name - for example LoadingScreenContainer
is in /loading-screen.container.jsx
.
Redux components has no special postfix, not even an Component
. Example: IconButton
is in /components/buttons/icon-button.component.jsx
.
All imports must import components/containers under their original name. (So once full text search is used, it must be simple to find particular component)
// GOOD
import LoadingScreenContainer from "./loading-screen.container";
// BAD
import MyVeryCreativeImportName from "./loading-screen.container";
The handler functions should be named of the form handle*
, for example handleClick
or handleStart
. When sent as props to a component, the property-keys should
be named of the form on*
, for example onClick
or onStart
.
example:
function Activator(props) {
return <button onClick={this.props.onActivation}>Activate</button>;
}
function Thing(props) {
const [isActive, setActive] = useState(false);
function handleActivation() {
setActive(true);
}
return (
<div>
Thing is {isActive ? "Active" : "Inactive"}
<Activator onActivation={handleActivation} />
</div>
);
}
Use SC
suffix for styled components.
const PanelSC = styled.div`
background: blue;
`;
const BarSC = styled.div`
color: red;
`;
const Bar = () => {
// maybe some code here
return (
<BarSC>
<PanelSC>earum nostrum cum</PanelSC>
Aut minima assumenda.
</BarSC>
);
};
export default Bar;
Do not export styled components directly (as it has a lot of props), but wrap it into simple React component with fewer props.
const FooterSC = styled.footer`
text-align: center;
`;
const Footer = () => <FooterSC>doloremque quasi similique</FooterSC>;
export default Footer;
Project structure is driven by LIFT Principle. Folder structure is organized with approach “folders-by-feature”
, not “folders-by-type”
Folders and files are named with all-lowercase, words separated by dash. File name suffix says, what type of code is in the file. Suffix is full stop separated sequence, starting with the more specific identifier, to the less specific ones:
/save-file-dialog
|-- save-file-dialog.actions.js
|-- save-file-dialog.actions.spec.js
|-- save-file-dialog.reducers.js
|-- save-file-dialog.reducers.spec.js
|-- save-file-dialog.component.jsx
|-- save-file-dialog.component.scss
Test file has the same name, as the tested unit, with “.spec.js”
suffix. Test file is in the same folder as the tested code.
/save-file-dialog
|-- save-file-dialog.actions.js
|-- save-file-dialog.actions.spec.js
Images are in the /img
sub-folder of the component, in folder. See React images
/button
|-- /img
| |-- icon-24x24.png
|
|-- button.component.jsx
|-- button.component.scss
When using typescript, use a tool like GraphQL Code Generator to automatically generate typescript type definitions for your graphql queries.
When writing GraphQL query that includes Relay connection type, make sure to include @connection
directive with key
set to name of the connection (see example).
Connection results are saved in cache with its name and input arguments as key (watchlistItemConnection(first: 10, after: "abcdefgh")
) - this means different pages are saved under diferent keys (before
/after
arguments are diferent each page). key
in @connection
directive makes sure results are saved and normalised under key
, ingoring connection arguments. This is important for adding/removing data from cache after successful mutation, as we wouldn't be able to do it otherwise.
Example:
query watchlistQuery($id: ID!, $first: Int!) {
...
watchlistItemConnection(first: $first) @connection(key: "watchlistItemConnection") {
edges {
node {...}
}
}
}
Mutations that update something, should always return every field that can go into its input
parameter. Some gql clients (Apollo) can update cache automatically through the whole application.
Example:
input WatchlistItemUpdateInput {
id: ID!
displayName: String
fullName: String
note: String
externalId: String
}
mutation updateWatchlistItem {
updateWatchlistItem($input: WatchlistItemUpdateInput!) {
watchlistItem {
id
displayName
fullName
note
externalId
}
}
}