Skip to content

Latest commit

 

History

History
353 lines (268 loc) · 9.5 KB

CODE_STYLE.md

File metadata and controls

353 lines (268 loc) · 9.5 KB

Coding Style

All teams work differently, a team's style should represent the deliberation and negotiation of each coding aspect by the members of that team.

  • These are not immutable rules, things change, evolve and serve different purposes in different stages of development and experience.
  • All conventions are right as long as people using them agree on their meaning and form.

The following is inspired from:


Table of contents

Thoughts on coding

  1. Continuous refactoring: Developers read much more code than they write. Don't stop at your fist draft while also not obsessing on getting it 100% right.

  2. Read the room: Functional programming, or any one paradigm, are not magic bullets. Figure out when to use what. When building primitives for example, it makes sense to be more conscious about performance and adopt a more imperative style - still being aware of immutability but not spreading all the object all the time.

  3. One way execution flow: Make code execution flow top -> down and left -> right. The more you go back, inside or outside the more you switch context and zoom level. These switches reset the readers mental parsing "state" making the code's meaning harder to transmit and more prone to mistakes.

  4. Let machines do their job: If there's a linting rule that can verify or enforce a conventions, use it.

Line length

80 characters, yes, just like our grand parents with punch cards.

It turns out that there is an optimal line size for legibility, between 40 and 80 - taking into account eye/head movement, focus etc. This is true for printed books and also code.

At the beginning of every new line the reader is focused, but this focus gradually wears off over the duration of the line.

"Typographie", E. Ruder

80-characters line length limit in 2017 (and later) and Readability: the Optimal Line Length are good intros on the topic.

Folder structure and imports

The principle of "One Way Data Flow" can also be applied to component imports/dependencies.

  • The project folder structure should resemble more a Polytree, not the required Directed acyclic graph

  • Use Hierarchical model–view–controller when creating pages, containers or multi part entities

    ▾ HMVC/
      ▾ todos/
          controller
          model
          view
      ▾ comments/
          controller
          model
          view
    ▾ MVC/
      ▾ controllers/
          todos
          comments
      ▾ models/
          todos
          comments
      ▾ views/
          todos
          comments
    
  • A child should never import something from it's parents or it's siblings, it can only have access to it's children

  • If a child needs something from it's parent, that piece of data should be handed down through props (data, functions or other Components)

Naming

Branches

  • Main: main
  • PR: feat|fix|docs/{task-id}/few-word-summary

Commit messages

Use Conventional Commits.

feat(ui): allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files

Functions

A functions is a thing that does something, performs an action, if it does something, it's a verb. The stronger and less noise around the verb, the better. renameFile better than fileRename or doRenameFile.

Functions start with a verb, optionally followed by the noun and/or an adjective:

const rename = () => { ... }
const renameFile = () => { ... }
const renameMarkdownFiles = () => { ... }

All functions ever written are a specialized form of one of the following verbs:

  • Create: "build", "initialize", "construct"
  • Read: "find", "get", "check"
  • Update: "change", "rename", "map", "filter"
  • Delete: "remove"

Use the verb in it's basic form (infinitive). rename, not renaming or doRename or renames.

Use a name intentionally and strengthen it's meaning by constantly implementing it with the same behavior, for ex:

  • getTodo throws if not found, findTodo returns null
  • updateComment implies persistence (API or DB interaction), changeCommentText refers to local state changes
  • sum adds items of an array, sumBy adds the field of an object array by providing an "extract" function

Prefix predicate functions, functions that return a boolean, with a verbs that imply truth seeking - check, verify, decide, determine - followed by a question:

const isToday = checkIsToday(new Date())

Booleans

Boolean variables, including React props, are prefixed with one of the following: can, should, is, has, was

const isAllowed = false
const shouldRender = true

Constants

Always UPPER_CASE.

const LANGUAGES = ["EN-us", "NL-nl", "NL-be"]

Handlers and Props

Function props are prefixed with on and phrased as an event:

onInputChange, onFormSubmit etc.

Functions passed as props for reacting to an event are prefixed with handle, followed by the main verb, optionally can use a noun and/or a adjective:

handleUpdateUser, handleSwitchTab, handleDisableWorkshopUser etc.

Example: Persist Users' language preference

const handleUpdateUserLocale = useCallback(
  input => update({ locale: input }),
  [update]
)

...

<Sidebar onLocaleChange={handleUpdateUserLocale} />

Types

  • Append Type to the name of the thing it's typing and CamelCase it.
type InputUIPropsType = {
  type: "password";
  value: string;
  hasShowUnmasked: boolean;
} | {
  type: "text";
  value: string;
}
  • Separate types from the "actual" code. Writing them inline makes it harder to distinguish actionable code from what fundamentally is compiler oriented code
// separate
const InputUI: FC<InputUIPropsType> = ({ value, type, hasShowUnmasked }) => {
...
}

// inlined
const InputUI = (
  props:
    | {
        type: 'password';
        value: string;
        hasShowUnmasked: boolean;
      }
    | {
        type: 'text';
        value: string;
      }
) => {
...
};

Generic names

  1. input and output

    const sum = (input = []) => {
      let output = 0
    
      for (let index = 0, length = input.length; index < length; index++) {
        output = output + input[index]
      }
    
      return output
    }
  2. input, item and accumulator

    const sum = (input = []) => input.reduce(
      (accumulator, item) => accumulator + item,
      0
    )
  3. needle, heystack and index

    const checkExistsWithValue = needle => (heystack = []) => {
      for (let index = 0; index < heystack.length; index++) {
        if (item === heystack[index]) {
          return true
        } 
      }
    
      return false
    }
  4. When using "functional primitives" like map or reduce, use item, array and accumulator

  5. When writing CRUD like functions, use id and data

    const handleCreateTodo = useCallback(
      data => POST("/todos", { body: data }), 
      []
    )
    
    const handleUpdateTodo = useCallback(
      (id, data) => PATCH(`/todos/${id}`, { body: data }), 
      []
    )
  6. properties and options, not args or opts or props. Aligns with React ecosystem, use it also when writing other functions that require configuring

Testing

React components

  • Tag all the essential parts of the component with a data-testid. It is the state and composition of these parts that determine the behavior of the component.
  • Check the component's configuration without coupling the test to HTML or CSS, making it less brittle. If I need to enforce a certain HTML tag, you can check the attributes of the element in question.
  • Using multiple selector functions (queryByRole, findByText, getAllByRole etc) creates noise and adds complexity in a space where everything should be as dumb/simple as possible - the subject matter is the component you're currently testing, not how fancy the API of the test library or the test runner are - hence I would use fewer different query functions and test what the state of the a part is.