In an AI-driven world, we should keep our basics clear at all times. This is the second post in my next.js learning series where we will learn about React as it's a fundamental part of Next.js. In the future, I may also add an optional HTML & CSS post, but since I'm already familiar with their fundamentals, I'm not creating one just now.
Modern React Essentials: From Component Architecture to Advanced Hooks
Index
- Modern React Development with Vite
- Core React Concepts
- React with TypeScript
- Building UIs with Components
- State Management and Hooks
- Advanced Hooks and Patterns
- Working with APIs: Loading and Error States
- Navigation (React Router)
- Performance and Optimization
- Professional Testing with Vitest
Modern React Development with Vite
While you can use React by adding script tags to an HTML file, the industry standard for building real-world applications is using a build tool. Currently, Vite is the most popular and recommended tool for creating new React projects.
Prerequisites: Node.js
Before using modern tools like Vite, you must have Node.js installed. Node.js is a JavaScript runtime that allows you to run JavaScript outside of a browser.
- npm (Node Package Manager): Used to download and manage libraries (like React).
- npx (Node Package Execute): Allows you to run a package without installing it globally. For example,
npx create-viteruns the creator tool once and then cleans up.
Why Vite?
Vite is significantly faster than older tools like create-react-app because of its modern architecture:
- Native ESM: It serves code as native ES modules, which browsers can parse directly, skipping the slow bundling step during development.
- Hot Module Replacement (HMR): Updates only the specific module you changed, keeping the application state intact.
Strict Mode: Why useEffect runs twice
In a new Vite project, you'll see your code wrapped in <React.StrictMode>.
- The Behavior: In Development Mode, React intentionally mounts every component, unmounts it, and mounts it again. This causes
useEffectto run twice. - The Why: This helps catch bugs where you've forgotten cleanup logic, but more importantly, it identifies Stale Closures and ensures your logic is Idempotent.
Idempotency Visualization:
Run 1 (Mount) → Effect Setup (e.g., Subscribe) Run 2 (Unmount) → Effect Cleanup (e.g., Unsubscribe) Run 3 (Re-mount) → Effect Setup (e.g., Subscribe) Result: App remains stable. If step 2 is missing, step 3 creates a DUPLICATE subscription.
- Production: This behavior is stripped away in the final build.
Environment Variables (.env)
In real applications, you never hardcode API keys or secret URLs. You store them in a .env file at the root of your project.
Definition (.env file):
# Variables MUST start with VITE_ to be visible to your React code
VITE_API_URL=https://api.myapp.com
VITE_ANALYTICS_KEY=abc123secret
Usage (in your JS code):
const apiUrl = import.meta.env.VITE_API_URL;
console.log("Connecting to:", apiUrl);
Using npm (Node Package Manager)
- Local Install (
npm install <package>ornpm i): Adds the package topackage.jsonunderdependencies. Used for libraries your app needs to run (like React or Axios). - Development Install (
npm install -D <package>): Adds todevDependencies. Used for tools needed only during coding (like Tailwind or Vite) and removed in production. - Global Install (
npm install -g <package>ornpm i -g): Installs the package system-wide. Typically used for CLIs (e.g.,firebase-tools,nodemon).
Dev vs. Prod: Different Commands
Modern development distinguishes between "writing code" and "shipping code."
| Command | Usage | Result |
|---|---|---|
npm run dev |
Development | Starts a local server with HMR. |
npm run build |
Production | Minifies JS/CSS, optimizes images into a dist/ folder. |
npm run preview |
Production Test | Locally serves the optimized dist/ folder for final checking. |
Proxying API Requests (vite.config.js)
To avoid CORS (Cross-Origin Resource Sharing) errors, where the browser blocks requests between different ports (e.g., 5173 to 3000).
The Scenario:
Imagine your backend has an endpoint http://localhost:3000/api/users.
- Before Proxy (CORS Error):
axios.get('http://localhost:3000/api/users')❌ Blocked by browser. - After Proxy (Success):
axios.get('/api/users')✅ Vite forwards this to localhost:3000.
Config (vite.config.js):
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
})
Core React Concepts
React Fundamentals
To see how React works "under the hood," you can run it in a single HTML file.
Definition:
<!DOCTYPE html>
<html>
<head>
<title>React Fundamentals</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return <h1>Hello from fundamental React!</h1>;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
The Virtual DOM and Reconciliation
One of the main reasons React is so fast is how it handles updates.
Visualization of the flow:
State Change → New Virtual DOM → Diffing (Compare with Old VDOM) → Patch Real DOM
- Virtual DOM: Instead of changing the real browser HTML (which is slow) every time data changes, React creates a lightweight "copy" of the UI in memory.
- Diffing: When state changes, React compares the new Virtual DOM with the old one to see exactly what changed.
- Reconciliation: React then updates only the parts of the real DOM that actually changed.
The Immutability Principle: Why Spread?
React only re-renders if it detects a new reference in state. Direct mutation fails to trigger re-renders.
Definition:
const [user, setUser] = useState({ name: 'Preyum', age: 25 });
// ❌ WRONG: Mutation (React won't re-render)
user.age = 26;
setUser(user);
// ✅ CORRECT: Create a NEW object (React re-renders)
setUser({ ...user, age: 26 });
Technical Insight: If asked why we use the spread operator, mention Referential Equality. React uses shallow comparison (
Object.is) to check if state has changed. If you mutate the object, the reference remains the same, and React assumes nothing has changed.
Babel and JSX
JSX is a syntax extension for JavaScript. Babel compiles it into React.createElement calls.
Key Rules and Why They Exist:
-
Strict Tag Closing: Every tag must be closed. If an element doesn't have children, you must use a self-closing tag.
<input type="text" /> // ✅ Correct className instead of class: In JS,
classis a reserved keyword for ES6 classes. React usesclassNameto avoid conflicts.One Root Element: A component must return a single element because a function can only return one value. Use Fragments
<> </>if you don't want a wrapperdiv.
The .render() Method
The .render() call is the entry point where your React tree is injected into the real browser DOM.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
React with TypeScript
TSX vs. JSX: The Power of Types
In .tsx, your code won't even compile if types don't match, catching errors before they reach the browser.
Before (JSX - Dangerous):
const User = ({ name }) => <h1>{name.toUpperCase()}</h1>;
<User name={123} /> // ❌ Crash: .toUpperCase() is not a function for numbers
After (TSX - Safe):
interface UserProps { name: string; }
const User = ({ name }: UserProps) => <h1>{name.toUpperCase()}</h1>;
// <User name={123} /> // ❌ TypeScript Error: 123 is not a string!
Interface vs. Type
For 90% of React work, they are interchangeable. The key difference is extensibility:
-
Interfaces are "Open": They support Declaration Merging.
interface User { name: string; } interface User { age?: number; } // ✅ Merged: User now has both -
Types are "Closed": They are aliases. To combine them, you must use Intersections (
&).
type Name = { name: string; }; type Age = { age: number; }; type User = Name & Age;
Required vs. Optional Properties
By default, every property is Required. Use the question mark (?) to make them optional.
Definition:
interface UserProps {
name: string; // Required
age?: number; // Optional
}
Usage:
const Greet = ({ name, age }: UserProps) => (
<h1>Hello {name} {age ? `(${age})` : ""}</h1>
);
const App = () => (
<>
<Greet name="Preyum" /> {/* ✅ Works (age omitted) */}
<Greet name="Kumar" age={25} /> {/* ✅ Works (age provided) */}
</>
);
Typing Events in TypeScript
A major pain point is knowing how to type function arguments for events.
Definition:
const Input = () => {
// Use React.ChangeEvent for inputs
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// Use React.FormEvent for form submissions
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
</form>
);
};
Type Inference: Implicit vs. Explicit
1. Implicit Type Inference
TypeScript "guesses" the type based on the initial value.
Example:
const [count, setCount] = useState(0);
// TypeScript automatically knows 'count' is a number.
2. Explicit Type Definition
Necessary when the initial value is empty (like null) or complex.
Example:
interface Task { id: number; text: string; }
const [tasks, setTasks] = useState<Task[]>([]);
const [user, setUser] = useState<User | null>(null);
Building UIs with Components
Note on File Structure: In real-world projects, every component is typically defined in its own file (e.g.,
Button.jsx) and then imported where needed. For clarity in these examples, we show the Definition and the Usage together.
Components
Definition:
const Button = ({ text, color, handleClick }) => (
<button
onClick={handleClick}
style={{ backgroundColor: color, color: 'white', padding: '10px' }}
>
{text}
</button>
);
Usage:
const App = () => (
<Button text="Save Changes" color="green" handleClick={() => alert("Saved!")} />
);
Event Handling: Passing Arguments
A common mistake is calling a function directly in the onClick.
Definition:
const List = () => {
const deleteItem = (id) => console.log("Deleting", id);
return (
<ul>
{/* ❌ WRONG: Runs immediately on render because of (5) */}
<button onClick={deleteItem(5)}>Delete 5</button>
{/* ✅ CORRECT: Wrapped in an arrow function */}
<button onClick={() => deleteItem(5)}>Delete 5</button>
{/* ✅ CORRECT: If no arguments, pass by reference */}
<button onClick={console.log}>Log Event</button>
</ul>
);
};
Props (Properties)
Props are read-only data passed from parent to child.
Definition (Destructuring with Default Values):
const Card = ({ title = "Untitled", children, category = "General" }) => (
<div className="card">
<small>{category}</small>
<h2>{title}</h2>
<div className="content">{children}</div>
</div>
);
The Magic "children" Prop
The children prop automatically passes whatever you put between the opening and closing tags.
Usage:
const App = () => (
<Card title="React Basics">
<p>This paragraph is passed as the children prop!</p>
</Card>
);
Component Composition: Avoiding Prop Drilling
Before reaching for useContext, you can often avoid prop drilling by passing components as props. This is a sign of clean architecture.
Definition:
const Layout = ({ sidebar, content }) => (
<div className="layout">
<aside>{sidebar}</aside>
<main>{content}</main>
</div>
);
Usage:
const App = () => (
<Layout
sidebar={<nav>Menu</nav>}
content={<h1>Dashboard</h1>}
/>
);
Portals: Rendering Outside the Tree
Sometimes you need a component (like a Modal or Tooltip) to render at the top level of the HTML hierarchy (near <body>), even if the component is deep inside your React tree.
1. Setup (in your index.html)
<body>
<div id="root"></div>
<div id="portal-root"></div> <!-- Add this for Portals -->
</body>
2. Definition (in JS)
import ReactDOM from 'react-dom';
const Modal = ({ children }) => {
return ReactDOM.createPortal(
<div className="modal-overlay">{children}</div>,
document.getElementById('portal-root')
);
};
Usage:
const App = () => (
<div style={{ overflow: 'hidden' }}>
<p>I am deep in the tree.</p>
<Modal>I am rendering outside this hidden div!</Modal>
</div>
);
Lifting State Up: Sibling Communication
When two sibling components need to share data, you move the state to their closest common parent.
Definition:
const Parent = () => {
const [text, setText] = useState("");
return (
<>
<InputComponent value={text} onType={setText} />
<DisplayComponent content={text} />
</>
);
};
This pattern is the foundation of React architecture because of the One-Way Data Flow principle. Data in React can only flow down from parent to child via props.
By "lifting" the state to the Parent:
- The
InputComponentcan update the state using theonTypefunction. - The
Parentre-renders with the new value. - The new value is automatically passed down to the
DisplayComponentas a prop.
Rendering Lists: The "No Index" Rule
Definition:
const Navbar = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.label}</li>
))}
</ul>
);
- Rule of Thumb: Avoid using the array
indexas a key if the list is dynamic (sortable, filterable, or removable). - The Nuance: React uses the
keyto identify which items have changed, been added, or removed. When the index is used, shifting an item changes the index of every subsequent item, forcing a full re-render of the entire list instead of just one targeted move. - Static Lists: If your list is strictly static (like a fixed footer menu), using
indexis perfectly fine and performant.
Conditional Rendering
Definition:
const AuthPanel = ({ user, error }) => (
<div className="auth">
{user ? (
<button>Logout ({user.name})</button>
) : (
<>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button>Login</button>
</>
)}
</div>
);
In React, we use standard JavaScript logic to decide which UI to show:
- Ternary Operator (
? :): Best for "If-Else" scenarios, like switching between a Login and Logout button. - Logical AND (
&&): Best for "Show-Hide" scenarios, where you only want to render an element (like an error message) if a specific condition is met.
Usage:
const App = () => {
const currentUser = { name: "Preyum" };
const loginError = "Session Expired";
return (
<AuthPanel user={null} error={loginError} />
);
};
Fragments and Keyed Fragments
Fragments <> </> allow you to group multiple elements without adding an extra <div> to the real DOM, which prevents breaking CSS layouts like Flexbox or Grid.
Keyed Fragments: If you are in a loop and need to use a fragment with a key, you cannot use the shorthand. You must use the full name.
Definition & Usage:
const Glossary = ({ items }) => (
<dl>
{items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.desc}</dd>
</React.Fragment>
))}
</dl>
);
Error Boundaries: The App Safety Net
A JavaScript error in a component shouldn't crash the whole app. Error Boundaries are Class Components that catch errors in their child tree.
Definition:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) return <h1>Something went wrong.</h1>;
return this.props.children;
}
}
Usage:
const App = () => (
<ErrorBoundary>
<MyBuggyComponent />
</ErrorBoundary>
);
Styling in React: Inline vs CSS Modules vs Tailwind
1. Inline Styles (JavaScript Objects)
Styles are written as objects using camelCase instead of kebab-case.
const style = { color: 'blue', fontSize: '20px' };
const Header = () => <h1 style={style}>Styled Header</h1>;
2. CSS Modules (The Vite Standard)
Prevents global naming conflicts. Styles only apply to the component that imports them.
// Button.module.css
// .btn { background: green; }
import styles from './Button.module.css';
const Button = () => <button className={styles.btn}>Module Button</button>;
3. Tailwind CSS (The Modern Standard)
Utility-first CSS. This is the go-to for modern developers.
const Button = () => (
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Tailwind Button
</button>
);
State Management and Hooks
The Rules of Hooks
To keep React stable, Hooks must follow two strict rules:
- Only at the Top Level: Don't call Hooks inside loops, conditions, or nested functions.
- Only in React Functions: Only call Hooks from Functional Components or Custom Hooks.
Why the "Top Level" Rule?
React keeps track of your state by the order in which Hooks are called.
Internal Visualization:
Render 1: [Hook 1 (useState), Hook 2 (useEffect), Hook 3 (useState)]
Render 2: [Hook 1 (useState), Hook 2 (useEffect), Hook 3 (useState)]
If you put a Hook inside an if statement, the order breaks, and React loses track of which state belongs to which variable!
Example: Why the "Top Level" rule matters
// ❌ WRONG: Hook inside a condition
if (user) {
useEffect(() => {
console.log("Logged in!");
}, []);
}
// ✅ CORRECT: Condition inside the Hook
useEffect(() => {
if (user) {
console.log("Logged in!");
}
}, [user]);
State vs. Props vs. Refs: Which one to use?
Data Flow Visualization:
Parent (Owner of State) --[Passes Props]--→ Child (Receiver)
Ref (Side Channel) ----------------------→ Target DOM Element
| Tool | Trigger Render? | Mutable? | Best For... |
|---|---|---|---|
| Props | Yes (Parent re-renders) | No | Receiving data from parent. |
| State | Yes (Instant) | No | Data that changes over time & affects UI. |
| Refs | No | Yes | Accessing DOM or storing "silent" values. |
useState: Controlled vs Uncontrolled
- Controlled (Recommended): React state handles the input value.
- Uncontrolled: The DOM handles the input value (using
useRef).
Definition (Controlled Input):
const Input = () => {
const [val, setVal] = useState("");
return <input value={val} onChange={(e) => setVal(e.target.value)} />;
};
Definition (Uncontrolled Input):
const UncontrolledInput = () => {
const inputRef = useRef(null);
const getValue = () => console.log(inputRef.current.value);
return (
<>
<input ref={inputRef} type="text" />
<button onClick={getValue}>Get Value</button>
</>
);
};
useState: Handling Multiple Inputs
Instead of multiple state calls, use a single object.
Definition:
const Form = () => {
const [data, setData] = useState({ name: "", email: "" });
const handleChange = (e) => {
const { name, value } = e.target;
setData(prev => ({ ...prev, [name]: value })); // Spread operator is KEY
};
return (
<form>
<input name="name" value={data.name} onChange={handleChange} />
<input name="email" value={data.email} onChange={handleChange} />
</form>
);
};
useState: Asynchronous Updates and Batching
React "batches" updates together for performance.
Definition:
const Profile = () => {
const [profile, setProfile] = useState({ name: 'Preyum', role: 'Dev' });
const promote = () => {
setProfile(prev => ({ ...prev, role: 'Senior Dev' }));
// Note: profile.role will STILL be 'Dev' on this line because state updates are scheduled, not immediate.
};
return <button onClick={promote}>Promote {profile.name}</button>;
};
Technical Insight: React 18+ uses Automatic Batching. Even if you update state inside a
setTimeoutor afetchcall, React will now group them into one single render to ensure high responsiveness.
useEffect: The Dependency Array
Determines when the effect should re-run.
1. No Dependency Array
Runs after every render. This is rarely used and can lead to performance issues or infinite loops if you update state inside the effect.
useEffect(() => {
console.log("I run on every single render!");
});
2. Empty Dependency Array ([])
Runs only once, immediately after the component is mounted. This is the ideal place for API calls or setting up subscriptions.
useEffect(() => {
console.log("I run only once on mount (componentDidMount)");
fetchData();
}, []);
3. Dependency Array with Values ([count])
Runs on mount and then re-runs whenever any value in the array changes. This is used to sync your effect with specific state or props.
useEffect(() => {
console.log("I run on mount and whenever 'count' changes");
}, [count]);
useEffect: The Lifecycle Mental Model
| Dependency Array | Lifecycle Phase |
|---|---|
[] (Empty) |
Mount |
[var] (Populated) |
Update |
return () => ... |
Unmount |
useEffect Cleanup: AbortController
To prevent memory leaks or "state updates on unmounted components," always clean up API calls.
Definition & Usage:
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await axios.get('/api/data', { signal: controller.signal });
setData(res.data);
} catch (err) {
// Bulletproof: Handle both native fetch and Axios abort patterns
if (err.name === 'AbortError' || axios.isCancel(err)) return;
console.error(err);
}
};
fetchData();
// 🧹 CLEANUP: Runs when component is destroyed
return () => controller.abort();
}, []);
useLayoutEffect vs useEffect
- useEffect: Runs after the screen is painted. Best for most tasks.
- useLayoutEffect: Runs before the screen is painted. Use for measuring DOM elements to prevent UI flickering.
useRef and forwardRef
While useRef lets you access a DOM node inside your component, forwardRef allows you to pass that access to a custom child component.
Definition (forwardRef):
import { forwardRef } from 'react';
const MyInput = forwardRef((props, ref) => (
<input ref={ref} className="fancy-input" {...props} />
));
Usage:
const Parent = () => {
const inputRef = useRef(null);
const handleFocus = () => inputRef.current.focus();
return (
<>
<MyInput ref={inputRef} placeholder="Focus me!" />
<button onClick={handleFocus}>Focus the Input</button>
</>
);
};
useImperativeHandle
Used together with forwardRef. It allows you to "expose" only specific functions or properties of a child component to its parent, rather than giving the parent full access to the underlying DOM node. This is often used for animations or manual focus management in reusable UI libraries.
Definition:
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
shake: () => {
console.log("Shaking the input!");
}
}));
return <input ref={inputRef} {...props} />;
});
Usage:
const App = () => {
const customRef = useRef();
return (
<>
<FancyInput ref={customRef} />
<button onClick={() => customRef.current.focus()}>Focus Child</button>
<button onClick={() => customRef.current.shake()}>Shake Child</button>
</>
);
};
Advanced Hooks
useReducer: Managing Complex State with Payloads
useReducer is better than useState when you have complex data logic.
Definition:
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
// 'action.payload' is the extra data passed
return [...state, action.payload];
case 'REMOVE_ID':
return state.filter(item => item.id !== action.payload);
default:
return state;
}
};
Usage:
const ShoppingCart = () => {
const [cart, dispatch] = useReducer(cartReducer, []);
const addToCart = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
return <button onClick={() => addToCart({ id: 1, name: 'Laptop' })}>Buy</button>;
};
useContext: The Provider Pattern
Professional apps refactor the context into a dedicated Provider component.
Definition (UserContext.jsx):
const UserContext = React.createContext();
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: "Preyum" });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
Usage:
// 1. Wrap your app (usually in App.js or main.js)
<UserProvider>
<Dashboard />
</UserProvider>
// 2. Consume in any child component
const Dashboard = () => {
const { user } = useContext(UserContext);
return <h1>Welcome, {user.name}</h1>;
};
Technical Insight: When does Context become a problem? Context is great for static-ish data (Theme, User). If the data changes every second, Context causes the entire child tree to re-render. For high-frequency state, reach for a global manager like Zustand.
Global State Alternatives (Zustand & Redux)
When Context isn't enough, developers move to dedicated state management libraries.
- Redux: The traditional industry standard. It uses a "Flux" architecture with a strict unidirectional data flow. However, it is famous for requiring a lot of "boilerplate" code (Actions, Reducers, Store, Dispatch).
- Zustand: A modern, lightweight alternative that is much simpler than Redux and doesn't require wrapping your whole app in Providers.
Definition (Zustand):
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}));
Custom Hooks: Reusing Logic
Custom hooks allow you to extract component logic into reusable functions. They must start with the word use.
Definition:
const useFetch = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return data;
};
Usage:
const UserProfile = () => {
const userData = useFetch('/api/user/1');
return <div>{userData?.name}</div>;
};
useId (Accessibility)
The useId hook generates unique, stable IDs that work correctly on both the server and the client. This is the professional standard for linking <label> elements to <input> fields, ensuring your forms are accessible to screen readers without risk of "ID collisions" (where two elements accidentally share the same ID).
Definition & Usage:
import { useId } from 'react';
const PasswordField = () => {
const id = useId();
return (
<>
<label htmlFor={id}>Password</label>
<input id={id} type="password" />
</>
);
};
Working with APIs: Loading and Error States
Real-world API calls are not instant. You must handle three states: Idle/Loading, Success, and Error.
Definition:
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
const res = await axios.get(`/api/users/${userId}`);
setUser(res.data);
} catch (err) {
setError("Failed to load user profile.");
} finally {
setLoading(false);
}
};
loadData();
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p style={{ color: 'red' }}>{error}</p>;
return <h1>{user.name}</h1>;
};
Next.js Bridge: While we use
useEffect+axioshere (Client-side fetching), Next.js 14+ often handles data fetching on the Server (Server Components). This eliminates Loading states entirely for the user!
Navigation (React Router)
For Single Page Applications (SPAs), we swap components based on the URL without refreshing the page (using React Router v6 syntax).
Definition (Dynamic Routes):
import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom';
const UserPage = () => {
const { id } = useParams(); // Extracts '123' from /user/123
return <h1>Profile of ID: {id}</h1>;
};
const RouterApp = () => (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/user/123">My Profile</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserPage />} />
</Routes>
</BrowserRouter>
);
Performance and Optimization
React.memo
Wraps a component to prevent re-renders if its props haven't changed.
Definition:
const PureChild = React.memo(({ name }) => {
console.log("Child render");
return <div>{name}</div>;
});
useMemo and useCallback
- useMemo: Caches a calculated Value.
- useCallback: Caches a Function instance.
Fundamental Rule: Referential Equality
In JavaScript, objects and functions are compared by reference, not by value.
{} === {} // false
() => {} === () => {} // false
Because of this, React will often re-render child components even if the "data" looks the same, simply because the reference changed. This is exactly what useMemo and useCallback prevent.
The Danger of Over-Optimization:
Don't wrap everything in useMemo. The overhead of dependency checking and memory allocation for the cache can be more expensive than the calculation itself.
Only use them for:
- Truly Expensive Calculations: (e.g., sorting 10,000 items).
- Referential Equality: When passing functions/objects to
React.memochildren.
useTransition (React 18)
Allows you to mark state updates as "non-urgent" to keep the UI responsive. It returns [isPending, startTransition].
Definition:
const [isPending, startTransition] = useTransition();
// Marking an update as non-urgent
startTransition(() => {
setNonUrgentState(newValue);
});
Usage:
import { useState, useTransition } from 'react';
function ListFilter({ names }) {
const [query, setQuery] = useState('');
const [highlight, setHighlight] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// Urgent update (input field)
setQuery(e.target.value);
// Non-urgent update (list filtering)
startTransition(() => {
setHighlight(e.target.value);
});
};
return (
<div>
<input type="text" value={query} onChange={handleChange} />
{isPending && <p>Filtering...</p>}
<ul>
{names.map(name => (
<li key={name} style={{ color: name.includes(highlight) ? 'red' : 'black' }}>
{name}
</li>
))}
</ul>
</div>
);
}
useDeferredValue
Defers updating a value until the main UI thread is free. This is perfect for expensive filtering or rendering that doesn't need to be immediate.
Definition:
const deferredValue = useDeferredValue(originalValue);
Usage:
import { useState, useDeferredValue, useMemo } from 'react';
function SearchResults({ query, items }) {
// Defer the query value itself
const deferredQuery = useDeferredValue(query);
// Use the deferred value for expensive filtering
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(deferredQuery));
}, [deferredQuery]);
return (
<ul style={{ opacity: query !== deferredQuery ? 0.5 : 1 }}>
{filteredItems.map(item => <li key={item}>{item}</li>)}
</ul>
);
}
Lazy Loading and Suspense
This splits your application into smaller chunks. Instead of loading the entire app on the first visit, React only loads the parts the user actually navigates to.
Definition:
import { lazy, Suspense } from 'react';
// This component is loaded only when needed
const HeavyDashboard = lazy(() => import('./Dashboard'));
const App = () => (
<Suspense fallback={<div>Loading Page Chunks...</div>}>
<HeavyDashboard />
</Suspense>
);
Professional Testing with Vitest
Testing ensures your components don't break when you add new features.
Definition (Install):
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
Usage (Simple Test):
import { render, screen } from '@testing-library/react';
import { test, expect } from 'vitest';
import Button from './Button';
test('renders button with correct text', () => {
render(<Button text="Click Me" />);
const btnElement = screen.getByText(/Click Me/i);
expect(btnElement).toBeInTheDocument();
});
Stay tuned for my next post, where we'll be exploring Next.js Fundamentals! While this covers the essentials of React, if there’s anything you feel I should have included, let me know in the comments below and I'll gladly add it!
Top comments (0)