Skip to content

Latest commit

 

History

History
483 lines (337 loc) · 10 KB

File metadata and controls

483 lines (337 loc) · 10 KB

Code Conventions

This document defines the coding conventions applied across the Next.js, React, TypeScript, and Tailwind CSS codebase.


Navigation

  1. React / Next.js
  2. TypeScript
  3. Imports
  4. Styling
  5. General

React / Next.js

Components

File Architecture

Each reusable component must reside in a dedicated .tsx file named in kebab-case:

../
├── product-card.tsx
├── search-result-item.tsx
└── ...

If a component's styling is extracted into a dedicated styles.css file in accordance with the styling conventions, the component must follow this structure:

../
└── my-component/
    ├── index.tsx       # Component definition
    └── styles.css      # Component styles

Declaration

Components must be named using PascalCase, matching the name of the file or directory they reside in:

// my-component.tsx
// or
// ../my-component/index.tsx

function MyComponent() {
    return (<></>);
}

'use client' Usage

Use 'use client' only when a component requires one of the following:

  • React state (useState, useReducer)
  • React lifecycle effects (useEffect)
  • Browser APIs (window, localStorage, etc.)
  • Event handlers (onClick, onChange, etc.)

If none of the above apply, leave it as a Server Component — no directive is needed.

The key design principle is: push 'use client' as deep into the component tree as possible, so the maximum amount of UI stays server-rendered.

async function MyServerComponent() {
    return (
        <MyClientComponent />
    );
}
'use client'

import { useState } from "react";

function MyClientComponent() {
    const [number, setNumber] = useState<number>(0);
    const handleClick = () => setNumber(prev => prev + 1);

    return (
        <button onClick={handleClick}>Number: {number}</button>
    );
}

Props Destructuring

Use inline destructuring for component props:

function MyComponent({ title, description }: MyComponentProps) {
    return <div>{title}</div>;
}

Custom Hooks

File Naming

Custom hook files must be named in kebab-case, prefixed with use-:

hooks/
└── use-my-hook.ts

Declaration

Custom hooks must be declared using a regular function declaration and named in camelCase, prefixed with use:

function useMyHook() {}

Exports

Use export default for components and page files:

// ./my-page.tsx
export default function MyPage() {}
// ./my-component.tsx
export default function MyComponent() {}

Use named exports for utilities, hooks, types, and constants:

export function myUtility() {}
export function useMyHook() {}
export type MyType = number;
export const MY_CONST = "my_const_value";

Folder Colocation

Component-specific utilities, types, and constants that are not shared across the codebase must be placed inside the component's file:

// my-component.tsx

function myComponentUtility() {}
interface MyComponentProps {}
const MY_COMPONENT_CONST = "my_component_const_value";

function MyComponent() {}

If the component already has a folder structure, those may live in dedicated files within that folder:

my-component/
├── index.tsx
├── styles.css
├── types.ts        # Component-specific types
└── utils.ts        # Component-specific utilities

Shared utilities, types, and constants must be placed in the corresponding files inside the lib/ directory:

// lib/types.ts
export type MyType = number;

// lib/utils.ts
export function myUtility() {}

// lib/data.ts
export const MY_CONST = "my_const_value";

TypeScript

Variables

Variables must be named in camelCase:

let myVariable;

Constants

Primitive constants and configuration values must be named in UPPER_SNAKE_CASE:

const MAX_ITEMS = 10;
const API_BASE_URL = "https://api.example.com";

Functions

Functions must be named in camelCase:

function myFunction() {}
const myArrowFunction = () => {};

Regular vs Arrow Function Declarations

Regular function declarations must be used for utility functions:

// ./lib/utils.ts
function myUtilityFunction() {}

Arrow function declarations must be used for event handlers inside React components. Event handlers must be named using the handle prefix followed by the action name, in accordance with the function naming convention:

function MyComponent() {
    const handleClick = () => {};
    const handleSubmit = () => {};

    return (
        <button onClick={handleClick} />
    );
}

Interfaces

