Why React Hooks Embrace Functional Programming: From Pure Functions to useEffect
This article explains how React’s functional nature, rooted in pure functions and side‑effect management, leads to the design of Hooks such as useState and useEffect, and walks through their implementation details and practical considerations.
From Functional Programming to Hooks
React is a highly functional framework; understanding its functional programming concepts helps grasp core APIs such as setState, render, function components, and Redux.
Functional programming originates from category theory, treating objects as sets and morphisms as functions that map inputs to outputs without side effects.
Pure functions have explicit input‑output via parameters and return values, produce the same output for identical inputs, and have no side effects.
// splice is impure
let arr = [1,2,3,4,5];
arr.splice(0,3); // modifies original array
// slice is pure
arr = [1,2,3,4,5];
arr.slice(0,3); // returns new arraySide Effects
In computer science, a function’s side effect is any impact beyond returning a value, such as modifying globals, parameters, I/O, or DOM queries.
Sending an HTTP request
new Date() / Math.random()
console.log / I/O
DOM queries
Pure functions alone cannot perform useful work; side effects must be managed, often via functors in functional languages.
Historical Background – Hook Origin
React Components
React components are either function components (stateless, no lifecycle) or class components (stateful, lifecycle).
class ComponentA extends React.Component {
constructor(props) {
super(props);
this.state = { displayContent: 'Hello World' };
}
render() {
return <div>{this.state.displayContent}</div>;
}
} function ComponentFunctionA = (props) => <div>{props.displayContent}</div>;Problems
Function components want state and lifecycle
When requirements change, developers may try to give function components state, leading to design issues.
Convert to class component
Lift state to a parent container
Class component lifecycle separates logic
class DemoA extends React.PureComponent {
constructor(props) {
super(props);
this.listener = () => { /* do something */ };
}
componentDidMount() {
document.addEventListener('click', this.listener);
}
componentWillUnmount() {
document.removeEventListener('click', this.listener);
}
}Solution – Hook Design
Hooks such as useState and useEffect let function components manage state and side effects while staying mostly pure.
useState
Data‑storage options include class fields, global state, DOM, localStorage, and closures; closures are preferred for minimizing side effects.
function Demo() {
const [count, setCount] = useState(0);
return <div onClick={() => { setCount(count++); }}>{count}<div>;
} var useState = (initState) => {
let data = initState;
const dispatch = (newData) => { data = newData; };
return [data, dispatch];
};A realistic implementation must distinguish between the mount phase and update phase, store hook objects in the FiberNode, and use a queue to batch state updates.
type Hook = { memorizedState: any, queue: UpdateQueue<any, any> | null, next: any };
type Queue = { last: Update, dispatch: any, lastRenderedState: any };
type Update = { action: any, next: Update };
function mountState(initState) {
const hook = mountWorkInProgressHook();
hook.memorizedState = initState;
const queue = (hook.queue = { last: null, dispatch: null, lastRenderedState: null });
const dispatch = dispatchAction.bind(null, queue);
queue.dispatch = dispatch;
return [hook.memorizedState, dispatch];
}
function dispatchAction(queue, action) {
const update = { action, next: null };
let last = queue.last;
if (last === null) {
update.next = update;
} else {
// link into circular list
update.next = last.next;
last.next = update;
}
queue.last = update;
scheduleWork();
}useEffect
useEffect creates an effect object with a create function, optional destroy function, and a dependency array. Effects are stored in a singly‑linked list on the FiberNode and executed after the commit phase.
type Effect = { create: any, destroy: any, deps: Array, next: any };
type EffectQueue = { lastEffect: Effect };
function useEffect(fn, deps) {
if (mounted) {
mountEffect(fn, deps);
} else {
updateEffect(fn, deps);
}
}
function mountEffect(fn, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = pushEffect('Effect', fn, undefined, deps);
}
function updateEffect(fn, deps) {
const hook = updateWorkInProgressHook();
const prev = hook.memorizedState;
if (!areHookInputsEqual(deps, prev.deps)) {
hook.memorizedState = pushEffect('Effect', fn, prev.destroy, deps);
} else {
hook.memorizedState = prev;
}
}
function pushEffect(tag, create, destroy, deps) {
const effect = { create, destroy, deps, next: null };
const queue = fiberNode.updateQueue = fiberNode.updateQueue || newEffectQueue();
if (queue.lastEffect) {
const first = queue.lastEffect.next;
queue.lastEffect.next = effect;
effect.next = first;
queue.lastEffect = effect;
} else {
queue.lastEffect = effect.next = effect;
}
return effect;
}During the commit phase, the stored create and destroy functions are invoked in order.
Conclusion
By tracing a problem, defining requirements, designing a solution, and iterating on that design, we understand the motivations behind Hook architecture and can apply similar reasoning to solve real‑world development challenges.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
