
It started as a simple data table. Filter, sort, paginate. Should've been a two-day job. It turned into a two-week experiment — and I'm glad it did.
Version 1: The "just ship it" approach
First pass: local state, useState for everything, manual filter logic in the component. It worked. It was also a mess by day three. The component was 400 lines. Every new filter requirement meant digging into a growing chain of derived values. Readable at first, painful to extend.
Version 2: Extract the logic
I pulled all the filter/sort logic into a custom hook — useTableState. The component shrank to under 100 lines. This is the version that shipped. It was good enough. But it had a problem: the state lived in memory, so a page refresh or a shared link lost all the filter state.
Version 3: URL as state
The third version stored everything in the URL via search params. Filter, sort, page — all serialized. Suddenly the table was shareable, deep-linkable, and bookmarkable. Back button worked correctly. No extra storage layer.
The right architecture depends entirely on requirements you might not have yet. Know what it would cost to migrate later.
— Zaid Maraqa
Ask better questions upfront. Ship faster. Refactor when requirements actually change — not before.