Functional Programming Patterns in JavaScript
JavaScript is a multi-paradigm language, but functional patterns dominate modern frontend code. React is fundamentally functional â pure components, immutable state, composition over inheritance. Understanding these patterns is understanding the architecture of modern web apps.
Pure Functions
A pure function:
- Given the same inputs, always returns the same output
- Has no side effects (doesn't modify external state)
// Pure â same input â same output, no side effects
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Impure â modifies external state
let runningTotal = 0;
function addToTotal(amount) {
runningTotal += amount; // Side effect!
return runningTotal;
}
// Impure â depends on external state
function formatPrice(amount) {
return `${window.currency}${amount.toFixed(2)}`; // Depends on global
}
// Pure version
function formatPrice(amount, currency) {
return `${currency}${amount.toFixed(2)}`;
}Why Purity Matters
- Testable â No setup/teardown, no mocking globals
- Cacheable â Same input = same output â memoization works
- Parallelizable â No shared state â safe for Web Workers
- Predictable â Easy to reason about, easy to debug
- React-compatible â React requires pure render functions for concurrent mode
Immutability
Never mutate data â create new copies with changes:
// â Mutation
const user = { name: 'Alice', age: 30 };
user.age = 31; // Mutates original
const items = [1, 2, 3];
items.push(4); // Mutates original
// â
Immutable update
const updatedUser = { ...user, age: 31 };
const updatedItems = [...items, 4];
// Nested update
const state = {
user: { name: 'Alice', address: { city: 'NYC' } },
settings: { theme: 'dark' },
};
const newState = {
...state,
user: {
...state.user,
address: { ...state.user.address, city: 'LA' },
},
};Immutable Array Operations
const items = [1, 2, 3, 4, 5];
// Add
const added = [...items, 6]; // [1,2,3,4,5,6]
const prepended = [0, ...items]; // [0,1,2,3,4,5]
const inserted = [...items.slice(0, 2), 99, ...items.slice(2)]; // [1,2,99,3,4,5]
// Remove
const removed = items.filter(x => x !== 3); // [1,2,4,5]
const withoutIndex = items.toSpliced(2, 1); // [1,2,4,5] (ES2023)
// Update
const updated = items.map(x => x === 3 ? 30 : x); // [1,2,30,4,5]
const replaced = items.with(2, 30); // [1,2,30,4,5] (ES2023)
// Sort (immutable)
const sorted = items.toSorted((a, b) => b - a); // [5,4,3,2,1] (ES2023)
const reversed = items.toReversed(); // [5,4,3,2,1] (ES2023)structuredClone â Deep Copy
const original = {
date: new Date(),
nested: { items: [1, [2, 3]] },
regex: /pattern/g,
};
const clone = structuredClone(original);
clone.nested.items.push(4);
original.nested.items; // [1, [2, 3]] â unchangedstructuredClone handles Date, RegExp, Map, Set, ArrayBuffer, and nested objects. It does NOT clone functions, DOM nodes, or Symbols.
Higher-Order Functions
Functions that take or return functions:
// Takes a function
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const loggedAdd = withLogging((a, b) => a + b);
loggedAdd(2, 3); // Logs: Calling with [2, 3], Result: 5
// Practical: validation wrapper
function withValidation(schema, handler) {
return function(data) {
const errors = schema.validate(data);
if (errors.length) throw new ValidationError(errors);
return handler(data);
};
}Function Composition
Building complex operations from simple ones:
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const processUser = pipe(
normalize, // { name: ' ALICE ' } â { name: 'alice' }
validate, // Throws if invalid
enrichWithDefaults, // Adds missing fields
sanitize, // Remove dangerous content
);
const result = processUser(rawInput);Practical Composition
const formatCurrency = (locale, currency) => (amount) =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
const addTax = (rate) => (amount) => amount * (1 + rate);
const applyDiscount = (percent) => (amount) => amount * (1 - percent / 100);
const calculatePrice = pipe(
applyDiscount(10), // 10% off
addTax(0.08), // 8% tax
formatCurrency('en-US', 'USD'), // Format
);
calculatePrice(100); // "$97.20"Currying
Transforming a multi-argument function into a chain of single-argument functions:
// Manual currying
const add = (a) => (b) => a + b;
const add5 = add(5);
add5(3); // 8
// Generic curry utility
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
};
}
const multiply = curry((a, b, c) => a * b * c);
multiply(2)(3)(4); // 24
multiply(2, 3)(4); // 24
multiply(2)(3, 4); // 24
multiply(2, 3, 4); // 24Partial Application (Practical Currying)
const createApiCall = (baseUrl) => (endpoint) => (options) =>
fetch(`${baseUrl}${endpoint}`, options).then(r => r.json());
const api = createApiCall('https://api.example.com');
const getUsers = api('/users');
const getUser = (id) => api(`/users/${id}`)({ method: 'GET' });
// Event handler factories
const handleChange = (field) => (event) => {
setState(prev => ({ ...prev, [field]: event.target.value }));
};
<input onChange={handleChange('name')} />
<input onChange={handleChange('email')} />Transducers (Advanced)
Composable transformations that avoid intermediate arrays:
// Normal: creates 3 intermediate arrays
const result = data
.filter(x => x > 10)
.map(x => x * 2)
.filter(x => x < 100);
// Transducer: single pass, no intermediates
const filterGt10 = (next) => (x) => x > 10 ? next(x) : undefined;
const double = (next) => (x) => next(x * 2);
const filterLt100 = (next) => (x) => x < 100 ? next(x) : undefined;
const xform = compose(filterGt10, double, filterLt100);
const result = data.reduce((acc, x) => {
const transformed = xform((v) => v)(x);
if (transformed !== undefined) acc.push(transformed);
return acc;
}, []);Monadic Patterns (Maybe/Option)
Handle null/undefined safely without null checks:
class Maybe {
#value;
constructor(value) { this.#value = value; }
static of(value) { return new Maybe(value); }
static empty() { return new Maybe(null); }
map(fn) {
return this.#value == null ? this : Maybe.of(fn(this.#value));
}
flatMap(fn) {
return this.#value == null ? this : fn(this.#value);
}
getOrElse(fallback) {
return this.#value ?? fallback;
}
}
// Usage â no null checks, chain freely
const userName = Maybe.of(response)
.map(r => r.data)
.map(d => d.user)
.map(u => u.name)
.map(n => n.toUpperCase())
.getOrElse('Anonymous');
// vs imperative null checking
const userName = response?.data?.user?.name?.toUpperCase() ?? 'Anonymous';The optional chaining (?.) operator covers most cases. The Maybe pattern shines in complex pipelines where you need map/flatMap semantics.
Interview Signal
Senior candidates demonstrate:
- Purity reasoning â Why pure functions matter for testing, caching, concurrency, and React
- Immutability patterns â Spread, Array methods, structuredClone, when to use each
- Composition skill â pipe/compose, building complex logic from small functions
- Practical currying â Partial application for event handlers, API factories, configuration
- Trade-off awareness â FP adds abstraction; knowing when OOP or imperative is simpler