1. React Secret: The "Lazy Loading" Pattern for Instant Load Times
Your bundle is 2MB and users wait 8 seconds for first paint!
Code splitting with React.lazy() can cut that to 200KB and 1 second. This is non-negotiable for production apps.
The Problem:
You're shipping EVERYTHING in one bundle. Users download code for routes they never visit.
❌ Everything in single Bundle:
// App.js - imports EVERYTHING upfront
import Dashboard from './pages/Dashboard'; // 300 KB
import Analytics from './pages/Analytics'; // 450 KB
import Reports from './pages/Reports'; // 380 KB
import Settings from './pages/Settings'; // 250 KB
import AdminPanel from './pages/AdminPanel'; // 520 KB
import UserManagement from './pages/UserManagement'; // 400 KB
function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/users" element={<UserManagement />} />
</Routes>
);
}
// 🚨 Problem: User visits "/" → Downloads 2.3 MB!
// 😱 But only needs Dashboard (300 KB)!
// Wasted: 2 MB downloaded
// User waits: 8 seconds on 3G
✅ Code Splitting with React.lazy():
import { lazy, Suspense } from 'react';
// ✨ Only import what you need, when you need it!
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Reports = lazy(() => import('./pages/Reports'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const UserManagement = lazy(() => import('./pages/UserManagement'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/users" element={<UserManagement />} />
</Routes>
</Suspense>
);
}
// ✨ User visits "/" → Downloads 300 KB only!
// Other routes load on-demand
// Initial load: 1 second on 3G! 🎉
🔥 Real Bundle Analysis:
✨Before Code Splitting:
npm run build
# Output:
File sizes after gzip:
2.31 MB build/static/js/main.chunk.js 😱
# Lighthouse Score:
First Contentful Paint: 8.2s
Time to Interactive: 12.4s
Performance Score: 34/100 🔴
✨After Code Splitting:
npm run build
# Output:
File sizes after gzip:
187 KB build/static/js/main.chunk.js ✨
310 KB build/static/js/1.chunk.js (Dashboard)
445 KB build/static/js/2.chunk.js (Analytics)
375 KB build/static/js/3.chunk.js (Reports)
# Lighthouse Score:
First Contentful Paint: 1.2s ⚡
Time to Interactive: 2.1s
Performance Score: 94/100 🟢
12x faster initial load!
💪 Component-Level Code Splitting:
// Don't just split routes - split heavy components!
import { lazy, Suspense } from 'react';
// ✨ Lazy load heavy chart library only when needed
const Chart = lazy(() => import('./components/HeavyChart'));
const DataTable = lazy(() => import('./components/DataTable'));
const PdfViewer = lazy(() => import('./components/PdfViewer'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<Chart data={chartData} />
</Suspense>
)}
</div>
);
}
// Chart only loads when user clicks button! 🎉
// Saves 450 KB on initial load
🎯 Modal/Dialog Lazy Loading:
// ❌ BAD - Modal code loaded even if never opened
import EditUserModal from './EditUserModal';
function UserList() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<UserTable />
{isModalOpen && <EditUserModal onClose={() => setIsModalOpen(false)} />}
</div>
);
}
// ✅ GOOD - Modal loads only when opened
const EditUserModal = lazy(() => import('./EditUserModal'));
function UserList() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<UserTable />
{isModalOpen && (
<Suspense fallback={<ModalSkeleton />}>
<EditUserModal onClose={() => setIsModalOpen(false)} />
</Suspense>
)}
</div>
);
}
// Modal code only downloaded when user clicks "Edit"! ✨
🔥 Tab Content Lazy Loading:
const OverviewTab = lazy(() => import('./tabs/OverviewTab'));
const AnalyticsTab = lazy(() => import('./tabs/AnalyticsTab'));
const SettingsTab = lazy(() => import('./tabs/SettingsTab'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
return (
<div>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tab value="overview">Overview</Tab>
<Tab value="analytics">Analytics</Tab>
<Tab value="settings">Settings</Tab>
</Tabs>
<Suspense fallback={<TabSkeleton />}>
{activeTab === 'overview' && <OverviewTab />}
{activeTab === 'analytics' && <AnalyticsTab />}
{activeTab === 'settings' && <SettingsTab />}
</Suspense>
</div>
);
}
// Each tab loads only when clicked! 🎉
// Initial load: Only active tab
💎 Library Lazy Loading:
// ❌ BAD - Huge library loaded upfront
import Moment from 'moment'; // 288 KB!
import Lodash from 'lodash'; // 71 KB!
import ChartJS from 'chart.js'; // 210 KB!
// ✅ GOOD - Lazy load heavy libraries
function DatePicker() {
const [moment, setMoment] = useState(null);
useEffect(() => {
// ✨ Load moment only when component mounts
import('moment').then(mod => setMoment(() => mod.default));
}, []);
if (!moment) return <div>Loading...</div>;
return <div>{moment().format('MMMM Do YYYY')}</div>;
}
// Even better: Use smaller alternatives
// moment (288 KB) → date-fns (13 KB)
// lodash (71 KB) → lodash-es with tree-shaking
✨ Preloading Strategy:
// ✨ Preload routes user is likely to visit
function App() {
const preloadAnalytics = () => {
import('./pages/Analytics'); // Start loading in background
};
return (
<div>
<nav>
<Link
to="/"
onMouseEnter={preloadAnalytics} // ✨ Preload on hover!
>
Dashboard
</Link>
<Link to="/analytics">Analytics</Link>
</nav>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
</div>
);
}
// Hover over link → Starts loading
// Click link → Already loaded! Instant! ⚡️
🎯 Progressive Loading Pattern:
function DashboardPage() {
return (
<div>
{/* ✨ Load critical content first */}
<Header />
<MainStats />
{/* Load secondary content lazily */}
<Suspense fallback={<ChartSkeleton />}>
<LazyChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<LazyDataTable />
</Suspense>
<Suspense fallback={<WidgetSkeleton />}>
<LazyWidgets />
</Suspense>
</div>
);
}
// Page appears instantly with skeletons
// Content loads progressively as chunks arrive! 🎨
🔥 Nested Suspense Boundaries:
function App() {
return (
// ✨ Outer Suspense for route-level loading
<Suspense fallback={<AppLoader />}>
<Routes>
<Route path="/" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* ✨ Inner Suspense for component-level loading */}
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
<Suspense fallback={<Skeleton />}>
<DataTable />
</Suspense>
</div>
);
}
// Route switches → Show AppLoader
// Components load → Show individual Skeletons
// Better UX! ✨
🚨 Common Mistakes:
// ❌ MISTAKE #1: Forgetting Suspense
const Dashboard = lazy(() => import('./Dashboard'));
<Dashboard /> // ERROR! Must wrap in Suspense
// ✅ FIX:
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
// ❌ MISTAKE #2: Lazy loading tiny components
const Button = lazy(() => import('./Button')); // 2 KB component
// Overhead of loading > component size!
// ✅ FIX: Only lazy load components > 20 KB
// ❌ MISTAKE #3: No loading state
<Suspense fallback={null}> // Blank screen during load!
<HeavyComponent />
</Suspense>
// ✅ FIX: Always show loading UI
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
📊 Real Results:
E-commerce App (Before):
├─ Bundle size: 2.8 MB
├─ Load time (3G): 11 seconds
├─ Bounce rate: 53%
└─ Lighthouse: 28/100
E-commerce App (After lazy loading):
├─ Initial bundle: 340 KB
├─ Load time (3G): 1.4 seconds
├─ Bounce rate: 19%
└─ Lighthouse: 92/100
Result:
├─ 8x smaller initial bundle
├─ 8x faster load time
├─ 64% reduction in bounce rate
└─ Revenue increased 34%! 💰
2. React Performance Secret: The "Bail Out" Optimization Pattern
This ONE weird trick makes React skip re-rendering entirely.
The Hidden React Behavior:
If you return the EXACT same state object, React bails out of rendering. Not just memo() - this works at the state level. Your component AND its children won't re-render.
💡 The Magic:
❌ This ALWAYS Re-renders:
function UserList() {
const [users, setUsers] = useState([]);
const addUser = (newUser) => {
// Even if user already exists, this triggers re-render!
setUsers([...users, newUser]);
};
return <ExpensiveList users={users} />;
}
✅ Smart Bail Out Pattern:
function UserList() {
const [users, setUsers] = useState([]);
const addUser = (newUser) => {
setUsers(prevUsers => {
// If user exists, return SAME array reference
if (prevUsers.some(u => u.id === newUser.id)) {
return prevUsers; // 🚀 React skips render!
}
return [...prevUsers, newUser];
});
};
return <ExpensiveList users={users} />;
}
🔥 Advanced: Conditional Updates Only:
function FilteredProducts() {
const [filters, setFilters] = useState({
category: 'all',
priceRange: [0, 1000],
inStock: false
});
const updateFilter = (key, value) => {
setFilters(prev => {
// Bail out if value hasn't actually changed
if (prev[key] === value) {
return prev; // ✨ No render if nothing changed!
}
return { ...prev, [key]: value };
});
};
return <ProductGrid filters={filters} />;
}
💪 Real-World: Smart Form State:
function useSmartForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const updateField = useCallback((name, value) => {
setValues(prev => {
// Deep equality check - bail if truly unchanged
if (JSON.stringify(prev[name]) === JSON.stringify(value)) {
return prev; // 🎯 Skip unnecessary validations!
}
return { ...prev, [name]: value };
});
}, []);
return { values, updateField, errors };
}
// Usage:
function ProfileForm() {
const { values, updateField } = useSmartForm({
name: 'John',
email: 'john@example.com'
});
// Typing same value won't trigger re-render! 🎉
return (
<input
value={values.name}
onChange={(e) => updateField('name', e.target.value)}
/>
);
}
🔥 Bonus: API Calls With Bail Out:
function useDataFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
const result = await fetch(url);
const json = await result.json();
setData(prevData => {
// Don't update if data is identical
if (JSON.stringify(prevData) === JSON.stringify(json)) {
return prevData; // 🎯 Skip render + child updates!
}
return json;
});
setLoading(false);
}, [url]);
return { data, loading, fetchData };
}
📊 The Performance Impact:
🚀 Eliminates 30-50% of unnecessary renders
⚡ Fewer child component updates
💰 Reduced React reconciliation work
🎮 Smoother UI, especially in forms and lists
PS: Don't add bail-out condition using JSON.stringify initially itself while writing code as comparing using JSON.stringify is slower for larger objects. Use this as an optimization step.
3. React Anti-Pattern: "useState for Derived Data" Mistake
If you can calculate it from existing state/props, DON'T put it in useState.
This bug pattern creates sync issues that haunt production.
The Problem:
Storing calculated/derived values in state creates two sources of truth. They get out of sync, causing difficult-to-debug issues.
❌ The Anti-Pattern (Double State):
function ShoppingCart({ items }) {
const [cartItems, setCartItems] = useState(items);
const [totalPrice, setTotalPrice] = useState(0); // 🚨 Derived!
const [itemCount, setItemCount] = useState(0); // 🚨 Derived!
const [hasDiscount, setHasDiscount] = useState(false); // 🚨 Derived!
// 🚨 Must remember to update ALL state together
const addItem = (item) => {
const newItems = [...cartItems, item];
setCartItems(newItems);
setTotalPrice(calculateTotal(newItems)); // Easy to forget!
setItemCount(newItems.length);
setHasDiscount(calculateTotal(newItems) > 100);
};
// 😱 BUG: What if you forget to update one?
// totalPrice and itemCount are now OUT OF SYNC!
return (
<div>
<p>Items: {itemCount}</p>
<p>Total: ${totalPrice}</p>
{hasDiscount && <p>Discount applied!</p>}
</div>
);
}
✅ The Correct Way (Single Source of Truth):
function ShoppingCart({ items }) {
const [cartItems, setCartItems] = useState(items);
// ✨ Calculate derived values - always in sync!
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
const itemCount = cartItems.length;
const hasDiscount = totalPrice > 100;
// ✨ Only update ONE state - everything else auto-updates!
const addItem = (item) => {
setCartItems([...cartItems, item]);
// That's it! No need to update anything else! 🎉
};
return (
<div>
<p>Items: {itemCount}</p>
<p>Total: ${totalPrice}</p>
{hasDiscount && <p>Discount applied!</p>}
</div>
);
}
✨ Common Derived State Examples:
1. Filtered Lists:
// ❌ BAD - Double state
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products); // 🚨 Derived!
const handleSearch = (term) => {
setSearchTerm(term);
setFilteredProducts(products.filter(p => p.name.includes(term))); // Can get out of sync!
};
return <div>...</div>;
}
// ✅ GOOD - Single source
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
// ✨ Always calculated from current values
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return <div>...</div>;
}
✨ Common Derived State Examples:
2. Form Validation:
// ❌ BAD - Validation state separate
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isValid, setIsValid] = useState(false); // 🚨 Derived!
const handleEmailChange = (e) => {
setEmail(e.target.value);
// 😱 Forgot to update isValid!
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
setIsValid(password.length >= 8 && email.includes('@')); // Out of sync!
};
return <button disabled={!isValid}>Sign Up</button>;
}
// ✅ GOOD - Validation derived
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// ✨ Always accurate
const isEmailValid = email.includes('@');
const isPasswordValid = password.length >= 8;
const isFormValid = isEmailValid && isPasswordValid;
return <button disabled={!isFormValid}>Sign Up</button>;
}
✨ Common Derived State Examples:
3. Totals & Calculations:
// ❌ BAD - Sync nightmare
function Invoice({ items }) {
const [invoiceItems, setInvoiceItems] = useState(items);
const [subtotal, setSubtotal] = useState(0); // 🚨 Derived!
const [tax, setTax] = useState(0); // 🚨 Derived!
const [total, setTotal] = useState(0); // 🚨 Derived!
const addItem = (item) => {
const newItems = [...invoiceItems, item];
const newSubtotal = calculateSubtotal(newItems);
const newTax = newSubtotal * 0.1;
setInvoiceItems(newItems);
setSubtotal(newSubtotal);
setTax(newTax);
setTotal(newSubtotal + newTax);
// 😱 So many places to make mistakes!
};
return <div>Total: ${total}</div>;
}
// ✅ GOOD - Single calculation
function Invoice({ items }) {
const [invoiceItems, setInvoiceItems] = useState(items);
// ✨ All calculated from ONE source
const subtotal = invoiceItems.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
const addItem = (item) => {
setInvoiceItems([...invoiceItems, item]);
// ✨ Done! Everything else updates automatically!
};
return <div>Total: ${total.toFixed(2)}</div>;
}
🎯 Performance Concern? Use useMemo:
function DataDashboard({ transactions }) {
const [dateRange, setDateRange] = useState('month');
const [category, setCategory] = useState('all');
// ✨ Expensive calculation? Memoize it!
const filteredTransactions = useMemo(() => {
return transactions
.filter(t => matchesDateRange(t, dateRange))
.filter(t => category === 'all' || t.category === category);
}, [transactions, dateRange, category]);
const totalAmount = useMemo(() =>
filteredTransactions.reduce((sum, t) => sum + t.amount, 0),
[filteredTransactions]
);
const averageAmount = totalAmount / filteredTransactions.length || 0;
return (
<div>
<p>Total: ${totalAmount}</p>
<p>Average: ${averageAmount.toFixed(2)}</p>
<p>Count: {filteredTransactions.length}</p>
</div>
);
}
💎 The Golden Rules:
// Ask yourself: "Can I calculate this from other state/props?"
// If YES → Don't use useState!
const fullName = `${firstName} ${lastName}`; // ✅ Derived
const isValid = email.includes('@'); // ✅ Derived
const total = items.reduce(...); // ✅ Derived
// If NO → Use useState
const [firstName, setFirstName] = useState(''); // ✅ User input
const [isModalOpen, setIsModalOpen] = useState(false); // ✅ UI state
const [data, setData] = useState(null); // ✅ Fetched data
🚨 Red Flags - You Probably Have Derived State:
// 🚨 Red Flag #1: Multiple setState calls together
const handleChange = () => {
setState1(newValue);
setState2(calculateFromNewValue); // Derived!
setState3(calculateOther); // Derived!
};
// 🚨 Red Flag #2: useEffect to sync states
useEffect(() => {
setDerivedValue(calculate(sourceValue));
}, [sourceValue]); // Just calculate it directly!
// 🚨 Red Flag #3: State that depends on other state
const [total, setTotal] = useState(0);
const [count, setCount] = useState(0);
const [average, setAverage] = useState(0); // total / count = derived!
📊 Comparison:
// With Derived State (useState):
✖ 4 useState calls
✖ Must update 4 states together
✖ Easy to forget one → bugs!
✖ Hard to maintain
✖ Out of sync bugs in production
// Without Derived State (calculate):
✅ 1 useState call
✅ Update 1 state, rest auto-updates
✅ Impossible to get out of sync
✅ Easy to maintain
✅ No sync bugs possible
🎯 Real Production Bug - Fixed:
// The Bug
function CheckoutForm() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const addItem = (item) => {
setItems([...items, item]);
// 😱 Forgot to update total!
};
// User sees $0 total, thinks checkout is broken!
}
// The Fix:
function CheckoutForm() {
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);
const addItem = (item) => {
setItems([...items, item]);
// ✨ Total automatically updates!
};
}
4. Best React/Next.js Tool: The "Knip" Dead Code Eliminator
Your codebase has 1000+ unused files and imports you don't even know about!
Knip finds them ALL in 10 seconds. This tool is a game-changer!
The Problem:
Dead code accumulates over time. Unused files, unused exports, orphaned components - they slow builds, confuse developers, and waste time!
❌ Manual Cleanup (Impossible):
# Your project:
src/
├── components/
│ ├── Button.jsx # Used? 🤔
│ ├── OldButton.jsx # Used? 🤔
│ ├── LegacyModal.jsx # Used? 🤔
│ ├── DeprecatedForm.jsx # Used? 🤔
│ └── ... 500 more files
# Manually checking each file? 😱
# Takes weeks, error-prone, nobody does it!
✅ Knip to the Rescue (Automated!):
# Install Knip
npm install -D knip
# Run it
npx knip
# Output - BOOM! 💥
✖ Unused files (47)
src/components/OldButton.jsx
src/components/LegacyModal.jsx
src/components/DeprecatedForm.jsx
src/utils/oldHelpers.js
... 43 more files
✖ Unused dependencies (12)
lodash-es (in package.json)
moment (in package.json)
... 10 more
✖ Unused exports (156)
export const oldFunction (src/utils/helpers.js)
export const deprecatedHook (src/hooks/useOldApi.js)
... 154 more
Total wasted: 47 files, 12 dependencies, 156 exports
Potential savings: 2.3 MB bundle size 🚀
🔥 Real Example - Before/After:
Before Running Knip:
My React project:
- 1,247 files
- 89 npm packages
- 15.2 MB node_modules
- 487 KB production bundle
- Build time: 45 seconds
- "Which components are actually used?" 🤷♂️
After Running Knip:
npx knip
# Found:
# - 47 unused files
# - 12 unused dependencies
# - 156 unused exports
# - 23 duplicate exports
# Cleaned up:
✅ Deleted 47 unused files
✅ Removed 12 unused packages
✅ Removed 156 unused exports
✅ Fixed 23 duplicates
My React project now:
- 1,200 files (47 less!)
- 77 npm packages (12 less!)
- 12.8 MB node_modules (2.4 MB saved!)
- 412 KB production bundle (75 KB saved!)
- Build time: 38 seconds (7s faster!)
- Clean, confident codebase! ✨
✅ What Knip Finds:
1. Unused Files:
✖ src/components/OldButton.jsx
→ Not imported anywhere
→ Safe to delete! 🗑️
✖ src/pages/BetaFeature.jsx
→ Not in routes
→ Remove or add to router
✖ src/utils/deprecatedHelpers.js
→ No imports found
→ Delete it!
2. Unused Dependencies:
✖ lodash (in package.json)
→ Never imported
→ npm uninstall lodash
→ Save 72 KB!
✖ moment (in package.json)
→ Replaced with date-fns
→ npm uninstall moment
→ Save 231 KB!
✖ react-router-dom v5 (in package.json)
→ Upgraded to v6, forgot to remove
→ npm uninstall react-router-dom@5
3. Unused Exports:
// src/utils/helpers.ts
export const oldFunction = () => {}; // ✖ Never imported
export const deprecatedUtil = () => {}; // ✖ Never imported
export const usedFunction = () => {}; // ✅ Used in 12 places
// Knip tells you: Remove first two exports!
4. Duplicate Exports:
// ✖ src/components/Button/index.ts
export { Button } from './Button';
// ✖ src/components/index.ts
export { Button } from './Button'; // Duplicate!
// Knip warns: Same export in multiple places!
5. Unlisted Dependencies:
// src/App.tsx
import axios from 'axios'; // ✖ Not in package.json!
// Knip says: Add axios to dependencies!
📦 Knip Works With:
✅ React
✅ Next.js
✅ Remix
✅ Vite
✅ TypeScript
✅ JavaScript
✅ Turborepo/Nx (monorepos)
✅ Webpack/Rollup/esbuild
Enjoying these tips? Download the complete collection of 50+ React Pro Tips Ebook.
5. React Security Disaster: The "ENV Variables"
Your .env file is leaking API keys into the frontend bundle!
Every user can see your secrets in 10 seconds. This mistake costs companies MILLIONS in stolen API credits.
The Problem:
React embeds ALL env variables starting with REACT_APP_ or VITE_ directly into your JavaScript bundle. Anyone can read them!
❌ The Security Nightmare:
# .env file
REACT_APP_API_KEY=sk_live_51HxT8fhj3k2l... # 🚨 EXPOSED!
REACT_APP_STRIPE_SECRET=sk_test_xyz123... # 🚨 EXPOSED!
REACT_APP_DATABASE_URL=mongodb://admin:pass@... # 🚨 EXPOSED!
REACT_APP_AWS_SECRET_KEY=AKIAI... # 🚨 EXPOSED!
// Your React code
function App() {
const apiKey = process.env.REACT_APP_API_KEY; // 🚨 In bundle!
fetch('https://api.stripe.com/charges', {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
}
// What users see in browser DevTools → Sources → main.js:
// const apiKey = "sk_live_51HxT8fhj3k2l..."; 😱
// Copy → Paste → FREE API ACCESS!
// Your keys are PUBLIC! 💀
🔥 How Hackers Find Your Keys:
# Method 1: View source
# Right-click → View Page Source
# Search for: "api", "key", "secret", "token"
# Found in 3 seconds! 😱
# Method 2: Check bundle files
# DevTools → Sources → static/js/main.chunk.js
# Search for: "REACT_APP_"
# All your env variables visible! 💀
# Method 3: Automated scanning
# Hackers run scripts that:
# 1. Download your main.js
# 2. Search for patterns (sk_live_, api_key, etc.)
# 3. Extract all secrets
# 4. Use your API for free!
# Result:
# - $50K Stripe bill from unauthorized charges
# - AWS account compromised
# - Database exposed
# - Company bankrupt! 🔥
✅ What's Safe vs Unsafe:
# SAFE - Public information (goes in .env)
REACT_APP_API_URL=https://api.myapp.com # Public endpoint ✅
REACT_APP_APP_VERSION=1.2.3 # Version number ✅
REACT_APP_FEATURE_FLAG_X=true # Feature flag ✅
REACT_APP_GOOGLE_MAPS_KEY=AIza... # Public API key* ✅
REACT_APP_SENTRY_DSN=https://... # Public DSN ✅
# UNSAFE - Secrets (NEVER in frontend!)
REACT_APP_STRIPE_SECRET_KEY=sk_live_... # SECRET! 🚨
REACT_APP_DATABASE_PASSWORD=mysecret123 # SECRET! 🚨
REACT_APP_JWT_SECRET=supersecret # SECRET! 🚨
REACT_APP_OPENAI_API_KEY=sk-proj-... # SECRET! 🚨
REACT_APP_AWS_SECRET=AKIAI... # SECRET! 🚨
💪 The Correct Architecture:
// ❌ WRONG - Secret in frontend
function Payment({ amount }) {
const stripeSecret = process.env.REACT_APP_STRIPE_SECRET; // 🚨 EXPOSED!
fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: {
'Authorization': `Bearer ${stripeSecret}` // 💀 Anyone can charge!
},
body: JSON.stringify({ amount })
});
}
// ✅ CORRECT - Secret on backend
// Frontend:
function Payment({ amount }) {
// ✨ No secrets! Only public API URL
fetch('/api/create-charge', { // Your backend endpoint
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}` // User's session token
},
body: JSON.stringify({ amount })
});
}
// Backend (Node.js example):
app.post('/api/create-charge', authenticateUser, async (req, res) => {
// ✨ Secret stays on server, never sent to client!
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
try {
const charge = await stripe.charges.create({
amount: req.body.amount,
currency: 'usd',
source: req.body.token
});
res.json({ success: true, chargeId: charge.id });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
😱 Real Attack Example:
// Your code:
const openaiKey = process.env.REACT_APP_OPENAI_KEY;
fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openaiKey}`
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: userInput }]
})
});
// What happens:
// 1. Hacker opens your site
// 2. Checks main.js bundle
// 3. Finds: "sk-proj-abc123xyz..."
// 4. Copies key
// 5. Makes 10,000 GPT-4 requests
// 6. Your bill: $15,000! 💀
// 7. OpenAI suspends your account
// 8. Your app breaks
// 9. Company loses customers
// This happens DAILY to startups! 🚨
🔥 The Correct Pattern:
// ✅ Frontend - No secrets
function ChatBot() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const sendMessage = async () => {
// ✨ Call YOUR backend, not OpenAI directly!
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getUserToken()}` // User's session
},
body: JSON.stringify({ message })
});
const data = await res.json();
setResponse(data.reply);
};
return (
....
);
}
// ✅ Backend - Secrets safe here
app.post('/api/chat', authenticateUser, async (req, res) => {
// ✨ Secret only exists on server!
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY // Never sent to client!
});
// Rate limiting per user
const userRequests = await checkUserRateLimit(req.user.id);
if (userRequests > 100) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo', // Cheaper model for users
messages: [{ role: 'user', content: req.body.message }],
max_tokens: 150 // Limit cost per request
});
res.json({ reply: completion.choices[0].message.content });
});
6. React Object Mutation Trap: The "Mutate State Directly" Silent Bug
You mutate state objects/arrays directly and React doesn't re-render! Changes happen in memory but UI stays frozen.
Users see stale data, clicks don't work, forms don't update. This invisible mutation bug corrupts apps silently.
The Problem:
React detects changes by comparing references.
Mutate object in place = same reference = React thinks nothing changed = no re-render!
❌ Direct Mutation (React Misses It):
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: false },
{ id: 3, text: 'Write code', done: false }
]);
const toggleTodo = (id) => {
const todo = todos.find(t => t.id === id);
// 🚨 MUTATING the object directly!
todo.done = !todo.done;
// 🚨 Setting same array reference!
setTodos(todos); // 💀 React sees: old array === new array → no re-render!
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
✅ Create New Reference (Immutable Update):
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: false },
{ id: 3, text: 'Write code', done: false }
]);
const toggleTodo = (id) => {
// ✨ Create NEW array with NEW objects!
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, done: !todo.done } // ✨ New object!
: todo // Keep existing objects unchanged
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
// Now:
// 1. User clicks checkbox for "Buy milk"
// 2. toggleTodo(1) called
// 3. ✨ .map creates NEW array
// 4. ✨ For id=1: { ...todo, done: !todo.done } creates NEW object
// 5. ✨ For id=2,3: returns existing objects (no change)
// 6. setTodos([newObj1, oldObj2, oldObj3])
// 7. ✅ React compares: oldArray !== newArray → TRUE!
// 8. ✅ React thinks: "Different reference, state changed!"
// 9. ✅ RE-RENDER triggered!
// 10. ✅ Checkbox updates on screen!
// 11. User happy! ✨
// React's comparison:
// prevState !== nextState → Re-render! ✅
// [1,2,3] !== [1,2,3] // Different array objects!
🔥 Real Mutation Bugs:
Bug 1: Nested Object Mutation
function UserSettings() {
const [user, setUser] = useState({
name: 'John',
settings: {
theme: 'light',
notifications: {
email: true,
push: false
}
}
});
const toggleEmail = () => {
// 🚨 DEEP mutation!
user.settings.notifications.email = !user.settings.notifications.email;
setUser(user); // 💀 Same reference!
};
return (
<div>
<h1>{user.name}</h1>
<label>
<input
type="checkbox"
checked={user.settings.notifications.email}
onChange={toggleEmail}
/>
Email Notifications
</label>
</div>
);
}
// Bug:
// 1. User clicks checkbox
// 2. toggleEmail() called
// 3. 💀 Mutates deep nested property
// 4. setUser(user) - same object reference
// 5. 💀 React doesn't re-render!
// 6. Checkbox doesn't update
// 7. User clicks 5 more times
// 8. 💀 Still no visual change!
// 9. Data in memory: true → false → true → false...
// 10. UI shows: true (frozen) 💀
// ✅ Fix: Immutable deep update
const toggleEmail = () => {
setUser({
...user, // ✨ Copy top level
settings: {
...user.settings, // ✨ Copy second level
notifications: {
...user.settings.notifications, // ✨ Copy third level
email: !user.settings.notifications.email // ✨ Change value
}
}
});
};
// Or use immer for easier deep updates:
import { produce } from 'immer';
const toggleEmail = () => {
setUser(produce(draft => {
draft.settings.notifications.email = !draft.settings.notifications.email;
// ✨ Immer creates new object behind the scenes!
}));
};
Bug 2: Array Push/Splice Mutation
function ShoppingCart() {
const [items, setItems] = useState([]);
const addItem = (product) => {
// 🚨 MUTATING array!
items.push(product); // 💀 Modifies original array!
setItems(items); // 💀 Same reference!
};
const removeItem = (index) => {
// 🚨 MUTATING array!
items.splice(index, 1); // 💀 Modifies original array!
setItems(items); // 💀 Same reference!
};
return (
<div>
<p>Cart: {items.length} items</p>
{items.map((item, index) => (
<div key={index}>
{item.name} - ${item.price}
<button onClick={() => removeItem(index)}>Remove</button>
</div>
))}
<button onClick={() => addItem({ name: 'Widget', price: 10 })}>
Add Widget
</button>
</div>
);
}
// Bug:
// 1. User clicks "Add Widget"
// 2. addItem() called
// 3. items.push() mutates array: [] → [widget]
// 4. setItems(items) - same array reference
// 5. 💀 React doesn't re-render!
// 6. UI still shows: "Cart: 0 items" 💀
// 7. User clicks "Add" 5 more times
// 8. Array in memory: [w, w, w, w, w, w]
// 9. UI still shows: "Cart: 0 items" 💀
// 10. User refreshes page
// 11. Cart data lost (wasn't persisted)
// 12. User: "I added 6 items! Where are they?!" 😡
// ✅ Fix: Immutable operations
const addItem = (product) => {
setItems([...items, product]); // ✨ New array!
};
const removeItem = (index) => {
setItems(items.filter((_, i) => i !== index)); // ✨ New array!
};
// Or:
const addItem = (product) => {
setItems(prevItems => [...prevItems, product]);
};
const removeItem = (index) => {
setItems(prevItems => prevItems.filter((_, i) => i !== index));
};
Bug 3: Sort Mutation
function ProductList({ products }) {
const [sortedProducts, setSortedProducts] = useState(products);
const [sortOrder, setSortOrder] = useState('asc');
const handleSort = () => {
// 🚨 MUTATING array!
sortedProducts.sort((a, b) => {
return sortOrder === 'asc'
? a.price - b.price
: b.price - a.price;
});
setSortedProducts(sortedProducts); // 💀 Same reference!
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
};
return (
<div>
<button onClick={handleSort}>
Sort by Price ({sortOrder})
</button>
{sortedProducts.map(product => (
<div key={product.id}>
{product.name} - ${product.price}
</div>
))}
</div>
);
}
// Bug:
// 1. User clicks "Sort by Price (asc)"
// 2. handleSort() called
// 3. sortedProducts.sort() mutates array in place
// 4. setSortedProducts(sortedProducts) - same reference
// 5. 💀 React doesn't re-render sortedProducts!
// 6. setSortOrder('desc') triggers re-render
// 7. ✅ Button text updates: "(desc)"
// 8. 💀 But product list DOESN'T re-sort!
// 9. User sees: Button says "desc" but list is still "asc" order
// 10. Clicks again
// 11. 💀 Still no visual change to list!
// 12. UI state inconsistent! 💀
// ✅ Fix: Create new sorted array
const handleSort = () => {
const newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
const sorted = [...sortedProducts].sort((a, b) => { // ✨ Copy first!
return newOrder === 'asc'
? a.price - b.price
: b.price - a.price;
});
setSortedProducts(sorted); // ✨ New array!
setSortOrder(newOrder);
};
💪 Mutation Methods to AVOID:
// ❌ Array mutations (modify original)
array.push(item) // Use: [...array, item]
array.pop() // Use: array.slice(0, -1)
array.shift() // Use: array.slice(1)
array.unshift(item) // Use: [item, ...array]
array.splice(i, 1) // Use: array.filter((_, index) => index !== i)
array.sort() // Use: [...array].sort()
array.reverse() // Use: [...array].reverse()
array[i] = value // Use: array.map((item, index) => index === i ? value : item)
// ❌ Object mutations (modify original)
object.property = value // Use: { ...object, property: value }
delete object.property // Use: const { property, ...rest } = object; return rest;
Object.assign(object, {}) // Use: { ...object, ...newProps }
// ✅ Immutable alternatives
// Arrays:
[...array, newItem] // Add to end
[newItem, ...array] // Add to start
array.filter(item => item.id !== id) // Remove
array.map(item => item.id === id ? newItem : item) // Update
[...array].sort() // Sort
array.slice(0, index).concat(array.slice(index + 1)) // Remove at index
// Objects:
{ ...object, key: newValue } // Update property
{ ...object, nested: { ...object.nested, key: value } } // Deep update
const { removeThis, ...keep } = object; keep; // Remove property
📊 Mutation Detection:
// React's state comparison:
// Primitives (compared by value):
const [count, setCount] = useState(0);
setCount(0); // 0 === 0 → No re-render ✅
setCount(1); // 0 !== 1 → Re-render ✅
// Objects/Arrays (compared by reference):
const [obj, setObj] = useState({ a: 1 });
// ❌ Mutation (same reference):
obj.a = 2;
setObj(obj); // obj === obj → No re-render 💀
// ✅ New object (different reference):
setObj({ ...obj, a: 2 }); // oldObj !== newObj → Re-render ✅
// Why mutation fails:
const array1 = [1, 2, 3];
array1.push(4); // Mutates array1
array1 === array1 // Still TRUE! Same reference!
const array2 = [1, 2, 3];
const array3 = [...array2, 4]; // New array
array2 === array3 // FALSE! Different references!
// React uses Object.is() for comparison:
Object.is(oldState, newState)
// If TRUE → Skip re-render
// If FALSE → Trigger re-render
✨ Immutable Update Patterns:
// Pattern 1: Array - Add item
setArray([...array, newItem]);
setArray(prev => [...prev, newItem]);
// Pattern 2: Array - Remove item
setArray(array.filter(item => item.id !== id));
setArray(prev => prev.filter(item => item.id !== id));
// Pattern 3: Array - Update item
setArray(array.map(item =>
item.id === id ? { ...item, name: 'New' } : item
));
// Pattern 4: Array - Replace at index
setArray(array.map((item, i) =>
i === index ? newItem : item
));
// Pattern 5: Object - Update property
setObject({ ...object, key: value });
setObject(prev => ({ ...prev, key: value }));
// Pattern 6: Object - Update nested
setObject({
...object,
nested: {
...object.nested,
deepKey: value
}
});
// Pattern 7: Object - Remove property
const { removeKey, ...rest } = object;
setObject(rest);
// Pattern 8: Object - Merge
setObject({ ...object, ...updates });
// Pattern 9: Array of objects - Update one
setArray(array.map(obj =>
obj.id === id ? { ...obj, done: !obj.done } : obj
));
// Pattern 10: Complex nested update (use immer)
import { produce } from 'immer';
setData(produce(draft => {
draft.users[0].profile.settings.theme = 'dark';
// ✨ Immer handles immutability!
}));
7. React Accessibility Trap: The "div onClick" Keyboard Trap
Your clickable divs are INVISIBLE to keyboard users! 15% of users can't use your app because you used onClick on div instead of button.
This is a lawsuit waiting to happen. This pattern destroys production performance.
The Problem:
Divs aren't keyboard accessible. Screen readers skip them. Tab key ignores them. You just blocked millions of users!
❌ The Accessibility Disaster:
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
{/* 🚨 ACCESSIBILITY NIGHTMARE! */}
<div
className="add-to-cart-button"
onClick={() => onAddToCart(product.id)}
>
Add to Cart
</div>
</div>
);
}
// What happens for keyboard users:
// 1. User presses Tab key
// 2. ❌ Focus skips the "button" (it's a div!)
// 3. User can't reach "Add to Cart"
// 4. User can't buy product
// 5. You lost a customer! 💀
// What happens for screen reader users:
// 1. Screen reader reads page
// 2. ❌ Announces "Add to Cart" as text, not a button
// 3. User doesn't know it's clickable
// 4. No way to activate it with voice commands
// 5. User leaves frustrated! 😡
// What happens legally:
// 1. User files ADA complaint
// 2. Company faces lawsuit
// 3. Settlement: $50,000-$500,000
// 4. Mandatory accessibility audit: $30,000
// 5. Remediation work: $100,000+
// Total cost: $180,000+ for using <div> instead of <button>! 💀
✅ Accessible Button (Correct):
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
{/* ✨ ACCESSIBLE! */}
<button
className="add-to-cart-button"
onClick={() => onAddToCart(product.id)}
>
Add to Cart
</button>
</div>
);
}
// What happens now:
// Keyboard users:
// 1. Tab key focuses the button
// 2. Enter/Space activates it
// 3. Can add to cart!
// Screen reader users:
// 1. Announces "Add to Cart, button"
// 2. User knows it's interactive
// 3. Voice commands work: "Click Add to Cart button"
// 4. Can buy product!
// Legal compliance:
// ✅ WCAG 2.1 compliant
// ✅ ADA compliant
// ✅ Section 508 compliant
// ✅ No lawsuits!
🔥 Real Accessibility Issues:
Issue 1: Modal Close Button
// ❌ BAD - Keyboard trap!
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal">
{/* 🚨 Not keyboard accessible! */}
<div className="close-button" onClick={onClose}>
×
</div>
{children}
</div>
</div>
);
}
// Keyboard user:
// 1. Opens modal
// 2. Presses Tab to close
// 3. ❌ Can't focus on × button
// 4. Presses Esc key
// 5. ❌ Nothing happens (no keyboard handler!)
// 6. 💀 TRAPPED IN MODAL FOREVER!
// ✅ GOOD - Fully accessible
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
// ✨ Close on Escape key
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
return (
<div
className="modal-overlay"
onClick={onClose}
role="dialog" // ✨ Semantic role
aria-modal="true" // ✨ Tells screen readers it's a modal
>
<div className="modal">
{/* ✨ Real button! */}
<button
className="close-button"
onClick={onClose}
aria-label="Close modal" // ✨ Screen reader text
>
×
</button>
{children}
</div>
</div>
);
}
// Now:
// 1. Tab focuses close button
// 2. Enter/Space closes modal
// 3. Escape key closes modal
// 4. Screen reader announces "Close modal, button"
// 5. No one trapped!
Issue 2: Card Click
// ❌ BAD - Entire card clickable with div
function ProductCard({ product }) {
const navigate = useNavigate();
return (
<div
className="product-card"
onClick={() => navigate(`/products/${product.id}`)}
style={{ cursor: 'pointer' }} // 🚨 Looks clickable, but...
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
// Problems:
// ❌ Can't Tab to card
// ❌ Can't activate with keyboard
// ❌ Screen reader doesn't know it's clickable
// ❌ Can't use with voice commands
// ✅ GOOD - Semantic link
function ProductCard({ product }) {
return (
<article className="product-card">
{/* ✨ Wrap in link for navigation */}
<Link
to={`/products/${product.id}`}
className="product-link"
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</Link>
</article>
);
}
// Or if it's an action, not navigation:
function ProductCard({ product, onQuickView }) {
return (
<article className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
{/* ✨ Button for action */}
<button onClick={() => onQuickView(product)}>
Quick View
</button>
</article>
);
}
// Now:
// 1. Tab to focus
// 2. Enter to activate
// 3. Screen reader: "Link, {product.name}" or "Button, Quick View"
// 4. Voice commands work
💪 The Keyboard Navigation Rules:
// Rule 1: Navigation = <Link> or <a>
// ✅ CORRECT
<Link to="/dashboard">Dashboard</Link>
<a href="/dashboard">Dashboard</a>
// ❌ WRONG
<div onClick={() => navigate('/dashboard')}>Dashboard</div>
// Rule 2: Actions = <button>
// ✅ CORRECT
<button onClick={handleSubmit}>Submit</button>
<button onClick={handleDelete}>Delete</button>
// ❌ WRONG
<div onClick={handleSubmit}>Submit</div>
<span onClick={handleDelete}>Delete</span>
// Rule 3: If it looks clickable, make it focusable
// ✅ CORRECT
<button className="icon-button">
<Icon name="trash" />
</button>
// ❌ WRONG
<div className="icon-button" onClick={handleClick}>
<Icon name="trash" />
</div>
// Rule 4: Custom interactive elements need tabIndex and role
// ✅ ONLY if you absolutely can't use button/link
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Custom Button
</div>
// But seriously, just use <button>! ✨
📊 Accessibility Impact:
Your e-commerce site with div buttons:
Users affected:
├─ Keyboard-only users: 5-10% of all users
├─ Screen reader users: 1-2%
├─ Voice control users: 2-3%
├─ Motor disability users: 3-5%
├─ Temporary disability (broken mouse): 5%
└─ Total: 15-20% of potential customers! 💀
If you have 100,000 monthly visitors:
├─ 15,000-20,000 can't use your site
├─ Average order value: $50
├─ Conversion rate: 2%
├─ Lost revenue: $15,000-$20,000/month
└─ Annual: $180,000-$240,000 lost! 💸
Plus legal risk:
├─ ADA lawsuit settlements: $50K-$500K
├─ Remediation costs: $50K-$200K
└─ Reputation damage: Priceless 💀
Fix: Replace <div onClick> with <button>
Cost: 5 minutes
Savings: Hundreds of thousands of dollars!
🔥 ESLint Rules:
// .eslintrc.js
{
"plugins": ["jsx-a11y"],
"rules": {
// ✨ Catches clickable divs!
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/no-noninteractive-element-interactions": "error",
// ✨ Enforces proper roles
"jsx-a11y/role-has-required-aria-props": "error",
// ✨ Requires alt text on images
"jsx-a11y/alt-text": "error"
}
}
// Install:
npm install --save-dev eslint-plugin-jsx-a11y
// Now ESLint will catch:
<div onClick={handleClick}> // ❌ Error!
// ⚠️ Visible, non-interactive elements with click handlers
// must have at least one keyboard listener
<button onClick={handleClick}> // ✅ No error!
💡 Testing Accessibility:
Manual test (takes 2 minutes):
1. Unplug your mouse
2. Use only Tab key to navigate
3. Can you reach every interactive element?
4. Can you activate them with Enter/Space?
If NO to any → You have accessibility issues!
Automated tools:
├─ axe DevTools (Chrome extension)
├─ Lighthouse (Chrome DevTools)
├─ WAVE (Browser extension)
└─ React axe (in development)
Screen reader test:
├─ Mac: Turn on VoiceOver (Cmd+F5)
├─ Windows: Turn on Narrator (Ctrl+Win+Enter)
├─ Navigate your site
└─ Do interactive elements announce correctly?
🎯 The Checklist:
For every interactive element:
☑️ Can you Tab to it?
☑️ Can you activate with Enter/Space?
☑️ Does screen reader announce it correctly?
☑️ Does it have visible focus indicator?
☑️ Can you use it without a mouse?
If NO to any:
✅ Use <button> for actions
✅ Use <a> or <Link> for navigation
✅ Add proper ARIA labels
✅ Add keyboard event handlers
✅ Test with keyboard only
🚨 Legal Reality:
Recent accessibility lawsuits:
Domino's Pizza: $4,000 + legal fees + remediation
Winn-Dixie: $100,000 settlement
Target: $6 million settlement
Bank of America: $3.5 million
Common violation: Clickable divs without keyboard access
Your risk:
├─ Anyone can file ADA complaint
├─ No warning required
├─ Settlements average $50K-$500K
└─ Plus your legal fees and remediation
Prevention cost: Use <button> instead of <div onClick>
Literally free! Just better HTML! ✨
8. React Async Trap: The "Promise Race Condition" Bug That Corrupts Data
Your async code shows wrong data and you have NO IDEA why! Fast clicks create race conditions that display stale responses. This bug is invisible until production.
The Problem:
Multiple async requests racing - the last one to START wins, but the last one to FINISH displays!
❌ The Race Condition:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 🚨 No cleanup! Race condition waiting to happen!
fetchUser(userId).then(data => {
setUser(data);
});
}, [userId]);
return <div>{user?.name}</div>;
}
// Bug scenario:
// 1. User clicks Profile #1 (slow server, takes 3 seconds)
// 2. User quickly clicks Profile #2 (fast server, takes 0.5 seconds)
// 3. Profile #2 loads: ✅ Shows User #2 (correct)
// 4. Profile #1 finishes: 😱 Shows User #1 (WRONG!)
// 5. URL says "profile/2" but shows User #1 data!
// Data corruption! User confused! 💀
// Timeline:
// 0.0s: Request User #1 (start)
// 0.1s: Request User #2 (start) ← Should cancel #1!
// 0.6s: User #2 arrives, setUser(#2) ✅
// 3.0s: User #1 arrives, setUser(#1) 😱 OVERWRITES!
// Result: Shows User #1 in User #2's profile!
✅ Fix #1 - Abort Controller:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// ✨ Create abort controller
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(data => {
setUser(data);
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
// AbortError is expected when switching users
});
// ✨ Cleanup: Cancel previous request!
return () => {
controller.abort();
};
}, [userId]);
return <div>{user?.name}</div>;
}
// Now:
// 1. Request User #1 starts
// 2. User clicks User #2
// 3. ✨ useEffect cleanup runs → abort Request #1
// 4. Request #2 starts
// 5. Only User #2 data displays!
// No race condition! ✨
✅ Fix #2 - Ignore Stale Responses:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let isCurrent = true; // ✨ Track if this effect is still relevant
fetchUser(userId).then(data => {
// ✨ Only update if this is still the current userId
if (isCurrent) {
setUser(data);
} else {
console.log('Ignoring stale response for userId:', userId);
}
});
// ✨ Cleanup: Mark as stale
return () => {
isCurrent = false;
};
}, [userId]);
return <div>{user?.name}</div>;
}
// Now:
// 1. Request User #1, isCurrent = true
// 2. User clicks User #2
// 3. ✨ Cleanup runs: isCurrent (for #1) = false
// 4. Request User #2, new isCurrent = true
// 5. User #2 arrives: isCurrent = true → setUser ✅
// 6. User #1 arrives late: isCurrent = false → ignored! ✨
🔥 Real Bug Examples:
Bug 1: Search Results
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 🚨 Race condition!
searchAPI(query).then(data => {
setResults(data);
});
}, [query]);
return results.map(r => <Result key={r.id} {...r} />);
}
// User types fast: "r" → "re" → "rea" → "reac" → "react"
// 5 requests fired!
// 😱 Results arrive in random order!
// 😱 Shows results for "rea" even though query is "react"!
// User confused: "Why am I seeing results for 'rea'?"
// ✅ Fix with AbortController:
useEffect(() => {
const controller = new AbortController();
searchAPI(query, { signal: controller.signal })
.then(data => setResults(data))
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, [query]);
// Now only the last search completes! ✨
🔥 Real Bug Examples:
Bug 2: Product Page
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);
useEffect(() => {
// 🚨 Two independent race conditions!
fetchProduct(productId).then(setProduct);
fetchReviews(productId).then(setReviews);
}, [productId]);
return (
<div>
<h1>{product?.name}</h1>
<Reviews reviews={reviews} />
</div>
);
}
// Bug:
// 1. User on Product #1
// 2. Clicks Product #2
// 3. Product #2 data loads fast ✅
// 4. Product #1 reviews load slow
// 5. 😱 Shows Product #2 details with Product #1 reviews!
// Mismatch! Wrong reviews displayed!
// ✅ Fix: Abort both requests
useEffect(() => {
const controller = new AbortController();
Promise.all([
fetchProduct(productId, { signal: controller.signal }),
fetchReviews(productId, { signal: controller.signal })
])
.then(([productData, reviewsData]) => {
setProduct(productData);
setReviews(reviewsData);
})
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, [productId]);
💪 Axios with Cancellation:
import axios from 'axios';
function DataFetcher({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
// ✨ Create cancel token
const source = axios.CancelToken.source();
axios.get(`/api/data/${id}`, {
cancelToken: source.token
})
.then(response => {
setData(response.data);
})
.catch(error => {
if (!axios.isCancel(error)) {
console.error('Error:', error);
}
});
// ✨ Cancel on cleanup
return () => {
source.cancel('Component unmounted or id changed');
};
}, [id]);
return <div>{data?.name}</div>;
}
🔥 React Query - Built-in Cancellation:
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
// ✨ React Query handles race conditions automatically!
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}
// React Query automatically:
// 1. Cancels old requests
// 2. Deduplicates requests
// 3. Caches results
// 4. Handles race conditions
// No manual cleanup needed! 🎉
😍 SWR Alternative:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function UserProfile({ userId }) {
// ✨ SWR handles race conditions too!
const { data: user, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <Spinner />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}
// SWR benefits:
// - Automatic race condition handling
// - Caching
// - Revalidation
// - Focus revalidation
🚨 Common Mistakes:
// ❌ MISTAKE #1: Aborting but not handling the error
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData);
// 🚨 No .catch! AbortError will be unhandled!
return () => controller.abort();
}, [url]);
// ✅ FIX: Always catch AbortError
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
// ❌ MISTAKE #2: Forgetting to pass signal
useEffect(() => {
const controller = new AbortController();
fetch(url); // 🚨 Missing { signal: controller.signal }!
return () => controller.abort(); // Does nothing!
}, [url]);
// ✅ FIX: Pass signal to fetch
fetch(url, { signal: controller.signal });
🚨 Common Mistakes:
// ❌ MISTAKE #3: Creating new controller on every render
const controller = new AbortController(); // 🚨 Outside useEffect!
useEffect(() => {
fetch(url, { signal: controller.signal });
return () => controller.abort();
}, [url]);
// ✅ FIX: Create inside useEffect
useEffect(() => {
const controller = new AbortController();
// ...
}, [url]);
😍 Ways To Fix:
1. Add AbortController (manual)
2. Add isCurrent flag (manual)
3. Use React Query (automatic)
4. Use SWR (automatic)
9. React's Anti-Pattern: Using Index As a Key
Using array index as key? You're creating silent bugs that corrupt user data! This mistake breaks forms, loses selections, and causes weird UI glitches.
The Problem:
Using index as key seems to work, but causes subtle bugs that are IMPOSSIBLE to debug!
❌ The Index Key Disaster:
function TodoList({ todos }) {
return todos.map((todo, index) => (
<TodoItem
key={index} // 🚨 DANGEROUS!
todo={todo}
/>
));
}
// Looks fine, but watch what happens...
// Initial list:
// [0] Buy milk
// [1] Walk dog
// [2] Code review
// User deletes "Walk dog" (index 1)
// React sees keys: 0, 1
// Old keys were: 0, 1, 2
// React thinks:
// - Key 0: Still there ✓
// - Key 1: Still there, but CONTENT changed (was "Walk dog", now "Code review")
// - Key 2: Removed
// 😱 React re-renders key 1 with NEW content!
// 😱 If TodoItem has internal state (checkbox, input), it's PRESERVED!
// 😱 User checked "Walk dog", but now "Code review" is checked!
🔥 Real Bug Example - Checkbox Corruption:
function TodoItem({ todo }) {
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState(todo.text);
return (
<div>
<input type="checkbox" defaultChecked={todo.completed} />
{isEditing ? (
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
) : (
<span>{todo.text}</span>
)}
<button onClick={() => setIsEditing(!isEditing)}>Edit</button>
</div>
);
}
function TodoList({ todos }) {
return todos.map((todo, index) => (
<TodoItem key={index} todo={todo} /> // 🚨 BUG!
));
}
// Bug reproduction:
// 1. User checks "Buy milk" (index 0)
// 2. User starts editing "Walk dog" (index 1)
// 3. User deletes "Buy milk" (index 0)
// 4. 😱 "Walk dog" moves to index 0
// 5. 😱 But React preserves state for index 0!
// 6. 😱 "Walk dog" now shows as checked (was "Buy milk")
// 7. 😱 Edit mode is lost!
// User data corrupted! 💀
✅ The Correct Way - Stable ID:
function TodoList({ todos }) {
return todos.map((todo) => (
<TodoItem
key={todo.id} // ✨ Use unique, stable ID!
todo={todo}
/>
));
}
// Now when "Walk dog" is deleted:
// React sees keys: "abc123", "xyz789"
// Old keys were: "abc123", "def456", "xyz789"
// React thinks:
// - "abc123": Still there ✓
// - "def456": Removed! Unmount it
// - "xyz789": Still there ✓
// ✨ React correctly removes the middle item!
// ✨ No state confusion!
// ✨ Checkboxes stay with correct items!
💪 When You Don't Have IDs:
// ❌ DON'T: Use index
todos.map((todo, index) => <Item key={index} />)
// ❌ DON'T: Generate random keys
todos.map((todo) => <Item key={Math.random()} />)
// Random keys remount components every render!
// ✅ DO: Generate stable IDs once
const todosWithIds = todos.map((todo) => ({
...todo,
id: todo.id || `${todo.text}-${Date.now()}-${Math.random()}`
}));
// ✅ DO: Use UUID library
import { v4 as uuidv4 } from 'uuid';
const addTodo = (text) => {
const newTodo = {
id: uuidv4(), // Generates unique ID
text,
completed: false
};
setTodos([...todos, newTodo]);
};
// ✅ DO: Use crypto.randomUUID (modern browsers)
const newTodo = {
id: crypto.randomUUID(), // Native browser API!
text: 'New task'
};
⚡️When Index IS Okay:
// ✅ ONLY use index when ALL these are true:
// 1. List never reorders
// 2. List never filters
// 3. List never adds/removes items
// 4. Items have no internal state
// 5. List is purely static display
// Example - Static display list:
const FEATURES = ['Fast', 'Secure', 'Reliable'];
function FeatureList() {
return FEATURES.map((feature, index) => (
<li key={index}> {/* ✅ OK here - list never changes */}
{feature}
</li>
));
}
// But if list CAN change → use IDs!
✅ Generating Keys - The Right Way:
// ✅ Option 1: Backend provides IDs
const todos = [
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Walk dog' }
];
// ✅ Option 2: Generate when adding
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([
...todos,
{
id: Date.now(), // Timestamp as ID (good for local state)
text
}
]);
};
// ✅ Option 3: UUID library
import { v4 as uuidv4 } from 'uuid';
const addTodo = (text) => {
setTodos([
...todos,
{
id: uuidv4(), // Universally unique
text
}
]);
};
// ✅ Option 4: Crypto API (no dependencies!)
const addTodo = (text) => {
setTodos([
...todos,
{
id: crypto.randomUUID(), // Browser native!
text
}
]);
};
// ✅ Option 5: Compound key for nested lists
<TodoItem
key={`${todoId}-${subtaskId}`} // Combine parent + child ID
task={subtask}
/>
📊 The Performance Impact:
Scenario: List of 1000 items, delete one from middle
❌ With index keys:
- React re-renders ALL 999 remaining items
- Each item's state potentially corrupted
- Time: 450ms
- Bugs: Many! 🐛🐛🐛
✅ With proper IDs:
- React removes exactly 1 item
- Other 999 items unchanged
- Time: 5ms
- Bugs: None! ✨
90x faster + zero bugs!
10. React Pro Secret: The "Data Fetching in Parallel" Pattern
Stop waiting for API calls one-by-one! Fetch in parallel and cut your loading time by 70%. This is how the pros do it!
The Problem:
Sequential API calls make users wait unnecessarily. Each request blocks the next one!
❌ Slow Sequential Fetching:
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
// 🚨 Waits at-least 1s for user
const userData = await fetch('/api/user').then(r => r.json());
setUser(userData);
// 🚨 Waits at-least 1s for posts
const postsData = await fetch('/api/posts').then(r => r.json());
setPosts(postsData);
// 🚨 Waits at-least 1s for comments
const commentsData = await fetch('/api/comments').then(r => r.json());
setComments(commentsData);
setLoading(false);
};
fetchData();
}, []);
// Total time: at-least 3 seconds! 😱
if (loading) return <Spinner />;
return <div>...</div>;
}
✅ Fast Parallel Fetching:
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
// ✨ All requests fire at once!
const [userData, postsData, commentsData] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
setUser(userData);
setPosts(postsData);
setComments(commentsData);
setLoading(false);
};
fetchData();
}, []);
// Total time: 1 second (fastest request)! 🎉
if (loading) return <Spinner />;
return <div>...</div>;
}
🔥 Better - Show Data as It Arrives:
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
const [comments, setComments] = useState(null);
useEffect(() => {
// ✨ Fire all requests, update as each completes
fetch('/api/user')
.then(r => r.json())
.then(setUser);
fetch('/api/posts')
.then(r => r.json())
.then(setPosts);
fetch('/api/comments')
.then(r => r.json())
.then(setComments);
}, []);
// ✨ Show each section as data arrives!
return (
<div>
{user ? <UserProfile user={user} /> : <UserSkeleton />}
{posts ? <PostsList posts={posts} /> : <PostsSkeleton />}
{comments ? <CommentsFeed comments={comments} /> : <CommentsSkeleton />}
</div>
);
}
// User sees content progressively - much better UX! 🎨
✨ Key Methods:
// Promise.all - Wait for all, fail if any fails
const results = await Promise.all([fetch1, fetch2, fetch3]);
// Promise.allSettled - Wait for all, never fails
const results = await Promise.allSettled([fetch1, fetch2, fetch3]);
// Promise.race - Use first to complete
const result = await Promise.race([fetch1, fetch2, fetch3]);
// Promise.any - Use first success, fail only if all fail
const result = await Promise.any([fetch1, fetch2, fetch3]);
💡 When to Use Each:
Promise.all: All requests must succeed (default choice)
Promise.allSettled: Some can fail, show partial data
Promise.race: Use fastest response (redundant APIs)
Promise.any: Fallback APIs (use first working one)
Top comments (0)