Skip to content

Instantly share code, notes, and snippets.

@liberium
Created October 22, 2019 08:54
Show Gist options
  • Save liberium/df01c145d9a3183785686ad5ed397749 to your computer and use it in GitHub Desktop.
Save liberium/df01c145d9a3183785686ad5ed397749 to your computer and use it in GitHub Desktop.
JS/React Coding rules

Module structure

ES module is a .js file with at least one export or folder containing index.js with at least one export.
The guiding module structure design principle is: you should imagine that any module might be extracted to its own package some day.

index.js of a module contains import-export statements only

In index.js we define a module interface, its exported object. This file should not contain business logic.
Placing business logic in index.js leads to following inconveniences, making code reading harder:

  • If components/Compo/index.js is opened in a tab in IDE, in many of them name of the tab would be "index.js", which is non-descriptive. There are addons in most IDEs that allow to show "Compo/index.js" instead of just "index.js" in tab name, but "Compo.js" is still more succinct yet descriptive.
  • TODO: example with quick opening main.js by filename

Module name is in kebab-case

First, file paths in MacOS and Windows are case-insesitive. Developer on MacOS could misspell a component module named SignUpForm as components/SignupForm/ then import {SignUpForm} from '@app/components' and not to get an error. After changes are pushed to the CI, it will crash with an error if being run on GNU/Linux, where file paths are case-sensitive.
Second, see the guiding principle. Package name cannot contain uppercase letters. Therefore, camelCase should never be used. This leaves snake_case and kebab-case. The latter is by far the most common convention today. The only use of underscores is for internal node packages, and this is simply a convention from the early days.

The module structure of React-based app follows this scheme:

components/ — exports presentational components; has no default export
containers/ — exports container components; has no default export
store/ — exports the store object by default
api/ — exports the api object by default
utils/ — export utility functions; has no default export
style/ — exports fonts, colors, dimensions objects; has no default export
  fonts — exports fonts object by default
  colors — exports colors object by default
  dimensions — exports dimensions object by default
i18n/ — exports an object with translated messages by locale by default
main — exports a reference to the mounted app by default

package.json contains property "main": "src"

The main field is a module ID that is the primary entry point to the program. That is, if a package is named foo, and a user installs it, and then does require("foo"), then the main module’s exports object will be returned.
We define it in order to facilitate publishing of the app in private NPM repo for embedding in another app.

The src module is aliased as @app-name

This way we import modules of the app as if we had them published in a scoped NPM package. Later we could publish that package in a private NPM repo. This supports the guiding principle and facilitates the code reuse across apps being deleloped by an organisation.

React component structure

Guiding principles

The Single Responsibility Principle: each component should have the single responsibility.

The steps for building React app from scratch

  1. Break the app into components Visual components often map tightly to their respective React components.
  2. Build a static version of the app Start off components without using state. Instead, make them pass static props down.
  3. Determine what should be stateful In order for the app to become interactive, user should be able to modify properties of components. Components have to be mutable and therefore stateful.
  4. Determine in which component each piece of state should live Follow the approach described in Thinking in React.
  5. Hard-code initial state Rewrite components to use this.state. Seed it from mocked collection.
  6. Add inverse data flow Define action handlers and pass them down to components that emit events.
  7. Add server communication

Module structure

/components/ module exports components

/* /components/index.js */

export { default as Button } from './Button'
export { default as Link } from './Link'

Component module tree structure is flat

Components should be contained in components and containers depending on their types.

There are should be no nested component modules. It obstructs code reuse and reasoning about the code.
Let's suppose app contains two components that have nested components with the same name SubCompo. If developer reads JSX of a top-level one and faces <SubCompo />, he cannot immediately know which one of the subcomponents is being referenced. To know it, developer needs to scroll a code editor buffer up to the import section or distract from reading the code by raising up his eyes on it.

There is a case when a single module can contain several components:

import { FirstStep, SecondStep, ThirdStep } from './'

class ThreeStepDialog extends React.Component {
  state = { stepNum: 1 }
  
  stepBack = () => this.setState(({ stepNum }) => ({ stepNum: stepNum - 1}))

  stepForward = () => this.setState(({ stepNum }) => ({ stepNum: stepNum + 1}))

  render() {
    switch (this.state.stepNum) {
      case 1: return <FirstStep stepForward={this.stepForward} />
      case 2: return <SecondStep stepForward={this.stepForward} stepBack={this.stepBack} />
      case 3: return <ThirdStep stepBack={this.stepBack} />
    }
  }
}

