ReactArchitectureTypeScriptPerformance
Building React Apps in 2024: Lessons from Scaling Production Applications
Practical patterns for maintainable React architecture learned from real-world projects
5 mins read
When I first inherited a large React codebase four years ago, I felt like I was navigating a maze of components. Through refactoring pains and performance battles, here's what I've learned about structuring React apps that don't collapse under their own weight.
Component Organization That Actually Works
The Folder Structure That Saved My Sanity
/src
/features # Product features
/components
/ui # Generic UI pieces
/layout # App-wide layouts
/lib # Utilities/config
/providers # Context providers
/types # TypeScript definitions
This structure helped me avoid the "components dumpster" problem. Here's how we use it:
// features/dashboard/components/MetricCard.tsx
// Feature-specific components stay with their feature
// components/ui/Button.tsx
// Reusable across the entire app
export function Button({ children }: { children: React.ReactNode }) {
return <button className="...">{children}</button>;
}
State Management: Choosing Your Weapons
When to Use What (From My Mistakes)
- useState - For simple UI state:
const [isMenuOpen, setIsMenuOpen] = useState(false);
- Context - For app-wide settings:
// lib/theme-context.tsx
const ThemeContext = createContext<"light" | "dark">("light");
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme] = useSystemTheme(); // Some custom hook
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
}
- Redux Toolkit - For complex app state:
// features/cart/cartSlice.ts
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] as CartItem[] },
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
state.items.push(action.payload);
},
},
});
Performance Tricks That Actually Matter
The Memoization Sweet Spot
// components/product/ProductList.tsx
const ProductList = memo(({ products }: { products: Product[] }) => {
return products.map((product) => (
<ProductCard key={product.id} product={product} />
));
});
// Only re-render when products array changes
Lazy Loading Done Right
// routes/main.tsx
const CheckoutPage = lazy(() => import("../features/checkout/CheckoutPage"));
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/checkout" element={<CheckoutPage />} />
</Routes>
</Suspense>;
Testing Strategy That Scales
The Testing Pyramid I Actually Use
// components/__tests__/Button.test.tsx
test("calls onClick when clicked", () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText(/click me/i));
expect(handleClick).toHaveBeenCalled();
});
// features/checkout/__tests__/payment.test.ts
test("handles payment failure", async () => {
mockPaymentAPI.mockRejectedValue(new Error("Payment failed"));
render(<CheckoutPage />);
await userEvent.click(screen.getByText(/pay now/i));
expect(await screen.findByText(/payment failed/i)).toBeInTheDocument();
});
Lessons from Production
The 3 AM Incident That Changed My Approach
- Always validate API responses:
// lib/api.ts
export function isApiError(response: unknown): response is APIError {
return (
typeof response === "object" && response !== null && "errorCode" in response
);
}
async function fetchData() {
try {
const response = await axios.get("/api/data");
if (isApiError(response.data)) {
throw new Error(response.data.message);
}
return response.data;
} catch (err) {
// Handle error
}
}
- Implement proper error boundaries:
// components/ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
Architecture Checklist I Wish I Had
When starting a new project, I now ask:
- Can this component be reused? → Move to
/components/ui
- Does this state need to be global? → Start local, lift up later
- Are we memoizing expensive operations? → Profile first!
- Is the TypeScript type actually helpful? → Avoid
any
at all costs
Remember: Good architecture is about making changes easy, not about theoretical perfection. 💡