Skip to content

Development Coding Standards Frontend

Mathew Storm edited this page Feb 12, 2026 · 5 revisions

Frontend Coding Standards

Beautiful, accessible code that works on every device — from the latest iPhone to a 5-year-old Android phone on 2G.

Why These Standards Matter

Our frontend serves students globally, which means our code must:

  • Load fast on 2G networks (50 kbps)
  • Work everywhere from old phones to new laptops
  • Be accessible for all abilities and languages
  • Stay maintainable for contributors worldwide

Our Goal: Write React code so clean that a student contributor can understand it quickly and users on slow connections never wait.


Table of Contents


Project Structure

Directory Organization

Frontend/EduLiteFrontend/src/
├── components/              # Reusable UI components
│   ├── common/             # Shared components (Button, Input, modals, selects)
│   ├── slideshow/          # Slideshow feature components
│   │   └── editor/         # Slideshow editor components
│   └── *.tsx/jsx           # Top-level shared components (Navbar, Footer, etc.)
├── pages/                  # Page components (one per route)
├── hooks/                  # Custom React hooks
├── services/               # API clients (coursesApi, slideshowApi, tokenService, etc.)
├── types/                  # TypeScript type definitions (*.types.ts)
├── contexts/               # React Context providers (AuthContext)
├── utils/                  # Helper functions (errorUtils, etc.)
├── i18n/                   # Internationalization
│   └── locales/            # Language JSON files (en.json, ar.json)
├── test/                   # Test setup and utilities
├── assets/                 # Images, fonts, static files
├── App.jsx                 # Main app component
└── main.jsx                # Entry point

File Naming Conventions

# Components: PascalCase
Button.tsx                   # TypeScript component (preferred for new files)
Navbar.tsx
BackToTopButton.jsx          # JavaScript component (still supported)

# Hooks: camelCase with 'use' prefix
useAuth.ts
useCourses.ts
useSlideLoader.ts

# Services: camelCase
coursesApi.ts
tokenService.ts
slideshowApi.ts

# Types: camelCase with .types.ts extension
courses.types.ts
slideshow.types.ts
auth.types.ts

# Utils: camelCase
errorUtils.ts

# Tests: same name as source + .test.ts/.test.tsx in __tests__/ folder
services/__tests__/coursesApi.test.ts
hooks/__tests__/useCourses.test.ts
components/common/__tests__/Button.test.tsx

React Components

Component Best Practices

1. Keep Components Small and Focused

// BAD: Everything in one component
function UserDashboard() {
  // 500 lines of mixed concerns...
}

// GOOD: Separated concerns
function UserDashboard() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
      <UserInfo />
      <NotificationPanel />
      <CourseList />
      <FriendActivity />
    </div>
  );
}

2. Extract Logic into Custom Hooks

// BAD: Logic mixed with UI
function AssignmentList() {
  const [assignments, setAssignments] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/assignments')
      .then(res => res.json())
      .then(data => { setAssignments(data); setLoading(false); })
      .catch(err => { setError(err); setLoading(false); });
  }, []);

  // ... 200 lines of UI ...
}

// GOOD: Logic extracted to custom hook
function useAssignments() {
  const [assignments, setAssignments] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchAssignments()
      .then(setAssignments)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { assignments, loading, error };
}

function AssignmentList() {
  const { assignments, loading, error } = useAssignments();

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  return <AssignmentGrid assignments={assignments} />;
}

3. Use ForwardRef When Components Need Refs

import React, { ChangeEvent, InputHTMLAttributes } from "react";

interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
  name: string;
  value: string;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
  label?: string;
  error?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ name, value, onChange, label, error, ...props }, ref) => {
    return (
      <div>
        {label && <label htmlFor={name}>{label}</label>}
        <input ref={ref} id={name} name={name} value={value} onChange={onChange} {...props} />
        {error && <span className="error">{error}</span>}
      </div>
    );
  }
);

Input.displayName = "Input";
export default Input;

TypeScript Guidelines

We're gradually migrating from JSX to TypeScript. New files should be .tsx/.ts. Both .jsx and .tsx are supported.

Quick Rules

  • No any — use specific types or unknown
  • Let TypeScript infer when the type is obvious (const name = 'Ahmed' not const name: string = 'Ahmed')
  • Define interfaces for component props, API responses, and state objects
  • Use utility typesPartial<T>, Pick<T, K>, Omit<T, K>

