
Modern Web App Architecture
- 41 installs
- 49 repo stars
- Updated February 11, 2026
- ratacat/claude-skills
Helps with ai & agent building tasks.
About
modern-web-app-architecture is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- modern-web-app-architecture
- AI & Agent Building
- AI-coding skill
Modern Web App Architecture by the numbers
- 41 all-time installs (skills.sh)
- Ranked #8,067 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ratacat/claude-skills --skill modern-web-app-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 41 |
|---|---|
| repo stars | ★ 49 |
| Last updated | February 11, 2026 |
| Repository | ratacat/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Modern Web App Architecture (SPA/SSR/SSG/RSC)
Overview
Comprehensive guidance for designing and building modern web applications (including SPAs and hybrid rendering apps). This skill emphasizes trade-offs, explicit boundaries, and production-ready practices (performance, accessibility, security, testing, delivery).
Core principle: Everything in architecture is a trade-off. There are no "right" answers, only least-worst combinations for your specific context.
Operating Mode (How to Use This Skill)
When activated, work in this order:
1. Clarify context (5–10 questions max) → users, routes, SEO, interactivity, data, auth, team, constraints. 2. Choose a rendering strategy per route (not “one strategy for the whole app”). 3. Define boundaries → feature/domain modules, shared libraries, ownership, and stable interfaces. 4. Plan state + data → local/shared/global/server/URL state, cache strategy, invalidation, optimistic updates. 5. Plan non-functionals → performance budgets + measurement, accessibility plan, security posture, observability. 6. Produce artifacts → short recommendations with explicit trade-offs, plus concrete next steps (folder structure, ADRs, checklists).
If the user can’t answer a question, state reasonable assumptions and continue (don’t block).
When to Use
- Starting a new SPA or web application
- Choosing rendering strategies (CSR, SSR, SSG, ISR, RSC)
- Implementing state management
- Optimizing Core Web Vitals (LCP/INP/CLS)
- Scaling for multiple frontend teams
- Making architecture trade-off decisions
- Migrating from legacy to modern frontend
Quick Discovery Questions (Ask First)
- What are the top 3 user journeys and their target latency (e.g., “search → product → checkout”)?
- Is SEO required for any routes? Which are public vs behind auth?
- What’s the data shape: mostly CRUD, real-time, offline-first, heavy forms, large tables/charts?
- What are the constraints: browser support, bundle limits, time-to-market, compliance (SOC2/HIPAA/PCI)?
- What’s the team topology: how many devs/teams, release cadence, ownership boundaries?
- What’s your preferred stack (React/Vue/Angular/vanilla), and are you open to TypeScript?
Reference Files
| Topic | When to Load |
|---|---|
| @references/design-patterns.md | Implementing JS patterns (Module, Observer, Factory, etc.) |
| @references/react-patterns.md | React components, hooks, state, composition |
| @references/spa-fundamentals.md | SPA architecture, routing, module organization |
| @references/micro-frontends.md | Scaling teams, independent deployments |
| @references/performance.md | Bundle size, loading, Core Web Vitals |
| @references/architecture-decisions.md | Trade-offs, coupling, fitness functions |
| @references/rendering-strategies.md | CSR vs SSR vs SSG vs ISR vs RSC |
| @references/state-management.md | Local, global, server state patterns |
| @references/security-and-auth.md | Auth choices, token storage, XSS/CSRF, CSP, API boundaries |
| @references/accessibility-and-i18n.md | WCAG basics, SPA focus mgmt, inclusive components, i18n pitfalls |
| @references/testing-and-quality.md | Testing strategy, CI quality gates, a11y checks, contract tests |
| @references/tooling-and-delivery.md | Bundling, environments, deployment, observability, feature flags |
Quick Architecture Decision Tree
Project Requirements?
├─ SEO critical + dynamic content → SSR (or SSR+streaming)
├─ SEO critical + mostly static → SSG/ISR (or hybrid)
├─ Mostly behind auth + app-like UX → CSR SPA (or hybrid with pre-rendered shell)
├─ Mixed (marketing + app) → Hybrid/Islands (route-level strategy)
│
Team Size?
├─ <5 developers → Modular monolith SPA
├─ 5-15 developers → Well-structured SPA or Service-based
├─ >15 developers, multiple teams → Consider micro-frontends
│
Domain Complexity?
├─ Simple CRUD → Layered architecture
├─ Complex workflows → Domain-partitioned (DDD)
├─ Multiple bounded contexts → Micro-frontendsDefault Outputs (What You Should Produce)
Depending on the user request, aim to output:
- Route strategy map: a small table of routes → CSR/SSR/SSG/ISR/RSC + why
- Module boundary sketch: feature folders, shared libs, interface contracts
- State map: local/shared/global/server/URL, plus the chosen tooling pattern
- Data plan: caching/invalidation, error states, optimistic updates, pagination
- Quality plan: testing layers + CI gates + accessibility checks
- Performance plan: budgets + measurement + concrete loading strategy (split points)
- Risk register: top 5 risks + mitigations (e.g., hydration cost, auth posture, team coupling)
Essential Patterns Quick Reference
Component Patterns
| Pattern | Use When |
|---|---|
| Container/Presentational | Separating data from UI |
| Compound Components | Building composable APIs (Select, Menu) |
| Hooks | Sharing stateful logic without HOCs |
| Provider | Avoiding prop drilling for global data |
State Management
| Approach | Use When |
|---|---|
| useState/useReducer | Local component state |
| Context | Theme, auth, low-frequency global state |
| Zustand/Jotai | Simple global state, minimal boilerplate |
| Redux Toolkit | Complex state, time-travel debugging |
| React Query/SWR | Server state, caching, background refresh |
| XState | Complex flows with explicit state machines |
Performance Essentials
| Technique | Impact |
|---|---|
| Code splitting | Reduce initial bundle |
| Lazy loading | Defer non-critical |
| React.memo | Prevent unnecessary re-renders |
| useMemo/useCallback | Stable references |
| Virtual lists | Handle large datasets |
Architecture Characteristics (Pick 3-7)
| Characteristic | Questions to Ask |
|---|---|
| Scalability | How many concurrent users? Growth rate? |
| Performance | What's acceptable TTI? LCP target? |
| Deployability | How often do you ship? Independent deploys? |
| Testability | How easy to verify changes? |
| Maintainability | What's the expected lifespan? |
| Modularity | How often do requirements change? |
Anti-patterns to Avoid
| Anti-pattern | Problem | Fix |
|---|---|---|
| Prop drilling | Tight coupling | Context or state management |
| God components | Too many responsibilities | Split by concern |
| Premature optimization | Complexity without evidence | Profile first |
| Shared mutable state | Race conditions, bugs | Immutable patterns |
| Monolithic bundle | Slow initial load | Code splitting |
| Over-fetching | Wasted bandwidth | GraphQL or BFF |
| LocalStorage tokens by default | XSS turns into account takeover | Prefer httpOnly cookies + CSP (see security refs) |
| Global store for server data | Cache invalidation pain | Use React Query/SWR for server state |
Performance Budgets
Budgets must be calibrated to your users/devices, but these are good starting points for a “fast by default” app:
| Metric | Target | Needs Work |
|---|---|---|
| LCP | <2.5s | 2.5–4s |
| INP | <200ms | 200–500ms |
| CLS | <0.1 | 0.1–0.25 |
| TTFB | <800ms | 800ms–1.8s |
| Route JS (initial) | <170KB gzip | <300KB gzip |
Sources
Synthesized from:
- Learning JavaScript Design Patterns (Osmani, 2023)
- React in Depth (Barklund, 2024)
- SPA Design and Architecture (Scott)
- Single Page Web Applications (Mikowski & Powell)
- Building Micro-Frontends (Mezzalira)
- Micro Frontends in Action (Geers)
- Responsible JavaScript (Wagner)
- High Performance Browser Networking (Grigorik)
- Web Performance in Action (Wagner)
- Frontend Architecture for Design Systems (Godbolt)
- Fundamentals of Software Architecture (Richards & Ford)
- Software Architecture: The Hard Parts (Ford et al.)
- Patterns.dev
Accessibility & Internationalization (i18n)
Accessibility Baseline (Don’t Negotiate)
Start with the fundamentals:
- Prefer semantic HTML (
<button>,<a>,<label>,<main>, headings) over div-based components. - Ensure full keyboard navigation (Tab order, visible focus, Escape handling in dialogs).
- Provide names for controls (label text,
aria-label,aria-labelledby). - Validate color contrast and support reduced motion.
- Avoid announcing “fake” states (don’t use ARIA to patch broken semantics).
SPA-Specific Accessibility Pitfalls
Single-page routing changes the DOM without a full navigation event, so you must implement:
- Document title updates: set meaningful titles per route.
- Focus management on route change: move focus to a page-level heading or main landmark.
- Route change announcements: announce navigations for screen readers (e.g., an
aria-liveregion). - Skip-to-content link: keep it functional even with client routing.
Component Checklists
Buttons and Links
- Buttons trigger actions; links navigate.
- Disabled buttons should be actual
disabledbuttons (not CSS only). - Links must have an
href(for accessibility and expected browser behavior).
Forms
- Every input has a
<label>(oraria-labelledby). - Validation errors are associated with inputs (
aria-describedby) and announced. - Error messages are specific (“Email is required”, not “Invalid”).
Dialogs/Modals
- Trap focus while open; restore focus to the trigger on close.
- Close on Escape.
- Use
role="dialog"+aria-modal="true"and a labeled title.
Menus/Comboboxes
- Follow established WAI-ARIA patterns; these are hard to “wing”.
- Prefer a vetted component library if you can’t invest in deep a11y work.
Automated + Manual Testing
- Automated: axe-core (unit/component tests), Lighthouse accessibility checks.
- Manual: keyboard-only pass, screen reader spot checks on critical flows.
- Regression: add a11y checks to Storybook/Chromatic flows if you use Storybook.
---
Internationalization (i18n) Basics
Treat Locale as a First-Class Input
- Centralize locale selection (user preference, browser default, account setting).
- Don’t hardcode dates/numbers/currency formatting.
Use Intl APIs
Intl.DateTimeFormatfor dates/times (time zones matter).Intl.NumberFormatfor currency/percent/compact notation.Intl.PluralRulesfor pluralization logic.
Design for Translation
- Avoid string concatenation (“Hello " + name) where grammar differs; use message templates.
- Expect text expansion (German/French) and contraction.
- Support RTL layouts if your product needs it (mirror spacing/icons, not just text direction).
i18n Checklist
- [ ] All user-facing strings are externalized
- [ ] Dates/numbers/currency use
Intl - [ ] Layout tolerates longer strings
- [ ] RTL support decision is explicit
- [ ] Time zone strategy is defined (user vs account vs server)
Architecture Decision Making
First Law of Software Architecture
Everything in software architecture is a trade-off.
There are no "right" answers, only least-worst combinations of trade-offs for your specific context.
Architecture Characteristics
Identifying Characteristics
Extract from requirements and domain:
- Explicit: "Must handle 10,000 concurrent users" → Scalability
- Implicit: Payment processing → Security (often unstated)
- Translated: "User satisfaction" → Performance, Availability
Common Characteristics
| Category | Characteristic | Description |
|---|---|---|
| Operational | Performance | Response time, throughput |
| Scalability | Handle growth | |
| Availability | Uptime percentage | |
| Reliability | Mean time between failures | |
| Elasticity | Handle burst traffic | |
| Structural | Modularity | Logical separation |
| Maintainability | Ease of change | |
| Testability | Ease of verification | |
| Deployability | Release frequency | |
| Cross-cutting | Security | Authentication, authorization |
| Accessibility | WCAG compliance | |
| Legal | Privacy, compliance |
Prioritization
Pick 3-7 characteristics. Supporting all characteristics equally is impossible—they conflict.
Common trade-offs:
- Performance vs Maintainability
- Security vs Usability
- Scalability vs Simplicity
- Flexibility vs Performance
---
Architecture Decision Records (ADRs)
Purpose
Document significant decisions:
- Create historical record
- Explain reasoning (the "why")
- Prevent revisiting same decisions
- Onboard new team members
Template
# ADR-001: Use React with TypeScript
## Status
Accepted
## Context
We need to choose a frontend framework for our new e-commerce platform.
Team has experience with React and Angular. Project requires:
- Complex state management
- Strong type safety
- Long-term maintainability
## Decision
We will use React with TypeScript.
## Consequences
### Positive
- Team expertise reduces ramp-up time
- Large ecosystem for e-commerce features
- TypeScript catches errors at compile time
### Negative
- More boilerplate than plain JavaScript
- Need to maintain type definitions
- Some libraries lack good TypeScript support
### Risks
- React ecosystem churn (mitigate: abstract framework specifics)
## Alternatives Considered
1. **Angular**: Full framework but steeper learning curve
2. **Vue**: Simpler but less team experience
3. **React + JavaScript**: Faster initial development but more runtime errors---
Coupling and Cohesion
Coupling Types
Static (Compile-time):
| Type | Description | Example |
|---|---|---|
| Name | Shared naming | Function calls |
| Type | Type agreement | Interface parameters |
| Meaning | Hard-coded values | Magic strings/numbers |
| Position | Parameter order | Function arguments |
| Algorithm | Shared algorithm | Hash functions |
Dynamic (Runtime):
| Type | Description | Example |
|---|---|---|
| Execution | Order dependency | A before B |
| Timing | Time dependency | Race conditions |
| Values | Transaction scope | Atomic updates |
| Identity | Shared references | Same object instance |
Coupling Guidelines
1. Minimize overall coupling by encapsulating 2. Minimize coupling across boundaries (modules, services) 3. Maximize coupling within boundaries (it's fine internally)
Cohesion Spectrum
From best to worst:
1. Functional: Everything related, all essentials present 2. Sequential: Output → Input chains 3. Communicational: Operate on same data 4. Procedural: Order-dependent execution 5. Temporal: Run at same time 6. Logical: Logically related, different functions 7. Coincidental: No meaningful relationship
---
Architecture Quantum
An independently deployable artifact with high functional cohesion and synchronous connascence.
Components
- Independently deployable: Includes everything needed to function
- High functional cohesion: Does something purposeful
- Synchronous connascence: Synchronous calls create coupling
Significance
Analyze architecture characteristics per quantum, not system-wide.
Example:
System has 3 quanta:
- User Service (high availability)
- Order Service (strong consistency)
- Reporting Service (high performance)
Each quantum may need different architecture characteristics.---
Fitness Functions
Definition
Any mechanism providing objective integrity assessment of architecture characteristics.
Examples
Cyclomatic Complexity:
// CI check: fail if CC > 10
const cc = analyzeComplexity(file);
if (cc > 10) {
throw new Error(`Complexity ${cc} exceeds threshold`);
}Bundle Size:
// Fail build if bundle exceeds budget
const stats = require('./dist/stats.json');
const mainBundle = stats.assets.find(a => a.name.includes('main'));
if (mainBundle.size > 170000) {
process.exit(1);
}Layer Dependencies:
// ArchUnit-style: presentation cannot import data layer
rules.push({
from: 'src/components/**',
disallow: ['src/data/**'],
message: 'Components cannot directly access data layer'
});Performance:
// Lighthouse CI
module.exports = {
ci: {
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
}
}
}
};Benefits
- Automate important-but-not-urgent concerns
- Enable continuous governance
- Fast feedback to developers
- Document constraints as code
---
Top-Level Partitioning
Technical Partitioning (Layered)
┌─────────────────────┐
│ Presentation │
├─────────────────────┤
│ Business Logic │
├─────────────────────┤
│ Data Access │
├─────────────────────┤
│ Database │
└─────────────────────┘Pros:
- Clear separation of technical concerns
- Familiar pattern
- Easy to locate code by type
Cons:
- Domains cut across all layers
- Changes often touch multiple layers
- Difficult to scale independently
Best for: Simple applications, CRUD-heavy systems
Domain Partitioning
┌─────────┬─────────┬─────────┐
│ Users │ Orders │Products │
│ ─────── │ ─────── │ ─────── │
│ UI │ UI │ UI │
│ Logic │ Logic │ Logic │
│ Data │ Data │ Data │
└─────────┴─────────┴─────────┘Pros:
- Aligns with business capabilities
- Independent development and deployment
- Easier team organization (Conway's Law)
Cons:
- Potential code duplication
- Requires good domain understanding
- Cross-cutting concerns need addressing
Best for: Complex domains, multiple teams
---
Conway's Law
Organizations which design systems are constrained to produce designs which are copies of the communication structures of those organizations.
Inverse Conway Maneuver
Structure teams to promote desired architecture:
Want microservices? Create small, cross-functional teams aligned to business domains.
Want modular monolith? Organize by feature teams with clear interfaces.
---
Architecture Anti-patterns
| Anti-pattern | Description | Solution |
|---|---|---|
| Architecture Sinkhole | Requests pass through layers without processing | Add value at each layer or simplify |
| Vendor King | Architecture dictated by vendor | Abstract vendor specifics |
| Groundhog Day | Revisiting same decisions | Document in ADRs |
| Email-Driven | Decisions scattered in emails | Centralize in ADRs |
| Frozen Caveman | Outdated expertise believed current | Continuous learning |
| Golden Hammer | One solution for all problems | Match tool to problem |
---
Decision Framework
Step 1: Identify Characteristics
- What does the business need?
- What are implicit requirements?
- Prioritize top 3-7
Step 2: Determine Scope
- How many architecture quanta?
- What are their boundaries?
- Different characteristics per quantum?
Step 3: Choose Partitioning
- Technical (simple, familiar)
- Domain (scalable, team-aligned)
Step 4: Select Style
- Monolith vs distributed?
- Which pattern fits characteristics?
- What trade-offs are acceptable?
Step 5: Document Decisions
- Write ADRs
- Implement fitness functions
- Review and update
---
Architect Responsibilities
1. Make architecture decisions (guide, don't dictate) 2. Analyze architecture continuously (identify decay) 3. Stay current with trends (technical breadth) 4. Ensure compliance (fitness functions) 5. Understand business domain (speak stakeholder language) 6. Navigate politics (negotiate trade-offs)
---
Practical Trade-off Analysis
Framework
For each decision:
1. List options (usually 2-4 realistic choices) 2. Identify criteria (from architecture characteristics) 3. Score each option (1-5 per criterion) 4. Weight criteria (not all equal importance) 5. Calculate weighted scores 6. Document reasoning (ADR)
Example: State Management Choice
| Criterion | Weight | Context | Redux | Zustand |
|---|---|---|---|---|
| Simplicity | 3 | 5 | 2 | 4 |
| Scalability | 4 | 3 | 5 | 3 |
| DevTools | 2 | 3 | 5 | 4 |
| Bundle Size | 3 | 5 | 2 | 5 |
| Weighted | 47 | 42 | 48 |
Decision: Zustand for this project (but document why Redux might be better for larger teams or more complex state).
JavaScript Design Patterns
Overview
Design patterns are proven solutions to common software problems. Modern JavaScript (ES2015+) has changed how many classic patterns are implemented.
Pattern Categories
Creational Patterns
Create objects in manner suitable to the situation.
Structural Patterns
Compose objects into larger structures.
Behavioral Patterns
Communication between objects.
---
Module Pattern
Purpose: Encapsulate code with public/private access.
Modern Implementation (ES Modules):
// basket.js
const basket = []; // private
export const addItem = (item) => basket.push(item);
export const getCount = () => basket.length;
export const getTotal = () => basket.reduce((sum, item) => sum + item.price, 0);When to Use:
- Organizing code into logical units
- Hiding implementation details
- Creating clear public APIs
---
Singleton Pattern
Purpose: Ensure only one instance exists globally.
ES Module Implementation:
// config.js - modules are singletons by default
let instance;
const config = { theme: 'dark', locale: 'en' };
export const getConfig = () => config;
export const setConfig = (key, value) => { config[key] = value; };When to Use:
- Application configuration
- Logger instances
- Database connections
Caution: In React, prefer Context or state management over singletons for better testability.
---
Factory Pattern
Purpose: Create objects without specifying exact class.
class VehicleFactory {
create(type, options) {
const vehicles = {
car: Car,
truck: Truck,
motorcycle: Motorcycle
};
const Vehicle = vehicles[type];
return Vehicle ? new Vehicle(options) : null;
}
}
// Usage
const factory = new VehicleFactory();
const car = factory.create('car', { color: 'red' });When to Use:
- Object creation logic is complex
- Type determined at runtime
- Decoupling code from specific classes
---
Observer Pattern
Purpose: Object (subject) maintains list of dependents (observers) and notifies them of changes.
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
(this.events[event] ||= []).push(listener);
return () => this.off(event, listener);
}
off(event, listener) {
this.events[event] = this.events[event]?.filter(l => l !== listener);
}
emit(event, data) {
this.events[event]?.forEach(listener => listener(data));
}
}When to Use:
- Event-driven architectures
- Decoupling components
- Real-time updates
React Equivalent: Custom hooks with subscription patterns, or RxJS for complex streams.
---
Publish/Subscribe Pattern
Purpose: Like Observer, but with intermediate event channel (more decoupled).
class PubSub {
constructor() {
this.topics = {};
}
subscribe(topic, handler) {
(this.topics[topic] ||= []).push(handler);
return () => {
this.topics[topic] = this.topics[topic].filter(h => h !== handler);
};
}
publish(topic, data) {
this.topics[topic]?.forEach(handler => handler(data));
}
}Difference from Observer: Publishers don't know subscribers exist. Complete decoupling.
---
Mediator Pattern
Purpose: Centralize complex communications between objects.
class ChatRoom {
constructor() {
this.users = {};
}
register(user) {
this.users[user.name] = user;
user.chatroom = this;
}
send(message, from, to) {
if (to) {
this.users[to]?.receive(message, from);
} else {
// Broadcast
Object.keys(this.users)
.filter(name => name !== from)
.forEach(name => this.users[name].receive(message, from));
}
}
}When to Use:
- Complex component interactions
- Reducing direct dependencies
- Centralizing business logic
Modern Example: Express.js middleware chain is a mediator pattern.
---
Decorator Pattern
Purpose: Add behavior to objects dynamically without affecting other instances.
// Functional approach
const withLogging = (fn) => (...args) => {
console.log(`Calling with:`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // Logs: Calling with: [2, 3], Result: 5Class Decorator (wrapping):
class Coffee {
cost() { return 5; }
}
class WithMilk {
constructor(coffee) { this.coffee = coffee; }
cost() { return this.coffee.cost() + 1; }
}
const latte = new WithMilk(new Coffee());
latte.cost(); // 6React Equivalent: Higher-Order Components (HOCs), though hooks are now preferred.
---
Facade Pattern
Purpose: Provide simplified interface to complex subsystem.
// Complex subsystems
class VideoDecoder { /* ... */ }
class AudioDecoder { /* ... */ }
class SubtitleLoader { /* ... */ }
class DisplayRenderer { /* ... */ }
// Facade
class VideoPlayer {
constructor() {
this.video = new VideoDecoder();
this.audio = new AudioDecoder();
this.subs = new SubtitleLoader();
this.display = new DisplayRenderer();
}
play(file) {
const video = this.video.decode(file);
const audio = this.audio.decode(file);
const subs = this.subs.load(file);
this.display.render(video, audio, subs);
}
}When to Use:
- Simplifying complex APIs
- Creating library interfaces
- Hiding implementation complexity
---
Proxy Pattern
Purpose: Placeholder object controlling access to another object.
const handler = {
get(target, prop) {
console.log(`Accessing ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
const user = new Proxy({ name: 'John' }, handler);
user.name; // Logs: Accessing name
user.age = 30; // Logs: Setting age to 30Use Cases:
- Validation
- Logging/profiling
- Lazy initialization
- Access control
---
Flyweight Pattern
Purpose: Share common data between similar objects to minimize memory.
DOM Example (Event Delegation):
// Instead of attaching listener to each button
document.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', handleClick); // Bad: many handlers
});
// Use event delegation (flyweight)
document.body.addEventListener('click', (e) => {
if (e.target.matches('button')) {
handleClick(e); // Good: one handler
}
});When to Use:
- Many similar objects
- Shared immutable data
- DOM event handling
---
Strategy Pattern
Purpose: Define family of algorithms, encapsulate each, make them interchangeable.
const strategies = {
credit: (amount) => amount * 1.02, // 2% fee
paypal: (amount) => amount * 1.03, // 3% fee
crypto: (amount) => amount * 1.01, // 1% fee
};
const processPayment = (amount, method) => {
const strategy = strategies[method];
if (!strategy) throw new Error(`Unknown method: ${method}`);
return strategy(amount);
};
processPayment(100, 'credit'); // 102When to Use:
- Multiple algorithms for same task
- Runtime algorithm selection
- Avoiding large conditionals
---
Command Pattern
Purpose: Encapsulate request as object, enabling parameterization and queuing.
class Command {
execute() { throw new Error('Override me'); }
undo() { throw new Error('Override me'); }
}
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.content += this.text;
}
undo() {
this.editor.content = this.editor.content.slice(0, -this.text.length);
}
}
// Command invoker with history
class Editor {
constructor() {
this.content = '';
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
this.history.pop()?.undo();
}
}When to Use:
- Undo/redo functionality
- Transaction logging
- Task queuing
---
Loading Patterns
Dynamic Import (Import on Interaction)
button.addEventListener('click', async () => {
const { sortBy } = await import('lodash-es');
const sorted = sortBy(data, 'name');
});Import on Visibility
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const { Chart } = await import('chart.js');
new Chart(canvas, config);
observer.disconnect();
}
});
observer.observe(chartContainer);---
Pattern Selection Guide
| Problem | Pattern |
|---|---|
| Need single global instance | Singleton (or ES Module) |
| Object creation is complex | Factory |
| Need to notify many objects of changes | Observer/Pub-Sub |
| Complex object interactions | Mediator |
| Add behavior without modifying class | Decorator |
| Simplify complex subsystem | Facade |
| Control access to object | Proxy |
| Many similar objects, memory concern | Flyweight |
| Multiple interchangeable algorithms | Strategy |
| Need undo/redo capability | Command |
Micro-frontends Architecture
Overview
Micro-frontends extend microservices principles to the frontend, creating vertically-sliced applications from database to UI. Each micro-frontend represents a business subdomain with end-to-end team ownership.
Core Principle: Optimize for feature development speed and team autonomy over code reuse.
When to Use Micro-frontends
Good Fit
- Team Scale: 10+ developers, multiple teams
- Longevity: Long-term maintenance expected
- Independence: Teams need parallel development
- Migration: Incremental legacy replacement
- Technology: Need framework flexibility
Poor Fit
- Small Teams: <5 developers
- Simple Domains: Single bounded context
- Rapid Pivoting: Frequently changing business model
- Limited DevOps: Infrastructure immaturity
---
Composition Strategies
Vertical Split (Page-based)
One micro-frontend per page/view:
example.com/products → Products MFE
example.com/cart → Cart MFE
example.com/checkout → Checkout MFECharacteristics:
- Application shell orchestrates loading
- Only one MFE active at a time
- Simpler developer experience
- No namespace conflicts
- Lower component reusability
Best For: Familiar to SPA developers, clear page boundaries
Horizontal Split (Fragment-based)
Multiple micro-frontends compose single views:
┌─────────────────────────────────────┐
│ Header MFE │
├───────────────┬─────────────────────┤
│ Sidebar MFE │ Content MFE │
│ │ │
└───────────────┴─────────────────────┘Characteristics:
- Higher modularity and reuse
- More complex coordination
- Requires namespace management
- Shared state challenges
Best For: Reusable fragments, multiple teams per page
---
Composition Techniques
Client-Side
Links (Simplest):
<!-- Navigation links between MFEs -->
<a href="/products">Products</a>
<a href="/cart">Cart</a>- High isolation
- Page transitions (not seamless)
- Different apps, same domain
Iframes:
<iframe src="https://checkout.example.com/widget"></iframe>- Strong isolation (separate browsing context)
- Layout/sizing challenges
- Performance overhead
- Limited parent-child communication
Web Components:
// checkout-button.js (MFE)
class CheckoutButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button>Checkout (${this.getAttribute('count')})</button>`;
}
}
customElements.define('checkout-button', CheckoutButton);
// Parent app
<checkout-button count="3"></checkout-button>- Encapsulation via Shadow DOM
- Framework-agnostic
- Lifecycle management
- Native browser support
Module Federation (Webpack 5):
// Remote (checkout MFE)
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/CheckoutButton',
},
shared: ['react', 'react-dom'],
});
// Host (shell)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
},
shared: ['react', 'react-dom'],
});
// Usage in shell
const CheckoutButton = React.lazy(() => import('checkout/Button'));- Runtime code sharing
- Automatic vendor deduplication
- No build-time coupling
- Version negotiation
Server-Side
SSI (Server-Side Includes):
<!--#include virtual="/fragments/header" -->
<main>Content</main>
<!--#include virtual="/fragments/footer" -->- Simple, well-understood
- Fast first-page load
- No client-side JavaScript needed
ESI (Edge-Side Includes):
<esi:include src="/fragments/header" />
<main>Content</main>
<esi:include src="/fragments/footer" />- Composition at CDN level
- Caching per fragment
- Reduces origin load
Podium (Node.js):
// Layout server
const layout = new Layout({ name: 'myLayout' });
const header = layout.client.register({ name: 'header', uri: 'http://header/manifest.json' });
app.get('/', async (req, res) => {
const incoming = res.locals.podium;
const [headerHtml] = await Promise.all([header.fetch(incoming)]);
res.send(`<html><body>${headerHtml}<main>...</main></body></html>`);
});---
Communication Patterns
Same-View Communication
Custom Events:
// Publisher (Cart MFE)
window.dispatchEvent(new CustomEvent('cart:updated', {
detail: { itemCount: 3, total: 99.99 }
}));
// Subscriber (Header MFE)
window.addEventListener('cart:updated', (e) => {
updateCartBadge(e.detail.itemCount);
});Event Bus (injected by shell):
// Shell provides event bus
window.eventBus = new EventEmitter();
// MFEs use it
window.eventBus.emit('user:logout');
window.eventBus.on('user:logout', clearUserData);Cross-View Communication
URL/Query Parameters:
/products?category=shoes&sort=price
↓ navigate to
/cart?from=productsWeb Storage:
// Auth MFE sets token
sessionStorage.setItem('auth_token', token);
// Other MFEs read
const token = sessionStorage.getItem('auth_token');Cookies (same subdomain):
document.cookie = 'user_id=123; path=/; domain=.example.com';Backend Communication
Service Dictionary:
{
"services": {
"products": "https://api.example.com/products",
"users": "https://api.example.com/users",
"orders": "https://api.example.com/orders"
}
}BFF per MFE:
Products MFE → Products BFF → Product Service
Cart MFE → Cart BFF → Cart Service, Inventory Service---
Team Organization
Cross-Functional Teams
Each team includes:
- Frontend developers
- Backend developers
- Designer
- QA
- Product owner
Team Boundaries
Align with business domains (DDD bounded contexts):
Team Discover: Help customers find products
→ Search MFE, Product Catalog MFE
Team Decide: Help customers make decisions
→ Product Detail MFE, Reviews MFE, Comparison MFE
Team Buy: Enable purchases
→ Cart MFE, Checkout MFE, Payment MFEOwnership Model
- Full Stack Ownership: Team owns DB → API → UI
- Independent Deployments: No cross-team coordination
- Autonomous Decisions: Teams choose tech within guardrails
---
Deployment Strategies
Independent Deployment
Each MFE deployable without others:
# CI/CD per MFE
name: Deploy Products MFE
on:
push:
paths:
- 'products-mfe/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- run: aws s3 sync dist/ s3://bucket/products/Blue-Green Deployment
Production (Blue): v1.0
Staging (Green): v1.1
1. Deploy v1.1 to Green
2. Test Green
3. Switch traffic Blue → Green
4. Blue becomes new stagingCanary Releases
// Edge function for gradual rollout
export function handler(event) {
const userId = getUserId(event);
const canaryPercentage = 10;
if (hash(userId) % 100 < canaryPercentage) {
return fetch('https://products-v2.example.com' + event.path);
}
return fetch('https://products-v1.example.com' + event.path);
}Strangler Pattern (Migration)
┌─────────────────────────────────┐
│ Router │
└───────┬───────────────┬─────────┘
│ │
┌────▼────┐ ┌─────▼─────┐
│ Legacy │ │ New MFE │
│ App │ │ │
└─────────┘ └───────────┘
Route by route, migrate legacy → MFE---
Namespacing
CSS
/* Team prefix all classes */
.decide_product-card { }
.decide_product-card__title { }
.inspire_recommendation { }Or use CSS Modules/CSS-in-JS for automatic scoping.
JavaScript
// IIFE to avoid globals
(function() {
// MFE code here
})();
// Or ES modules (naturally scoped)Events
// Namespace custom events
dispatchEvent(new CustomEvent('checkout:item-added', { detail }));
dispatchEvent(new CustomEvent('checkout:payment-started', { detail }));Storage
// Prefix storage keys
localStorage.setItem('decide:recent-views', JSON.stringify(items));
sessionStorage.setItem('checkout:cart-id', cartId);---
Error Handling
Timeouts
// Fragment fetch with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(fragmentUrl, { signal: controller.signal });
clearTimeout(timeout);
return response.text();
} catch (e) {
return '<div class="fallback">Content unavailable</div>';
}Fallbacks
<!-- Server-side with fallback -->
<esi:include src="/header" onerror="continue">
<esi:fallback>
<nav class="minimal-header">...</nav>
</esi:fallback>
</esi:include>Circuit Breaker
class CircuitBreaker {
constructor(threshold = 5, timeout = 30000) {
this.failures = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED';
}
async call(fn) {
if (this.state === 'OPEN') {
throw new Error('Circuit open');
}
try {
const result = await fn();
this.failures = 0;
return result;
} catch (e) {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
setTimeout(() => this.state = 'HALF-OPEN', this.timeout);
}
throw e;
}
}
}---
Performance Considerations
Shared Dependencies
// Module Federation shared config
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
}Bundle Budgets
// Fitness function in CI
const stats = require('./dist/stats.json');
const mainBundle = stats.assets.find(a => a.name.includes('main'));
if (mainBundle.size > 150000) {
console.error('Bundle exceeds 150KB budget!');
process.exit(1);
}Lazy Loading
// Load MFE on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMicroFrontend(entry.target.dataset.mfe);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('[data-mfe]').forEach(el => observer.observe(el));---
Architecture Comparison
| Aspect | Vertical Split | Horizontal Split |
|---|---|---|
| Complexity | Lower | Higher |
| Reusability | Lower | Higher |
| Coordination | Minimal | More required |
| Testing | Simpler | More complex |
| Performance | Better (one MFE) | Overhead (multiple) |
| Team Independence | High | Medium |
Web Performance Optimization
Core Principle
Latency, not bandwidth, is the performance bottleneck. Network round-trips and JavaScript execution time dominate perceived performance.
Key Metrics
Core Web Vitals
| Metric | Good | Needs Work | Poor | Measures |
|---|---|---|---|---|
| LCP | <2.5s | 2.5-4s | >4s | Loading (largest content) |
| INP | <200ms | 200-500ms | >500ms | Interactivity (responsiveness to inputs) |
| CLS | <0.1 | 0.1-0.25 | >0.25 | Visual stability |
Note: FID was an older Core Web Vital and has been replaced by INP. Prefer INP for modern guidance.
Additional Metrics
| Metric | Target | Description |
|---|---|---|
| FCP | <1.8s | First Contentful Paint |
| TTI | <3.8s | Time to Interactive |
| TBT | <200ms | Total Blocking Time |
| TTFB | <600ms | Time to First Byte |
---
Network Optimization
TCP Fundamentals
Three-way handshake: Adds 1 RTT before data transfer Slow start: Connection ramps up gradually
New connection cost:
DNS lookup: ~50ms
TCP handshake: 1 RTT (~50ms)
TLS handshake: 1-2 RTT (~100ms)
First byte: 1 RTT
─────────────────────────
Total: ~200-300ms before first byteOptimization:
- Keep connections alive (HTTP/2)
- Use
preconnectfor known origins - Minimize cross-origin requests
HTTP/2 Benefits
- Multiplexing: Multiple requests over single connection
- Header compression: HPACK reduces redundant headers
- Stream prioritization: Important resources first
Caution: HTTP/2 server push is effectively deprecated in modern browsers. Prefer preload, modulepreload, and server-side hints (e.g., 103 Early Hints) instead.
HTTP/3 (QUIC) Note
HTTP/3 can reduce connection setup latency and mitigate head-of-line blocking at the transport layer. It’s not a silver bullet, but it’s often beneficial on lossy mobile networks.
Resource Hints
<!-- DNS prefetch for future navigations -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- Preconnect for critical third-parties -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<!-- Prefetch for likely next navigations -->
<link rel="prefetch" href="/dashboard.js">
<!-- Modulepreload for ES modules -->
<link rel="modulepreload" href="/app.js">---
JavaScript Optimization
Loading Strategies
<!-- Blocks rendering (avoid) -->
<script src="app.js"></script>
<!-- Downloads in parallel, executes when ready (may break order) -->
<script src="app.js" async></script>
<!-- Downloads in parallel, executes after HTML parsing (maintains order) -->
<script src="app.js" defer></script>
<!-- Best: place at end of body with defer -->
<script src="app.js" defer></script>
</body>Code Splitting
Route-based:
// Only load code for current route
const routes = {
'/': () => import('./pages/Home'),
'/products': () => import('./pages/Products'),
'/cart': () => import('./pages/Cart'),
};Component-based:
// Load heavy components on demand
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</>
);
}Import on Interaction:
button.addEventListener('click', async () => {
const { processData } = await import('./heavy-processor.js');
processData(data);
});Import on Visibility:
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const { renderWidget } = await import('./widget.js');
renderWidget(container);
observer.disconnect();
}
});
observer.observe(container);Tree Shaking
Requirements:
- ES Modules (import/export)
- Webpack/Rollup configured correctly
- Side-effect-free code
// package.json
{
"sideEffects": false,
// or specify files with side effects
"sideEffects": ["*.css", "./src/polyfills.js"]
}// Good: named imports enable tree shaking
import { debounce } from 'lodash-es';
// Bad: imports entire library
import _ from 'lodash';Bundle Size Budgets
| Asset Type | Budget |
|---|---|
| Critical JS | <170KB gzipped |
| Total JS | <300KB gzipped |
| Critical CSS | <14KB |
| Total page weight | <1MB |
---
Rendering Optimization
Critical Rendering Path
1. HTML parsing → DOM construction 2. CSS parsing → CSSOM construction 3. Render tree → DOM + CSSOM 4. Layout → Calculate positions 5. Paint → Draw pixels
Optimization:
- Inline critical CSS
- Defer non-critical CSS
- Avoid render-blocking scripts
- Minimize DOM depth
Critical CSS
<head>
<!-- Inline critical (above-the-fold) CSS -->
<style>
/* Critical styles for initial viewport */
header { ... }
.hero { ... }
</style>
<!-- Load full CSS asynchronously -->
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>Layout Thrashing Prevention
// Bad: forces layout recalculation each iteration
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px'; // Read then write
});
// Good: batch reads, then batch writes
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px';
});---
Image Optimization
Responsive Images
<img
srcset="
image-320.jpg 320w,
image-640.jpg 640w,
image-1280.jpg 1280w
"
sizes="(max-width: 640px) 100vw, 640px"
src="image-640.jpg"
alt="Description"
loading="lazy"
decoding="async"
>Modern Formats
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>Lazy Loading
<!-- Native lazy loading -->
<img src="image.jpg" loading="lazy" alt="...">
<!-- With Intersection Observer for more control -->
<img data-src="image.jpg" class="lazy" alt="...">const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('.lazy').forEach(img => observer.observe(img));---
Caching Strategies
HTTP Caching
# Immutable assets (hashed filenames)
Cache-Control: public, max-age=31536000, immutable
# HTML (always revalidate)
Cache-Control: no-cache
# API responses
Cache-Control: private, max-age=0, must-revalidateService Worker Caching
// Cache-first for static assets
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image' ||
event.request.destination === 'script' ||
event.request.destination === 'style') {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
const clone = response.clone();
caches.open('static-v1').then(cache => cache.put(event.request, clone));
return response;
});
})
);
}
});
// Network-first for API
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone();
caches.open('api-v1').then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
}
});---
React-Specific Optimization
Preventing Re-renders
// Memoize expensive components
const ExpensiveList = memo(function ExpensiveList({ items }) {
return items.map(item => <Item key={item.id} {...item} />);
});
// Stable references for callbacks
const handleClick = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
// Memoize computed values
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);Virtual Lists
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
>
{Row}
</FixedSizeList>
);
}---
Measuring Performance
Lab Tools
- Lighthouse: Chrome DevTools, comprehensive audit
- WebPageTest: Real browsers, multiple locations
- Chrome DevTools Performance: Detailed timeline
Field Tools
- Chrome UX Report (CrUX): Real user data
- web-vitals library: Measure in production
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
// Send to analytics
function sendToAnalytics({ name, delta, id }) {
gtag('event', name, {
event_category: 'Web Vitals',
// CLS is typically a small decimal; scale it for integer metrics.
value: Math.round(name === 'CLS' ? delta * 1000 : delta),
event_label: id,
non_interaction: true,
});
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);---
Performance Checklist
Critical Path
- [ ] Inline critical CSS (<14KB)
- [ ] Defer non-critical CSS
- [ ] Async/defer all JavaScript
- [ ] Preconnect to critical origins
- [ ] Preload critical resources
JavaScript
- [ ] Code split by route
- [ ] Lazy load non-critical components
- [ ] Tree shake unused code
- [ ] Bundle size under budget
- [ ] Avoid large third-party libraries
Images
- [ ] Modern formats (WebP, AVIF)
- [ ] Responsive images with srcset
- [ ] Lazy load below-fold images
- [ ] Properly sized (not scaled down)
Caching
- [ ] Long cache for hashed assets
- [ ] Service worker for offline
- [ ] Proper cache headers
React
- [ ] Memoize expensive components
- [ ] Virtual lists for large datasets
- [ ] Avoid unnecessary re-renders
- [ ] Use production builds
React Patterns & Architecture
Component Patterns
Container/Presentational Pattern
Presentational Components:
- Concerned with how things look
- Receive data via props
- No side effects or state management
- Easy to test and reuse
Container Components:
- Concerned with how things work
- Manage state and side effects
- Pass data to presentational components
// Presentational
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => onDelete(user.id)}>Delete</button>
</li>
))}
</ul>
);
}
// Container
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
const handleDelete = (id) => {
setUsers(users.filter(u => u.id !== id));
};
return <UserList users={users} onDelete={handleDelete} />;
}Modern Take: With hooks, this separation often happens within a single component using custom hooks to extract logic.
---
Compound Components Pattern
Purpose: Create components that work together, sharing implicit state.
function Select({ children, value, onChange }) {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
Select.Option = function Option({ value, children }) {
const { value: selected, onChange } = useContext(SelectContext);
return (
<div
className={selected === value ? 'selected' : ''}
onClick={() => onChange(value)}
>
{children}
</div>
);
};
// Usage - clean, flexible API
<Select value={country} onChange={setCountry}>
<Select.Option value="us">United States</Select.Option>
<Select.Option value="uk">United Kingdom</Select.Option>
<Select.Option value="ca">Canada</Select.Option>
</Select>When to Use:
- Complex components with related parts (Tabs, Accordion, Menu)
- Want flexible, declarative API
- Parent-child components share state
---
Provider Pattern
Purpose: Share data across component tree without prop drilling.
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = useCallback(() => {
setTheme(t => t === 'light' ? 'dark' : 'light');
}, []);
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook with safety check
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be within ThemeProvider');
return context;
}Best Practices:
- Create custom hook for consuming context
- Memoize context value to prevent unnecessary re-renders
- Split contexts by update frequency (auth rarely changes, theme might)
---
Hooks Pattern
Purpose: Extract reusable stateful logic.
// Custom hook for form input
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((e) => {
setValue(e.target.value);
}, []);
const reset = useCallback(() => {
setValue(initialValue);
}, [initialValue]);
return { value, onChange, reset };
}
// Custom hook for async data
function useAsync(asyncFn, deps = []) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
setState(s => ({ ...s, loading: true }));
asyncFn()
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, deps);
return state;
}Rules:
- Name must start with
use - Only call at top level (not in conditions/loops)
- Only call from React functions
---
Render Props Pattern
Purpose: Share code using prop whose value is a function.
function Mouse({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Usage
<Mouse render={({ x, y }) => (
<div>Mouse at: {x}, {y}</div>
)} />Modern Alternative: Custom hooks are usually cleaner:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... same effect logic
return position;
}---
HOC Pattern (Higher-Order Components)
Purpose: Enhance components with additional functionality.
function withAuth(Component) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Redirect to="/login" />;
return <Component {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);When to Prefer Hooks:
- Logic needed in multiple components
- Want to avoid wrapper component overhead
- Need access to multiple data sources
When to Use HOC:
- Need to modify rendered output (wrapping)
- Cross-cutting concerns (auth, logging)
- Working with class components
---
Performance Optimization
Memoization Patterns
React.memo - Component Memoization:
const ExpensiveList = memo(function ExpensiveList({ items }) {
return items.map(item => <ExpensiveItem key={item.id} {...item} />);
});useMemo - Value Memoization:
// Expensive calculation
const sortedItems = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Referential equality for objects passed to memoized children
const filters = useMemo(
() => ({ category, minPrice, maxPrice }),
[category, minPrice, maxPrice]
);useCallback - Function Memoization:
// Stable function reference for memoized children
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // Empty deps - setItems is stableWhen to Memoize:
- Component receives complex objects/arrays as props
- Child component is wrapped in React.memo
- Calculation is expensive (>1ms)
- Function is passed to memoized child
- Value is dependency of other hooks
When NOT to Memoize:
- Primitive props (strings, numbers)
- Component rarely re-renders anyway
- Memoization cost > computation cost
---
Code Splitting Patterns
Route-based Splitting:
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}Component-based Splitting:
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Analytics() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}---
State Management Patterns
State Lifting
// Lift state to nearest common ancestor
function Parent() {
const [shared, setShared] = useState(initialValue);
return (
<>
<ChildA value={shared} onChange={setShared} />
<ChildB value={shared} />
</>
);
}State Colocation
Keep state as close to where it's used as possible:
// Bad - state in parent when only one child needs it
function Parent() {
const [localThing, setLocalThing] = useState('');
return <Child value={localThing} onChange={setLocalThing} />;
}
// Good - state in component that uses it
function Child() {
const [localThing, setLocalThing] = useState('');
return <input value={localThing} onChange={e => setLocalThing(e.target.value)} />;
}Reducer Pattern
function reducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
case 'SET_LOADING':
return { ...state, loading: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function useShoppingCart() {
const [state, dispatch] = useReducer(reducer, { items: [], loading: false });
const addItem = useCallback((item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
}, []);
return { ...state, addItem };
}---
Testing Patterns
Testing Library Philosophy
Test user behavior, not implementation:
// Bad - testing implementation
expect(component.state.isOpen).toBe(true);
// Good - testing behavior
expect(screen.getByRole('dialog')).toBeVisible();Component Testing Pattern
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits form with user data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<ContactForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/message/i), 'Hello!');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
message: 'Hello!'
});
});Hook Testing Pattern
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});---
TypeScript Patterns
Props Typing
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
function Button({ variant = 'primary', size = 'md', loading, children, ...props }: ButtonProps) {
return (
<button className={`btn-${variant} btn-${size}`} disabled={loading} {...props}>
{loading ? <Spinner /> : children}
</button>
);
}Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage with type inference
<List
items={users}
renderItem={user => user.name}
keyExtractor={user => user.id}
/>Hook Typing
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}---
Error Boundaries (Production Resilience)
React error boundaries catch render-time errors in their subtree and allow graceful fallback UI.
Important: Error boundaries must be class components (as of React 18+).
import React from 'react';
export class ErrorBoundary extends React.Component<
{ fallback?: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: unknown) {
// Send to monitoring (Sentry, etc.)
console.error(error);
}
render() {
if (this.state.hasError) return this.props.fallback ?? <p>Something went wrong.</p>;
return this.props.children;
}
}Placement guidance:
- Route/page-level boundaries: prevent a whole app white-screen.
- “Risky” widgets: charts, rich text renderers, third-party embeds.
- Pair with retry/reset logic (reset boundary state when navigation succeeds).
---
Suspense (Code Splitting + Async Boundaries)
Code splitting (stable, common):
import { lazy, Suspense } from 'react';
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
function AppRoutes() {
return (
<Suspense fallback={<p>Loading…</p>}>
<SettingsPage />
</Suspense>
);
}Boundary guidance:
- Prefer multiple small boundaries over one giant boundary.
- Put fallbacks where users “expect” waiting (page region vs whole screen).
- Avoid nesting boundaries that constantly remount (it can lose state).
---
Concurrency Patterns (Responsiveness)
Use transitions to keep input responsive while expensive updates happen.
import { useTransition } from 'react';
function Search({ onQuery }) {
const [isPending, startTransition] = useTransition();
return (
<div>
<input
onChange={(e) => {
const q = e.target.value;
startTransition(() => onQuery(q));
}}
/>
{isPending && <span>Searching…</span>}
</div>
);
}When to use:
- Large lists/tables where filtering/sorting is expensive
- Navigations that trigger large renders
- Typing into inputs that updates multiple components
Rendering Strategies
Overview
How and where HTML is generated significantly impacts performance, SEO, and user experience. Modern frameworks often combine multiple strategies.
Strategy Comparison
| Strategy | Generation | SEO | TTI | Use Case |
|---|---|---|---|---|
| CSR | Client | Poor | Slow | Apps behind auth |
| SSR | Server/request | Good | Medium | Dynamic, personalized |
| SSG | Build time | Good | Fast | Static content |
| ISR | Build + revalidate | Good | Fast | Semi-static |
| RSC | Server (component) | Good | Fast | Mixed static/dynamic |
---
Client-Side Rendering (CSR)
How It Works
1. Server sends minimal HTML shell 2. Browser downloads JavaScript bundle 3. JavaScript renders content 4. Data fetched after initial render
<!-- Server response -->
<!DOCTYPE html>
<html>
<head>
<script src="app.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>// app.js renders everything
ReactDOM.createRoot(document.getElementById('root')).render(<App />);Pros
- Rich interactivity
- Clear client/server separation
- Simple deployment (static files)
- Good for app-like experiences
Cons
- Poor SEO (crawlers may not execute JS)
- Slow initial paint (wait for JS)
- Large JavaScript bundles
- Performance varies by device
When to Use
- Authenticated applications (dashboards, admin)
- Highly interactive tools
- When SEO doesn't matter
- Internal applications
Optimization
- Code splitting by route
- Lazy load non-critical components
- Service worker for caching
- Skeleton screens during load
---
Server-Side Rendering (SSR)
How It Works
1. Server renders full HTML on each request 2. HTML sent to browser (fast FCP) 3. JavaScript "hydrates" the page (makes it interactive)
// Next.js pages router
export async function getServerSideProps(context) {
const data = await fetchData(context.params.id);
return { props: { data } };
}
export default function Page({ data }) {
return <div>{data.title}</div>;
}// Next.js app router (default server component)
async function Page({ params }) {
const data = await fetchData(params.id);
return <div>{data.title}</div>;
}Pros
- Good SEO (full HTML for crawlers)
- Faster First Contentful Paint
- Works without JavaScript (progressive enhancement)
- Dynamic, personalized content
Cons
- Higher server load
- Slower Time to First Byte
- Full page generated per request
- Hydration overhead
When to Use
- SEO-critical dynamic pages
- Personalized content
- Frequently changing data
- E-commerce product pages
Optimization
- Streaming SSR (send HTML in chunks)
- Selective hydration
- Cache where possible
- Edge rendering (closer to users)
---
Static Site Generation (SSG)
How It Works
1. Pages generated at build time 2. HTML files served from CDN 3. Optional client-side hydration
// Next.js pages router
export async function getStaticProps() {
const posts = await fetchPosts();
return { props: { posts } };
}
export async function getStaticPaths() {
const posts = await fetchPosts();
return {
paths: posts.map(post => ({ params: { id: post.id } })),
fallback: false
};
}Pros
- Fastest Time to First Byte
- Excellent SEO
- Cheap hosting (static files)
- Global CDN distribution
- High reliability
Cons
- Build time increases with pages
- Stale content until rebuild
- Not suitable for dynamic content
- Long builds for large sites
When to Use
- Marketing pages
- Documentation
- Blogs
- Product catalogs (stable inventory)
Optimization
- Incremental builds (only changed pages)
- On-demand revalidation
- Hybrid with SSR for dynamic sections
---
Incremental Static Regeneration (ISR)
How It Works
Combines SSG with periodic background regeneration:
1. Page generated at build time 2. Served from cache 3. After revalidation period, regenerated in background 4. Next visitor gets fresh page
// Next.js pages router
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 60 // Regenerate every 60 seconds
};
}// Next.js app router
async function Page() {
const data = await fetchData();
return <div>{data}</div>;
}
export const revalidate = 60; // Page-level revalidationOn-Demand Revalidation
// API route to trigger revalidation
export default async function handler(req, res) {
await res.revalidate('/products');
return res.json({ revalidated: true });
}Pros
- Static performance
- Content stays fresh
- Scales to millions of pages
- No full rebuild needed
Cons
- Some staleness acceptable
- Complexity in revalidation logic
- First visitor after expiry waits
When to Use
- Product pages (inventory updates)
- User profiles (infrequent changes)
- News sites
- Large catalogs
---
React Server Components (RSC)
How It Works
Components render on server, sending serialized output (not HTML) to client:
1. Server components run only on server 2. Can directly access databases, file system 3. Zero JavaScript sent for server components 4. Client components hydrate as needed
// Server Component (default in app router)
async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</ul>
);
}
// Client Component
'use client';
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await addToCart(productId);
setLoading(false);
}
return <button onClick={handleClick} disabled={loading}>Add to Cart</button>;
}Pros
- Zero-bundle server components
- Direct data access (no API layer)
- Automatic code splitting
- Better performance
- Simpler data fetching
Cons
- New mental model
- Can't use hooks in server components
- Framework-specific (Next.js, etc.)
- Ecosystem still maturing
When to Use
- Data-heavy pages
- Want minimal client JavaScript
- Using Next.js 13+ App Router
- Complex data fetching needs
Patterns
Container/Presentational with RSC:
// Server container
async function ProductPage({ id }) {
const product = await fetchProduct(id);
return <ProductDetails product={product} />;
}
// Client presentational
'use client';
function ProductDetails({ product }) {
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} />
</div>
);
}---
Islands Architecture
How It Works
Static HTML with interactive "islands" that hydrate independently:
1. Most content is static HTML 2. Interactive components marked as islands 3. Each island hydrates independently 4. Minimal JavaScript shipped
---
// Astro example
import Header from './Header.astro'; // Static
import SearchBar from './SearchBar.tsx'; // Interactive island
---
<html>
<body>
<Header /> <!-- No JS -->
<SearchBar client:visible /> <!-- Hydrates when visible -->
<main>{staticContent}</main>
</body>
</html>Pros
- Excellent performance
- Progressive enhancement
- Minimal JavaScript
- Independent hydration
Cons
- Different development model
- Limited framework options
- Complexity for highly interactive apps
When to Use
- Content-heavy sites with some interactivity
- Marketing sites
- Documentation
- Blogs with interactive widgets
---
Streaming SSR
How It Works
Send HTML in chunks as it's generated:
// React 18 streaming
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
});
});// With Suspense boundaries
function Page() {
return (
<Layout>
<Header /> {/* Sent immediately */}
<Suspense fallback={<Spinner />}>
<SlowComponent /> {/* Streamed when ready */}
</Suspense>
</Layout>
);
}Pros
- Faster Time to First Byte
- Progressive rendering
- Better perceived performance
- Prioritize critical content
Cons
- More complex setup
- Not all hosting supports streaming
- Error handling complexity
---
Decision Guide
Is content mostly static?
├─ Yes → Does it change frequently?
│ ├─ No → SSG
│ └─ Yes → ISR
│
└─ No → Is it personalized/user-specific?
├─ Yes → Is SEO critical?
│ ├─ Yes → SSR
│ └─ No → CSR (app behind auth)
│
└─ No → Is it highly interactive?
├─ Yes → CSR or Islands
└─ No → SSR or RSCHybrid Approaches
Modern frameworks support mixing strategies:
// Next.js: Different strategies per route
// pages/index.js - SSG
export async function getStaticProps() { }
// pages/dashboard.js - CSR (no data fetching)
export default function Dashboard() {
const { data } = useSWR('/api/user');
}
// pages/products/[id].js - ISR
export async function getStaticProps() {
return { props: {}, revalidate: 60 };
}
// pages/cart.js - SSR
export async function getServerSideProps() { }Performance Comparison
| Metric | CSR | SSR | SSG | ISR | RSC |
|---|---|---|---|---|---|
| TTFB | Fast | Slow | Fastest | Fast | Medium |
| FCP | Slow | Fast | Fastest | Fast | Fast |
| TTI | Slow | Medium | Fast | Fast | Fast |
| JS Size | Large | Medium | Small | Small | Smallest |
| Server Load | None | High | None | Low | Medium |
Security & Authentication for Web Apps
Security Posture (Defaults)
Assume:
- XSS is your #1 frontend risk (it turns into account takeover if tokens are stealable).
- Security is a system property: frontend + backend + infrastructure.
- Client-side “route guards” are UX, not authorization. Enforce authorization on the server/API.
Auth Strategies (Choose Intentionally)
1) Cookie-Based Sessions (Often Best for Browsers)
How it works: server sets a session cookie; browser sends it automatically.
Recommended cookie flags:
HttpOnly(prevents JS access)Secure(HTTPS only)SameSite=Lax(orStrictif feasible;NonerequiresSecure)
Pros: resistant to token theft via XSS (session id not readable by JS), simple browser ergonomics. Cons: you must handle CSRF when cookies are ambient authority.
2) Token-Based (SPA + API)
If you must use bearer tokens:
- Prefer access token in memory (short-lived) + refresh token in httpOnly cookie.
- Avoid long-lived bearer tokens stored in
localStorageby default.
Pros: simple API auth, works well across services. Cons: bearer tokens are high-value; storage choices matter.
3) OAuth/OIDC (Public Clients)
For SPAs using OAuth/OIDC:
- Prefer Authorization Code + PKCE.
- Avoid the legacy “implicit flow”.
CSRF (If Using Cookies)
Mitigations (combine):
SameSite=Lax/Strictcookies- CSRF token pattern (synchronizer token or double-submit)
- Require
Origin/Refererchecks for state-changing requests - Use idempotent semantics correctly (GET must not mutate)
XSS (Prevent + Limit Blast Radius)
Prevention
- Treat all user content as untrusted.
- Avoid injecting raw HTML (
dangerouslySetInnerHTML) unless strictly necessary. - Sanitize user-generated HTML on the server (and again on the client if needed).
- Never build HTML with string concatenation from untrusted values.
Blast Radius Reduction
- Adopt a strong Content Security Policy (CSP) (ideally without
unsafe-inline). - Consider Trusted Types to prevent DOM XSS sinks in large apps.
- Minimize third-party scripts; prefer self-hosting.
Common Web Security Headers (Baseline)
Server-side headers to consider:
Content-Security-Policy(script/style/img/connect/frame directives; addframe-ancestorsas needed)Strict-Transport-Security(HSTS)X-Content-Type-Options: nosniffReferrer-PolicyPermissions-Policy
Frontend Data Handling Rules of Thumb
- Don’t put secrets in the browser (API keys, private tokens).
- Be careful with PII in URLs (URLs leak via logs, referrers, screenshots).
- Keep auth state changes observable and reversible (logout clears caches, tabs, and storage).
- Prefer a BFF (Backend-for-Frontend) when it reduces CORS complexity, hides secrets, or simplifies aggregation.
Minimal Auth/Session Checklist
- [ ] Auth enforced on server/API (not just client routing)
- [ ] Tokens not stored in
localStorageby default - [ ] CSP in place (and tested)
- [ ] CSRF mitigations if using cookies
- [ ] Logout clears relevant caches/state
- [ ] Error monitoring configured without leaking secrets/PII
SPA Architecture Fundamentals
What is an SPA?
A Single Page Application is delivered to the browser and doesn't reload during use. The entire application runs as one web page with the shell loaded once. Navigation occurs through view swapping, not page refreshes.
Paradigm Shift from MPA
| Aspect | Multi-Page App | Single Page App |
|---|---|---|
| Initial Load | Complete HTML per request | Shell + assets once |
| Navigation | Full page refresh | View swap, no refresh |
| Presentation Logic | Server | Client |
| HTML Generation | Server-side | Client-side |
| Data Transfer | HTML documents | JSON payloads |
| Server Role | Everything | Auth, validation, data API |
Core Components
The Shell
The master controller providing structure:
- Loads initially with core styles and scripts
- Manages feature containers
- Handles application-wide state
- Coordinates module loading/unloading
- Controls routing decisions
// Shell structure example
const Shell = {
init() {
this.initRouter();
this.initEventBus();
this.loadInitialModules();
},
loadModule(name) {
import(`./modules/${name}.js`).then(module => {
module.init(this.container);
});
},
unloadModule(name) {
this.modules[name]?.destroy();
}
};Feature Modules
Independent, loosely-coupled units:
- Self-contained functionality (chat, navigation, etc.)
- Attached to Shell via defined APIs
- Own lifecycle management
- Communicate through events or shared services
// Module structure
const ChatModule = {
init(container) {
this.container = container;
this.bindEvents();
this.render();
},
destroy() {
this.unbindEvents();
this.container.innerHTML = '';
},
bindEvents() {
this.eventBus.on('user:login', this.onUserLogin);
}
};Views
HTML fragments dynamically created:
- Not complete pages
- Swapped in/out by router
- May have associated controllers
- Rendered from templates + data
Models
Data representation with business logic:
- Validation rules
- Computed properties
- Serialization/deserialization
- Server sync methods
---
Routing
Client-Side Routing
Router controls browser navigation without server round-trips:
Hash-based Routing:
// Uses fragment identifier (#)
// URL: example.com/#/users/123
window.addEventListener('hashchange', () => {
const route = window.location.hash.slice(1);
router.navigate(route);
});History API Routing:
// Uses pushState/replaceState
// URL: example.com/users/123
history.pushState({ page: 'users' }, '', '/users/123');
window.addEventListener('popstate', (e) => {
router.navigate(window.location.pathname);
});Route Configuration
const routes = [
{ path: '/', component: Home },
{ path: '/users', component: UserList },
{ path: '/users/:id', component: UserDetail },
{ path: '/users/:id/edit', component: UserEdit },
{ path: '*', component: NotFound } // Default/catch-all
];The Anchor Interface Pattern
URI anchor drives bookmark-able state:
1. History event changes anchor 2. Anchor change triggers state change 3. Single code path for all bookmark-able states 4. Supports Forward/Back, bookmarks, sharing
// State encoded in URL
// example.com/#/products?category=shoes&sort=price
function onAnchorChange() {
const anchor = parseAnchor(location.hash);
const proposedState = anchorToState(anchor);
if (isValidTransition(currentState, proposedState)) {
applyState(proposedState);
currentState = proposedState;
} else {
restoreAnchor(currentState); // Reject invalid change
}
}---
Module Organization
Module Pattern Benefits
- Namespace isolation: Prevents collisions
- Privacy: Internal implementation hidden
- Public API: Clear contract for consumers
- Encapsulation: Change internals without breaking consumers
IIFE Module Pattern (Legacy)
const MyModule = (function() {
// Private
let privateVar = 0;
function privateMethod() {
return privateVar++;
}
// Public API
return {
increment() {
return privateMethod();
},
getValue() {
return privateVar;
}
};
})();ES Module Pattern (Modern)
// myModule.js
let privateVar = 0;
function privateMethod() {
return privateVar++;
}
export function increment() {
return privateMethod();
}
export function getValue() {
return privateVar;
}File Organization
By Feature (Recommended for SPAs):
/src
/features
/auth
/components
/hooks
/services
index.js
/products
/components
/hooks
/services
index.js
/shared
/components
/hooks
/utilsBy Type (Traditional):
/src
/components
/hooks
/services
/utils
/pages---
State Management
State Categories
Bookmark-able State:
- Stored in URI (hash or path)
- Survives refresh
- Shareable via URL
- Examples: current page, filters, search query
Session State:
- Stored in sessionStorage or memory
- Lost on tab close
- Examples: form drafts, UI preferences
Persistent State:
- Stored in localStorage or database
- Survives browser close
- Examples: user preferences, tokens
State Flow
User Action
↓
Update Anchor/URL
↓
hashchange/popstate Event
↓
Parse New State
↓
Validate Transition
↓
Update Model → Notify View
↓
Re-render Affected ComponentsState Management Requirements
1. Browser history works: Forward/Back functional 2. Bookmarks restore state: URL captures app state 3. Partial updates: Only changed parts re-render 4. Predictable updates: Single source of truth
---
Client-Server Communication
Data Layer Responsibilities
- Abstract HTTP communication
- Handle request/response serialization
- Manage authentication headers
- Provide error handling patterns
- Cache responses when appropriate
RESTful API Consumption
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(method, path, data) {
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: data ? JSON.stringify(data) : undefined
});
if (!response.ok) {
throw new ApiError(response.status, await response.json());
}
return response.json();
}
get(path) { return this.request('GET', path); }
post(path, data) { return this.request('POST', path, data); }
put(path, data) { return this.request('PUT', path, data); }
delete(path) { return this.request('DELETE', path); }
}BFF Pattern (Backend for Frontend)
Dedicated backend per frontend type:
Mobile App → Mobile BFF →
→ Microservices
Web App → Web BFF →Benefits:
- Optimized payloads per client
- Aggregation of multiple services
- Client-specific transformations
- Reduces client complexity
---
Event-Driven Communication
Pub/Sub for Module Communication
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
(this.events[event] ||= []).push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
this.events[event] = this.events[event]?.filter(cb => cb !== callback);
}
emit(event, data) {
this.events[event]?.forEach(cb => cb(data));
}
}
// Usage
eventBus.on('cart:updated', (cart) => {
updateCartIcon(cart.itemCount);
});
eventBus.emit('cart:updated', { itemCount: 3, total: 99.99 });Benefits
- Modules don't know about each other
- Easy to add new subscribers
- Testable in isolation
- Loose coupling
Considerations
- Can make data flow hard to trace
- No type safety without extra tooling
- Memory leaks if subscriptions not cleaned up
---
SPA Trade-offs
Advantages
| Benefit | Description |
|---|---|
| Responsiveness | No page reloads, instant feedback |
| Reduced Server Load | Static assets cached, only data transferred |
| Rich Interactions | Desktop-app-like experience |
| Offline Capable | Service workers enable offline mode |
| Separation of Concerns | Clear frontend/backend boundary |
Disadvantages
| Challenge | Mitigation |
|---|---|
| Initial Load | Code splitting, lazy loading |
| SEO | SSR/SSG, prerendering |
| JavaScript Dependency | Progressive enhancement, SSR fallback |
| Memory Leaks | Proper cleanup, monitoring |
| Browser History | Proper router implementation |
| Bundle Size | Tree shaking, dynamic imports |
---
When to Choose SPA
Good Fit:
- App-like experience required
- Heavy user interaction
- Real-time features (chat, collaboration)
- Behind authentication (SEO less critical)
- Complex client-side state
Poor Fit:
- Content-heavy sites (blogs, news)
- SEO-critical public pages
- Simple forms or informational pages
- Users on very slow connections
- Extremely constrained devices/networks and no budget to invest in performance + accessibility engineering
Accessibility Considerations (SPA-Specific)
SPAs can be fully accessible, but you must handle a few things explicitly:
- Focus management on navigation: move focus to the new page’s main heading or main landmark after route changes.
- Route change announcements: announce page title/heading changes (e.g.,
aria-live) so screen reader users understand navigation occurred. - Skip links + landmarks: keep a working “Skip to content” link and stable landmarks (
<header>,<nav>,<main>,<footer>). - Dialog/menu patterns: use established WAI-ARIA patterns for modals, menus, comboboxes, and avoid “div soup”.
Hybrid Approach: Many modern frameworks (Next.js, Remix, Nuxt) allow mixing:
- SSR for public, SEO-critical pages
- SPA behavior for authenticated sections
- Best of both worlds
State Management Patterns
State Categories
| Type | Scope | Storage | Examples |
|---|---|---|---|
| Local | Single component | useState/useReducer | Form inputs, toggles |
| Shared | Component subtree | Lifted state, Context | Filters, selections |
| Global | Entire app | Store (Redux, Zustand) | User, theme, cart |
| Server | Remote data | React Query, SWR | API responses |
| URL | Navigation | Router | Page, filters, search |
---
Local State
useState
function Counter() {
const [count, setCount] = useState(0);
// Updater function for state based on previous
const increment = () => setCount(prev => prev + 1);
return <button onClick={increment}>{count}</button>;
}useReducer
For complex state with multiple sub-values:
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
/>
</>
);
}When to use useReducer:
- Next state depends on previous
- Multiple sub-values
- Complex update logic
- Want to test reducer separately
---
Shared State
Lifting State Up
Share state via nearest common ancestor:
function Parent() {
const [selected, setSelected] = useState(null);
return (
<>
<List items={items} onSelect={setSelected} />
<Detail item={selected} />
</>
);
}React Context
For state that many components need:
// Create context
const AuthContext = createContext(null);
// Provider component
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth().then(user => {
setUser(user);
setLoading(false);
});
}, []);
const login = async (credentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => {
authService.logout();
setUser(null);
};
const value = useMemo(
() => ({ user, loading, login, logout }),
[user, loading]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}Context Best Practices:
- Split contexts by update frequency
- Memoize context value
- Create custom hooks for consumption
- Don't overuse (prop drilling is fine for 2-3 levels)
---
Global State Libraries
Zustand (Minimal)
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));
// Usage
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}With persistence:
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'app-storage' }
)
);Pros: Minimal boilerplate, no provider needed, good performance Cons: Less structure for large apps, fewer dev tools
Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
incrementBy: (state, action) => { state.value += action.payload; },
},
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
// Usage
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
{count}
</button>
);
}Pros: Time-travel debugging, middleware, large ecosystem Cons: More boilerplate, learning curve
Jotai (Atomic)
import { atom, useAtom } from 'jotai';
// Primitive atom
const countAtom = atom(0);
// Derived atom
const doubledAtom = atom((get) => get(countAtom) * 2);
// Writable derived atom
const countryAtom = atom('us');
const currencyAtom = atom(
(get) => currencies[get(countryAtom)],
(get, set, newCurrency) => {
set(countryAtom, Object.entries(currencies).find(([, c]) => c === newCurrency)?.[0]);
}
);
// Usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Pros: Atomic model, minimal re-renders, React-like API Cons: Different mental model, smaller ecosystem
XState (State Machines)
import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: { TOGGLE: 'active' }
},
active: {
entry: assign({ count: (ctx) => ctx.count + 1 }),
on: { TOGGLE: 'inactive' }
}
}
});
function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.value} (toggled {state.context.count} times)
</button>
);
}Pros: Explicit states prevent impossible states, visual diagrams Cons: Learning curve, overkill for simple state
---
Server State
React Query / TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetching
function Products() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <ProductList products={data} />;
}
// Mutations
function AddProduct() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
mutation.mutate(new FormData(e.target));
}}>
{/* form fields */}
</form>
);
}Features:
- Automatic caching
- Background refetching
- Optimistic updates
- Pagination support
- Offline support
SWR
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <div>Hello, {data.name}</div>;
}Simpler than React Query, good for read-heavy apps.
---
URL State
React Router
import { useSearchParams } from 'react-router-dom';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'name';
const updateFilters = (key, value) => {
setSearchParams(prev => {
prev.set(key, value);
return prev;
});
};
return (
<>
<select
value={category}
onChange={(e) => updateFilters('category', e.target.value)}
>
<option value="all">All</option>
<option value="shoes">Shoes</option>
</select>
{/* Products filtered by URL params */}
</>
);
}Benefits of URL State:
- Shareable links
- Browser history works
- Server can read initial state
- Bookmarkable
---
State Management Selection Guide
What kind of state?
│
├─ Server data (API responses)
│ └─ React Query or SWR
│
├─ UI state (modals, dropdowns)
│ └─ Local useState
│
├─ Form state
│ └─ Local state or React Hook Form
│
├─ Shared between few components
│ └─ Lift state up
│
├─ Used by many components
│ ├─ Changes frequently → Zustand/Redux
│ └─ Changes rarely → Context
│
├─ Complex flows with explicit states
│ └─ XState
│
└─ Should persist in URL
└─ URL/Search params---
Patterns Comparison
| Library | Boilerplate | Learning Curve | DevTools | Best For |
|---|---|---|---|---|
| Context | Low | Low | React DevTools | Low-frequency global |
| Zustand | Minimal | Low | Yes | Simple global |
| Redux TK | Medium | Medium | Excellent | Complex, large apps |
| Jotai | Minimal | Medium | Yes | Atomic state |
| XState | High | High | Excellent | Complex flows |
| React Query | Low | Medium | Excellent | Server state |
---
Anti-patterns
| Anti-pattern | Problem | Solution |
|---|---|---|
| Global for local | Unnecessary re-renders | Keep state local |
| Prop drilling 10 levels | Maintenance nightmare | Context or state lib |
| Storing derived data | Out of sync | Compute from source |
| Mixing server/client state | Complex sync | React Query for server |
| Mutating state directly | Bugs, no re-render | Immutable updates |
| Giant store objects | Performance issues | Split into slices |
Testing & Quality Strategy for Web Apps
Guiding Principles
- Prefer tests that validate user-visible behavior, not implementation details.
- Put tests at the lowest level that proves the behavior (fast feedback).
- Make “quality” measurable with CI gates (types, lint, tests, budgets).
Test Layers (Practical Pyramid)
1. Static checks (fastest)
- Typecheck (TypeScript)
- Lint (ESLint)
- Formatting (Prettier)
2. Unit tests
- Pure functions (formatters, reducers, domain logic)
3. Component tests
- Render components with Testing Library
- Mock network with MSW
4. Integration tests
- Real router + data layer + key flows
5. E2E tests (few, high value)
- Critical flows: auth, purchase, onboarding, permissions
Tooling (Common Choices)
- Unit/component: Vitest or Jest + Testing Library
- Network mocking: MSW
- E2E: Playwright (preferred) or Cypress
- a11y: axe-core (component/E2E checks), Lighthouse CI (budgets)
- Visual regression (optional): Storybook + Chromatic or Playwright snapshots
CI Quality Gates (Good Defaults)
typecheckmust passlintmust pass- unit/component tests must pass
- E2E smoke suite on main branch and before deploy
- Bundle size budgets (fail build if exceeded)
- Lighthouse CI thresholds for key routes (especially public/SEO routes)
Test Data & Determinism
- Use factories/fixtures for consistent data.
- Avoid time-dependent tests (freeze time).
- Avoid relying on real networks or shared environments in CI.
Contract Testing (When Frontend/Backend Move Independently)
Use contract tests when you have:
- multiple teams changing API + UI in parallel
- frequent API evolution
- BFF/GraphQL schema changes
Approaches:
- Schema-driven (OpenAPI/GraphQL schema validation)
- Consumer-driven contracts (Pact-style)
“Definition of Done” (Frontend)
- [ ] Loading, empty, and error states implemented
- [ ] Accessibility checked for critical flows
- [ ] Error boundary strategy exists (no white-screens)
- [ ] Performance budget not regressed
- [ ] Observability hooks in place (errors + key events)
Tooling, Delivery, and Production Readiness
Build Tooling (Pick the Simplest That Fits)
SPA Builds
- Vite is a strong default for CSR SPAs (fast dev server, modern bundling).
- Ensure code splitting is aligned to your router and large feature boundaries.
Hybrid/SSR Builds
- Use a framework when SSR/SSG/ISR/RSC is a requirement (routing, loaders, caching, streaming are hard to hand-roll).
- Decide whether rendering happens on origin servers vs edge runtime.
Module Boundaries That Scale
Prefer feature/domain folders with stable public APIs:
features/<domain>/components|hooks|services|routesshared/<components|hooks|utils>- Avoid “god”
utils/dumping grounds; create small, owned libraries.
Environments and Configuration
Make config explicit:
- Build-time values (public) vs runtime values (server-only).
- Don’t ship secrets to the browser.
- Prefer a single config module that validates required variables at startup.
Deployment Basics
Static Assets
- Content-hashed filenames + long-lived caching (
immutable). - HTML should revalidate frequently (or be cached with strategy per route).
Rollouts
- Use canaries/feature flags for risky launches.
- Have fast rollback (previous artifact still deployable).
Observability (You’ll Want This Later)
Baseline:
- Error monitoring (e.g., Sentry) with release tagging and sourcemaps
- Product analytics events for key funnels (privacy-aware)
- Web Vitals/RUM collection (LCP/INP/CLS) by route and device class
Feature Flags and Experiments
Use flags when:
- You need safe incremental rollout
- You want A/B testing
- You’re migrating legacy to new routes/components
Rules:
- Flags must have owners and cleanup dates
- Don’t let flags leak into every component; centralize decision points
Delivery Checklist
- [ ] Source maps uploaded to error monitoring
- [ ] Cache headers correct for hashed assets
- [ ] Runtime config validated (no “undefined” at runtime)
- [ ] CI gates enforced (tests + budgets)
- [ ] Rollback plan documented