Mastering React Hooks: Best Practices and Streamlined Patterns
This article reviews React Hooks, compares them with class components, demonstrates change‑driven coding with practical examples, introduces marble diagrams for visualizing state flows, and explores advanced patterns like custom hooks, immutable data handling, and global state management for more maintainable frontend development.
Review of React Hooks
React Hooks can be seen as a new framework that replaces class‑based components with function components driven by useState and useEffect.
Hooks as a New Framework
Top‑down flow : Logic proceeds linearly rather than inter‑method calls.
Weakened handlers : Callbacks are less natural than class methods.
Simplified lifecycle : useEffect replaces multiple lifecycle methods.
Distributed state/effect registration : State can be declared where needed.
Dependency‑driven : Hooks accept a deps array to trigger updates.
Because of these characteristics, Hooks should be written in a "change‑driven" style rather than a callback‑driven one.
Example 1: Declaring a Request with useEffect
Callback‑style (class‑like) implementation
const Demo: React.FC = () => {
const [state, setState] = useState({ keyword: '' });
const query = useCallback((queryState) => {
// ...
}, []);
const handleKeywordChange = useCallback((e) => {
const latestState = { ...state, keyword: e.target.value };
setState(latestState);
query(latestState);
}, [state, query]);
return // view
}Problems: stale state, manual dependency management, unclear responsibilities.
Change‑driven implementation
const Demo: React.FC = () => {
const [state, setState] = useState({ keyword: '' });
const handleKeywordChange = useCallback((e) => {
const nextKeyword = e.target.value;
setState(prev => ({ ...prev, keyword: nextKeyword }));
}, []);
useEffect(() => {
// query
}, [state]);
return // view
}This version lets the query run automatically whenever state changes, eliminating manual calls.
Example 2: Registering a Window‑size Listener
Callback‑style
const Demo: FC = () => {
const callback = // ...
useEffect(() => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}, []);
return // view
}The callback reference changes on each render, causing stale listeners.
Change‑driven implementation
const Demo: FC = () => {
const [windowSize, setWindowSize] = useState([window.innerWidth, window.innerHeight] as const);
useEffect(() => {
const handleResize = () => {
setWindowSize([window.innerWidth, window.innerHeight]);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const callback = // ...
useEffect(callback, [windowSize]);
return // view
};By turning the size into state, the effect that depends on it runs declaratively.
The key of the change‑driven approach is converting actions into state .
Marble Diagrams
Marble diagrams visualize data streams; each marble represents a value emitted over time, similar to how Hook state changes can be viewed as a stream.
Immutable Data Flow and Execution Frames
State updates should be immutable so that reference changes match value changes. Execution frames correspond to each render triggered by state or prop changes.
Immutable Data Flow
Ensure every setState produces a new object, optionally using utilities or libraries like ImmutableJS.
Execution Frame
When state or props change, React schedules a render frame; each marble aligns with these frames.
Sources of Change
Props change
Event (e.g., window resize)
Scheduler (animationFrame, interval, timeout)
Packaging an Event
const useClickEvent = () => {
const [clickEvent, setClickEvent] = useState<{x:number; y:number}>(null);
const dispatch = useCallback((e) => {
setClickEvent({ x: e.clientX, y: e.clientY });
}, []);
return [clickEvent, dispatch] as const;
};Packaging a Scheduler (interval)
const useInterval = (interval) => {
const [count, setCount] = useState();
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), interval);
return () => clearInterval(id);
}, []);
return count;
};Stream‑like Operators in Hooks
Mapping, skipping, taking, debounce, throttle, and async flows can be expressed with custom hooks.
Map (useMemo)
const [state1, setState1] = useState(initial1);
const [state2, setState2] = useState(initial2);
const computedState = useMemo(() => {
return Array(state2).fill(state1).join('');
}, [state1, state2]);Skip / Take (useCountMemo, useCountEffect)
const useCountMemo = <T>(callback: (count:number)=>T, deps:any[]):T => {
const countRef = useRef(0);
return useMemo(() => {
const val = callback(countRef.current);
countRef.current++;
return val;
}, deps);
};Debounce / Throttle
const useDebounce = (value, time=250) => {
const [debounced, setDebounced] = useState(null);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), time);
return () => clearTimeout(timer);
}, [value]);
return debounced;
};Action / Reducer Async Flow (useAsync)
const responseState = useAsync(initial, actionState, function* (action, prev) {
switch(action?.type) {
case 'clear': return null;
case 'request': {
const { data } = yield apiService.request(action.payload);
return data;
}
default: return prev;
}
});Singleton Hooks – Global State Management
Libraries like Hox provide createModel to generate a global singleton hook that lives for the app’s lifetime.
Limitations of Stream‑like Hooks
Over‑Frequent Changes
React has three frequencies: reconcile, rendering frames, and event dispatch. When events fire faster than rendering frames, additional hacks (e.g., debouncing) are needed.
Avoiding "Flow for Flow's Sake"
While RxJS‑style streams excel at messaging, forcing every piece of logic into a stream can make code verbose; Hooks remain multi‑paradigm and should be used where they add clarity.
Vision
The author plans to release a set of reusable stream‑style hooks called Marble Hooks .
References
RxMarble: https://rxmarbles.com/
ImmutableJS: https://immutable-js.github.io/immutable-js/
Hox: https://github.com/umijs/hox
Marble Hooks: https://github.com/pierrejacques/marble-hooks
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.
