Introduction
What it is: React is a JavaScript library for building user interfaces (UI). A UI is what the user sees and clicks, like buttons, forms, and lists.
Why it is used (real-life example): Think about a chat app. New messages appear without refreshing the page. React helps you update only the parts that change.
Step-by-step
- You build UI using small pieces called components.
- A component can show HTML-like code called JSX.
- When data changes, React updates the UI for you.
Code example 1: A tiny component
function Hello() {
return (
<h1>Hello React</h1>
);
}
export default Hello;Explanation: Hello is a component. It returns JSX. React will show the h1 on the page.
Code example 2: Using the component
import Hello from "./Hello";
function App() {
return (
<div>
<Hello />
</div>
);
}
export default App;Explanation: App is another component. <Hello /> means “render the Hello component here”.
Common mistakes
- Thinking React is a full framework that includes everything by default. React focuses mainly on UI.
- Forgetting to export the component, so it cannot be imported.
Tips / best practices
- Keep components small and focused on one job.
- Name components with a capital letter, like
HelloandApp.
JSX
What it is: JSX is a syntax that looks like HTML, but it is written inside JavaScript. React uses it to describe the UI.
Why it is used (real-life example): In a dashboard, you might show a title, a user name, and a button. JSX lets you write this UI in a clear, readable way.
Step-by-step
- JSX uses tags like HTML, such as
<div>. - To insert JavaScript values, use curly braces:
{ }. - JSX must return one parent element (one top-level wrapper).
Code example 1: Insert a value
function Welcome() {
const name = "Amina";
return (
<h2>Welcome, {name}!</h2>
);
}Explanation: {name} prints the value of the JavaScript variable name inside the UI.
Code example 2: One parent element
function Card() {
return (
<div>
<h3>Account</h3>
<p>Status: Active</p>
</div>
);
}Explanation: h3 and p are both inside one parent div. This is required in JSX returns.
Common mistakes
- Using
classinstead ofclassNamein JSX. - Forgetting the wrapper element and returning two top-level tags.
Tips / best practices
- Use curly braces only for JavaScript expressions (like variables), not statements (like
ifblocks). - Keep JSX readable by using parentheses and good indentation.
Components
What it is: A component is a reusable UI piece. It can be as small as a button or as big as a page section.
Why it is used (real-life example): In an online store, every product can be shown using the same “Product Card” component. You reuse it for many products.
Step-by-step
- Create a function that starts with a capital letter.
- Return JSX from the function.
- Use the component like an HTML tag:
<MyComponent />.
Code example 1: A reusable button
function PrimaryButton() {
return (
<button className="primary">Buy now</button>
);
}
export default PrimaryButton;Explanation: This component always shows the same button text. Later, you will learn how to make it flexible with props.
Code example 2: Compose components
import PrimaryButton from "./PrimaryButton";
function ProductCard() {
return (
<div>
<h3>T-Shirt</h3>
<p>$20</p>
<PrimaryButton />
</div>
);
}Explanation: This is called composition. You build bigger UI by combining smaller components.
Common mistakes
- Naming a component with a lowercase first letter. React may treat it like a normal HTML tag.
- Putting too much logic and UI in one huge component, making it hard to read.
Tips / best practices
- Keep components focused: one component, one main job.
- Reuse components instead of copying and pasting JSX.
What it is
Props are values you pass from a parent component to a child component. Props let you reuse the same component with different data.
Why it is used (real-life example)
Think about a UserCard that shows a name. You can reuse the same card for many users by passing a different name each time.
Step-by-step
- Create a child component that receives a parameter called props.
- Read values like
props.nameinside the child. - In the parent, render the child and add attributes like
name="Ava". - React sends those values into the child as props.
Code examples
Example 1: Passing one prop
function Greeting(props) {
return Hello, {props.name}!
;
}
export default function App() {
return (
);
}Example 2: Destructuring props
function PriceTag({ label, price }) {
return {label}: ${price}
;
}
export default function App() {
return (
);
}Common mistakes
- Trying to change props inside the child. Props are read-only.
- Forgetting curly braces for numbers/booleans: use
price={3}notprice="3"if you want a number. - Spelling mismatch between parent and child (example:
userNamevsusername).
Tips / best practices
- Keep props small and clear. Use simple names like
name,title,count. - Use destructuring to make code easier to read.
- When a value can be missing, add a safe fallback in your UI.
What it is
State is data that can change over time inside a component. When state changes, React re-renders the UI to show the new value.
Why it is used (real-life example)
A counter in a shopping app changes when you press “+” or “-”. That changing number is state.
Step-by-step
- Import
useStatefrom React. - Call
useState(initialValue)to create state. - Use the first value (like
count) to read state. - Use the second value (like
setCount) to update state. - Update state in an event like a button click.
Code examples
Example 1: Counter state
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}Example 2: Text input state
import { useState } from "react";
export default function App() {
const [name, setName] = useState("");
return (
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Type your name"
/>
You typed: {name}
);
}Common mistakes
- Changing state directly (wrong):
count = count + 1. Always usesetCount. - Expecting state to update instantly in the same line every time. React may batch updates.
- Using state when a simple constant is enough. Not everything needs state.
Tips / best practices
- Name your setter like
setXfor clarity. - Keep state as small as possible. Store the source of truth, not many copies.
- When the next state depends on the previous state, prefer a function update:
setCount(c => c + 1).
What it is
Events are actions in the UI, like clicking a button or typing in an input. In React, you handle events by passing a function, for example onClick.
Why it is used (real-life example)
When a user presses “Login”, you want to run code. That button press is an event.
Step-by-step
- Create a function that does something (like show an alert or update state).
- Attach it to an element using an event prop like
onClickoronChange. - If you need event data, accept a parameter (commonly named
e). - Use
e.target.valuefor input text.
Code examples
Example 1: Button click
export default function App() {
function handleClick() {
alert("Button clicked");
}
return (
);
}Example 2: Reading input with onChange
import { useState } from "react";
export default function App() {
const [email, setEmail] = useState("");
function handleChange(e) {
setEmail(e.target.value);
}
return (
Email: {email}
);
}Common mistakes
- Calling the function instead of passing it: wrong
onClick={handleClick()}. Correct:onClick={handleClick}. - Forgetting to use
onChangewith avaluewhen making a controlled input. - Putting too much logic inside the JSX. It can make code hard to read.
Tips / best practices
- Name handler functions like
handleClick,handleSubmit. - Keep handlers small. Move long logic into helper functions.
- Use inline arrow functions only when needed (for passing a value), because they create a new function each render.
Lists
What it is: A list is when you show many items on the screen by repeating a small UI template.
Why it is used (real-life example): Think about a shopping app. It shows a list of products. React helps you create that list from an array of data.
Step-by-step
- Start with an array (like products or names).
- Use
map()to turn each item into JSX. - Render the result inside a parent element like
<ul>.
Code example 1: Simple list of names
function NameList() {
const names = ["Ava", "Ben", "Cleo"];
return (
<ul>
{names.map((name) => (
<li>{name}</li>
))}
</ul>
);
}Explanation: map() goes through the array and returns one <li> for each name.
Code example 2: Render cards from data
function ProductList() {
const products = [
{ id: 1, title: "Phone", price: 399 },
{ id: 2, title: "Headphones", price: 49 }
];
return (
<div>
{products.map((p) => (
<div>
<h4>{p.title}</h4>
<p>Price: ${p.price}</p>
</div>
))}
</div>
);
}Explanation: You store data as objects. Then you read fields like p.title and p.price inside JSX.
Common mistakes
- Forgetting to return JSX inside
map()when using curly braces. - Trying to render an object directly (React cannot show plain objects).
- Mutating the array while rendering (keep rendering pure).
Tips / best practices
- Keep each list item simple. If it grows, extract a small component for one row/card.
- Prepare your data first, then render. This keeps your JSX clean.
Keys
What it is: A key is a special value you give to items in a list so React can track them correctly.
Why it is used (real-life example): Imagine a todo list. When you delete one todo, React needs to know exactly which row to remove. Keys help React match old items to new items.
Step-by-step
- When using
map(), add akeyprop to the top element you return. - Use a stable unique value (like an
idfrom your data). - Avoid using the array index as a key when items can be added/removed/reordered.
Code example 1: Correct keys with id
function TodoList() {
const todos = [
{ id: "t1", text: "Buy milk" },
{ id: "t2", text: "Read book" }
];
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}Explanation: todo.id stays the same even if you add or remove items. This makes updates safe.
Code example 2: Bad keys (index) and why
function BadKeyExample() {
const items = ["A", "B", "C"];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}Explanation: If you remove "A", the indexes change. React may reuse the wrong DOM nodes, which can cause weird UI bugs (like wrong input values).
Common mistakes
- Putting
keyon a child element inside the returned element instead of the top element frommap(). - Using random keys like
Math.random()(keys change every render, which breaks React’s optimization). - Using non-unique keys (two items sharing the same key).
Tips / best practices
- Prefer real IDs from your data (database ID, UUID, or stable string).
- If you do not have an ID, create one when you create the data, not during render.
Conditional
What it is: Conditional rendering means showing different UI based on a true/false condition.
Why it is used (real-life example): On a website, you might show “Login” when the user is logged out, and show “Logout” when the user is logged in.
Step-by-step
- Decide what condition you need (example:
isLoggedIn). - Use one of the common patterns:
ifbefore return, ternary? :, or&&. - Keep each condition small and easy to read.
Code example 1: Ternary operator
function LoginStatus({ isLoggedIn }) {
return (
<p>
{isLoggedIn ? "You are logged in" : "Please log in"}
</p>
);
}Explanation: The ternary picks one text when the condition is true, and another text when it is false.
Code example 2: Show something only when true
function Message({ hasNewMessage }) {
return (
<div>
<h4>Inbox</h4>
{hasNewMessage && <p>You have a new message.</p>}
</div>
);
}Explanation: With &&, the paragraph only renders when hasNewMessage is true.
Common mistakes
- Returning nothing by accident (example: forgetting
returninside anifblock). - Using
&&with a number that can be 0 (0 will render on screen). - Making JSX too complex with many nested ternaries.
Tips / best practices
- If the UI logic gets long, create a variable first (example:
let content = ...) then render{content}. - Prefer clear code over clever code. Readability matters most.
What it is
An effect is code that runs after React shows your UI on the screen. In React, we use useEffect for this.
Why it is used (real-life example)
You use effects when you need to do something outside the UI update. Example: load user data from an API after the page opens.
Step-by-step explanation
- Import useEffect from React.
- Write
useEffect(() => { ... })inside your component. - If you pass an empty array
[], the effect runs one time after the first render. - If you pass values in the array, the effect runs again when those values change.
Code examples
Example 1: Run once when the page opens
import { useEffect } from "react";
export default function Welcome() {
useEffect(() => {
console.log("Page opened");
}, []);
return Hello
;
}Here the empty array [] means: run only one time after the first UI paint.
Example 2: Run when a value changes
import { useEffect, useState } from "react";
export default function CounterLog() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count changed to:", count);
}, [count]);
return (
Count: {count}
);
}This effect runs after render, but only when count changes.
Common mistakes
- Forgetting the dependency array and causing the effect to run after every render.
- Putting state updates in an effect without dependencies, creating an infinite loop.
- Thinking
useEffectruns before UI shows (it runs after paint).
Tips / best practices
- Use effects for side work like fetching data, timers, and manual DOM work.
- Keep effects small: one clear job per effect.
- Always think: "When should this run?" and set dependencies carefully.
What it is
Cleanup is code that runs when a component is removed from the screen, or before an effect runs again. It helps you stop timers, remove listeners, and avoid memory leaks.
Why it is used (real-life example)
If you start a timer (like a clock) and the user leaves the page, you should stop the timer. If you don’t, it can keep running in the background.
Step-by-step explanation
- Inside
useEffect, return a function. - That returned function is the cleanup function.
- React calls cleanup when the component unmounts, or before re-running the effect (if dependencies change).
Code examples
Example 1: Clear an interval timer
import { useEffect, useState } from "react";
export default function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => {
clearInterval(id);
};
}, []);
return Seconds: {seconds}
;
}The cleanup stops the interval so it does not keep running after the component is gone.
Example 2: Remove an event listener
import { useEffect, useState } from "react";
export default function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function onResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
return Width: {width}
;
}Cleanup removes the listener so the app stays fast and safe.
Common mistakes
- Forgetting to clean timers and listeners, causing slow apps or strange bugs.
- Using a different function reference in
removeEventListener(it must be the same function). - Clearing the wrong timer id, because the id is not saved.
Tips / best practices
- If you "start" something in an effect, plan how to "stop" it in cleanup.
- Keep cleanup close to the code that created the side effect.
- Prefer one effect for one resource (one timer, one listener).
What it is
Forms are UI parts where users type input, like names, emails, and passwords. In React, you often connect inputs to state so React always knows the latest value.
Why it is used (real-life example)
You need forms to log in, sign up, search products, and send messages. Example: a simple login form that reads the email and password.
Step-by-step explanation
- Create state to store the input value.
- Set the input
valueto that state (this makes it controlled). - Use
onChangeto update state when the user types. - Handle submit with
onSubmitand callevent.preventDefault()to stop page reload.
Code examples
Example 1: Single input (controlled)
import { useState } from "react";
export default function NameForm() {
const [name, setName] = useState("");
function handleSubmit(e) {
e.preventDefault();
alert("Hello, " + name);
}
return (
);
}The input value comes from state. When the user types, onChange updates the state.
Example 2: Multiple inputs with one state object
import { useState } from "react";
export default function LoginForm() {
const [form, setForm] = useState({ email: "", password: "" });
function updateField(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
console.log("Send to server:", form);
}
return (
);
}The name attribute tells React which field to update. We copy the old object using ...prev so we do not lose other fields.
Common mistakes
- Forgetting
preventDefault()and the page refreshes on submit. - Not controlling the input (missing
value), which can make state and UI get out of sync. - Updating an object without copying, which can remove other fields.
Tips / best practices
- Keep inputs controlled for predictable behavior.
- Use one change handler for many fields by reading
e.target.name. - Validate inputs step by step (for example, check email is not empty before submit).
Routing
What it is: Routing lets your app show different pages (screens) based on the URL, like /home or /about.
Why it is used: In a real website, users click links. Routing updates the screen without reloading the whole page. Example: an online shop has /products and /cart.
Step-by-step
- Install React Router.
- Wrap your app with
BrowserRouter. - Create routes that map a URL to a component.
- Use
Linkto navigate without page refresh.
Code example 1: Basic routes
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function Home() {
return Home
;
}
function About() {
return About
;
}
export default function App() {
return (
} />
} />
);
}Code example 2: Not found page
import { Routes, Route } from "react-router-dom";
function NotFound() {
return Page not found.
;
}
export default function AppRoutes() {
return (
Home} />
About} />
} />
);
}Common mistakes
- Forgetting to wrap the app with
BrowserRouter. - Using
<a href>instead ofLink, which causes full page reloads. - Putting
Routesoutside the router.
Tips / best practices
- Keep route components small and focused.
- Add a
*route for a friendly 404 page. - Use clear URL names like
/profileinstead of vague ones.
Context
What it is: Context is a way to share data with many components without passing props through every level.
Why it is used: Imagine a logged-in user name. Many screens need it (header, profile page, settings). Context lets all of them read it easily.
Step-by-step
- Create a context with
createContext. - Wrap your app (or part of it) with a Provider.
- Put shared values in the Provider
value. - Read the value using
useContextin any child component.
Code example 1: Theme context
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext();
function ThemeButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
);
}
export default function App() {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme(t => (t === "light" ? "dark" : "light"));
return (
);
}Code example 2: User context with default value
import { createContext, useContext } from "react";
const UserContext = createContext({ name: "Guest" });
function Header() {
const user = useContext(UserContext);
return Hello, {user.name}!
;
}
export default function App() {
const user = { name: "Amina" };
return (
);
}Common mistakes
- Using Context for everything. It can make code harder to follow.
- Forgetting to wrap components with the Provider, so values are missing.
- Putting a new object in
valueeach render without need, causing extra re-renders.
Tips / best practices
- Use Context for app-wide data like theme, user, language.
- Keep context values small and clear.
- If the value is complex, consider splitting into multiple contexts.
Memoization
What it is: Memoization means remembering a result, so React can skip work when nothing important changed.
Why it is used: In real apps, a slow list or expensive calculation can make typing feel laggy. Memoization helps keep the UI fast.
Step-by-step
- Use
React.memoto prevent a child component from re-rendering when props are the same. - Use
useMemoto cache an expensive computed value. - Use
useCallbackto keep the same function reference between renders. - Measure first: only optimize when you see a real slowdown.
Code example 1: React.memo for a child
import { useState, memo } from "react";
const CounterDisplay = memo(function CounterDisplay({ count }) {
console.log("CounterDisplay render");
return Count: {count}
;
});
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
return (
setText(e.target.value)} />
);
}Code example 2: useMemo for expensive work
import { useMemo, useState } from "react";
function slowSum(n) {
let total = 0;
for (let i = 0; i < n; i++) {
total += i;
}
return total;
}
export default function App() {
const [n, setN] = useState(500000);
const [label, setLabel] = useState("Demo");
const total = useMemo(() => slowSum(n), [n]);
return (
setLabel(e.target.value)} />
Sum is: {total}
);
}Common mistakes
- Using memoization everywhere. It can add complexity and sometimes makes performance worse.
- Forgetting dependencies in
useMemooruseCallback. - Passing new objects/functions as props each render, which breaks
React.memobenefits.
Tips / best practices
- Optimize only when you notice a real problem (measure with dev tools).
- Memoize expensive calculations, not simple ones.
- Keep props stable when using
React.memo(useuseCallbackfor handlers when needed).
Reducer
What it is: A reducer is a function that takes the current state and an action, then returns the next state. In React, reducers are often used with the useReducer hook.
Why it is used (real-life example): Think about a counter with buttons: add, subtract, reset. A reducer keeps all update rules in one place, so the logic is easy to manage.
Step-by-step
- 1) Create a reducer function:
(state, action) => newState. - 2) Decide action types like
incrementandreset. - 3) Call
useReducer(reducer, initialState)to getstateanddispatch. - 4) Use
dispatchwhen the user clicks a button.
Code example 1: Simple counter reducer
import { useReducer } from "react";
function reducer(state, action) {
if (action.type === "increment") {
return { count: state.count + 1 };
}
if (action.type === "decrement") {
return { count: state.count - 1 };
}
if (action.type === "reset") {
return { count: 0 };
}
return state;
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
Count: {state.count}
);
}Code example 2: Action with data (payload)
import { useReducer } from "react";
function cartReducer(state, action) {
if (action.type === "add") {
return { items: [...state.items, action.item] };
}
if (action.type === "clear") {
return { items: [] };
}
return state;
}
export default function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
function addApple() {
dispatch({ type: "add", item: { id: 1, name: "Apple" } });
}
return (
{state.items.map((it) => (
- {it.name}
))}
);
}Common mistakes
- Changing state directly, like
state.count++. Always return a new object. - Forgetting to handle an action type, which can make the UI not update.
- Putting side effects inside the reducer (like API calls). Reducers should stay pure.
Tips / best practices
- Keep reducer logic small and easy to read.
- Use clear action names like
addorremove. - Return the old state for unknown actions to avoid crashes.
Ref
What it is: A ref is a way to keep a value that does not cause a re-render when it changes. It is also used to access a real DOM element (like an input). In React, you often use useRef.
Why it is used (real-life example): You can focus an input when a page opens, or keep a timer id for setInterval so you can stop it later.
Step-by-step
- 1) Create a ref with
const myRef = useRef(null). - 2) Attach it to an element using
ref={myRef}. - 3) Read the DOM node from
myRef.current. - 4) For storing values, put them in
myRef.currentand update it anytime.
Code example 1: Focus an input
import { useEffect, useRef } from "react";
export default function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
);
}Code example 2: Store a timer id without re-render
import { useRef, useState } from "react";
export default function Timer() {
const [seconds, setSeconds] = useState(0);
const timerIdRef = useRef(null);
function start() {
if (timerIdRef.current !== null) return;
timerIdRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
}
return (
Seconds: {seconds}
);
}Common mistakes
- Expecting a ref change to update the UI. Refs do not trigger re-renders.
- Reading
ref.currentbefore the element is mounted. It can benullat first. - Using refs for normal UI state (like text you show on screen). Use state for that.
Tips / best practices
- Use refs for DOM access, timers, and values that should not re-render the component.
- Always check for
nullwhen using DOM refs. - Prefer state for anything you display in JSX.
Callback
What it is: A callback is a function you pass to another component (or function) so it can call it later. In React, you often pass callbacks as props, like onSave or onDelete.
Why it is used (real-life example): A child component can tell the parent that something happened, like “the user clicked delete” or “the form was submitted”.
Step-by-step
- 1) Parent creates a function that changes parent state.
- 2) Parent passes that function to the child as a prop.
- 3) Child calls the function when needed (for example, on button click).
- 4) Parent receives the data and updates the UI.
Code example 1: Child notifies parent
import { useState } from "react";
function DeleteButton({ onDelete }) {
return (
);
}
export default function Parent() {
const [lastDeletedId, setLastDeletedId] = useState(null);
function handleDelete(id) {
setLastDeletedId(id);
}
return (
Last deleted: {lastDeletedId}
);
}Code example 2: Callback with form data
import { useState } from "react";
function NameForm({ onSave }) {
const [name, setName] = useState("");
function submit(e) {
e.preventDefault();
onSave({ name: name });
setName("");
}
return (
);
}
export default function App() {
const [savedName, setSavedName] = useState("");
function handleSave(data) {
setSavedName(data.name);
}
return (
Saved: {savedName}
);
}Common mistakes
- Calling the callback too early, like
onClick={onDelete(123)}. This runs immediately. Use a function:onClick={() => onDelete(123)}. - Forgetting to pass needed data (like the id), so the parent cannot update correctly.
- Making callback names unclear. Avoid generic names like
doIt.
Tips / best practices
- Use clear names:
onAdd,onRemove,onSave. - Keep callbacks small and focused on one job.
- Pass only the data the parent needs (not the whole event object unless necessary).
Fragments
What it is: A Fragment lets you return many elements without adding an extra wrapper tag like a div in the HTML.
Why it is used (real-life example): When you build a list row for a table, adding extra div tags can break the table layout. Fragments let you group elements without changing the HTML structure.
Step-by-step
- Step 1: Create a component that needs to return more than one element.
- Step 2: Wrap the elements with
<></>(short Fragment) or<React.Fragment></React.Fragment>. - Step 3: Keep your HTML clean (no unnecessary wrapper nodes).
Code example 1: Short Fragment
function ProfileHeader() {
return (
<>
<h1>Amina</h1>
<p>Frontend learner</p>
</>
);
}This returns two elements without adding a wrapper div to the DOM.
Code example 2: Fragment in a table row
function RowCells() {
return (
<React.Fragment>
<td>Apple</td>
<td>$2</td>
</React.Fragment>
);
}
function PriceTable() {
return (
<table>
<tbody>
<tr>
<RowCells />
</tr>
</tbody>
</table>
);
}A div inside a tr is invalid HTML, so Fragment is the clean solution.
Common mistakes
- Adding a
divwrapper everywhere and making the DOM too deep. - Forgetting that the short Fragment
<>cannot receive props likekey(useReact.Fragmentwhen you need a key).
Tips / best practices
- Use Fragments to keep the DOM simple and easier to style.
- When mapping a list of grouped elements, use
<React.Fragment key={...}>.
Portals
What it is: A Portal lets React render a component in a different place in the HTML (outside the normal parent DOM tree).
Why it is used (real-life example): Modals, dropdowns, and tooltips often need to appear above everything else. If they stay inside a parent with overflow: hidden, they can get cut off. Portals avoid that.
Step-by-step
- Step 1: Add a special container element in
public/index.html(example:#modal-root). - Step 2: Create a Modal component that uses
createPortal. - Step 3: Render the Modal from anywhere, but it will appear inside
#modal-root.
Code example 1: Add modal root in HTML
<!-- public/index.html -->
<div id="root"></div>
<div id="modal-root"></div>Now you have a separate place for modals to render.
Code example 2: Create a Portal modal
import { createPortal } from "react-dom";
function Modal({ open, onClose, children }) {
if (!open) return null;
const modalRoot = document.getElementById("modal-root");
return createPortal(
<div className="backdrop" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
modalRoot
);
}
export default function App() {
const [open, setOpen] = React.useState(false);
return (
<div>
<button onClick={() => setOpen(true)}>Open modal</button>
<Modal open={open} onClose={() => setOpen(false)}>
<p>Hello from a Portal!</p>
</Modal>
</div>
);
}Even though Modal is called inside App, it is rendered into #modal-root.
Common mistakes
- Forgetting to add
#modal-rootin the HTML file. - Not stopping click propagation, so clicking inside the modal closes it.
- Trying to use a Portal to fix state problems. Portals only change where DOM is rendered, not how state works.
Tips / best practices
- Use Portals for UI that must “float” above the page (modals, toasts, tooltips).
- Keep accessibility in mind: focus trapping and escape key handling are important for real modals.
Error Boundaries
What it is: An Error Boundary is a React component that catches JavaScript errors in its child component tree and shows a fallback UI instead of crashing the whole app.
Why it is used (real-life example): In a dashboard, one broken widget should not break the entire page. An Error Boundary can show “This widget failed” while the rest still works.
Step-by-step
- Step 1: Create a class component (Error Boundaries currently use class lifecycle methods).
- Step 2: Implement
static getDerivedStateFromErrorto switch to an error state. - Step 3: Implement
componentDidCatchto log the error. - Step 4: Wrap risky parts of the UI with the boundary.
Code example 1: Create an Error Boundary
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.log("Caught error:", error);
console.log("Component info:", info);
}
render() {
if (this.state.hasError) {
return <p>Something went wrong in this area.</p>;
}
return this.props.children;
}
}
export default ErrorBoundary;If any child throws during render, React shows the fallback message instead.
Code example 2: Wrap a risky component
function RiskyWidget({ user }) {
// This will throw if user is null
return <h2>Hello {user.name}!</h2>;
}
export default function Dashboard() {
const user = null;
return (
<div>
<h1>Dashboard</h1>
<ErrorBoundary>
<RiskyWidget user={user} />
</ErrorBoundary>
<p>Other content still renders.</p>
</div>
);
}Only the wrapped area fails. The rest of the page remains usable.
Common mistakes
- Expecting Error Boundaries to catch errors in event handlers. They do not. Use
try/catchinside the handler instead. - Wrapping the whole app once and showing a blank page on any small error. This is not friendly for users.
- Forgetting to provide a helpful fallback UI (like a retry button or message).
Tips / best practices
- Wrap smaller areas (widgets, panels) so one part failing does not kill everything.
- Log errors to a service in real apps (Sentry, LogRocket) inside
componentDidCatch.