10 React Best Practices for Clean and Scalable Code

text
$ node –version
v20.11.0
$ npm audit

npm audit report

cross-fetch <3.1.8
Severity: high
Regular Expression Denial of Service – https://github.com/advisories/GHSA-7gc6-9fqp-82jj
fix available via npm install [email protected]
node_modules/cross-fetch

128 vulnerabilities (42 high, 12 critical)

$ vite build

<— Last few GCs —>
[14022:0x65c2e00] 15232 ms: Mark-sweep 2032.4 (2048.5) -> 2031.1 (2048.5) MB, 1120.4 / 0.0 ms (average mutation fixed under 10% [last 5 GCs]) allocation failure; last resort GC in old space requested
[14022:0x65c2e00] 16340 ms: Mark-sweep 2031.1 (2048.5) -> 2031.1 (2048.5) MB, 1108.2 / 0.0 ms (average mutation fixed under 10% [last 5 GCs]) allocation failure; last resort GC in old space requested

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed – JavaScript heap out of memory
1: 0xb83f70 node::Abort() [node]
2: 0xa9490e [node]
3: 0xd63f20 v8::Utils::ReportOOMFailure(v8::internal::Isolate, char const, bool) [node]
4: 0xd642c7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate, char const, bool) [node]
5: 0xf419f5 [node]
6: 0xf42bdc v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
7: 0xf526a5 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
8: 0xf53510 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xf2db4e v8::internal::Heap::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
10: 0xf2dbd7 v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
“`


TO: Engineering Management, Frontend “Architects”

FROM: Senior Systems Architect (Infrastructure & Core R&D)

SUBJECT: Technical Audit of Project [REDACTED] – A Sinking Ship

I spent twenty years in the trenches of C++ writing memory-mapped I/O drivers and high-frequency trading engines where every byte was a liability. I have now spent forty-eight hours looking at your React 18.2.0 codebase. I feel like a structural engineer being asked to repair a skyscraper built out of wet cardboard and prayer.

The terminal log above isn’t a “glitch.” It is the sound of your application’s engine seizing because you’ve filled the crankcase with sludge. You are running Node 20.11.0, a modern LTS release, and you are still managing to blow the 2GB default heap limit during a production build. That takes effort. That takes a fundamental misunderstanding of how memory allocation and dependency trees work.

You’ve treated Vite 5.0.0 like a magic wand that excuses you from understanding the build pipeline. It isn’t. Your “development environment” is a house of cards, and the wind is picking up.

Here is the cleanup manifesto. Read it. Implement it. Or find another profession.


1. The useEffect Death Spiral: Stop Cooking the User’s CPU

In C++, if I wrote a loop that triggered its own exit condition to reset, I’d be fired before the binary finished linking. In your React code, you treat useEffect like a catch-all bucket for “stuff that should happen sometimes.”

I see components where a useEffect updates a state variable, which is then passed as a prop to a child, which triggers a callback, which updates the original state, triggering the effect again. You aren’t building a UI; you’re building a distributed denial-of-service attack against the user’s browser.

The react best way to handle useEffect is to not use it. If you can calculate a value during render, calculate it during render. If you are transforming data from props, do it in the function body. If you are handling a user event, put the logic in the event handler.

The dependency array is not a suggestion. It is a set of pointers. When you pass an object literal or an anonymous array into that dependency list, React performs a shallow comparison. Every time the parent re-renders, that object gets a new memory address. React sees a “change,” fires the effect, and the cycle repeats. You are thrashing the V8 garbage collector until it gives up and dies.

Directive: Every useEffect in this project must be audited. If it doesn’t have a specific, primitive-driven dependency array, it is a bug. If it’s being used to sync two states, it’s a design failure.

2. Context API: The Lazy Man’s Global Variable

You’ve discovered useContext and decided that prop-drilling is “too hard.” So, you’ve wrapped the entire application in a single, massive GlobalProvider.

This is the architectural equivalent of using a single global void* pointer in C to store your entire heap. It’s lazy, it’s dangerous, and it’s killing performance. Every time you update a single boolean—like isSidebarOpen—at the top level of your Context, every single component subscribed to that context re-renders.

React 18.2.0’s reconciliation engine is fast, but it’s not magic. When you force a re-render of 400 components because a user clicked a toggle, you are wasting cycles. You are forcing the browser to rebuild the virtual DOM tree, diff it, and patch the real DOM for no reason.

The react best way to manage state is to keep it as local as possible. If only two components need a piece of data, lift the state to their immediate common ancestor. If you absolutely need global state, use a library designed for atomic updates or split your Contexts into small, specialized providers. A ThemeContext should not be in the same provider as your UserAuthContext.

Directive: Break the GlobalProvider into five specific domains. Any component using useContext must be profiled to ensure it isn’t re-rendering more than once per user action.

3. Cargo Cult Memoization: Profiler or It Didn’t Happen

I see useMemo and useCallback sprinkled everywhere like holy water. This is cargo cult programming. You’re adding the overhead of dependency tracking and memory allocation for memoization caches to functions that take 0.02ms to execute.

Memoization is not free. You are trading memory for CPU cycles. In a system where you are already hitting heap limits, adding more cached objects is like trying to put out a fire with gasoline.

You use useMemo when you have a computationally expensive operation—like sorting a 5,000-row array or performing complex regex on a large string. You do not use it to memoize a string concatenation.

The react best approach to memoization is to leave it out until the React DevTools Profiler shows a bottleneck. If a component re-renders in under 16ms (the window for 60fps), you don’t need to memoize it. You are complicating the code and making it harder to debug for a performance gain that is literally invisible to the human eye.

Directive: Remove all useMemo and useCallback hooks that were added “just in case.” If you can’t show me a flame graph proving that a function is a bottleneck, the hook is deleted.

4. The 400kb Bundle Crisis: Tree-Shaking and Structural Integrity

Your production JS bundle is 1.2MB. After Gzip, it’s still 400kb. On a 3G connection, your app takes 8 seconds to become interactive. This is unacceptable. This is a failure of basic engineering.

Vite 5.0.0 uses Rollup under the hood. Rollup is good at tree-shaking, but it can’t shake what you’ve bolted down. You are importing entire icon libraries (import { * } from 'huge-icon-lib') just to use a single “ChevronDown” icon. You are importing heavy utility libraries like Moment.js when the native Intl object in modern browsers—and certainly in Node 20.11.0—does the job better and for zero bytes.

The react best methods for code-splitting involve React.lazy and dynamic import() statements. Your “Admin Dashboard” should not be loaded when a user is on the “Login” page. You are forcing the user to download the plumbing for the entire building when they’re just standing in the foyer.

Directive: Implement route-based code splitting immediately. Audit the package.json. Any library over 20kb must be justified in writing or replaced with a lighter alternative. If I see lodash imported without cherry-picking, someone is getting a formal warning.

5. Prop-Drilling vs. Composition: The Plumbing Problem

You’ve complained that prop-drilling is “messy.” So you use Context (see point 2) or you create “God Components” that take 50 props and pass them down.

In C++, we have the principle of Dependency Inversion. In React, we have Component Composition. Instead of passing userAvatarUrl through six layers of navigation components, pass the Avatar component itself as a child or a prop.

The current architecture is a series of leaky pipes. You’re passing data through components that don’t care about it, just to get it to a component that does. This makes the intermediate components impossible to test in isolation and forces them to re-render whenever the data changes, even if they don’t use it.

The react best way to structure a tree is to use composition to keep components decoupled. If a component doesn’t need to know about the data, it shouldn’t see the data. This isn’t just about “clean code”; it’s about reducing the surface area of your bugs.

Directive: Refactor the Navigation and Sidebar hierarchies. Use children props to inject content rather than passing data through the “pipes” of the layout.

6. The Dependency Landfill: npm install is Not a Strategy

Your node_modules folder is 1.4GB. You have three different CSS-in-JS libraries, two different state management wrappers, and a dozen “utility” packages that consist of five lines of code you could have written yourself.

Every dependency you add is a security risk (see the 128 vulnerabilities in your audit), a build-time bottleneck, and a potential runtime failure point. You are building on shifting sand. When one of these “micro-packages” gets deprecated or hijacked, your entire project goes dark.

We are using Vite 5.0.0. It is fast because it leverages ES modules. But it can’t save you from a dependency graph that looks like a bowl of spaghetti. You have circular dependencies in your internal modules that are making the HMR (Hot Module Replacement) fail, forcing a full page reload every time you change a CSS class.

Directive: No new dependencies without a peer review. We are moving to a “Zero-Dependency” mindset for utility functions. If you need to format a date, use the language features. If you need a debounced function, write the ten lines of code.


The Path Forward

This project is currently a liability. It is slow, it is bloated, and it is unmaintainable. We are not “delivering features” anymore. We are “recovering technical debt” until the system is stable.

I don’t care about your “vibe” or your “developer experience.” I care about the fact that the heap is overflowing and the user’s fans are spinning up to max speed because you don’t know how to manage a dependency array.

You will start by fixing the useEffect loops. Then you will dismantle the GlobalProvider. Then you will prune the node_modules landfill.

I will be monitoring the build logs. If I see another JavaScript heap out of memory error, I will start deleting components alphabetically until the build passes.

Get to work.

ARCHITECT’S DIRECTIVE:
1. Audit all 42 high-severity vulnerabilities by EOD.
2. Implement React.lazy on all top-level routes.
3. Remove useMemo from any function not processing more than 1,000 elements.
4. Replace all “God Contexts” with localized state or atomic providers.

No excuses. The hardware doesn’t lie. Your code is failing the machine. Fix the code.

Related Articles

Explore more insights and best practices:

Leave a Comment