React Best Practices: Build Scalable Apps Like a Pro

TO: CTO, [REDACTED] Enterprise Solutions
FROM: Lead Forensic Consultant (Contract #882-B)
DATE: October 24, 2024
SUBJECT: Technical Debt Post-Mortem & Recovery Plan: Project “Phoenix” (Current Status: Incinerated)


$ npm audit
# npm audit report

axios  0.21.1 - 1.5.1
Severity: high
Server-Side Request Forgery in axios - https://github.com/advisories/GHSA-848p-39gv-v7gv
fix available via `npm install [email protected]`
node_modules/axios

lodash  <4.17.21
Severity: critical
Regular Expression Denial of Service (ReDoS) in lodash - https://github.com/advisories/GHSA-29mw-wpgm-h2jf
fix available via `npm install [email protected]`
node_modules/lodash

48 vulnerabilities (12 moderate, 29 high, 7 critical)

$ npm run build
> [email protected] build
> tsc && vite build

src/components/Dashboard/Metrics.tsx:44:21 - error TS2339: Property 'data' does not exist on type 'any'.
src/hooks/useLegacyAuth.ts:112:9 - error TS7006: Parameter 'user' implicitly has an 'any' type.
src/context/GlobalState.tsx:254:12 - error TS2532: Object is possibly 'undefined'.

[vite:build-html] Unable to parse HTML: Unexpected token (142:4)
error during build:
Error: Build failed with 142 errors. This is a disaster.

I’ve spent the last 72 hours staring into the abyss of your codebase, and the abyss didn’t just stare back—it filed a restraining order. Your previous engineering team didn’t build a React application; they built a digital suicide pact. I’m currently on my sixth double-espresso, and I’ve reached the level of caffeine-induced clarity where I can see the individual fiber nodes screaming for mercy.

If you want to avoid bankruptcy, we need to stop pretending this is “legacy code” and start treating it like a hazardous waste site. Here is the forensic breakdown of why your application is failing and the “react best” practices required to stabilize this wreckage.

1. The UseEffect Suicide Pact: A Study in Infinite Loops

The most egregious violation of human rights in this codebase is the abuse of the useEffect hook. Your previous developers treated useEffect like a catch-all lifecycle bucket, ignoring the fact that we are now on React 18.3.1. I found components where useEffect was being used to sync state from props, triggering a secondary render, which then triggered another effect, resulting in a cascade of re-renders that would make a GPU cry.

The Garbage:

// Found in src/components/User/ProfileCard.tsx
const ProfileCard = ({ userData }) => {
  const [internalName, setInternalName] = useState('');

  // This is a crime. Syncing props to state in an effect.
  useEffect(() => {
    if (userData.name) {
      setInternalName(userData.name);
    }
  }, [userData.name]);

  return <div>{internalName}</div>;
};

The Refactored “React Best” Approach:
In React 18.3.1, we don’t sync state in effects. We derive it. If you need a value based on props, you calculate it during the render phase. If you need to reset state when a prop changes, you use a key.

// Refactored for sanity
interface ProfileCardProps {
  userData: { name: string };
}

const ProfileCard = ({ userData }: ProfileCardProps) => {
  // Derived state. No effect needed. No double-render.
  const internalName = userData.name || 'Anonymous';

  return <div>{internalName}</div>;
};

The previous team clearly didn’t understand that useEffect is for escaping the React rendering cycle to talk to external systems (APIs, WebSockets, Manual DOM manipulation), not for managing internal data flow. By removing these redundant effects, I’ve already cut the CPU idle time by 40%.

2. Context API as a Global Garbage Disposal

I found a single GlobalContext.tsx file that is 4,500 lines long. It contains everything from the user’s authentication status to the toggle state of a sidebar on the settings page. Because every component in the app is wrapped in this provider, every single state change—even a character typed into a search bar—triggers a full-tree re-render.

This is not state management; it’s a performance bottleneck disguised as “clean code.” To implement “react best” patterns, we must move away from this monolithic context.

The Failure:
The previous team used Context for high-frequency updates. In React 18, while concurrent features help, they cannot save you from a context that forces 400 components to re-evaluate their virtual DOM nodes every time a “isHovered” boolean changes.

The Recovery Plan:
1. Atomic State: Use a library like Zustand or Jotai for global UI state.
2. Server State: Move all API data to TanStack Query (React Query).
3. Context for DI: Use Context only for truly static dependency injection (e.g., Theme, Localization).

3. Prop Drilling as a Form of Torture

While the previous team overused Context for some things, they simultaneously practiced extreme prop drilling for others. I found a DashboardLayout component passing a handleLogout function through twelve layers of components that didn’t need it, just to reach a LogoutButton in the footer.

This makes the code impossible to refactor. If I change the signature of handleLogout, I have to touch thirteen files. This is where “react best” practices regarding component composition come in.

Instead of drilling, we should be composing.

The Garbage:

<PageHeader user={user} onLogout={onLogout} settings={settings} />
// ... inside PageHeader
<UserMenu user={user} onLogout={onLogout} />
// ... inside UserMenu
<LogoutButton onLogout={onLogout} />

The Refactored Approach (Composition):

<PageHeader>
  <UserMenu>
    <LogoutButton onClick={onLogout} />
  </UserMenu>
</PageHeader>

By using the children prop or specialized component slots, we decouple the layout from the logic. This isn’t just about aesthetics; it’s about preventing the entire application from becoming a brittle house of cards.

4. Memoization Theater: Why Your App is Still Slow

I see useCallback and useMemo sprinkled everywhere like holy water on a possessed child. The problem is, the previous team used them incorrectly. They were wrapping simple primitive calculations in useMemo (which actually costs more in memory allocation than the calculation itself) while passing anonymous functions to memoized child components, breaking the memoization anyway.

$ npx react-devtools-profiler
# Profiling Results:
# Component "LargeTable" re-rendered 142 times in 2 seconds.
# Reason: "Props changed: (onRowClick)"

The Technical Reality:
In React 18.3.1, the reconciliation engine is fast, but it’s not magic. If you pass onRowClick={() => doSomething()} to a component wrapped in React.memo, that component will re-render every time the parent renders because the function identity changes.

The “React Best” Fix:
We only memoize when the cost of re-rendering outweighs the cost of the dependency check. And when we do it, we do it correctly.

// Garbage: Anonymous function breaks memoization
<MemoizedTable onRowClick={() => dispatch({ type: 'SELECT' })} />

// Refactored: Stable identity
const handleRowClick = useCallback(() => {
  dispatch({ type: 'SELECT' });
}, []); // Empty deps = stable identity

<MemoizedTable onRowClick={handleRowClick} />

Furthermore, we should be utilizing useTransition for non-urgent updates. Your “Search-as-you-type” feature is currently locking the main thread because it’s trying to filter 10,000 rows synchronously. By wrapping that state update in startTransition, we allow React to keep the input field responsive while the list filters in the background.

5. Folder Structure as a Psychological Warfare Tactic

The current project structure is “Folder by Type.” You have a components folder with 300 files in it. You have a hooks folder with 50 files. Finding the logic for the “Invoice” module requires jumping between six different top-level directories. It’s a cognitive tax that your developers are paying every hour.

I am moving us to a “Feature-based” architecture. If the “Invoice” module dies, I want to be able to delete one folder and have the rest of the app still compile.

Proposed Structure:

src/
  features/
    invoicing/
      components/
      hooks/
      api/
      types.ts
      index.ts (Public API)
    auth/
    dashboard/
  shared/
    components/ (Buttons, Inputs, etc.)
    hooks/
  lib/ (Axios config, etc.)

This isn’t just about organization; it’s about encapsulation. A developer working on auth shouldn’t be accidentally importing a private utility from invoicing.

6. TypeScript: Use It or Resign

I found the word any used 1,402 times in the codebase. Using any in a TypeScript project is like buying a high-end security system and then leaving the front door wide open and the keys in the lock. It’s a professional insult.

The build errors I listed at the start of this report are the result of “Type Lying.” Your developers told the compiler “trust me, I know what I’m doing,” and then they didn’t.

The Mandate:
1. No more any. Ever. If a developer uses any, the PR is automatically rejected.
2. Strict Mode: We are enabling strict: true in tsconfig.json. Yes, it will break the build. Yes, we will fix it.
3. Zod for Validation: We will use Zod to validate API responses at the network boundary. We will no longer assume the backend is sending us a valid object; we will prove it.

// The "React Best" way to handle API data
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  role: z.enum(['ADMIN', 'USER']),
});

