Eddy Dev Handbook
Code practice

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, and reduce instead 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:

  1. Props interface/type definition
  2. Component declaration
  3. Hooks (useState, useEffect, custom hooks)
  4. Derived state and computed values
  5. Event handlers and callbacks
  6. Helper functions (consider extracting if complex)
  7. 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: Use unknown for truly unknown types, then narrow with type guards
  • Type Inference: Let TypeScript infer types for simple variable assignments
  • Interfaces vs Types: Use interface for object shapes, type for 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:

  1. External dependencies (React, Next.js, libraries)
  2. Internal modules (utils, types, services)
  3. Components
  4. 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)
}

On this page