DEV Community

Stefano Magni
Stefano Magni

Posted on • Edited on

RouteManager UI coding patterns: Generic ones

This is a non-exhaustive list of the coding patterns the WorkWave RouteManager's front-end team follows. The patterns are based on years of experience writing, debugging, and refactoring front-end applications with React and TypeScript but evolves constantly. Most of the possible improvements and the code smells are detected during the code reviews and the pair programming sessions.

(please note: I do not work for WorkWave anymore, these patterns will not be updated)

(last update: 2022, March)

Use named functions for components and utilities

Named functions ease debugging, both in the browser devTools and in the React devTools.

// ❌ don't
export const getFoo = () => {}
export const useFoo = () => {}
export const FooComponent = () => {}

// ✅ do
export function getFoo() {}
export function useFoo() {}
export function FooComponent() {}
Enter fullscreen mode Exit fullscreen mode

Anyway, avoid them for inline functions.

// ❌ don't
useEffect(function myEffect() {})
addEventListener(function listener() {})

// ✅ do
useEffect(() => {})
addEventListener(() => {})
Enter fullscreen mode Exit fullscreen mode

Use consistent names

Consistent names favor moving into the codebase through the IDE's fuzzy search and reduce ambiguity.

// ❌ don't
// file: hooks/useValidateEmail.ts
function useValidateEmail() {
  /* ... rest of the code... */
}
// file: actions/emailActions.ts
function isEmailValidAction() {
  /* ... rest of the code... */
}
// file: sagas/validEmailSaga.ts
function checkEmailValidity() {
  /* ... rest of the code... */
}
// ✅ do
// file: hooks/useValidateEmail.ts
function useValidateEmail() {
  /* ... rest of the code... */
}
// file: actions/validateEmail.ts
function validateEmail() {
  /* ... rest of the code... */
}
// file: sagas/validateEmail.ts
function validateEmail() {
  /* ... rest of the code... */
}
Enter fullscreen mode Exit fullscreen mode

Prefer self-explicative code instead of comments

Documentation and comments are helpful, but you can reduce them with self-explanatory code.

// ❌ don't
/**
 * Filter out the non-completed orders.
 */
function util(array: Order[]) {
  const ok: Order[] = []

  for(const item of array) {
    const bool = item.status === 'completed'
    if(bool) {
      ok.push(item)
    }
  }

  return ok
}

// ✅ do
function filterOutUncompletedOrders(orders: Order[]) {
  const completedOrders: Order[] = []

  for(const order of orders) {
    if(order.status === 'completed') {
      completedOrders.push(order)
    }
  }

  return completedOrders
}
Enter fullscreen mode Exit fullscreen mode

Code must be simple (KISS principle)

Always prefer readability over smartness, the future-reader will thank you.

// ❌ don't
function getLabel(cosmetic?: CardCosmetic, isToday?: boolean): string {
  const pieces: string[] = []
  if (isToday) {
    pieces.push('today')
  }

  if (cosmetic === 'edge-of-selection' || cosmetic === 'selected') {
    pieces.push('selected')
  }

  return pieces.join(' ')
}

// ✅ do
function getLabel(cosmetic?: CardCosmetic, isToday?: boolean): string {
  const selected = cosmetic === 'edge-of-selection' || cosmetic === 'selected'

  if (isToday && selected) return 'today selected'
  if (selected) return 'selected'
  if (isToday) return 'today'

  return ''
}
Enter fullscreen mode Exit fullscreen mode

Never spread function's arguments

Spreading functions' arguments prevent debugging the source object, avoid it.

// ❌ don't
function foo({ bar, baz }) {
  /* ... rest of the code... */
}

// ✅ do
function foo(params) {
  const { bar, baz } = params
  /* ... rest of the code... */
}
Enter fullscreen mode Exit fullscreen mode

Use an underscore to ignore the arguments

Using an underscore for the ignored argument improves readability.

// ❌ don't
function foo(_event, type) {
  /* ... rest of the code... */
}
// ✅ do
function foo(_, type) {
  /* ... rest of the code... */
}
Enter fullscreen mode Exit fullscreen mode

