NIPUNO. ← Log

What we actually find in legacy React codebases

Four places we keep finding the same problems, across different teams and different codebases. None of these are exotic — they're just the kind of thing that's easy to introduce one line at a time and hard to notice until someone goes looking.

The React upgrade nobody finished

Most "we're on React 18" claims mean the package.json says 18. The codebase itself is often still full of patterns from three major versions ago — class components with lifecycle methods sitting next to hooks, componentWillMount quietly deprecated but never migrated, effects that assumed React batched updates a certain way before concurrent rendering changed the rules.

The actual risk isn't the old patterns existing — it's that they were never revisited after the upgrade, so nobody knows which ones are load-bearing and which are just inertia. Bumping the version number is the easy 10%. Actually reconciling the codebase with what the new version expects is the part that gets skipped.

Node versions nobody's watching

A backend or build toolchain quietly running on an end-of-life Node version is one of the most common findings, and one of the easiest to miss because it doesn't show up as a bug — it shows up as nothing, until a dependency drops support, a security patch never arrives, or a deploy platform stops supporting the runtime and it becomes an emergency instead of a scheduled upgrade.

This is a maintenance-discipline problem more than a technical one: nobody owns "check the Node version" as a recurring task, so it only gets noticed when something breaks.

useEffect doing state's job

The single most common React-specific issue: an effect computing something that should have been a plain derived value, or a useState + useEffect pair reimplementing what a single calculation during render would do. Every one of these adds a render cycle, adds a place where the dependency array can be subtly wrong, and adds a spot where two pieces of state can drift out of sync.

The second most common: effects that fetch data without handling cancellation, so a fast component unmount or a rapid prop change leaves a stale response landing after a newer one — a race condition that's invisible in dev and shows up as "sometimes the wrong data flashes" in production, which is exactly the kind of bug that's expensive to track down after the fact and cheap to prevent with a cleanup function.

React Query, mirrored into a second store

The pattern we see most often isn't a cache-key typo — it's a query result getting manually copied into Redux (or similar) right after it loads:

const useCurrentUser = () => {
  const { isAuthenticated } = useAuth();
  const dispatch = useDispatch();
  const query = useQuery(['user'], fetchUser, { enabled: isAuthenticated });

  useEffect(() => {
    if (query.status === 'success' && query.data) {
      dispatch({ type: 'SET_USER', payload: query.data });
    }
  }, [query.status, query.data, dispatch]);

  return query;
};

React Query already owns caching, loading state, and reactivity for this data — that's the whole point of using it. Mirroring the result into a second store creates a second source of truth that can silently drift: a mutation that invalidates or updates the query cache doesn't touch Redux, so two parts of the UI can end up disagreeing about the same piece of data depending on which one they read from. It's usually a sign the team hasn't fully trusted the library yet — still reaching for "fetch once, push into global state, read from global state everywhere" out of habit, when colocating the query where it's needed would have worked fine (React Query dedupes by key automatically, so this was never really about avoiding extra fetches).

Why this list, and why now

None of these four are hard to fix individually. What's hard is knowing which ones are lurking in a specific codebase, how much behavioral risk each one actually carries, and which order to fix them in without breaking something else in the process — which is the same triage question from the last post, applied to a specific stack instead of stated in the abstract.

If any of these sound familiar, that's usually not a sign the team did something wrong — it's a sign the codebase has been shipping features faster than anyone's had time to look back.

Have a codebase like this? Get in touch →