Component Props

interface ButtonProps {
  children: ReactNode;
  onClick?: MouseEventHandler<HTMLButtonElement>;
  type?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  className?: string;
}

const Button: React.FC<ButtonProps> = ({ children, onClick, type = 'primary', ...props }) => {
  return <button onClick={onClick} {...props}>{children}</button>;
};

API Response Types

Define types in src/types/ that mirror the backend serializers:

// types/courses.types.ts
export type CourseVisibility = "public" | "restricted" | "private";
export type CourseRole = "teacher" | "student" | "assistant";

export interface CourseListItem {
  id: number;
  title: string;
  outline: string | null;
  visibility: CourseVisibility;
  member_count: number;
  // ...
}

export interface CoursePaginatedResponse<T> {
  count: number;
  next: string | null;
  previous: string | null;
  total_pages: number;
  current_page: number;
  page_size: number;
  results: T[];
}

Custom Hooks

function useCourses(params?: CourseListParams): {
  courses: CoursePaginatedResponse<CourseListItem> | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

For a full 30-minute TypeScript tutorial with migration guides and troubleshooting, see the TypeScript Guide.


API Services & Data Fetching

Layered Architecture

We follow a strict layered pattern:

types/*.types.ts     →  TypeScript types mirroring backend serializers
services/*Api.ts     →  API functions (axios calls + error handling)
hooks/use*.ts        →  React hooks wrapping API functions (loading/error/refetch)
pages/*.tsx          →  Page components consuming hooks

API Service Pattern

All API services follow the same pattern established in slideshowApi.ts:

// services/coursesApi.ts
import axios from "axios";
import { getSafeErrorMessage } from "../utils/errorUtils";

const API_BASE_URL = "http://localhost:8000/api";

// Auth headers handled automatically by tokenService interceptors

export const listCourses = async (
  params?: CourseListParams
): Promise<CoursePaginatedResponse<CourseListItem>> => {
  try {
    const response = await axios.get(`${API_BASE_URL}/courses/`, {
      params,
      timeout: 10000,
    });
    return response.data;
  } catch (error) {
    throw new Error(getSafeErrorMessage(error, "Failed to load courses"));
  }
};

Key rules:

  • Use the global axios instance (tokenService interceptors handle JWT auth automatically)
  • Use getSafeErrorMessage() from errorUtils.ts for all error handling — never expose raw backend errors to users
  • Timeout: 10000ms for reads, 15000ms for writes
  • Return types must match the backend serializer response shape exactly

Data Fetching Hook Pattern

// hooks/useCourses.ts
export function useCourses(params?: CourseListParams) {
  const [courses, setCourses] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [fetchKey, setFetchKey] = useState(0);

  const refetch = useCallback(() => setFetchKey(prev => prev + 1), []);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const data = await listCourses(params);
        if (!cancelled) setCourses(data);
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load");
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, [/* deps */, fetchKey]);

  return { courses, loading, error, refetch };
}

Key rules:

  • Always use a cancelled flag to prevent state updates on unmounted components
  • Always return { data, loading, error, refetch }
  • refetch uses a fetchKey counter pattern

State Management

Local State

Use useState for UI-local state:

const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);

Context for Global State

Use React Context for app-wide state. See AuthContext.tsx for the canonical pattern:

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // ... auth logic ...

  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

Styling with Tailwind

Key Rules

  • Mobile-first: start with base styles, add sm:, md:, lg: breakpoints
  • Always include dark mode: every color class needs a dark: counterpart
  • Use logical properties for RTL: ms-auto / me-auto instead of ml-auto / mr-auto

Responsive Design

<div className="
  grid gap-4
  grid-cols-1
  sm:grid-cols-2
  lg:grid-cols-3
  xl:grid-cols-4
">
  {courses.map(course => <CourseCard key={course.id} course={course} />)}
</div>

Dark Mode

<span className="
  px-2 py-1 rounded-full text-xs font-medium
  bg-green-100 text-green-800
  dark:bg-green-900 dark:text-green-200
">
  Online
</span>

Accessibility

