Building a React app without a checklist is just luck
I built my first production app in 2017 using what amounted to instinct and bad habits. Three months later I was rewriting half the state management because I'd thrown everything into Redux before understanding what React's built-in patterns could actually handle. If someone had handed me a practical checklist back then, I would have saved roughly sixty hours of debugging and re-architecture. The React Practical Guide Checklist below isn't some polished framework documentation. It's the stuff I actually consult when starting a new project or joining an existing codebase. It's organized by decision points rather than by feature category because that's how the work actually happens. You don't decide components first, then state, then routing. You make tradeoffs across all of them simultaneously and adjust as the project reveals its shape.
React Practical Guide Checklist
Project setup and tooling decisions Pick Next.js or Vite immediately. Both are viable. Next.js gives you server components, file-based routing, and API routes out of the box. It adds build complexity and couples you to the framework's conventions. Vite is lighter, faster to boot, and lets you choose your routing and server strategies separately. If your project needs SSR or static export from day one, go Next.js. If you're building a dashboard, admin panel, or SPA that talks to a separate backend, Vite is usually the faster path. Do not use create-react-app. It hasn't been maintained since late 2023 and is functionally dead. Set up TypeScript from the start. Not "eventually." The type system catches about forty percent of runtime errors before they hit a browser, and refactoring becomes tractable instead of terrifying. I've seen projects spend two full weeks untangling prop drilling after skipping types because someone thought "we'll add them later." They never do.
Choose your package manager once and stick with it. npm, pnpm, or bun. pnpm is my preference because it uses content-addressable storage and hard links, which makes installations roughly three times faster than npm and halves disk usage on monorepos. This matters more than people admit when you're pulling dependencies every day. State management: what actually belongs where Most state lives in three places and nowhere else. Component local state via useState or useReducer for UI-only concerns like form fields, toggle visibility, and temporary selections. Context for data that genuinely needs to be global, like auth sessions or theme preferences. External state via TanStack Query or SWR for server data. That third category is where most teams make mistakes.
The common failure mode is putting API data into Zustand, Redux, or Context. Server state and client state have fundamentally different concerns. Server state changes when the network changes, when the cache expires, when the user returns to a tab. Client state changes when the user clicks a button. Mixing them causes stale data bugs that are nearly impossible to reproduce reliably. TanStack Query handles pagination, caching, deduplication, and background refetching automatically. Setting it up takes about twenty minutes and removes an entire class of bugs from your codebase. Here's the counter-intuitive part: Context is not a state management solution. It's a dependency injection mechanism. When you put large objects or frequently updating values into Context, every consumer re-renders because Context triggers re-renders on reference change, not on value change. I once tracked down a performance regression where a Context provider holding an authentication object was causing the entire page to re-render on every token refresh. Splitting the context into separate providers for stable and volatile data fixed it instantly. Component architecture patterns
Use composition over inheritance and over prop drilling. React was designed around children props and composable components. The render prop pattern and higher-order components are largely historical at this point. Compound components handle related state across multiple sibling elements elegantly. A tabs component, for example, keeps active tab state inside a provider and lets individual tab panels and tab buttons read from it without any prop drilling through intermediate layers. Keep components small by splitting them at responsibility boundaries, not at file size boundaries. A fifty-line component that handles one thing is better than a twenty-line component that handles three. The rule of thumb I use is: if renaming a component would require renaming three other things in the same file, they should probably be separate. Custom hooks are where you extract reusable logic, not where you put side effects that belong in components. A custom hook should be pure with respect to React's rendering cycle. If your hook sets up event listeners, subscriptions, or timers, those should be cleaned up in the component that calls the hook, not buried inside the hook itself where they become invisible to the component lifecycle.
A specific problem I ran into Last year I was working on a real-time dashboard that pulled data from multiple WebSocket sources and displayed them in a grid of widgets. Each widget was a separate React component with its own subscription. The problem wasn't the subscriptions themselves. It was that when any single widget received a new message, the parent component's state updated, and React re-rendered every widget in the grid even though most of them had nothing to do with that data update. The grid had twelve widgets. Eleven of them were doing useless work on every message from the WebSocket. The fix wasn't adding useMemo everywhere. That's the wrong tool for this problem. The actual solution was separating the data layer from the presentation layer completely. I created a dedicated store that held the WebSocket data and used a selector pattern so each widget subscribed only to the slice of data it needed. When component B's data changed, component A didn't re-render at all because its selector returned the same reference. This reduced unnecessary re-renders from roughly twelve per message to one per message, which is a meaningful difference when you're processing dozens of messages per second.
If you're dealing with high-frequency updates in any React app, the immediate instinct to reach for useMemo or React.memo on every component will give you marginal gains at best and false confidence at worst. The real win comes from architectural separation of data and presentation, combined with fine-grained subscriptions. Performance: what matters and what doesn't Optimizing React performance before you have a measurement is mostly theater. Profiling with the React DevTools profiler should come before any optimization attempt. The typical pattern is: identify the actual bottleneck, fix that, move on. Don't preemptively wrap everything in memo and don't chase micro-optimizations on components that aren't causing problems.
Virtualization is necessary when you're rendering more than a couple hundred items in a list. Libraries like react-window or tanstack-virtual handle this. Without virtualization, a list of five hundred rows means five hundred DOM nodes existing simultaneously. That's not a React problem. That's a browser problem. Virtualization renders only the visible rows plus a small buffer, which keeps DOM node count roughly constant regardless of list size. The bundle size check is a real gating factor. I've seen initial load times double simply because a developer imported an entire UI library when they only needed three components from it. Use bundlephobia.com or include-analyze before adding any dependency. Tree-shaking works when libraries are structured properly, but many popular packages have weak module boundaries that defeat it. Testing strategy
Test behaviors, not implementation details. A test that checks for a specific component tree structure breaks every time you refactor the internal implementation. A test that checks "when the user clicks submit, the form validates and sends data to the API" survives refactoring. React Testing Library enforces this philosophy by design because it queries elements the way a user would interact with them. Unit tests for pure logic and utility functions. Integration tests for component interactions and data flow. End-to-end tests only for critical user paths that touch multiple systems. The golden rule is: the deeper in the stack a test lives, the more expensive it is to run and maintain. You want the fewest possible e2e tests and the most possible unit tests at the correct abstraction level. Mock the network layer, not the React internals. Using msw for network mocking gives you consistent test behavior regardless of whether your app uses fetch, axios, or TanStack Query. Mocking at the React level creates tests that are tightly coupled to your implementation and break constantly.
When React is the wrong tool This deserves a straightforward answer. If your application is primarily static content with minimal interactivity, React adds unnecessary overhead. A static site generator or even plain HTML and CSS is faster to build and faster to load. If you're building a native mobile app and your team has no web experience, React Native introduces an entirely different set of constraints that may not be worth the learning curve compared to Flutter or native development. Server components in Next.js are a significant shift in how you think about data fetching and component architecture. They solve real problems around bundle size and data fetching patterns, but they also introduce a mental model split between server and client components that can confuse teams. If your team has never dealt with this distinction before, the onboarding cost is real. Plain client-side rendering with TanStack Query handles the vast majority of applications without requiring server components.
The short version Start with Next.js or Vite and TypeScript. Use TanStack Query for server state and context sparingly. Separate data from presentation when dealing with high-frequency updates. Profile before optimizing. Test behaviors not implementation. Import only what you need. React works well when you understand where its strengths actually are instead of treating it as a general-purpose UI framework and forcing it to solve problems it wasn't designed for.