Getting Useful React Code in Production

Most React projects ship with bugs that come from skipping the basic stuff, not from misunderstanding complex patterns. I keep a running list of things I check before anything touches production. People have asked for it enough times that I decided to write it out properly. The first thing most teams get wrong is component boundaries. You will find yourself creating components that are too small because you read an article about atomic design. A button wrapper around another button doesn't help anyone. The real signal is prop count. If a component takes more than six props, you are probably passing data through it that it should own itself. Lift state up, or use context, or just accept that the component needs to be bigger. I ran into a specific issue last year where a parent component was calling useState for three separate pieces of form data. The child components received each value as a separate prop. When one input changed, the entire form re-rendered because React compares props reference-by-reference and the parent's render function recreated all three props objects on every keystroke. The fix was wrapping those three useState calls into a single useReducer that returned a plain object. Since object references stayed stable until an actual action dispatched, re-renders dropped by about eighty percent on that page. That is the kind of thing nobody warns you about until you are profiling a slow form.

React Field Guide Checklist

Here is the actual list I use. It is not exhaustive. It is what I have found matters in practice. Before committing any component: verify that every useMemo and useCallback actually prevents a downstream re-render by checking what it protects. I spent two days hunting a performance issue where a useMemo was calculating correctly but being passed as a prop to a component that did not wrap the receiving part in React.memo. The memoization was pure overhead at that point. Removed it, performance improved. Effect cleanup is non-optional. Every useEffect that sets up a subscription, an event listener, or an interval needs a cleanup function. Browsers do not clean these up when you navigate away in SPAs. I have seen memory leaks where a stale interval kept firing after component unmount because someone copy-pasted an effect from Stack Overflow and forgot to add the return clause.

Dependency arrays in useEffect are stricter than they look. If you reference a variable inside the effect body, it must be in the dependency array. ESLint's exhaustive-deps rule exists for this exact reason. Disabling that rule with a comment is almost always the wrong call. The rare exceptions are when you genuinely need a reference to a mutable ref or when the dependency is a function defined outside the component, but even those cases are uncommon. I have maybe disabled that rule on four effects across five years of React work. Keys matter more than people think. Using array index as a key is fine for static lists. It becomes a problem the moment your list is sortable, filterable, or has items that can be added and removed in the middle. React uses keys to match elements across renders. A shifting key means React destroys and recreates DOM nodes that could have been reused. This causes input focus loss, animation resets, and state corruption in list items. Use stable unique identifiers. UUIDs, database IDs, anything that does not change when the list order changes. State colocation is harder than it sounds. The principle is simple: keep state as close to where it is used as possible. In practice this means moving state upward when multiple siblings need it, but then you are passing callbacks down, and then you have prop drilling, and people reach for context as a solution. Context is not a state management library. It is a delivery mechanism. Using context for values that change frequently will cause unnecessary re-renders across your entire component tree because every consumer subscribes to every change. I learned this the hard way when a theme toggle triggered a full app re-render because someone put the entire app state into a single context provider.

Get the Full Details

Freelance React Developer's Checklist [Infographics and Free Email ...
Freelance React Developer's Checklist [Infographics and Free Email ...

Custom hooks are where logic goes to die if you do not name them right. A hook named useFetch is fine. A hook named useHandleAllTheDataForTheDashboardWithSideEffects is not. The naming convention matters because React hooks rules are enforced by the linter based on function names starting with use. Bad names cause bugs that the linter will let slip through if you misspell use in the middle of a name. I have written three hooks that were silently treated as regular functions because I wrote useDataFetcher as handleUseData. Took me an hour to figure out what was happening. Server components changed the game for data fetching. If you are using Next.js App Router or similar, stop putting fetch calls inside useEffect for initial data. Server components can await data directly. The tradeoff is that server components cannot use state or effects, so you end up splitting components into server and client versions. This is normal. It feels messy at first but it is the correct architecture. Client components that wrap server-fetched data in useState and useEffect are fighting the framework. Form handling in React is still a mess. Libraries like React Hook Form and Formik exist for a reason. Writing forms from scratch with useState for every input works until you have validation, async submission, and error display. Then it is a lot of boilerplate. I have used both libraries and both have gotchas. React Hook Form's Controller component is necessary for integrating with non-standard inputs like date pickers. Formik's validation schema runs on every render by default unless you configure it otherwise. Neither is perfect. Choose one and stick with it across the project.

Testing should cover behavior, not implementation. A test that checks whether a component calls useState is brittle. A test that checks whether clicking a button changes the visible text is durable. Render the component, interact with it, assert on the output. That is it. I once rewrote forty tests in a single afternoon because someone had been asserting on internal component structure. The tests passed until someone refactored an implementation detail, then they all failed for no reason. Bundles are a real concern. Tree shaking only works if packages are written to support it. Import the whole lodash library instead of individual functions and your bundle size does not care. Dynamic imports for heavy components can cut initial load time significantly. A charting library that only renders on one page of your app should not be in the main bundle. React.lazy with Suspense handles this, but make sure your Suspense boundary is placed correctly or you will see blank screens instead of loading states. Error boundaries are still awkward. They catch rendering errors, not event handler errors or promise rejections. You need to wrap them around component trees, not individual components in many cases. The class-based syntax is the only way to make them work currently. Function component equivalents exist as libraries but they are not part of React itself. Plan for this limitation when designing your error handling strategy.

The checklist above is not a complete reference. It is the things I have found that actually matter after building and maintaining React applications for several years. The gaps in a team's knowledge are usually not in the documentation. They are in the edge cases that documentation does not cover because they depend on your specific setup.

React Self-Evaluation Checklist | PDF
React Self-Evaluation Checklist | PDF