Interfaces must be named in PascalCase:

interface MyInterface {}

Types

Custom type aliases must be named in PascalCase, suffixed with Type:

type MyCustomType = number;

Types representing a set of string literal values must use snake_case for the values:

type StatusType = "value_one" | "value_two";

Interface vs Type

Interface

Use interface for component props, following the naming pattern [ComponentName]Props:

interface MyComponentProps {}

Use interface for types that describe an object structure:

interface MyObjectType {
    key1: number;
    key2: string;
    key3: MyCustomType;
}

Type

Use type for aliases assigned to variables or constants:

type MyCustomType = "my_custom_type_value";

let array: MyCustomType[] = [];

Enum vs Const

Avoid enum in favour of as const objects:

const Direction = {
    Up: "UP",
    Down: "DOWN",
    Left: "LEFT",
    Right: "RIGHT",
} as const;

type DirectionType = typeof Direction[keyof typeof Direction];

const move = (direction: DirectionType) => {};
move(Direction.Up);

Explicit Return Types on Functions

Define explicit return types for utilities and hooks:

function myUtility(): number {
    return 1;
}

function useMyHook(): boolean {
    return true;
}

Define explicit return types for components using React.ReactNode:

function MyComponent(): React.ReactNode {
    return <div />;
}

Return types for event handlers are optional, as they always return void and are inferred correctly by TypeScript:

function MyComponent(): React.ReactNode {
    const handleClick = () => {};

    return <button onClick={handleClick} />;
}

null vs undefined

Prefer undefined over null:

const myVariable: string | undefined = undefined;

This also applies to optional props in component interfaces:

interface MyComponentProps {
    title?: string; // equivalent to string | undefined
}

Imports

Import Ordering

Imports must be grouped in the following order, with each group preceded by a descriptive comment:

React → Next.js → Components → Hooks → Lib → Icons → Styles

/* React */
import { useMemo, useState } from "react";

/* Next.js */
import Link from "next/link";

/* Components */
import CartItem from "@/components/cart-item";
import { Button } from "@/components/ui/button";

/* Hooks */
import { useMyHook } from "@/hooks/use-my-hook";

/* Lib */
import { CartItemType } from "@/lib/types";
import { getEuro } from "@/lib/utils";

/* Icons */
import { Search } from "lucide-react";

/* Styles */
import "./styles.css";

Absolute vs Relative Imports

Use a relative import when the imported file resides in the same directory as the origin file:

// ./components/cart-drawer/index.tsx
import "./styles.css";

Use an absolute import in all other cases:

// ./components/pages/account/cart/cart-page-client.tsx
import CartItem from "@/components/cart-item";

Styling

Tailwind CSS is used as the primary styling solution. Class names must be defined as a plain string inside the className attribute:

<div className={"relative bg-red-200 text-base text-black ..."}></div>

When the same styling is applied across multiple elements within a component, the repeated classes must be extracted into a dedicated CSS class following the BEM methodology, placed in a styles.css file co-located with the component:

/* ../my-component/styles.css */

.block-name__element-name {
    @apply relative bg-red-200;
}

.block-name__element-name--modifier-name {
    @apply bg-red-400;
}

General

Comment Conventions

We follow a self-documenting code first, comments only when necessary approach:

  • No comments for obvious code — if the code is readable, a comment adds noise
  • Inline comments for non-obvious logic — explain why, not what
  • JSDoc for all exported utilities and hooks — provides IDE tooltips and a clearer API surface
// ❌ Unnecessary — the code already says this
// Increment counter
const handleClick = () => setCount(prev => prev + 1);

// ✅ Explains the why, not the what
// Prices are stored in cents to avoid floating point precision issues
const price = amount * 100;

// ✅ JSDoc for exported utilities
/**
 * Converts a price from cents to a formatted euro string.
 * @param amount - Price in cents
 * @returns Formatted string, e.g. "€12.99"
 */
export function getEuro(amount: number): string {}

console.log Policy

console.log is forbidden in committed code. It may be used freely during development but must be removed before committing.