If the step components are only used in the dialog, it's allowed to keep them in the same module.

components/
  ThreeStepDialog/
    ThreeStepDialog.js
    FirstStep.js
    SecondStep.js
    ThirdStep.js
    index.js

Component module has the default export named after the module

/* Button.js */
import React from 'react'

export default class Button extends React.Component { }
/* SignInFormContainer.js */
import { connect } from 'react-redux'
import { SignInForm } from 'components'

const SignInFormContainer = connect()(SignInForm)

export default SignInFormContainer

Naming convention

Component name follows the naming convention

Component name reflects its responsibility.
The naming scheme is [Prefix]Entity[Postfix], e.g. User, FormGroup, ActiveUserList.
All name parts are in PascalCase; Entity cannot be empty.
Prefix specifies a component’s distinctive quality, e.g. Active, Main, Bottom, Small.
Entity is a business domain entity or a visual component being rendered, e.g. User, Magazine, Breadcrumb, Card, Button.
Postfix specifies a meta-entity, e.g. List, Dashboard, Group, Container.
Component name should be in the single form. If the responsibility of a component is to display a list of entities, name it EntityList instead of Entities. This facilitates adherence to the SRP: developer is provoked to extract Entity as a component which responsibility is to render an item of the list. Container component name has postfix Container.

Typical component name prefixes:

  • Filterable
  • Editable
  • Toggleable

Typical component name postfixes:

  • List
  • Form
  • Dashboard

Component property name follows the naming convention

A prop name reflects its type.

For a function that is not event handler, it starts with a verb, e.g. fetchUsers, deleteEntity.
Typical examples: getSubscriptionsCallback()(incorrect) — getSubscriptions()(correct).

For event handler, it starts with on, e.g. onUserLoggedOut, onUserAddButtonClick.

For a boolean, it starts with is, are, has, have, should, e.g. isLoading, isOpen, areItemsLoaded, shouldUpdateUserProfile, hasBouncer, haveBeenTransformationsApplied.

Id of an instance of particular entity should be named as entityId not just id.
Even in this case: <User userId={1} />. Why couldn't we parameterize User with just id? Isn't it obvious that the id is user id? No, it's not: it can be confused with id of DOM element, for example. Moreover, if we decide to add an id of other entity as a prop, we could leave userId unrenamed, thus eliminating additional refactoring chore.

Prop name should reflect its purpose exactly, not being too verbose.
Typical examples: <Modal closeModal={...} />(incorrect) — <Modal close={...}>(correct).

Component state item name follows the scheme

The scheme is analogous to the one for property names.

Component method name follows the scheme

Action handler name should start with handle, e.g. handleSubmit, handleUserAddButtonClick.
Render helper name should start with render, e.g. renderListItem.

Component has propTypes static property defined if it's not typed strictly with TypeScript

children should have type PropTypes.node, and not

PropTypes.oneOfType([
  PropTypes.arrayOf(PropTypes.node),
  PropTypes.node,
  PropTypes.string
])

or something like that.
PropTypes.node represents anything that can be rendered: numbers, strings, elements or an array (or fragment) containing these types.

Component file extension is js, or tsx if written in TypeScript

Do not use jsx. It was necessary in 2013, when JSX was compiled by react-tools. Now JS is compiled by Babel which supports JSX natively and there is no need in jsx extension.

Component property that is not required has a default value

Component state is not redundant

If a datapoint needed to display a visual component can be derived from existing ones, it should be.
A practical example:

class SellModal extends Component {
    constructor(props) {
        super(props);

        this.state = {
            isAcceptError: false,
            priceError: false,
            price: 1,
            isAccept: false,
            minPriceError: false,
        };
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleSaleClickBtn = this.handleSaleClickBtn.bind(this);
    }
    handleInputChange(event) {
        const target = event.target;
        const value = target.type === 'checkbox'
            ? target.checked
            : +target.value || 1;
        const name = target.name;
        this.setState({
            [name]: value,
            [`${name}Error`]: false,
        });

    }

