Skip to content
Insights · Engineering

What to check before you inherit a React codebase

Eight checkable signals decide what an existing React application costs to own — the state model, Effect misuse, whether the data layer caches, Strict Mode, real TypeScript, bundle waste, tests on the paths that carry money, and what one major upgrade would cost. None of them is how the code looks.

Scroll
EngineeringSep 11, 20268 min readBy Salman Naqvi, Founder & CEO
What to check before you inherit a React codebase

Eight things decide what an existing React application will cost you to own, and not one of them is how the code looks. What manages state, and whether that decision is still load-bearing. Whether Effects are doing work that rendering should be doing. Whether the data layer caches or refetches on every mount. Whether Strict Mode is switched on, and what was deleted to keep it quiet. Whether the TypeScript is real, or any with extra steps. What the bundle ships that nobody uses. Which of the paths that carry money have tests. And what one major-version upgrade would actually cost. Each is checkable in an afternoon, several by somebody who does not write React at all, and together they separate a codebase you can extend from one you will quietly rewrite.

This is how most React engagements now begin. The buyer is not commissioning a front-end; they are taking one over — from a founder's own build, an offshore vendor whose contract ended, a contractor who left, or an AI coding tool that produced something that works. And React is what they are most likely to be handed. In Stack Overflow's 2025 Developer Survey, 44.7% of the 23,678 respondents who answered the web-frameworks question reported extensive development work in React over the past year, rising to 46.9% of the 19,460 professional developers, second only to Node.js at 48.7% (Stack Overflow, 2025 Developer Survey: Technology). An inherited React application is the industry's default inheritance, and what follows is the pass we run before quoting.

What manages state, and whether that decision is still load-bearing. The useful question is never which library won; it is whether the reason for the original choice still exists. A store with three reducers, wired through a page of boilerplate while every screen separately fetches its own data, is ceremony left from a decision nobody revisited. The opposite failure is one React context holding a value that changes on every keystroke, re-rendering everything beneath the provider whether or not it reads that value. Count the state libraries in package.json: more than one is an abandoned migration that will be finished at your expense. Then ask an engineer to name the single place a given piece of state lives — if the answer runs longer than a sentence, that length is the price of every future feature.

Whether Effects are doing work that rendering should be doing. This is the most reliable predictor of how a React codebase behaves under change, and React's own documentation is blunt about it: "Effects are an escape hatch from the React paradigm. They let you 'step outside' of React and synchronize your components with some external system like a non-React widget, network, or the browser DOM. If there is no external system involved (for example, if you want to update a component's state when some props or state change), you shouldn't need an Effect" (React, You Might Not Need an Effect, react.dev). Of state derived from props inside an Effect the page says the pattern "does an entire render pass with a stale value ... then immediately re-renders with the updated value," and of a chain — one Effect setting state that triggers a second — that "there are three unnecessary re-renders of the tree below." So the check is countable: get the ratio of useEffect calls to components, then read every Effect ending in a set call and ask which external system it synchronises with. The ones whose honest answer is "none" are your render loops and your flickering values.

Whether the data layer caches or refetches on every mount. React lists four downsides of fetching inside Effects, each a symptom visible in the running application. "Effects don't run on the server," so "the initial server-rendered HTML will only include a loading state with no data." The pattern "makes it easy to create 'network waterfalls'" because "you render the parent component, it fetches some data, renders the child components, and then they start fetching their data." It "usually means you don't preload or cache data," so "if the component unmounts and then mounts again, it would have to fetch the data again." And "it's not very ergonomic," because avoiding the bugs takes real boilerplate (React, Synchronizing with Effects, react.dev).

Two minutes in the network tab settles it. Navigate away from a data-heavy screen and back, and count the duplicate requests. Then click quickly between two records: the first request can land after the second and paint the first record's data under the second record's heading. React's fix is a four-line cleanup that discards the stale response — invisible on a fast connection, routine on a slow one. A codebase with a caching layer made a decision here; one that fetches on every mount made none.

Whether Strict Mode is on, and what was deleted to keep it quiet. Strict Mode is a development-only setting that makes components "re-render an extra time to find bugs caused by impure rendering" and "re-run Effects an extra time to find bugs caused by missing Effect cleanup" (React, StrictMode, react.dev). React states what that buys: it "remounts every component once after mount (state and DOM are preserved). This helps you find Effects that need cleanup and exposes bugs like race conditions early." A codebase with Strict Mode removed — and there is usually a commit message explaining that it caused double requests — has not fixed the double requests; it switched off the instrument that found them. Turn it back on in a branch and count what breaks: each failure is a missing cleanup production also lacks, appearing there as a leaked subscription or a duplicated write.