type User = z.infer<typeof UserSchema>;

export const fetchUser = async (): Promise<User> => {
  const response = await api.get('/me');
  return UserSchema.parse(response.data); // Throws early if data is corrupt
};

7. The Dependency Graveyard

Your package.json looks like a museum of abandoned JavaScript projects from 2017. You have three different CSS libraries (Styled Components, Tailwind, and some random SCSS files), two different state management libraries, and a version of moment.js that is so old it probably thinks the Queen is still alive.

$ npm list --depth=0
├── @tanstack/[email protected] (Wait, why is this here but not used?)
├── [email protected] (Why?)
├── [email protected] (Deprioritize this immediately)
├── [email protected]
├── [email protected]
└── [email protected]

We are stripping this back. We are moving to Tailwind for styling because I don’t want to wait 30 seconds for Styled Components to inject CSS-in-JS into the head of the document. We are replacing moment.js with date-fns or dayjs to save 200KB of bundle size.

8. Concurrent React and the Future of the App

We are running React 18.3.1, yet we are using none of its features. The app feels “janky” because the main thread is constantly blocked by heavy renders.

We will implement Suspense for data fetching and code-splitting. Currently, your main.js bundle is 4.2MB. That is unacceptable. A user on a 3G connection would have to wait 15 seconds before they see a single pixel.

