26
RouteManager UI coding patterns: Generic ones
This is an 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.
(last update: 2021, July)
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() {}
Anyway, avoid them for inline functions.
// ❌ don't
useEffect(function myEffect() {})
addEventListener(function listener() {})
// ✅ do
useEffect(() => {})
addEventListener(() => {})
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
}
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 ''
}
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... */
}
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'
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) {
// ...
}
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... */
}
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'
}
}
Nullish Coalescing assignment can be compressed by using the
??=
operator.// ❌ don't
obj.foo = obj.foo ?? {}
// ✅ do
obj.foo ??= {}
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)
}
}
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'
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... */
}
}
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'
}