Six Years of Hooks
Six Years of Hooks#
Six years ago I wrote my first custom hook. It was a useDebounce that I was unreasonably proud of. A colleague pointed out on a call that it was basically a setTimeout with a dependency array — lodash.debounce but worse. He was right. But something about the idea of composing stateful logic through function calls instead of class inheritance had clicked in a way that nothing in React had before.
This is post #33. The first post on this blog was called "Why React?" and it was an argument with myself about whether to stay in the Angular world or commit to a framework that felt so different from everything I'd been doing. That post is embarrassing to read now. Not because the reasoning was wrong, but because you can hear someone trying to convince himself. The answer was already known. It just needed to be written down.
That's what this blog has been: writing things down to figure out what I think.
The hooks announcement at React Conf 2018 landed and I watched the recording without fully understanding useEffect. I thought it was componentDidMount + componentDidUpdate + componentWillUnmount squished together. It took three months of production bugs to understand it's actually about synchronization with external systems. Three months. useEffect is genuinely a different mental model, and class-component intuitions get in the way.
I wrote about this in post #5. Re-reading it now, I'd change almost everything about the explanation, but the core observation holds: useEffect is the most misunderstood API in React, and most of the misunderstanding comes from developers trying to map it onto lifecycle methods it was designed to replace.
The irony is that by 2025, the correct answer to "how should I use useEffect?" is often "you shouldn't." Most effects written between 2019 and 2023 were data fetching. All of that data fetching belongs in Server Components now. Every useEffect that says "fetch data on mount and refetch when this prop changes" is a Server Component waiting to be written:
// 2020: this was the standard pattern
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetchUser(userId).then((u) => {
setUser(u);
setIsLoading(false);
});
}, [userId]);
if (isLoading) return <Skeleton />;
if (!user) return null;
return <UserCard user={user} />;
}
// 2025: why was this ever on the client
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId);
if (!user) return null;
return <UserCard user={user} />;
}
I look at that second version and I think: this is what we were trying to write all along. The first version is a Rube Goldberg machine for reading data from a server. The second version is just... reading data from a server.
Post #12 was about Redux. What I actually wrote was a breakup letter. After two years of writing reducers and action creators and selectors and normalized state trees, the realization landed: more code was going toward managing state than toward building features. The post said "Redux is dead for 90% of new projects" and it got more comments than anything else on this blog.
The core claim was right. But it was right for the wrong reasons. The theory was that Redux was dying because useReducer + Context replaced it. It actually became unnecessary (for most projects) because Server Components eliminated the category of state it was managing. When server-fetched data lives in Server Components and mutations go through server actions, the client-side state shrinks to "UI interaction state" — which dropdown is open, what the user typed in the filter, whether the modal is showing. You don't need Redux for that. You don't even need Context for most of it. useState is enough.
This is one of the biggest lessons of the whole six-year arc: we were solving problems on the client that should never have been on the client in the first place. Not because we were doing it wrong, but because the framework didn't give us a server-first model until 2023. We worked with what we had.
Post #25 was about architecture — hexagonal architecture specifically, and how it maps to React. The pattern fit React applications almost perfectly: domain logic in pure functions, application use cases as the orchestration layer, infrastructure (React components, MongoDB, server actions) at the edges. That post was the most important one here, not because it was the best-written (it wasn't), but because the architecture it described survived contact with reality across a large, multi-module codebase. That's the highest compliment I can give an architecture.
The React Compiler arriving in 2025 felt like the closing of a circle. Earlier on this blog I wrote about the memoization tax — useMemo, useCallback, React.memo, all the defensive performance boilerplate required because React couldn't skip unnecessary re-renders automatically. "Surely they'll automate this eventually." It took six years. When it landed and 63 memoization calls were deleted across three codebases with every test still passing, that felt like vindication for the React team's long-term design.
The hook rules create a static analyzability guarantee. The compiler exploits that guarantee. The chain from "always call hooks at the top level" to "automatic memoization" is a straight line if you squint hard enough. It just took six years to draw it.
After six years of this, there are still React APIs where my mental model has gaps. Suspense edge cases — nested boundaries with different fallbacks, error boundaries interacting with Suspense, use() with promise identity — require looking things up every time. I'm not embarrassed about this anymore. The shift from "I should know everything" to "I should know how to find answers quickly" is one of the most important shifts in a developer's career.
If I could tell a 2020 version of myself one thing: stop trying to do everything on the client.
The 2020 approach was building React apps where the client fetched all the data, managed all the state, handled all the caching, and the server was a JSON API endpoint. That was the orthodoxy. It produced apps that were slower, more complex, and harder to maintain than they needed to be.
The server is not the enemy. The network boundary is not something to abstract away — it's something to design around. The data lives on the server. The business logic should run on the server. The client is for interactivity — clicks, drags, animations, sounds, the things that need to respond in milliseconds. Everything else can wait for the server.
Here's what holds up after six years.
Hooks changed how I think about composition. Not just in React — in general. The idea that you can take a piece of stateful behavior, extract it into a function, and compose it with other stateful behaviors without inheritance or wrapper components is a genuine insight about software design. Hooks are function-level combinators: small, composable units that chain together to build complex behavior. That analogy has held up better than any other mental model I've used for this subject.
The React ecosystem moves in cycles: revolution (hooks), consolidation (React 17), expansion (concurrent features), and now simplification (Server Components, Compiler). We're in a simplification phase. The code written today is shorter, more correct, and more boring than what was written in 2020. Boring is a compliment. Boring means the framework is handling the hard parts and you're just specifying what you want.
The AI integration work I've been doing is pointing toward something not yet fully visible. Server Components and server actions are making React more of a full-stack framework than it's ever been. The compiler is making client-side code simpler. The direction is clear even if the destination isn't.
I'll still be writing about it. Not because anyone asked, but because writing is how I figure out what I think. That hasn't changed in 33 posts and it won't change in the next 33.
See you in the next one.