Key Rules

  • ARIA attributes: role, aria-label, aria-expanded, aria-modal on interactive elements
  • Keyboard navigation: support Escape, ArrowDown, ArrowUp, Enter on dropdowns and modals
  • Focus management: trap focus in modals, auto-focus cancel button in confirmation dialogs
  • Semantic HTML: use <button> for actions, <a> for navigation, <ul>/<li> for lists
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
  <h2 id="modal-title">Confirm Action</h2>
  <button onClick={onClose} aria-label="Close modal">
    <span aria-hidden="true">&times;</span>
  </button>
</div>

Internationalization

All user-facing text must use translation keys via react-i18next:

import { useTranslation } from 'react-i18next';

function WelcomeMessage({ userName }) {
  const { t } = useTranslation();

  return (
    <h1>{t('welcome.title', { name: userName })}</h1>
  );
}

Translation files live in src/i18n/locales/:

// en.json
{
  "welcome": {
    "title": "Welcome back, {{name}}!"
  },
  "common": {
    "loading": "Loading...",
    "error": "Something went wrong",
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete"
  }
}

Never hardcode user-facing strings. Always add both en.json and ar.json entries.


Testing

We use Vitest with React Testing Library for frontend testing.

Quick Start

npm run test           # Run all tests once
npm run test:watch     # Watch mode (re-runs on file changes)
npm run test:ui        # Interactive browser UI
npm run test:coverage  # With coverage report

Test Structure

Tests live in __tests__/ folders next to the code they test:

src/
├── components/common/
│   ├── Button.tsx
│   └── __tests__/
│       ├── Button.test.tsx
│       └── Button.edge.test.tsx
├── hooks/
│   ├── useCourses.ts
│   └── __tests__/
│       └── useCourses.test.ts
└── services/
    ├── coursesApi.ts
    └── __tests__/
        └── coursesApi.test.ts

What to Test

Layer What to Test
Components Rendering, user interactions, conditional states, accessibility
API services Success responses, error handling, query parameters, HTTP error codes
Custom hooks Loading/error states, data fetching, refetch, cleanup on unmount

Key Tools

Tool Purpose
vitest Test runner (Jest-compatible API)
@testing-library/react Component rendering and DOM queries
@testing-library/user-event Realistic user interaction simulation
axios-mock-adapter HTTP request mocking for API service tests
renderWithProviders Custom render with Router/Auth/i18n contexts (src/test/utils.tsx)

Before Every PR

npm run test          # All tests pass
npm run type-check    # TypeScript compiles
npm run lint          # No linting errors
npx vite build        # Build succeeds

For the full testing guide with detailed patterns and examples, see Code Testing for Frontend.


Code Quality

ESLint

We use the flat config format (eslint.config.js) with react-hooks and react-refresh plugins. Run npm run lint before every PR.

Import Organization

// 1. React imports
import { useState, useEffect, useCallback } from 'react';

// 2. Third-party libraries
import { useTranslation } from 'react-i18next';
import axios from 'axios';

// 3. Internal imports — absolute paths
import { useAuth } from '@/contexts/AuthContext';

// 4. Internal imports — relative paths
import { listCourses } from '../services/coursesApi';
import type { CourseListItem } from '../types/courses.types';

Comments

Comments explain why, not what:

// BAD
counter++; // Increment counter

// GOOD
// Students on 2G often need 3-5 attempts due to packet loss
counter++;

// GOOD
// Students in conflict zones might lose internet for days.
// Keep their work in localStorage for 30 days instead of the default 7.
const OFFLINE_CACHE_DURATION = 30 * 24 * 60 * 60 * 1000;

Error Boundaries

Wrap page-level components in error boundaries for graceful failure:

// components/ErrorBoundary.jsx — already exists in the codebase
<ErrorBoundary>
  <App />
</ErrorBoundary>

Summary

  1. Performance — fast loading on slow networks
  2. Accessibility — usable by everyone
  3. Maintainability — easy for our global volunteer team
  4. Reliability — works in all conditions
  5. Type safety — catch errors at build time, not in production

Remember

  • Components — small, focused, separated concerns
  • TypeScript — preferred for all new files
  • API layer — types → services → hooks → pages
  • Test everything — students depend on us
  • Accessibility always — no student left behind

Related Pages


These standards evolve with our community. Suggest improvements via pull request!

Beautiful code isn't just about aesthetics — it's about creating interfaces that work for a student in rural Sudan on a 2G connection just as well as they work in a Toronto university. When we code with empathy, we code for everyone.

Clone this wiki locally