mirror of
https://github.com/wshobson/agents
synced 2026-06-21 14:13:58 +00:00
refactor: complete SKILL.md trimming for JS/frontend/docs skills
This commit is contained in:
@@ -487,380 +487,20 @@ describe("OrderService", () => {
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### Pattern 1: API Integration Tests
|
||||
Integration tests verify real database operations and HTTP endpoints using `supertest` and a test database instance. Always truncate tables in `beforeEach` and tear down in `afterAll`.
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.api.test.ts
|
||||
import request from "supertest";
|
||||
import { app } from "../../src/app";
|
||||
import { pool } from "../../src/config/database";
|
||||
|
||||
describe("User API Integration Tests", () => {
|
||||
beforeAll(async () => {
|
||||
// Setup test database
|
||||
await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear data before each test
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
describe("POST /api/users", () => {
|
||||
it("should create a new user", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
});
|
||||
expect(response.body).toHaveProperty("id");
|
||||
expect(response.body).not.toHaveProperty("password");
|
||||
});
|
||||
|
||||
it("should return 400 if email is invalid", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "invalid-email",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty("error");
|
||||
});
|
||||
|
||||
it("should return 409 if email already exists", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
await request(app).post("/api/users").send(userData);
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(409);
|
||||
|
||||
expect(response.body.error).toContain("already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/users/:id", () => {
|
||||
it("should get user by id", async () => {
|
||||
const createResponse = await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const userId = createResponse.body.id;
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/users/${userId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: userId,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 404 if user not found", async () => {
|
||||
await request(app).get("/api/users/999").expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should require authentication for protected routes", async () => {
|
||||
await request(app).get("/api/users/me").expect(401);
|
||||
});
|
||||
|
||||
it("should allow access with valid token", async () => {
|
||||
// Create user and login
|
||||
await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const loginResponse = await request(app).post("/api/auth/login").send({
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const token = loginResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/users/me")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.email).toBe("john@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Database Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.repository.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "../../src/repositories/user.repository";
|
||||
|
||||
describe("UserRepository Integration Tests", () => {
|
||||
let pool: Pool;
|
||||
let repository: UserRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
pool = new Pool({
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "test_db",
|
||||
user: "test_user",
|
||||
password: "test_password",
|
||||
});
|
||||
|
||||
repository = new UserRepository(pool);
|
||||
|
||||
// Create tables
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
it("should create a user", async () => {
|
||||
const user = await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
expect(user).toHaveProperty("id");
|
||||
expect(user.name).toBe("John Doe");
|
||||
expect(user.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should find user by email", async () => {
|
||||
await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
const user = await repository.findByEmail("john@example.com");
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user?.name).toBe("John Doe");
|
||||
});
|
||||
|
||||
it("should return null if user not found", async () => {
|
||||
const user = await repository.findByEmail("nonexistent@example.com");
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
For full API integration test examples (supertest + PostgreSQL) and database repository integration tests, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
### Pattern 1: React Component Testing
|
||||
Test React components by rendering them and querying by role, placeholder, or test ID. Test hooks with `renderHook` + `act`. Prefer semantic queries (`getByRole`, `getByPlaceholderText`) over `data-testid`.
|
||||
|
||||
```typescript
|
||||
// components/UserForm.tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onSubmit: (user: { name: string; email: string }) => void;
|
||||
}
|
||||
|
||||
export function UserForm({ onSubmit }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ name, email });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// components/UserForm.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { UserForm } from './UserForm';
|
||||
|
||||
describe('UserForm', () => {
|
||||
it('should render form inputs', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should update input values', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
|
||||
const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } });
|
||||
fireEvent.change(emailInput, { target: { value: 'john@example.com' } });
|
||||
|
||||
expect(nameInput.value).toBe('John Doe');
|
||||
expect(emailInput.value).toBe('john@example.com');
|
||||
});
|
||||
|
||||
it('should call onSubmit with form data', () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<UserForm onSubmit={onSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('name-input'), {
|
||||
target: { value: 'John Doe' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('email-input'), {
|
||||
target: { value: 'john@example.com' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Hooks
|
||||
|
||||
```typescript
|
||||
// hooks/useCounter.ts
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const [count, setCount] = useState(initialValue);
|
||||
|
||||
const increment = useCallback(() => setCount((c) => c + 1), []);
|
||||
const decrement = useCallback(() => setCount((c) => c - 1), []);
|
||||
const reset = useCallback(() => setCount(initialValue), [initialValue]);
|
||||
|
||||
return { count, increment, decrement, reset };
|
||||
}
|
||||
|
||||
// hooks/useCounter.test.ts
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useCounter } from "./useCounter";
|
||||
|
||||
describe("useCounter", () => {
|
||||
it("should initialize with default value", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it("should initialize with custom value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
|
||||
it("should increment count", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it("should decrement count", () => {
|
||||
const { result } = renderHook(() => useCounter(5));
|
||||
|
||||
act(() => {
|
||||
result.current.decrement();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(4);
|
||||
});
|
||||
|
||||
it("should reset to initial value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(12);
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
});
|
||||
```
|
||||
For complete React component test examples (UserForm, hooks with `renderHook`/`act`), see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
Use `@faker-js/faker` to generate realistic test data factories. Factories accept optional `overrides` so tests can set only the fields they care about:
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from "@faker-js/faker";
|
||||
@@ -874,71 +514,9 @@ export function createUserFixture(overrides?: Partial<User>): User {
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createUsersFixture(count: number): User[] {
|
||||
return Array.from({ length: count }, () => createUserFixture());
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
import {
|
||||
createUserFixture,
|
||||
createUsersFixture,
|
||||
} from "../fixtures/user.fixture";
|
||||
|
||||
describe("UserService", () => {
|
||||
it("should process user", () => {
|
||||
const user = createUserFixture({ name: "John Doe" });
|
||||
// Use user in test
|
||||
});
|
||||
|
||||
it("should handle multiple users", () => {
|
||||
const users = createUsersFixture(10);
|
||||
// Use users in test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Snapshot Testing
|
||||
|
||||
```typescript
|
||||
// components/UserCard.test.tsx
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserCard } from './UserCard';
|
||||
|
||||
describe('UserCard', () => {
|
||||
it('should match snapshot', () => {
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const { container } = render(<UserCard user={user} />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with loading state', () => {
|
||||
const { container } = render(<UserCard loading />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Coverage Reports
|
||||
|
||||
```typescript
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:ui": "vitest --ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
For snapshot testing, coverage configuration, test organization patterns, promise testing, and timer mocking, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -957,58 +535,3 @@ describe('UserCard', () => {
|
||||
13. **Test error handling**: Not just success cases
|
||||
14. **Use data-testid sparingly**: Prefer semantic queries
|
||||
15. **Clean up after tests**: Prevent test pollution
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
describe("UserService", () => {
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", () => {});
|
||||
it("should throw error if email exists", () => {});
|
||||
it("should hash password", () => {});
|
||||
});
|
||||
|
||||
describe("updateUser", () => {
|
||||
it("should update user", () => {});
|
||||
it("should throw error if not found", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Promises
|
||||
|
||||
```typescript
|
||||
// Using async/await
|
||||
it("should fetch user", async () => {
|
||||
const user = await service.fetchUser("1");
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
// Testing rejections
|
||||
it("should throw error", async () => {
|
||||
await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Timers
|
||||
|
||||
```typescript
|
||||
import { vi } from "vitest";
|
||||
|
||||
it("should call function after delay", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
setTimeout(callback, 1000);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
```
|
||||
|
||||
@@ -418,476 +418,61 @@ async function withTimeout(promise, ms) {
|
||||
|
||||
## Functional Programming Patterns
|
||||
|
||||
### 1. Array Methods
|
||||
Functional programming in JavaScript centers on pure functions, immutability, and composable transformations.
|
||||
|
||||
**Map, Filter, Reduce:**
|
||||
|
||||
```javascript
|
||||
const users = [
|
||||
{ id: 1, name: "John", age: 30, active: true },
|
||||
{ id: 2, name: "Jane", age: 25, active: false },
|
||||
{ id: 3, name: "Bob", age: 35, active: true },
|
||||
];
|
||||
|
||||
// Map - Transform array
|
||||
const names = users.map((user) => user.name);
|
||||
const upperNames = users.map((user) => user.name.toUpperCase());
|
||||
|
||||
// Filter - Select elements
|
||||
const activeUsers = users.filter((user) => user.active);
|
||||
const adults = users.filter((user) => user.age >= 18);
|
||||
|
||||
// Reduce - Aggregate data
|
||||
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
|
||||
const avgAge = totalAge / users.length;
|
||||
|
||||
// Group by property
|
||||
const byActive = users.reduce((groups, user) => {
|
||||
const key = user.active ? "active" : "inactive";
|
||||
return {
|
||||
...groups,
|
||||
[key]: [...(groups[key] || []), user],
|
||||
};
|
||||
}, {});
|
||||
|
||||
// Chaining methods
|
||||
const result = users
|
||||
.filter((user) => user.active)
|
||||
.map((user) => user.name)
|
||||
.sort()
|
||||
.join(", ");
|
||||
```
|
||||
|
||||
**Advanced Array Methods:**
|
||||
|
||||
```javascript
|
||||
// Find - First matching element
|
||||
const user = users.find((u) => u.id === 2);
|
||||
|
||||
// FindIndex - Index of first match
|
||||
const index = users.findIndex((u) => u.name === "Jane");
|
||||
|
||||
// Some - At least one matches
|
||||
const hasActive = users.some((u) => u.active);
|
||||
|
||||
// Every - All match
|
||||
const allAdults = users.every((u) => u.age >= 18);
|
||||
|
||||
// FlatMap - Map and flatten
|
||||
const userTags = [
|
||||
{ name: "John", tags: ["admin", "user"] },
|
||||
{ name: "Jane", tags: ["user"] },
|
||||
];
|
||||
const allTags = userTags.flatMap((u) => u.tags);
|
||||
|
||||
// From - Create array from iterable
|
||||
const str = "hello";
|
||||
const chars = Array.from(str);
|
||||
const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
|
||||
|
||||
// Of - Create array from arguments
|
||||
const arr = Array.of(1, 2, 3);
|
||||
```
|
||||
|
||||
### 2. Higher-Order Functions
|
||||
|
||||
**Functions as Arguments:**
|
||||
|
||||
```javascript
|
||||
// Custom forEach
|
||||
function forEach(array, callback) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
callback(array[i], i, array);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom map
|
||||
function map(array, transform) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
result.push(transform(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Custom filter
|
||||
function filter(array, predicate) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
if (predicate(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Functions Returning Functions:**
|
||||
|
||||
```javascript
|
||||
// Currying
|
||||
const multiply = (a) => (b) => a * b;
|
||||
const double = multiply(2);
|
||||
const triple = multiply(3);
|
||||
|
||||
console.log(double(5)); // 10
|
||||
console.log(triple(5)); // 15
|
||||
|
||||
// Partial application
|
||||
function partial(fn, ...args) {
|
||||
return (...moreArgs) => fn(...args, ...moreArgs);
|
||||
}
|
||||
|
||||
const add = (a, b, c) => a + b + c;
|
||||
const add5 = partial(add, 5);
|
||||
console.log(add5(3, 2)); // 10
|
||||
|
||||
// Memoization
|
||||
function memoize(fn) {
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = fn(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const fibonacci = memoize((n) => {
|
||||
if (n <= 1) return n;
|
||||
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Composition and Piping
|
||||
|
||||
```javascript
|
||||
// Function composition
|
||||
const compose =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), x);
|
||||
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
|
||||
// Example usage
|
||||
const addOne = (x) => x + 1;
|
||||
const double = (x) => x * 2;
|
||||
const square = (x) => x * x;
|
||||
|
||||
const composed = compose(square, double, addOne);
|
||||
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
const piped = pipe(addOne, double, square);
|
||||
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
// Practical example
|
||||
const processUser = pipe(
|
||||
(user) => ({ ...user, name: user.name.trim() }),
|
||||
(user) => ({ ...user, email: user.email.toLowerCase() }),
|
||||
(user) => ({ ...user, age: parseInt(user.age) }),
|
||||
);
|
||||
|
||||
const user = processUser({
|
||||
name: " John ",
|
||||
email: "JOHN@EXAMPLE.COM",
|
||||
age: "30",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Pure Functions and Immutability
|
||||
|
||||
```javascript
|
||||
// Impure function (modifies input)
|
||||
function addItemImpure(cart, item) {
|
||||
cart.items.push(item);
|
||||
cart.total += item.price;
|
||||
return cart;
|
||||
}
|
||||
|
||||
// Pure function (no side effects)
|
||||
function addItemPure(cart, item) {
|
||||
return {
|
||||
...cart,
|
||||
items: [...cart.items, item],
|
||||
total: cart.total + item.price,
|
||||
};
|
||||
}
|
||||
|
||||
// Immutable array operations
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
// Add to array
|
||||
const withSix = [...numbers, 6];
|
||||
|
||||
// Remove from array
|
||||
const withoutThree = numbers.filter((n) => n !== 3);
|
||||
|
||||
// Update array element
|
||||
const doubled = numbers.map((n) => (n === 3 ? n * 2 : n));
|
||||
|
||||
// Immutable object operations
|
||||
const user = { name: "John", age: 30 };
|
||||
|
||||
// Update property
|
||||
const olderUser = { ...user, age: 31 };
|
||||
|
||||
// Add property
|
||||
const withEmail = { ...user, email: "john@example.com" };
|
||||
|
||||
// Remove property
|
||||
const { age, ...withoutAge } = user;
|
||||
|
||||
// Deep cloning (simple approach)
|
||||
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
// Better deep cloning
|
||||
const structuredClone = (obj) => globalThis.structuredClone(obj);
|
||||
```
|
||||
Key topics covered in [references/advanced-patterns.md](references/advanced-patterns.md):
|
||||
- **Array methods** — `map`, `filter`, `reduce`, `find`, `findIndex`, `some`, `every`, `flatMap`, `Array.from`
|
||||
- **Higher-order functions** — custom `forEach`/`map`/`filter`, currying, partial application, memoization
|
||||
- **Composition and piping** — `compose`/`pipe` utilities with practical data transformation examples
|
||||
- **Pure functions and immutability** — immutable array/object operations, deep cloning with `structuredClone`
|
||||
|
||||
## Modern Class Features
|
||||
|
||||
```javascript
|
||||
// Class syntax
|
||||
class User {
|
||||
// Private fields
|
||||
#password;
|
||||
|
||||
// Public fields
|
||||
id;
|
||||
name;
|
||||
|
||||
// Static field
|
||||
static count = 0;
|
||||
|
||||
constructor(id, name, password) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.#password = password;
|
||||
User.count++;
|
||||
}
|
||||
|
||||
// Public method
|
||||
greet() {
|
||||
return `Hello, ${this.name}`;
|
||||
}
|
||||
|
||||
// Private method
|
||||
#hashPassword(password) {
|
||||
return `hashed_${password}`;
|
||||
}
|
||||
|
||||
// Getter
|
||||
get displayName() {
|
||||
return this.name.toUpperCase();
|
||||
}
|
||||
|
||||
// Setter
|
||||
set password(newPassword) {
|
||||
this.#password = this.#hashPassword(newPassword);
|
||||
}
|
||||
|
||||
// Static method
|
||||
static create(id, name, password) {
|
||||
return new User(id, name, password);
|
||||
}
|
||||
}
|
||||
|
||||
// Inheritance
|
||||
class Admin extends User {
|
||||
constructor(id, name, password, role) {
|
||||
super(id, name, password);
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
greet() {
|
||||
return `${super.greet()}, I'm an admin`;
|
||||
}
|
||||
}
|
||||
```
|
||||
ES2022 classes support private fields (`#field`), static fields, getters/setters, and private methods. See [references/advanced-patterns.md](references/advanced-patterns.md) for a full example with inheritance.
|
||||
|
||||
## Modules (ES6)
|
||||
|
||||
```javascript
|
||||
// Exporting
|
||||
// math.js
|
||||
// Named exports
|
||||
export const PI = 3.14159;
|
||||
export function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
export class Calculator {
|
||||
// ...
|
||||
}
|
||||
export function add(a, b) { return a + b; }
|
||||
|
||||
// Default export
|
||||
export default function multiply(a, b) {
|
||||
return a * b;
|
||||
}
|
||||
export default function multiply(a, b) { return a * b; }
|
||||
|
||||
// Importing
|
||||
// app.js
|
||||
import multiply, { PI, add, Calculator } from "./math.js";
|
||||
// Import
|
||||
import multiply, { PI, add } from "./math.js";
|
||||
|
||||
// Rename imports
|
||||
import { add as sum } from "./math.js";
|
||||
|
||||
// Import all
|
||||
import * as Math from "./math.js";
|
||||
|
||||
// Dynamic imports
|
||||
const module = await import("./math.js");
|
||||
// Dynamic import (code splitting)
|
||||
const { add } = await import("./math.js");
|
||||
|
||||
// Conditional loading
|
||||
if (condition) {
|
||||
const module = await import("./feature.js");
|
||||
module.init();
|
||||
}
|
||||
```
|
||||
|
||||
For re-exports, namespace imports, and conditional dynamic loading see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Iterators and Generators
|
||||
|
||||
```javascript
|
||||
// Custom iterator
|
||||
const range = {
|
||||
from: 1,
|
||||
to: 5,
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
current: this.from,
|
||||
last: this.to,
|
||||
|
||||
next() {
|
||||
if (this.current <= this.last) {
|
||||
return { done: false, value: this.current++ };
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
for (const num of range) {
|
||||
console.log(num); // 1, 2, 3, 4, 5
|
||||
}
|
||||
|
||||
// Generator function
|
||||
function* rangeGenerator(from, to) {
|
||||
for (let i = from; i <= to; i++) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
for (const num of rangeGenerator(1, 5)) {
|
||||
console.log(num);
|
||||
}
|
||||
|
||||
// Infinite generator
|
||||
function* fibonacci() {
|
||||
let [prev, curr] = [0, 1];
|
||||
while (true) {
|
||||
yield curr;
|
||||
[prev, curr] = [curr, prev + curr];
|
||||
}
|
||||
}
|
||||
|
||||
// Async generator
|
||||
async function* fetchPages(url) {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const response = await fetch(`${url}?page=${page}`);
|
||||
const data = await response.json();
|
||||
if (data.length === 0) break;
|
||||
yield data;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const page of fetchPages("/api/users")) {
|
||||
console.log(page);
|
||||
}
|
||||
```
|
||||
Generators (`function*`) and async generators (`async function*`) enable lazy sequences and async pagination. See [references/advanced-patterns.md](references/advanced-patterns.md) for custom iterator, range generator, fibonacci, and `for await...of` examples.
|
||||
|
||||
## Modern Operators
|
||||
|
||||
```javascript
|
||||
// Optional chaining
|
||||
const user = { name: "John", address: { city: "NYC" } };
|
||||
// Optional chaining — safe property access
|
||||
const city = user?.address?.city;
|
||||
const zipCode = user?.address?.zipCode; // undefined
|
||||
|
||||
// Function call
|
||||
const result = obj.method?.();
|
||||
|
||||
// Array access
|
||||
const first = arr?.[0];
|
||||
|
||||
// Nullish coalescing
|
||||
// Nullish coalescing — default only for null/undefined (not 0 or "")
|
||||
const value = null ?? "default"; // 'default'
|
||||
const value = undefined ?? "default"; // 'default'
|
||||
const value = 0 ?? "default"; // 0 (not 'default')
|
||||
const value = "" ?? "default"; // '' (not 'default')
|
||||
const zero = 0 ?? "default"; // 0
|
||||
|
||||
// Logical assignment
|
||||
let a = null;
|
||||
a ??= "default"; // a = 'default'
|
||||
|
||||
let b = 5;
|
||||
b ??= 10; // b = 5 (unchanged)
|
||||
|
||||
let obj = { count: 0 };
|
||||
obj.count ||= 1; // obj.count = 1
|
||||
obj.count &&= 2; // obj.count = 2
|
||||
a ??= "default"; // assign if null/undefined
|
||||
obj.count ||= 1; // assign if falsy
|
||||
obj.count &&= 2; // assign if truthy
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
```javascript
|
||||
// Debounce
|
||||
function debounce(fn, delay) {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const searchDebounced = debounce(search, 300);
|
||||
|
||||
// Throttle
|
||||
function throttle(fn, limit) {
|
||||
let inThrottle;
|
||||
return (...args) => {
|
||||
if (!inThrottle) {
|
||||
fn(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const scrollThrottled = throttle(handleScroll, 100);
|
||||
|
||||
// Lazy evaluation
|
||||
function* lazyMap(iterable, transform) {
|
||||
for (const item of iterable) {
|
||||
yield transform(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Use only what you need
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
const doubled = lazyMap(numbers, (x) => x * 2);
|
||||
const first = doubled.next().value; // Only computes first value
|
||||
```
|
||||
See [references/advanced-patterns.md](references/advanced-patterns.md) for debounce, throttle, and lazy evaluation with generators.
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -907,13 +492,4 @@ const first = doubled.next().value; // Only computes first value
|
||||
14. **Handle errors properly**: Use try/catch with async/await
|
||||
15. **Use strict mode**: `'use strict'` for better error catching
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **this binding confusion**: Use arrow functions or bind()
|
||||
2. **Async/await without error handling**: Always use try/catch
|
||||
3. **Promise creation unnecessary**: Don't wrap already async functions
|
||||
4. **Mutation of objects**: Use spread operator or Object.assign()
|
||||
5. **Forgetting await**: Async functions return promises
|
||||
6. **Blocking event loop**: Avoid synchronous operations
|
||||
7. **Memory leaks**: Clean up event listeners and timers
|
||||
8. **Not handling promise rejections**: Use catch() or try/catch
|
||||
For common pitfalls (this binding, promise anti-patterns, memory leaks), see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
Reference in New Issue
Block a user