Code Style & Best Practices
Guidelines on functional programming, React components, TypeScript, and general code style.
Code Style & Best Practices
Source: This page is based on docs/PRACTICE_AND_POLICY/CODE_GUIDELINES.md (Code Style section)
Functional Programming Principles
We favor functional programming paradigms throughout the codebase:
- Immutability: Avoid mutating data directly. Use spread operators,
map,filter, andreduceinstead of mutating arrays or objects.
// Good: Create new objects/arrays
const updatedUser = { ...user, name: 'New Name' }
const newItems = items.filter(item => item.active)
// Avoid: Mutating existing data
user.name = 'New Name'
items.splice(0, 1)-
Pure Functions: Functions should return the same output for the same input without side effects when possible.
-
Signal Side Effects: When side effects are necessary (API calls, database operations, DOM manipulation), make them explicit and isolated.
// Good: Side effect is clear from function name and placement
async function fetchUserData(userId: string) {
return await knex('users').where({ id: userId }).first()
}
// Good: Side effect isolated in a specific function
function updateDOMTitle(title: string): void {
document.title = title
}React Component Best Practices
Minimize useEffect Usage
useEffect can introduce complexity and bugs. Before using it, consider alternatives:
// Often unnecessary - derive state instead
// Avoid:
const [filteredItems, setFilteredItems] = useState([])
useEffect(() => {
setFilteredItems(items.filter(item => item.active))
}, [items])
// Good: Derive directly
const filteredItems = items.filter(item => item.active)When useEffect is necessary (side effects like API calls, subscriptions), ensure:
- Dependencies are correct and complete
- Cleanup functions are provided when needed
- The effect has a single, clear responsibility
Use useMemo Sparingly
Only use useMemo when you have measured performance issues:
- For expensive computations that run on every render
- When referential equality matters for child component re-renders
// Generally unnecessary - JavaScript is fast
// Avoid premature optimization:
const sum = useMemo(() => a + b, [a, b])
// Good use case: Expensive filtering/sorting of large datasets
const sortedAndFiltered = useMemo(
() => largeDataset.filter(filterFn).sort(sortFn),
[largeDataset, filterFn, sortFn]
)Keep Components Focused and Small
- Single Responsibility: Each component should have one clear purpose
- Size Guideline: If a component exceeds ~150-200 lines, consider breaking it down
- Extract Sub-Components: Isolate distinct UI sections into their own components
- Co-locate Components: Keep tightly coupled components together in the same file or subdirectory
// Instead of one massive component:
function LargeWorkflowBuilder() {
// 500 lines of mixed concerns...
}
// Break into focused components:
function WorkflowBuilder() {
return (
<>
<WorkflowHeader />
<WorkflowCanvas />
<WorkflowSidebar />
<WorkflowToolbar />
</>
)
}Component Structure
Organize component internals in a consistent order:
- Props interface/type definition
- Component declaration
- Hooks (useState, useEffect, custom hooks)
- Derived state and computed values
- Event handlers and callbacks
- Helper functions (consider extracting if complex)
- Render logic
interface WorkflowCardProps {
workflow: Workflow
onEdit: (id: string) => void
}
export default function WorkflowCard({ workflow, onEdit }: WorkflowCardProps) {
// Hooks
const [isHovered, setIsHovered] = useState(false)
const user = useCurrentUser()
// Derived values
const canEdit = checkWorkflowPermission(user, workflow, 'edit')
// Event handlers
const handleEdit = () => {
onEdit(workflow.id)
}
// Render
return (...)
}TypeScript Best Practices
- Explicit Types: Define types for function parameters and return values
- Avoid
any: Useunknownfor truly unknown types, then narrow with type guards - Type Inference: Let TypeScript infer types for simple variable assignments
- Interfaces vs Types: Use
interfacefor object shapes,typefor unions/intersections/utilities
// Good: Explicit parameter and return types
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Avoid: Implicit any
function process(data) { ... }
// Good: Use unknown and narrow
function parseInput(input: unknown): ParsedData {
if (typeof input !== 'object' || input === null) {
throw new Error('Invalid input')
}
// Type guard narrows unknown to object
return input as ParsedData
}Import Organization
Group and order imports logically for readability:
- External dependencies (React, Next.js, libraries)
- Internal modules (utils, types, services)
- Components
- Styles
// External
import { useState } from 'react'
import knex from 'knex'
import { NextApiRequest, NextApiResponse } from 'next'
import { ApiErrorT } from '../../../types/error'
import { WorkflowT } from '../../../types/workflow'
// Internal utilities and types
import { validateUser } from '../../../util/api/validateUser'
// Components
import { Button } from '../../components/basic/Button'
import { Modal } from '../../components/basic/Modal'
// Styles
import styles from './WorkflowCard.module.css'Comments & Documentation
- Code Should Be Self-Documenting: Write clear code that doesn't need comments
- Comment the "Why", Not the "What": Explain reasoning, not obvious operations
- Document Complex Logic: Add explanations for non-obvious algorithms or business rules
- JSDoc for Public APIs: Use JSDoc for functions meant to be used across the codebase
// Avoid: Stating the obvious
// Increment counter by 1
counter++
// Good: Explaining non-obvious reasoning
// Use a 30-second delay to debounce rapid user actions
// and prevent overwhelming the API with requests
const DEBOUNCE_DELAY = 30000
// Good: Documenting complex business logic
/**
* Validates that a user can transition a workflow stage.
*
* Rules:
* - User must be assigned to the current stage
* - All required blocks in the stage must be completed
* - Stage must not be locked by another user
*/
function validateStageTransition(user: User, stage: Stage): boolean {
// ...
}Error Handling
- Be Specific: Provide clear, actionable error messages
- Fail Fast: Validate inputs early and return errors immediately
- Handle Errors Appropriately: Use try-catch for unexpected errors, conditional checks for expected ones
- User-Facing Errors: Make error messages understandable for end users
// Good: Clear validation with specific error
if (!workflow.name || workflow.name.trim().length === 0) {
return sendError(res, 'Workflow name is required', 400)
}
// Good: Specific, actionable error message
if (session.status === 'completed') {
return sendError(res, 'Cannot modify a completed session', 400)
}