Prefer string unions over booleans

String unions prevent boolean states to explode and inconsistencies.

// ❌ don't
type Status = {
  idle: boolean
  loading: boolean
  complete: boolean
  error: boolean
}

// ✅ do
type Status = 'idle' | 'loading' | 'complete' | 'error'
Enter fullscreen mode Exit fullscreen mode

Prefer positive conditions

Positive conditions avoid the reader's mind to reverse the variable name meaning.

// ❌ don't
const allowViewers = false
if(userType === 'viewer' && !allowViewers) {
  // ...
}

// ✅ do
const blockViewers = true
if(userType === 'viewer' && blockViewers) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Prefer variables over long conditions

Variable names are way easier to read compared to long conditions.

// ❌ don't
if(userType === 'viewer' && blockViewers) {
  // ...
}
// ✅ do
const userEnabled = userType === 'viewer' && blockViewers
if(userEnabled) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Bailouts

Keep pre-checks and bailouts on a single line, without braces. Reducing the function's height improves readability.

// ❌ don't
function foo() {
  if(/* condition 1 */) {
    return
  }
  if(/* condition 2 */) {
    return
  }
  if(/* condition 3 */) {
    return
  }
}

// ✅ do
function foo() {
  if(/* condition 1 */) return
  if(/* condition 2 */) return
  if(/* condition 3 */) return

  /* ... rest of the code... */
}
Enter fullscreen mode Exit fullscreen mode

Avoid fake bailouts

If a function returns some possible valid values, don't confuse the reader by treating one of them as a "default" value and the other ones as bailouts or early-returns.

// ❌ don't
function getMainColor(theme: 'light' | 'dark') {
  if(theme === 'dark') {
    return 'black'
  }

  return 'white'
}

// ✅ do
function getMainColor(theme: 'light' | 'dark') {
  if(theme === 'dark') {
    return 'black'
  } else {
    return 'white'
  }
}

// or

function getMainColor(theme: 'light' | 'dark') {
  switch(theme) {
    case 'light':
      return 'white'
    case 'dark':
      return 'black'
  }
}
Enter fullscreen mode Exit fullscreen mode

Prefer Nullish Coalescence over ternaries

Sometimes, ternaries can be compressed by using the ?? operator.

// ❌ don't
function get(key: string) {
  return object[key] ? object[key] : '-'
}
// ✅ do
function get(key: string) {
  return object[key] ?? '-'
}
Enter fullscreen mode Exit fullscreen mode

Logical nullish assignment

Nullish Coalescing assignment can be compressed by using the ??= operator.

// ❌ don't
obj.foo = obj.foo ?? {}

// ✅ do
obj.foo ??= {}
Enter fullscreen mode Exit fullscreen mode

Prefix higher-order functions with create

Higher-order functions (functions that create other pre-configured functions) must be prefixed with "create".

// ❌ don't
function getLogger(tag: string) {
  return function logger(message: string) {
    console.log(tag, message)
  }
}

// ✅ do
function createLogger(tag: string) {
  return function logger(message: string) {
    console.log(tag, message)
  }
}
Enter fullscreen mode Exit fullscreen mode

Avoid default exports

Default exports force the consumer to give a name to the imported module, removing control from the module itself. Always prefer named exports.

// ❌ don't
const foo = 'bar'
export default foo

// ✅ do
export const foo = 'bar'
Enter fullscreen mode Exit fullscreen mode

Prefer async code for promises

Async code is more condensed and easier to read, even for non Promise-experts.

// ❌ don't
function load() {
  return service.then(() => {
    /* ... rest of the code... */
  }).cetch(() => {
    /* ... rest of the code... */
  })
}

// ✅ do
function async load() {
  try {
    await service()
    /* ... rest of the code... */
  } catch {
    /* ... rest of the code... */
  }
}
Enter fullscreen mode Exit fullscreen mode

Pass only the needed arguments

Functions must be passed only with the used arguments.

// ❌ don't
function isComplete(order: Order) {
  return order.status === 'complete'
}

// ✅ do
function isComplete(status: OrderStatus) {
  return status === 'complete'
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)