Base
- TypeScript - A typed superset of JavaScript designed with large scale applications in mind
- Zod - TypeScript-first schema validation with static type inference to check types at the runtime level as well as the type level.
- NextJS
- Client only: use it as a file-based router with clean suspense and error boundary.
- With server side rendering (SSR): set up minimal API routes in edge runtime for better SEO and snappier user experience, which usually means faster initial load.
- Full stack: serverless infrastructure for your entire JavaScript backend (v8 & Node) for easy code sharing and end-to-end type safety.
- Limitations: NextJS App Router uses React Server Component (RSC) by default, requiring some minimal extra work to opt out. CI/CD is perfect when deploying to Vercel, which can get pricey. Other platforms may lack support for all NextJS’s features on the server side. Even with Vercel, edge and Node runtime both come with some limitations.
- Vite - Feature rich and highly optimized frontend tooling with TypeScript support out of the box. See my other blog post on how it provides one of the best developer experiences for frontend development.
- limitations: React developers use it with React or Remix (unstable), which means foregoing a file-based routing system offered by NextJS out of the box. And, as it’s almost always used for client-side-only projects, some non-trivial work is required if you need server side rendering.
State Management
I categorize projects I work on into:
- Not much state: mostly static sites like developer documentation, news websites and blogs.
- Mostly client state: “desktop apps on the web”, like Figma, Google Sheets, web games.
- Mostly server state: most “CRUD” apps, including most SaaS apps, e-commerce, social networks.
For not much state, don’t use any state manager. For mostly client state, Redux is necessary. For mostly server state, Redux is not well-suited for the challenges, including:
- Fetching the right data at the right time.
- Making sure the data doesn’t grow stale by revalidating it (fetching it again), and customizing the revalidation for the specific situation.
- Caching the data so that multiple components don’t repeat requests unnecessarily.
- Optimistic UI updates, so that we “predict” how network requests will resolve.
- Pagination, requesting small slices of the data and letting the user pull new data as-needed.
As almost all my projects nowadays fall into the mostly server state category, I’ve found my sweet spot to be:
- React Query - Declarative, always-up-to-date auto-managed queries and mutations, like Apollo for GraphQL. Out-of-the-box infinite queries, suspense queries, prefetching, and more. See React Query as a State Manager on why a data fetching tool is categorized under State Management.
- Zustand - An unopinionated, small, fast and scalable bearbones state-management solution for purely client states.
UI
I tend to not use full-on component libraries like Material UI or Next UI because:
- They come with all sorts of bells and whistles that I can’t remove, causing bloat in my code and slowing down performance. For example:
- They may use CSS-in-JS libraries under the hood, which duplicates styling libraries I use for my own code. I usually just use vanilla CSS or Tailwind.
- They may use a big JavaScript library to support their elaborate animations, which IMO is unnecessary. Most animations I need can be done with only CSS.
- They have complex and inflexible component API. Most of the time, the time I spend reading their docs to customize a component is longer than what it takes for me to build the component from scratch.
- If I don’t customize the components, my app looks like everyone else’s that didn’t bother to customize their UI.
I used to not use any UI library / framework. For example, letsgo, Carrrd, and this personal site were all built with vanilla CSS and some unstyled accessibility library like Radix UI. This was until I discovered the following:
- shadcn - Ultra minimalistic, flexible and re-usable components for building high-quality, accessible design systems and web apps. Uses Radix UI under the hood for accessibility, and comes with a few things that I like:
- tailwindCSS - CSS utility classes.
- Class Variance Authority - a sensible way to build type-safe UI components with variants.
Shadcn saves me so much time scaffolding the file structures and composition for building UI components while giving me all the flexibility to customize them. I would recommend it to any team who has a designer!
Forms
- React Hook Form - super nice validation and handling with Zod and shadcn’s form components.