    handleSaleClickBtn() {
        if(!this.state.isAccept) {
            this.setState({
                isAcceptError: true,
            })
            return;
        }
        if(!this.state.price) {
            this.setState({
                priceError: true,
            })
            return;
        }
        if (+this.state.price < 1) {
            this.setState({
                minPriceError: true,
            })
        }
        const subscription_id = this.props.id;
        const resale_price = this.state.price * 100;
        addPlacedSubscription(subscription_id, resale_price)
            .then(data => {
                this.props.getSubscriptionsCallback();
                this.props.closeModal()
            })
            .catch(error => {
                console.log('addPlacedSubscription error',error)
                this.props.closeModal();
            })
    }

    render() {
        const {
            name,
            id,
        } = this.props;
        const {
            priceError,
            price,
            isAccept,
            isAcceptError,
            minPriceError,
        } = this.state;
        return (
            <div>
                <div className="m_header">
                    {`Are you sure you want to sell  ${name}s subscription?`}
                </div>
                <div>
                    <label className="m_check_wrap">
                        <input
                            className="m_check"
                            name="isAccept"
                            type="checkbox"
                            onChange={this.handleInputChange}
                            checked={isAccept}
                        />
                        Yes, I'm sure that I want to sell this subscription
                    </label>
                    {isAcceptError && (
                        <div className="m_error">select to continue</div>
                        )}
                </div>
                <div className="m_price">
                    <div className="m_set-price">
                        Set price for which you want to sell this subscription ($)
                    </div>
                    <input
                        name="price"
                        min="1"
                        className={priceError || minPriceError ? "m_text_error" : "m_text"}
                        type="number"
                        placeholder="Set price here"
                        value={price}
                        onChange={this.handleInputChange}
                    />
                    {priceError && (
                        <div className="m_error">Empty line</div>
                        )}
                    <div className="m_comission">
                        The commission for the sale is 3% of the payment amount
                    </div>
                </div>
                <div className="unsub_modal_btn" onClick={this.handleSaleClickBtn}>
                    sale
                </div>
            </div>
        );
    }
}

Component state

Store only data points that influence component rendering

Data that is needed to be persisted between rendering passes but does not influence result of rendering, should be stored as a ref (in case of function component) or instance property (in case of class component). Antipattern:

class extends React.Component {
  state = {
    ...
    fcmToken: null // Firebase cloud messaging token
  }
  
  async componentDidMount() {
    const isPermissionGranted = await firebase.messaging().hasPermission();
    if (isPermissionGranted) {
      const fcmToken = await firebase.messaging().getToken()
      this.setState({ fcmToken })
    }
  }
  
  handleSignUpButtonPress = () => {
    const {email, fcmToken} = this.state
    this.props.signUp({email, fcmToken})
  }
}

Error handling in React components

TODO: console.log vs debugger

TODO: error handling using error boundaries vs catching in components and promises Antipattern:

firebase.notifications()
.displayNotification(localNotification)
.catch(err => console.error(err));

https://reactjs.org/docs/error-boundaries.html facebook/react#11334

CSS-in-JS

Naming convention

Antipattern:

errorStyle: { ... }

Name styles without redundant suffix Style

Rules of use

Keep all styles in stylesheet

Antipattern:

<Text style={[styles.confirmCodeDesc, {textAlign: 'center'}]}>

Application state management

All functionality related to application state management is isolated in the /store module

The sign of good design: store module could be shared between web and mobile apps without modification.

Don't manage screen or page transitions in store

Thunks, sagas or epics should not refer history or navigation objects. Instead, manage it on component level.

The /store module has the following structure:

store/ — exports the store object by default
  actions/ — exports Redux async action creators; has no default export
  reducers/ — exports Redux reducers; exports the root reducer by default
    rootReducer.js
    index.js
  sagas/ — (if redux-saga is used) exports Redux sagas; exports the root saga by default
    rootSaga.js
    index.js
  epics/ — (if redux-observable or redux-most is used) exports Redux epics; exports the root epic by default
  store.js
  index.js

/store/store.js has the following structure:

(without support for SSR)

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction'
import rootReducer from './reducers'
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  rootReducer,
  composeWithDevtools({})(
    applyMiddleware(sagaMiddleware)
  )
)

sagaMiddleware.run(rootSaga)

export default store

/store/reducers/index.js has the following structure:

export { default as auth } from './auth'
export { default as entities } from './entities'
export { default } from './rootReducer'

/store/reducers/rootReducer.js has the following structure:

import { combineReducers } from 'redux'
import * as reducers from './'

const rootReducer = combineReducers(reducers)