Whether the TypeScript is real, or any with extra steps. A .ts extension guarantees nothing; the switch that decides it is one line of configuration. The TypeScript documentation describes strict as a flag that "enables a wide range of type checking behavior that results in stronger guarantees of program correctness," equivalent to "enabling all of the strict mode family options." Two of them carry most of the value. Without noImplicitAny, "TypeScript will fall back to a type of any for a variable when it cannot infer the type," and "this can cause some errors to be missed." Without strictNullChecks, "null and undefined are effectively ignored by the language," which "can lead to unexpected errors at runtime" (TypeScript, TSConfig Reference, typescriptlang.org). Read tsconfig.json, then count the escapes: bare anys, casts through any, and the comment that suppresses an error instead of fixing it. Then ask what no compiler can answer — is the shape coming back from your API validated at the boundary, or merely asserted? A hand-written type over an unvalidated response is only a comment with a build step.

What the bundle ships that nobody uses. This is the one item a non-engineer can size unaided: the build prints the numbers. The HTTP Archive's Web Almanac put the median page at 558 kilobytes of JavaScript on mobile and 613 kilobytes on desktop in its 2024 edition, up from 359 and 391 kilobytes in 2019, and found "approximately half the bytes downloaded being unused during page load" — 206 kilobytes, or 44% of bytes delivered, at the median on mobile (HTTP Archive, Web Almanac 2024: JavaScript). React's remedy is about one line per route: lazy "lets you defer loading component's code until it is rendered for the first time" (React, lazy, react.dev). One chunk containing every route means the marketing page downloads the admin console. A date library imported whole or an icon set shipped entire is a visible number and a day's work.

Which of the paths that carry money have tests. Coverage percentage is the most misleading number here: it is trivially raised by testing the parts that never break. Do it the other way round: name the five flows where failure costs cash or credibility — checkout, a subscription change, a permission boundary, the export a customer depends on, whatever an AI feature is allowed to write — and for each, ask to be shown the test that would go red if it broke. Then prove it. A codebase at 80% coverage with nothing guarding checkout is worse than one at 20% that does: the first number sells a confidence the second never claimed.

What one major-version upgrade would actually cost. Dependency currency is a schedule question in a hygiene question's clothes. npm's documentation describes the report to ask for: "The audit command submits a description of the dependencies configured in your project to your default registry and asks for a report of known vulnerabilities," each advisory carrying "a name, url, id, severity, vulnerable_versions, and title" (npm, npm-audit, npm CLI documentation). The same page names where the cost hides: if the chain "extends all the way to the root project, and it cannot be updated without changing its dependency ranges, then npm audit fix will require the --force option." Force means a breaking change, so the list of fixes needing it is a list of small projects. The expensive item is almost never React itself; it is the three or four abandoned libraries around it that never shipped a compatible version, and each of those is a replacement rather than an upgrade.

The sequence, ordered to find the most in the least time. One: get it running locally from the written instructions, with a timer — the only honest test of the documentation. Two: read tsconfig.json and package.json, counting state libraries, type escapes, and stale major versions. Three: build it and read the chunk sizes. Four: run the suite, then break a line in the checkout path and confirm something goes red. Five: switch Strict Mode on and read the console. Six: navigate away from a data-heavy screen and back, network tab open. Seven: get the ratio of useEffect calls to components. Eight: run an audit and separate what upgrades cleanly from what needs forcing. None of it requires understanding the business logic, which is why it belongs first: you are measuring the cost of change, not grading the author.

Most inherited React applications are not bad. They are undecided — the state model never settled, the data layer never chosen, Strict Mode silenced rather than satisfied, the type system installed rather than switched on. Four decisions, a week each, skipped in a hurry and now blamed on the code. The rewrite instinct is expensive precisely because it discards the one asset the codebase certainly has: it works, and real users are on it.

Two adjacent pieces cover what this pass leaves out on purpose. The handover: what you must receive is the ownership half — repositories, accounts, domains, contracts — where failures are unrecoverable rather than merely expensive, because no refactor fixes an account nobody at your company can log into. What breaks when an AI-built prototype meets real users is the server side: authorisation, permission inheritance, migrations, error paths.

The consolidation behind Cove's operator portal began from this position — content, device provisioning and subscriptions living in three disconnected systems, reduced to one operator experience on a stack where React sits in front of Laravel, with device provisioning 10x faster afterwards. Running this pass before quoting is what any React development company should be willing to do, and it answers the question buyers actually ask: can this be taken over rather than started again. It is also what an AI product development company does before touching an inherited system, because the same eight answers decide whether an AI feature can be added to it at all. If you would rather have the failure modes ranked first, our free production-readiness diagnostic reads a description of what you have and returns what is most likely to bite soonest, saying where it is inferring rather than being told.

Find this useful? Tell Google to show you more of it.

Let's put AI to work in your business.

A 30-minute call. You bring the workflow or the roadmap — we'll tell you what's feasible, what it costs, and what we'd build first.

Book a call