How I built the frontend of MyStocks
📚 Blog series: How I build MyStocks
- 1.Why I built MyStocks
- 2.How I built the frontend of MyStocks(current)
This post is the second part of a series. If you haven’t read it yet, here’s my introduction to MyStocks and why I built it.
When I set out to build MyStocks, I wanted a stock tracker that felt snappy and alive: create a watchlist, see prices tick every few seconds, search instantly, and never think about refresh buttons or “Syncing…” spinners. Also, I wanted it to cost basically nothing and not require me to babysit WebSocket connections at 2 a.m.
This post is about the frontend: the part that you feel. It’s the story of choosing boring, proven tools, gluing them together with types, and getting the “real-time” feel without hauling in a bunch of infrastructure I’d later regret.
What I needed from the UI
- Feel live: Prices should move on their own. Search should feel like autocomplete in your head.
- Be predictable: No mysterious state bugs or timing gremlins.
- Be maintainable: Solo-dev friendly. If I put this project down for a few months and then come back to it, I don’t want to have to re-learn a custom event bus just to fix a typo.
The stack
- React + TypeScript: The baseline.
- tRPC: Shared types front-to-back; no hand-written API clients.
- React Query: Fetching, caching, polling, dedupe, retries—done.
- Tailwind CSS: Utility classes, fast iteration.
- shadcn/ui: Copy-pasteable components I can fully own. Tailwind native to boot.
None of this is exotic. That’s the point. The elegance of a good design lies in how it’s all wired together and its ergonomics. I’ll go through each major part of the decision to elaborate.
React + TypeScript: composable type-safe building blocks
React and TypeScript are table stakes for me these days. React gives me ubiquity and familiarity; TypeScript brings structural stability. On solo projects, types aren’t “nice to have”—they’re that future-you who taps present-you on the shoulder and says, “you forgot this edge case.” I constantly reach for TS-driven refactors; it’s my favorite test suite.
tRPC: let the types be the contract
I skipped REST API design altogether in favor of tRPC. Why?
My only client is TypeScript-based, so custom endpoints with crystal-clear object shapes are a win-win for the frontend and backend alike. These definitions eliminate most sources of miscommunication (between devs and between machines as code gets updated), to wit:
- The contract is code. No OpenAPI drift, no client stubs, no “Did we change that enum?” Slack archeology.
- Frontend breakage is compile-time. If I reshape a payload on the backend, TypeScript draws red boxes around every consumer on the frontend. That feedback loop is hard to give up once you have it.
- Less glue code. No serializers, no client factories, no keeping types in sync manually. It’s all one typed surface.
tRPC + React Query: “real-time” via polite polling
I tried WebSockets. I tried SSE (Server-Sent Events). Both added significant friction because they required custom client code to manage. So I took a step back and remembered that my data needs so far are straightforward. After all, a human can’t tell the difference between “every 5 seconds” and “continuous” for stock ticks in a watchlist. The value is perceived immediacy, not sub-second fidelity. So I went with polling.
tRPC makes it trivial. It leverages React Query under the hood and gives us:
- Poll every 5s for changes to watched stocks.
- Type-safe end-to-end (backend → frontend).
- Automatic auth header injection, configured once in tRPC client setup.
- Retry on hiccups; back off when offline.
- Cache to keep the UI warm between navigations.
The result feels fully live without managing socket lifecycles, heartbeats, reconnection jitter, or multiplexing streams. And it’s debuggable with standard network tooling.
My Git history tells the long story of moving from WebSockets to SSE to HTTP Polling. It’s mostly a backend design story, so I’ll save it for my following posts.
Instant search: Load once, filter in-memory
The ticker vocabulary (3,000 stocks) is 38KB gzipped. I fetch it once when you open the search modal, cache it in React Query (staleTime: Infinity), and do all filtering client-side. The search code filters the list based on your query and then sorts it by relevance.
Here’s why this fits my needs so well:
- 3,000 items is small enough to brute-force filter on every keystroke, so I don’t need to debounce, throttle, or otherwise complicate the logic.
- Modern JS engines handle this in < 1ms (vs 200ms+ for API round-trip).
- React Query’s cache means the 38KB is fetched only once per session. I could optimize this further by persisting the cache, but until I know what traffic patterns look like on the site, it would be premature optimization.
Looking ahead: If the dataset grows to 50K+ tickers, I’d likely switch to a tree-based prefix search or server-side fuzzy search. For now, a flat array and reading from memory in the browser are the most straightforward approaches, and they work well.
State management: keep server state and UI state in their lanes
- Server state: This is our data; React Query owns it. It knows what’s fresh, what’s stale, and when to refetch.
- UI state: Local component state. Open/closed, selected tab, current input—keep it where it lives.
- Global state: The URL. If it should survive refresh or be shareable, it’s in the location.
I didn’t need Redux or a global store. If I ever do, it’ll be for something truly cross-cutting (think feature flags or auth), not to shuttle externally loaded data around.
Styling: Tailwind + shadcn/ui
Tailwind helps me move from “box on a page” to “this feels deliberate” quickly. shadcn/ui is ideal because it’s not a black-box dependency—you copy the components in, own the code, and can tailor it without waiting on a release. The ergonomics of “open the file and change the variant” beat chasing theme tokens through a compiled library.
Developer experience, in practice
- Types as GPS: Refactors become “change the source, follow the errors.”
- No client/server drift: tRPC makes the API a non-event.
- Real-time enough: Polling without ceremony, elastic with tabs and focus.
- Styling velocity: I spend time designing, not fighting CSS scopes.
What I’d change next time
- Start with polling. Save sockets for when they’re the only answer.
- Adopt React Query earlier. I wrote my own fetch wrappers, then ripped them out. It’s not just about fetching—it’s cache policy, dedupe, reactivity, and retries.
Closing
This frontend isn’t especially clever; it’s intentionally calm. React + TypeScript provide shape and guardrails. tRPC ensures the backend and frontend always speak the same language. React Query handles the heavy lifting of polling and caching. And Tailwind + shadcn/ui make it easy to pick the project back up later, rearrange parts, or change the design without fighting the framework.
It delivers “live” without a control plane, and it’s cheap—in dollars and in brain cycles.
Next up, the backend: Cloudflare Workers, Durable Objects, and why I chose dumb polling over “smart” sockets. It’s about getting the data on time, without the headache of maintaining a live connection.
📚 Blog series: How I build MyStocks
- 1.Why I built MyStocks
- 2.How I built the frontend of MyStocks(current)