The Recovery Plan for Bundling:
1. Route-based Splitting: Using React.lazy for every major route.
2. Component-based Splitting: Lazy loading heavy components like charts or editors.
3. Tree Shaking: Auditing our imports to ensure we aren’t pulling in the entire lodash library when we only need debounce.

9. The “React Best” Implementation Roadmap

To save this company, we need a three-phase approach. We cannot do this “seamlessly” (I hate that word, nothing about this will be seamless). It will be painful, it will be loud, and it will be expensive.

Phase 1: Stabilization (Weeks 1-4)

  • Fix all 48 high-severity vulnerabilities.
  • Enable TypeScript strict mode and fix the resulting 1,000+ errors.
  • Implement a global error boundary to stop the “White Screen of Death.”
  • Replace the monolithic GlobalContext with Zustand for UI state.

Phase 2: Performance Surgery (Weeks 5-8)

  • Migrate all data fetching to TanStack Query.
  • Audit every useEffect and remove 80% of them.
  • Implement useTransition for search and filter operations.
  • Enforce the new feature-based folder structure.

Phase 3: Modernization (Weeks 9-12)

  • Complete the migration from Styled Components to Tailwind.
  • Implement Suspense and React.lazy to bring the initial bundle size under 500KB.
  • Establish a rigid testing suite (Vitest + Testing Library) to ensure this disaster never happens again.

10. Final Assessment

The previous team failed because they treated React like a jQuery plugin. They ignored the fundamental principles of one-way data flow, component purity, and the reconciliation lifecycle. They built a system that is “clever” instead of “predictable.”

I don’t care about “vibrant ecosystems” or “empowering developers.” I care about code that doesn’t break when a user clicks a button. I care about an application that doesn’t drain a laptop’s battery in twenty minutes.

If you follow this plan, we can turn “Phoenix” into something that actually flies instead of just smoldering in a pile of its own technical debt. If you don’t, I suggest you start looking for a buyer for the company’s office furniture now, because this software will not survive another quarter of scaling.

I’m going to get another coffee. Don’t call me unless you’ve signed off on the TypeScript mandate.

End of Report.

Related Articles

Explore more insights and best practices:


$ git commit -m "chore: initial cleanup of the absolute disaster left by the previous team"
[main 4a2b1c3] chore: initial cleanup of the absolute disaster left by the previous team
 142 files changed, 4502 insertions(+), 8921 deletions(-)
 rewrite src/App.tsx (98%)
 delete mode 100644 src/context/GlobalState.tsx
 create mode 100644 src/store/useUIStore.ts

Leave a Comment