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
- Use consistent names
- Prefer self-explicative code instead of comments
- Code must be simple (KISS principle)
- Never spread function's arguments
- Use an underscore to ignore the arguments
- Prefer string unions over booleans
- Prefer positive conditions
- Prefer variables over long conditions
- Bailouts
- Avoid fake bailouts
- Prefer Nullish Coalescence over ternaries
- Logical nullish assignment
- Prefix higher-order functions with create
- Avoid default exports
- Prefer async code for promises
- Pass only the needed arguments
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() {}
Anyway, avoid them for inline functions.
// ❌ don't
useEffect(function myEffect() {})
addEventListener(function listener() {})
// ✅ do
useEffect(() => {})
addEventListener(() => {})
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... */
}
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
}
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 ''
}
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... */
}
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... */
}
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'
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) {
// ...
}
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) {
// ...
}
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... */
}
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'
}
}
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] ?? '-'
}
Logical nullish assignment
Nullish Coalescing assignment can be compressed by using the ??=
operator.
// ❌ don't
obj.foo = obj.foo ?? {}
// ✅ do
obj.foo ??= {}
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)
}
}
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'
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... */
}
}
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'
}
Top comments (0)