I didn’t have much frontend experience. This post covers the struggles I ran into.
Starting Point: React + TypeScript + Plain CSS
React with TypeScript felt like the obvious choice — popular, good ecosystem, type safety. I started writing plain CSS modules for styling. Full control, right?
The problem wasn't the code. It was that I had no design vision. I'd write a component, look at it, and know it looked bad but not know how to fix it. What colors? How much padding? How should this align? I couldn't answer these questions. The feedback loop was: write code → look bad → feel stuck → repeat. This went on for weeks.
Tailwind CSS Didn't Solve the Real Problem
I switched to Tailwind CSS thinking it would help. It sped things up — utility classes are fast to write, no context switching between files. But Tailwind is a tool for people who already know what they want to build. It doesn't give you design vision, it just makes it faster to execute one. I still didn't know what I wanted.
I tried Figma. I couldn't make anything that looked good there either. The problem wasn't the tools.
I restarted the project multiple times during this period — changing UI approach, structure, and direction. It felt like progress but mostly wasn't. This lasted around 3-4 weeks. Eventually I accepted that I wasn't going to figure out design from scratch and looked for a component library.
shadcn/ui
I looked at Material UI and Ant Design. Both felt heavy and opinionated in ways that would fight me later. I wanted something that:
- Looked good out of the box
- Integrated with Tailwind
- I could actually own the code
shadcn/ui fit all of this. The key difference is that components are generated into your codebase rather than installed as a dependency. This gives full control over behavior and styling, without being constrained by a library’s abstraction. That turned out to matter a lot later when I needed to adapt components for React Native.
Next.js: Tried It, Left It
Around this time I migrated to Next.js. SSR, SSG, the whole thing. After a while I realized most of my components were client-side anyway. ASTRING is a chat app — it's dynamic content fetched after load, not static pages that benefit from SSR. I moved back to React with Vite. Faster dev server, simpler setup, no framework fighting back.
State Management: Jotai
Chat state is genuinely complex — active rooms, message lists, unread counts, real-time updates coming in from WebSocket, user presence. Redux felt like too much ceremony for this. Context API caused re-render problems as state got more interconnected.
Jotai worked well. Atoms are simple to create, updates are granular, and the mental model maps cleanly to "this piece of state, these components that care about it." Chat state in particular became much cleaner — each room's state is an atom, components subscribe only to what they need.
React Native: Why I Left Ionic
I built the mobile version with Ionic React first. Code reuse from the web was easy. But Ionic started showing limits for a chat app specifically — animations felt off, native components were lacking, the "native feel" wasn't there. Chat apps have specific UX expectations: smooth scrolling through message history, keyboard handling, swipe gestures, native-feeling transitions. Ionic's web-based approach couldn't deliver these well enough.
I moved to React Native with Expo. Expo makes the setup significantly easier — no manual native build configuration, good tooling, OTA updates.
Current Setup: Monorepo with pnpm Workspaces
Moving to React Native meant I had two apps — web (React + Vite) and mobile (React Native + Expo). Rather than duplicate code, I set up a monorepo with pnpm workspaces. Nothing fancy.
Shared packages:
- Services — business logic
- API clients — all backend communication
- Jotai atoms — shared state: rooms, chats, user cache
Web and mobile each have their own UI layer, but everything underneath is shared. State is defined once — a room list atom, a message cache atom, a user presence atom — and both platforms consume the same atoms. This means a bug fix or API change happens once, and state behavior is consistent across platforms.
For styling on React Native I use NativeWind — Tailwind for React Native. Same utility classes I use on web, works on mobile. Makes the styling consistent and fast.
shadcn/ui on React Native — since shadcn components live in my codebase, I adapted them for React Native manually. div → View, button → Pressable, CSS → NativeWind classes. It's tedious but straightforward, and the result is consistent components across both platforms without two completely separate design systems.
Where It Stands
The code is messy in places. The UI is functional but not something I'm proud of visually. The monorepo structure is working well. Mobile is better than Ionic was for the specific things chat needs.
The main thing I learned: frontend is a different skill set. I kept thinking better tools would solve a design problem. They didn't. What helped was accepting I needed pre-built components, adapting them rather than fighting them, and not over-engineering the state management.
Top comments (0)