From Ag-Grid to shadcn: nine years, same three questions
Nine years of frontend stacks I've shipped on — and the three questions that survived all of them.
In 2017 I was wiring jQuery into a .NET project at FPT Software in Saigon. The grid was Ag-Grid, the build was MSBuild, the deploy was someone walking files to a server. In 2026 I'm composing shadcn primitives on top of Next.js Server Components for an autonomous storefront. Most of what I learned in between still applies — it just got renamed.
The first grid I shipped
The first feature I owned end-to-end was a tester productivity report at FPT. I was nominally a manual tester, but the team was short a frontend developer and the report needed filtering, sorting, and column groups by week. I dropped Ag-Grid into the page, copied the configuration from a co-worker, and shipped.
It was slow. Sorting locked the tab for a couple of seconds on a few thousand rows because I was sending everything to the client and letting Ag-Grid sort in memory. The lead engineer didn't tell me to switch to server-side row models. He asked, "what does the user want to see first?" The answer was: last week's failed cases, grouped by feature. That collapsed the visible row count by more than an order of magnitude. The page got fast without me touching Ag-Grid.
The first frontend lesson I keep is that performance is usually a product question pretending to be an engineering one.
I didn't have language for any of this in 2017. I wrote a long bug ticket against myself, fixed the query, and moved on. The grid stayed. The grid is probably still there.
The React years, where state ate the calendar
In 2020 I moved to Remolution and went all-in on React. The product was a recruiting platform with real-time signals — interviewer status, calendar holds, payment events flowing in from a provider webhook. The frontend stack at the start was CRA, Redux, redux-saga, react-router. By the end it was Next.js Pages Router, Zustand for client state, SWR for server state, and a calendar component I'd rewritten three times.
The third rewrite was the one I remember. We had a recurring bug where a recruiter would drag an interview to a new slot, the optimistic update would land, and then thirty seconds later it would silently revert. The interviewer on the other end would show up at the old time. I shipped the third rewrite in late 2022. The fix wasn't in the calendar — it was in how I was reconciling websocket events with the local cache. I'd been treating the websocket as the source of truth and the optimistic update as a temporary lie, but the lie was the better answer most of the time.
I rewrote the reconciliation to keep the optimistic state until the server confirmed or sent a structured conflict. The calendar stopped reverting. Don't model server state as the eventual truth when the user just told you the truth ten milliseconds ago. It still applies, almost verbatim, to the agent systems I'm building now — except now "the user" is sometimes another agent.
Fullstack pulled the framework noise down a level
In 2023 I moved to the fullstack side of the same product. We had a hundred-plus enterprise customers by then. The frontend conversations stopped being about which library and started being about which seam.
Server-rendering a recruiter's pipeline view? The bottleneck wasn't React — it was the Postgres query running RLS through three joins. A search box that felt sluggish? Not a debounce problem — a missing GIN index. The actual frontend code shrank. I deleted entire reducers when I moved their state behind a server action. The shadcn migration started here, almost by accident. We needed a Combobox that didn't fight Radix's focus management, and shadcn's recipe was the shortest path. A few months later we'd swapped out half the design system without anyone noticing on the customer side.
What I'd tell past me
The framework debate gets quieter the closer you sit to the data. If a button takes too long to feel right, the answer is almost never in the button.
I shipped one feature in this period that I'm still proud of: a candidate timeline that pulled from four different event sources and rendered server-side with streaming. The first version had a flash of empty state because I was awaiting all four sources in parallel before sending the first byte. The fix was to fire each source as its own Suspense boundary. The flash went away. Total wall-clock time was the same. Users said it felt much faster.
What shadcn made visible
At PSA I'm building agentic commerce — autonomous storefronts where an LLM picks the product, writes the copy, and decides when to discount. The UI surface is small but every component has to be themeable by the agent at runtime. shadcn fits this because it isn't a library — it's a folder of components I own, written in primitives I can re-style without forking. When the agent wants a softer CTA for a hesitant shopper, I can hand it a token to flip, not a component to swap.
This is the first stack I've worked in where the frontend isn't the slowest part of the product. Inference latency is. A button that renders instantly doesn't matter when the model behind it took the better part of two seconds to decide what the button should say. So the frontend work has shifted into hiding latency: streaming partial outputs, optimistic placeholders, structured loading states that don't lie about what's coming.
If 2020's frontend was "render the truth fast," 2026's frontend is "render a useful guess while the truth arrives."
The three questions that survived
Nine years in, the framework names changed five times and the answer to "what should the user see first?" didn't change once. Three questions still carry every frontend decision I make.
Is the thing the user is waiting for actually the thing taking the time? Most of the time, it isn't. The bottleneck moved from in-browser sorting to network round-trips to model inference, but the shape of the question is identical.
Does the interface tell the truth about what's happening, including when nothing is happening yet? Empty states, loading states, and conflict states are the same problem wearing different costumes.
When the system disagrees with the user, who wins, and how quickly? Optimistic updates, undo, override paths — all variations of this one question.
Ag-Grid, Redux, SWR, shadcn, Server Components, agent runtimes. The tools changed. The questions didn't.