export default rootReducer

/store/sagas/index.js has the following structure:

export { default } from './rootSaga'

/store/sagas/rootSaga.js has the following structure:

export default function* rootSaga() { }

Application state is normalised

Naming convention

Async action creator name starts with a verb

Interfacing with REST API

Module structure

All functionality related to interfacing with REST API should be isolated in the /api module

The /api module exposes a singleton object as the default export

It's methods correspond to API actions. E.g., logIn, fetchEntities, createEntity.

The /api module has the following structure:

api/
  mixins/
    withAuth.js
    index.js
  api.js
  index.js

/api/index.js has the following contents:

import { compose } from 'lodash/fp'
import mixins from './mixins'

const enhanceWithMixins = compose(...mixins)

class Api {
  baseUrl = 'https://example.com'

  makeUrl(path) {
    return `${this.baseUrl}${path}`
  }

  async request(path, options) {
    const response = await fetch(this.makeUrl(path), {
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json'
      },
      ...options
    })
    const json = await response.json()
    return response.ok ? json : Promise.reject(json)
  }

  get(path, options) {
    return this.request(path, { method: 'get', ...options })
  }

  post(path, body) {
    return this.request(path, { method: 'post', body: JSON.stringify(body) })
  }

  put(path, options) {
    return this.request(path, { method: 'put', ...options })
  }

  delete(path, options) {
    return this.request(path, { method: 'delete', ...options })
  }
}

export default new (enhanceWithMixins(Api))()

/api/mixins/index.js has the following contents:

import withAuth from './withAuth'

/* export an iterable to spread it out in importing module */
export default [withAuth]

/api/mixins/withAuth.js has the following contents:

import routes, { makeUri } from 'api/routes'
import snakeCaseKeys from 'snakecase-keys'

export default function withAuth(superclass) {
  return class extends superclass {
    signIn(credentials) {
      return this.post(makeUri(routes.login), credentials)
    }

    signOut() {
      return this.post(makeUri(routes.logout))
    }

    signUp(data) {
      return this.post(makeUri(routes.register), snakeCaseKeys(data))
    }

    verifyEmail(userId, data, getParams) {
      return this.get(makeUri(routes['verification.verify'], { id: userId }, getParams))
    }

    resendEmailVerificationLink(data) {
      return this.get(makeUri(routes['verification.resend']), snakeCaseKeys(data))
    }

    resetPassword(data) {
      return this.post(makeUri(routes['password.reset']), snakeCaseKeys(data))
    }

    requestResetPasswordLink(email) {
      return this.post(makeUri(routes['password.sendResetLinkEmail']), email)
    }

    changeCredentials(newCredentials) {
      return this.post('/users/me/change-credentials', newCredentials)
    }
  }
}

Naming convention

API method name starts with a verb. There are a few idiomatic verbs:

  • fetch In order to fetch entity from server, we call corresponding method fetchEntity.
  • create
  • update
  • delete

Javascript

The main principle of naming things: statements involving the named identifier should be easy to understand. Difficulty of reading the code originates from necessity of storing a context (identifiers from the scope) in short term memory. The effect of each statement should be understandable without referencing other statements. Signs of good named code:

  • Statements are read like written in a natural language. Examples: Good: isSidebarVisible ? ... : ... Bad: showSidebar ? ... : ... Good: fetchEntities() Bad: getEntitiesRequest() Good: PaymentConfirmationScreen Bad: PayConfirm

To decide if a chosen name is good, try to read statements involving it isolated from the context. If you immediately understand what the effect of each statement is, then the name is good.

Name of a non-constructor function starts with a verb

An action is expressed by a verb in English.

Don't use var

It lets you write code that is less readable. There is no use-cases where it has advantages over let. Use let instead.

Don't use console.log()

Remove console.log(). For debugging, use debugger statement or set breakpoint in IDE.

Infrastructure

Babel, Jest, ESLint, Prettier are configured in package.json

The motivation for this is a mixture of keeping the project root clean and keeping static configuration all in one place. This approach becomes more common in JS community with time.

There are Git hooks for commit and push

They should be configured in package.json:

  "devDependencies": {
    "husky": "^1.2.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "eslint . && jest --passWithNoTests --lastCommit",
      "pre-push": "yarn build"
    }
  }

App folder contains these configuration files

.env
.env.local
.editorconfig
.nvmrc
.gitignore
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment