How React Hooks Changed the Way We Write Components
React 16.8 introduced the Hooks API: how it shifted component authoring from classes to functions, what you gain, and what to watch out for.
Written 7 years ago — it reflects the conditions of that time.
React 16.8 shipped in February 2019, and hooks became stable. It was the last link in a chain that runs all the way back to getting to know ES6’s let, const and arrow functions; much of what makes hooks readable rests on that syntax. I had been following the announcement since Dan Abramov’s talk at React Conf in October 2018 — the problems he described were painfully familiar: logic scattered across lifecycle methods, bloated class components, the endless confusion around this. I have been using hooks in real projects for a few weeks now and wanted to share my thoughts.
What Was Exhausting About Class Components?
The most tedious part of writing a class component was the way related logic ended up split across different lifecycle methods. Take a subscription, for example: you start it in componentDidMount, tear it down in componentWillUnmount, and restart it in componentDidUpdate whenever the id changes. Three separate methods in three separate places, yet all of them belong to the same “subscription management” concern.
class UserProfile extends React.Component {
componentDidMount() {
this.subscription = userService.subscribe(this.props.userId, this.handleUpdate);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.subscription.unsubscribe();
this.subscription = userService.subscribe(this.props.userId, this.handleUpdate);
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
handleUpdate = (data) => {
this.setState({ user: data });
};
render() {
return <div>{this.state.user?.name}</div>;
}
}
The same logic with hooks:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const subscription = userService.subscribe(userId, setUser);
return () => subscription.unsubscribe();
}, [userId]);
return <div>{user?.name}</div>;
}
Everything lives together. The function returned from useEffect acts as the cleanup. The effect re-runs whenever userId changes. This is the exact equivalent of what the class version was doing — but without the fragmentation.
useState and useEffect: Basic, but Sufficient
Two hooks cover a surprisingly wide surface area. useState is straightforward: it returns a value and a function to update it. useEffect handles side effects — data fetching, subscriptions, timers, and direct DOM manipulation.
The dependency array, useEffect’s second argument, is a critical concept. Leave it empty and the effect runs only on the initial render. Provide values and the effect re-runs whenever those values change. Omit it entirely and the effect runs on every render — which is rarely what you want.
// Only on initial render:
useEffect(() => {
fetchInitialData();
}, []);
// On every render (use with caution):
useEffect(() => {
document.title = `${count} items`;
});
// Whenever count changes:
useEffect(() => {
saveToStorage(count);
}, [count]);
Custom Hooks: Sharing Logic the Right Way
The most valuable aspect of hooks is the ability to write custom hooks. Any function whose name starts with use can be a hook; inside it you can call other hooks. This is far cleaner than the mixin or Higher-Order Component (HOC) alternatives.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return width;
}
// Usable in any component:
function Header() {
const width = useWindowWidth();
return <header>{width > 768 ? <DesktopNav /> : <MobileNav />}</header>;
}
In the class-based world I would have reached for an HOC or a render prop to share this kind of logic. Both work, but both create wrapper hell.
Things I Have Had to Watch Out For
A few pitfalls caught me when I first started using hooks:
Stale closure problem. If you access props or state inside useEffect and forget to include them in the dependency array, you can end up reading stale values. ESLint’s exhaustive-deps rule catches most of these cases — I strongly recommend keeping it enabled.
Not reaching for useEffect for every side effect. You do not need useEffect to fetch data inside a click handler — you can call it directly from the event handler. useEffect is for side effects that are a genuine consequence of rendering.
Not rushing to rewrite class components. The React team itself says it: class components are not going away. Rather than spending time migrating existing, working class components to hooks, it makes more sense to write all new code with hooks going forward.
In Summary
Hooks move React toward a more functional place. They allow logic to be grouped by related concept rather than by lifecycle method. I see this change as an improvement, not a revolution. Does it solve every problem that class components introduced? No — but it meaningfully addresses the most common pain points. After a few weeks I have no desire to go back.
That said, when I introduce hooks to a new team member, I still make a point of explaining what class components were solving first. The conveniences hooks bring only make sense when you can see the problem behind them. Using a tool without knowing what it replaced is using it at half capacity — and in a fast-moving ecosystem like React, that gap becomes consequential within a few years.
Experiments on this topic
A 4X that ends in one sitting: the constraint chose the scope
A dice-free strategy core that resolves 480 hexes in 60 turns and runs offline in the browser; it shipped as Regnarium.
What it does today
Runs a full 4X match on a 480-hex map in at most 60 turns; rules, map generation and the opponent AI live in one pure TypeScript core, and combat rolls no dice. Anyone with half an hour and an appetite for strategy can open it in a browser.
Garajdaş: model-specific community without the forum sprawl
A Reddit-like community platform where every vehicle make and model has its own club, holding ICE and EV under one roof; web and mobile run in parallel.
What it does today
A community platform where every car make and model has its own club; identity, content, notifications and moderation all run on a single-schema API, with the web and mobile clients sharing that one schema. Anyone looking for a community specific to their car's model can join today.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.