Introduction
React is a popular JavaScript library used to build user interfaces, especially for web applications that need to update content quickly without reloading the entire page.
It was created to solve a common frontend problem: as applications grow, managing the user interface with plain JavaScript becomes difficult, repetitive, and error-prone.
React addresses this by breaking the interface into small, reusable pieces called components.
Each component is responsible for its own structure and behavior, which makes applications easier to understand, test, and maintain.
In real life, React is used in dashboards, ecommerce websites, social platforms, admin panels, streaming apps, and many mobile apps through React Native.
Teams choose React because it encourages reusable design, predictable UI updates, and a strong ecosystem of tools.
When data changes, React efficiently updates only the necessary parts of the page using a concept commonly described through the virtual DOM.
This improves both developer productivity and user experience.
At a high level, React applications are built from components, props, state, events, and JSX.
Components are reusable UI blocks.
Props are inputs passed into components.
State stores changing data inside a component.
Events respond to user actions like clicks and typing.
JSX is a syntax that lets developers write HTML-like structures inside JavaScript.
Together, these ideas help developers describe what the UI should look like for a given set of data.
React is commonly used in modern frontend development with tools such as Vite, npm, and bundlers that help organize code for production.
You will often see React used with routing libraries for navigation, API calls for dynamic data, and state management patterns for larger applications.
Even though React is called a library rather than a full framework, it is powerful enough to support everything from tiny widgets to enterprise-grade platforms.
Step-by-Step Explanation
To begin with React, think of the page as a tree of components.
A simple app may have an App component, a Header, a ProductList, and a Footer.
Each component returns JSX that describes the UI.
React then renders that JSX into the browser.
A beginner usually starts by creating a component as a JavaScript function.
That function returns JSX.
JSX looks like HTML, but it follows JavaScript rules.
For example, you use className instead of class, and JavaScript expressions are placed inside curly braces like {name}.
This makes UI and logic work together cleanly.
After creating components, you pass data using props.
If the data needs to change over time, you use state.
When a user clicks a button or types in a field, event handlers can update state, and React re-renders the affected component automatically.
This declarative style means you describe the desired result instead of manually changing the DOM line by line.
Comprehensive Code Examples
function App() {
return Hello, React!
;
}
export default App;function Welcome(props) {
return Welcome, {props.name}!
;
}
function App() {
return ;
}
export default App; import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
Counter: {count}
);
}
export default App;Common Mistakes
- Using HTML attributes directly: Beginners write
classinstead ofclassName.
Use React-specific JSX attribute names. - Forgetting components must return JSX: A function component should return a valid UI element or
null. - Trying to update the page manually: Avoid direct DOM manipulation for normal UI updates.
Use state and props.
Best Practices
- Keep components small and focused on one responsibility.
- Use meaningful component names like
UserCardandNavBar. - Pass data through props clearly and avoid unnecessary complexity early on.
- Use state only for data that changes over time.
Practice Exercises
- Create a React component that displays your name and favorite technology.
- Build a
Greetingcomponent that receives a name through props and shows a custom message. - Create a simple counter with a button that increases the displayed number.
Mini Project / Task
Build a small profile card app with a title, short bio, and a button that counts how many times it was clicked.
Challenge (Optional)
Create a tiny dashboard with three reusable components and pass different props to each one so they display unique content.
How React Works
React is a JavaScript library designed to help developers build user interfaces by breaking screens into small, reusable pieces called components. It exists because managing dynamic interfaces directly with the browser DOM becomes difficult as applications grow. In real projects such as dashboards, ecommerce sites, chat apps, and admin panels, many parts of the screen change based on user actions or incoming data. React makes this easier by letting developers describe what the UI should look like for a given state, and React handles updating the page efficiently.
At its core, React works through components, JSX, props, state, and rendering. A component is a function that returns UI. JSX is a syntax that looks like HTML but is actually JavaScript-powered markup. Props are inputs passed from one component to another. State is internal data that can change over time. When props or state change, React re-renders the component and compares the new output with the previous one using a process often called reconciliation. Instead of rebuilding the entire page, React updates only the necessary parts of the real DOM through its virtual DOM strategy.
In practice, React follows a predictable flow. First, developers define components. Next, React creates a virtual representation of the UI. When data changes, React generates a new virtual UI tree, compares it with the previous version, identifies differences, and updates the browser efficiently. This model reduces manual DOM manipulation and helps keep UI logic organized. React is commonly used with tools like Vite, Next.js, or Create React App-style setups, but the underlying idea stays the same: UI is a function of data.
Step-by-Step Explanation
Begin by creating a component. A component is usually a JavaScript function with a capitalized name. It returns JSX. JSX may look like HTML, but under the hood it becomes JavaScript instructions that React understands.
Next, React renders that component into a root element in the page. During rendering, React reads the component function and builds a virtual DOM tree. If the component receives props, React uses them as inputs. If the component uses state, React stores and tracks that data internally.
When a user clicks a button or data changes, state updates. React then calls the component again to calculate what the UI should now look like. It compares the new virtual DOM with the old one, finds what changed, and updates only those specific DOM nodes. This makes updates efficient and keeps code declarative: developers describe the desired UI instead of writing low-level DOM commands.
Comprehensive Code Examples
function Welcome() {
return Hello, React!
;
}
export default Welcome;function ProductCard(props) {
return (
{props.name}
Price: ${props.price}
);
}
export default function App() {
return ;
} import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
export default Counter;import { useState } from 'react';
function SearchBox() {
const [query, setQuery] = useState('');
return (
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products"
/>
You are searching for: {query}
);
}
export default SearchBox;Common Mistakes
- Changing variables instead of state: Updating a normal variable does not trigger a re-render. Use state setters such as
setCount. - Using lowercase component names: React treats lowercase tags as HTML elements. Name components like
Header, notheader. - Mutating state directly: Do not modify arrays or objects in place. Create new values so React can detect changes properly.
- Expecting immediate state updates: State updates are scheduled. Do not rely on the old value changing instantly in the same line.
Best Practices
- Keep components small and focused on one responsibility.
- Use props for passing data down and state for data that changes inside a component.
- Write declarative UI: describe what should appear for each state.
- Use meaningful component names and organize files clearly.
- Avoid direct DOM manipulation unless absolutely necessary.
Practice Exercises
- Create a component called
Greetingthat displays a message using JSX. - Build a
UserCardcomponent that receivesnameandroleas props and displays them. - Create a counter component with a button that increases state and shows how React re-renders the UI.
Mini Project / Task
Build a simple profile viewer with one parent component and two child components. Pass user data as props, display it on the screen, and add a button that changes part of the displayed information using state.
Challenge (Optional)
Create a small shopping list interface where users can type an item name, add it to a list stored in state, and render the updated list without manually touching the DOM.
Setting Up with Vite
Vite is a modern frontend build tool designed to make development faster and simpler. It exists because older tooling often took too long to start, reload, and bundle projects. In real-world React teams, Vite is widely used to create new applications quickly, provide fast local development servers, and prepare production-ready builds with minimal configuration. When you use Vite with React, you get a lightweight project structure, instant feedback during coding, and support for modern JavaScript features. For beginners, it is one of the easiest ways to start a React app because it removes much of the setup complexity. The basic setup usually includes creating a project, installing dependencies, running the development server, and understanding the generated files. You may also see variations such as JavaScript-based projects and TypeScript-based projects, depending on whether you want static typing. Vite is especially useful for dashboards, admin panels, landing pages, e-commerce frontends, and internal company tools where fast iteration matters.
The main idea behind Vite is simple: during development it serves source files directly and updates only what changed, which makes the experience much faster than traditional full rebundling workflows. In production, it still creates optimized assets for deployment. When a React project is created with Vite, you commonly get files like index.html, src/main.jsx, src/App.jsx, and a package.json file for scripts and dependencies. Understanding these files early helps you become comfortable with the React project lifecycle.
Step-by-Step Explanation
First, make sure Node.js is installed. Then open a terminal and run the create command. A common syntax is npm create vite@latest. You will be asked for a project name, a framework, and a variant. Choose React, then choose JavaScript or TypeScript. Next, move into the project folder with cd project-name, install packages using npm install, and start the app with npm run dev. Vite will print a local URL, often http://localhost:5173. Open that in the browser to see the starter app.
Inside the project, main.jsx is the entry point that renders the root React component. App.jsx contains the main interface. You can edit App.jsx and instantly see changes in the browser. For production, use npm run build to generate an optimized dist folder. You can preview it locally with npm run preview.
Comprehensive Code Examples
npm create vite@latest my-react-app
cd my-react-app
npm install
npm run dev// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
)// src/App.jsx
function App() {
return (
Hello from Vite + React
My project is running successfully.
)
}
export default App// Real-world example: starter layout
function App() {
return (
Team Dashboard
Built with React and Vite for fast development.
)
}
export default App// Advanced usage: environment value
function App() {
const appName = import.meta.env.VITE_APP_NAME || 'React App'
return {appName}
}
export default AppCommon Mistakes
- Running commands outside the project folder: Use
cd your-project-namebeforenpm installornpm run dev. - Choosing the wrong template: Make sure you select React, not another framework like Vue or Vanilla.
- Editing the wrong file: Most beginner UI changes should start in
src/App.jsx. - Using Node.js versions that are too old: Install a modern LTS version for compatibility.
Best Practices
- Use clear project names that match the app purpose.
- Keep components inside the
srcfolder and organize early. - Use environment variables with the
VITE_prefix when needed. - Run
npm run buildbefore deployment to catch issues early. - Start with the default structure, then refactor gradually as the app grows.
Practice Exercises
- Create a new Vite React app and replace the default content with your name and a short welcome message.
- Add a button and a paragraph in
App.jsxto simulate a simple homepage. - Run a production build and locate the generated
distfolder.
Mini Project / Task
Build a simple developer portfolio starter using Vite and React with a heading, short bio, skills list, and contact button.
Challenge (Optional)
Create a Vite React project that reads an app title from a .env file and displays it dynamically in the interface.
JSX Syntax
JSX stands for JavaScript XML. It is a syntax extension used in React that lets developers write HTML-like structures directly inside JavaScript. React uses JSX to describe what the user interface should look like, making component code easier to read and maintain. Instead of manually calling functions like React.createElement() for every element, JSX gives you a cleaner and more natural way to build UI. In real projects, JSX is used for rendering buttons, forms, cards, navigation bars, dashboards, and entire application layouts. It is especially helpful because React applications are built from reusable components, and JSX makes those components feel close to visual markup while still keeping JavaScript power available.
JSX looks similar to HTML, but it is not exactly HTML. It follows JavaScript rules and is transformed into regular JavaScript behind the scenes. Some syntax differences are important: use className instead of class, write JavaScript expressions inside curly braces, and close all tags properly. JSX can render simple text, dynamic values, lists, conditions, and nested components. You can also store JSX in variables, return it from functions, and compose multiple UI parts together. Understanding these patterns is essential because almost every React component you write will use JSX in some form.
Step-by-Step Explanation
Start with a React component, which is usually a JavaScript function. Inside that function, return JSX. JSX must return a single parent element, such as a div, section, or React fragment. To display dynamic data, place JavaScript expressions inside curly braces like {name}. Attributes are written similarly to HTML, but some names change to match JavaScript conventions. For example, use htmlFor for labels and className for CSS classes. Inline styles are written as JavaScript objects, such as style={{ color: 'blue', fontSize: '18px' }}. JSX also supports self-closing tags like and . If you need conditional display, use expressions such as ternary operators or logical AND. JSX is powerful because it blends structure and behavior in one place while still remaining component-based.
Comprehensive Code Examples
Basic example
function Welcome() {
const name = 'Ava';
return (
Hello, {name}!
Welcome to React.
);
}Real-world example
function ProductCard() {
const product = {
title: 'Wireless Mouse',
price: 29.99,
inStock: true
};
return (
{product.title}
Price: ${product.price}
{product.inStock ? 'In Stock' : 'Out of Stock'}
);
}Advanced usage
function DashboardHeader() {
const user = 'Mina';
const notifications = 3;
const headerStyle = {
backgroundColor: '#20232a',
color: 'white',
padding: '16px'
};
return (
Admin Dashboard
Hello, {user}
{notifications > 0 && You have {notifications} new notifications.
}
);
}Common Mistakes
- Using
classinstead ofclassName: React expectsclassNamefor CSS classes. - Returning multiple sibling elements without a wrapper: wrap them in one parent element or a fragment.
- Forgetting to close tags: self-closing elements like images and inputs must end with
/>. - Writing statements inside curly braces: JSX accepts expressions, not full statements like
ifdirectly inside markup.
Best Practices
- Keep JSX readable by breaking large UIs into smaller components.
- Use descriptive variable names for dynamic values rendered inside JSX.
- Prefer conditional expressions that are easy to understand instead of deeply nested logic.
- Use semantic elements such as
header,main, andsectionwhen appropriate. - Move complex calculations outside the returned JSX to keep markup clean.
Practice Exercises
- Create a component that displays your name, role, and favorite programming language using JSX.
- Build a simple profile card with a heading, paragraph, and one image using correct JSX attributes.
- Create a status component that shows either
OnlineorOfflineusing a ternary expression inside JSX.
Mini Project / Task
Build a small product showcase component that displays a product name, price, short description, and availability message using JSX expressions and proper element structure.
Challenge (Optional)
Create a dashboard summary component that displays a username, a dynamic task count, and a warning message only when the number of pending tasks is greater than zero.
Components
Components are the building blocks of a React application. Instead of writing one large page full of HTML, styling, and behavior, React encourages developers to split the interface into small, reusable pieces called components. A button, navigation bar, product card, search box, and user profile can each be their own component. This exists to improve reuse, readability, testing, and maintenance. In real projects, teams use components to create design systems, dashboards, checkout pages, and form-heavy business tools. React mainly uses function components today, although class components exist in older codebases. A function component is simply a JavaScript function that returns JSX, which is a syntax that looks like HTML but is actually JavaScript. Components can be simple presentational pieces or smart containers that coordinate data and events.
Function components are the modern default because they are simpler and work well with hooks. Class components use ES6 classes and lifecycle methods, but they are less common in new projects. Components can also be categorized by purpose: presentational components focus on displaying UI, while container components manage logic and pass data down. Another useful idea is composition. Instead of inheriting behavior, React components are often combined by nesting one inside another. This makes layouts flexible and keeps responsibilities clear.
Step-by-Step Explanation
To create a component, start with a JavaScript function. Its name should begin with an uppercase letter because React treats lowercase tags as regular HTML elements. Inside the function, return JSX. JSX lets you describe UI using tag-like syntax. Then export the component so other files can import it. To use a component, place it in JSX like a custom tag. Components may receive inputs called props, which are read-only values passed from a parent. A component can also render children, meaning nested content placed between opening and closing tags.
function Welcome() {
return Hello, React!
;
}
export default Welcome;You can then use it inside another component with . If you pass props, define a parameter such as function Welcome(props) or destructure it as function Welcome({ name }).
Comprehensive Code Examples
function Greeting({ name }) {
return Welcome, {name}!
;
}
export default function App() {
return ;
} function ProductCard({ title, price, inStock }) {
return (
{title}
${price}
{inStock ? 'In stock' : 'Out of stock'}
);
}
export default function App() {
return ;
} function Card({ title, children }) {
return (
{title}
{children}
);
}
function UserProfile({ user }) {
return (
Name: {user.name}
Role: {user.role}
);
}
export default function App() {
const user = { name: 'Lina', role: 'Frontend Developer' };
return ;
} Common Mistakes
Using lowercase component names like
when you meant a custom component. Fix: start custom component names with uppercase letters. Trying to return multiple sibling elements without wrapping them. Fix: wrap them in a parent element.
Modifying props inside a component. Fix: treat props as read-only and compute new values instead.
Putting too much logic into one giant component. Fix: split repeated or complex UI into smaller reusable parts.
Best Practices
Keep components focused on one responsibility.
Prefer function components for modern React code.
Use descriptive names such as
ProductCardorSidebarMenu.Pass only the props a component actually needs.
Use composition with
childrenfor flexible layouts.
Practice Exercises
Create a
StudentCardcomponent that shows a student name and grade using props.Build a
MessageBoxcomponent and render it three times with different messages.Create a reusable
Panelcomponent that displays a title and nested content throughchildren.
Mini Project / Task
Build a small profile dashboard with separate components for a header, user card, skills list, and footer, then assemble them inside a main App component.
Challenge (Optional)
Create a reusable StatusBadge component that changes its displayed text based on props and use it inside multiple other components without duplicating code.
Props
Props, short for properties, are the way React components receive data from other components. They exist so that user interfaces can be broken into small, reusable pieces while still allowing each piece to display different content. In real applications, props are used everywhere: a product card receives a product name and price, a profile component receives a user image and bio, and a button component receives text, color, and click behavior. Without props, every component would be hardcoded and much less reusable.
At a basic level, props are passed from a parent component to a child component as attributes that look similar to HTML attributes. The child component then reads those values and uses them inside JSX. Props can hold strings, numbers, booleans, arrays, objects, and even functions. A very common subtype of props is children, which represents whatever content is placed between a component’s opening and closing tags. Another important pattern is destructured props, where you extract values directly in the function parameter for cleaner code.
Props are read-only. This is one of the most important ideas for beginners. A child component should never try to change its own props. Instead, if data must change, the parent should manage that change and pass updated props down again. This one-way data flow is a core React design principle because it makes applications easier to reason about and debug.
Step-by-Step Explanation
To use props, first create a component. Next, render that component from another component and pass values as attributes. Finally, access those values inside the child component.
Example syntax: the parent writes . The child can receive all props as one object, like function Welcome(props), then use props.name. A cleaner style is destructuring: function Welcome({ name }).
You can also pass non-string values using curly braces. For example, age={25}, isOnline={true}, or skills={["React", "JS"]}. To pass content between tags, use children.
Comprehensive Code Examples
function Welcome(props) {
return Hello, {props.name}!
;
}
export default function App() {
return ;
} function ProductCard({ title, price, inStock }) {
return (
{title}
Price: ${price}
{inStock ? "In stock" : "Out of stock"}
);
}
export default function App() {
return (
);
}function Card({ title, children, onSelect }) {
return (
{title}
{children}
);
}
export default function App() {
const handleSelect = () => alert("Card selected");
return (
This plan includes support and analytics.
);
}Common Mistakes
- Treating props like editable variables: Do not change props inside a child component. Fix: update data in the parent and pass new props.
- Forgetting curly braces for JavaScript values: Writing
price="29.99"makes it a string. Fix: useprice={29.99}for a number. - Using the wrong prop name: Passing
usernamebut readingnamecauses undefined output. Fix: keep prop names consistent. - Ignoring children: Some beginners pass nested content but never render
{children}. Fix: include it in the component where needed.
Best Practices
- Use descriptive prop names like
productName,isActive, andonSubmit. - Destructure props in function parameters for cleaner components.
- Keep props focused; pass only the data a component needs.
- Use callback props such as
onClickoronDeleteto let children communicate actions upward. - Design reusable components by replacing hardcoded values with props.
Practice Exercises
- Create a
UserBadgecomponent that receivesnameandroleprops and displays both. - Build a
MovieCardcomponent that receivestitle,year, andratingprops. - Create a
Panelcomponent that uses thechildrenprop to display custom nested content.
Mini Project / Task
Build a small product showcase page with three reusable ProductCard components. Pass each card different props such as name, price, image label text, and stock status.
Challenge (Optional)
Create a reusable Button component that accepts label, variant, and onClick props, then render multiple buttons with different styles and behaviors.
State with useState
In React, 'state' refers to data that changes over time and affects the rendering of a component. It's essentially a snapshot of a component's data at a given moment. When a component's state changes, React re-renders the component to reflect those changes in the UI. This mechanism is fundamental to building dynamic and interactive user interfaces.
Before the introduction of Hooks, state was primarily managed within class components using `this.state` and `this.setState()`. However, with the advent of React Hooks in version 16.8, functional components gained the ability to manage their own state using the `useState` Hook. This revolutionized how developers write React components, promoting a more functional and concise approach, and making state management accessible directly within functional components without converting them to class components.
The `useState` Hook is incredibly useful for managing any data that needs to change and trigger a re-render. Common real-world examples include managing form input values, toggling UI elements (like a modal or a dropdown), tracking the number of items in a shopping cart, controlling the visibility of a loading spinner, or maintaining the current step in a multi-step form. Any UI element that needs to respond to user interaction or asynchronous data loading will likely leverage `useState` to manage its internal state.
The primary core concept here is the `useState` Hook itself. It's a function that you call inside your functional component to add some local state to it. It returns an array with two elements: the current state value and a function that lets you update it. This update function is crucial because directly modifying the state variable will not trigger a re-render; you must always use the setter function provided by `useState`.
There aren't really 'sub-types' of `useState` in the same way there are different types of loops, but its usage can vary depending on the data type it manages: numbers, strings, booleans, arrays, or objects. The principle remains the same: `useState` initializes and provides a way to update the state.
Step-by-Step Explanation
To use `useState`, you first need to import it from React:
import React, { useState } from 'react';Then, inside your functional component, you call `useState` with the initial state value as its argument. It returns an array, which you typically destructure into two variables:
const [stateVariable, setStateVariable] = useState(initialValue);- `stateVariable`: This is the current value of your state.
- `setStateVariable`: This is a function that you use to update `stateVariable`.
- `initialValue`: This is the value that `stateVariable` will have on the first render. It can be any valid JavaScript data type (number, string, boolean, object, array, null, etc.).
When you want to update the state, you call the `setStateVariable` function with the new value:
setStateVariable(newValue);React will then re-render the component with the new `stateVariable` value. If the new state depends on the previous state, it's a best practice to pass a function to the setter. This function receives the previous state as an argument and returns the new state:
setStateVariable(prevValue => prevValue + 1);This ensures you are always working with the most up-to-date state, especially in scenarios involving asynchronous updates or multiple state updates.
Comprehensive Code Examples
Basic example: Counter
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initialize count to 0
return (
You clicked {count} times
);
}
export default Counter;Real-world example: Toggle visibility
import React, { useState } from 'react';
function ToggleMessage() {
const [isVisible, setIsVisible] = useState(false); // Initial state: hidden
const toggleVisibility = () => {
setIsVisible(!isVisible); // Toggle the boolean value
};
return (
{isVisible && This is a secret message!
}
);
}
export default ToggleMessage;Advanced usage: Managing object state
import React, { useState } from 'react';
function UserProfileEditor() {
const [user, setUser] = useState({
name: 'Jane Doe',
email: '[email protected]',
age: 30
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prevUser => ({
...prevUser, // Spread previous state to maintain other properties
[name]: value // Update the specific property
}));
};
return (
Edit User Profile
Current Profile:
Name: {user.name}
Email: {user.email}
Age: {user.age}
);
}
export default UserProfileEditor;Common Mistakes
Directly modifying state: A common mistake is trying to modify the state variable directly (e.g., `count = count + 1;` or `user.name = 'New Name';`). This will not trigger a re-render, and your UI will not update. Always use the setter function provided by `useState` (e.g., `setCount(count + 1);` or `setUser({...user, name: 'New Name'});`).
Forgetting to spread objects/arrays: When updating an object or array in state, you must create a new object or array. Directly mutating the existing one will lead to unexpected behavior and missed re-renders. Use the spread operator (`...`) to create a new copy and then apply your changes (e.g., `setItems(prevItems => [...prevItems, newItem]);` or `setUser(prevUser => ({ ...prevUser, age: 31 }));`).
Incorrect initial state for complex logic: If your initial state calculation is expensive, providing a direct value to `useState` will cause it to run on every re-render. Instead, pass a function to `useState` that returns the initial value. This function will only execute once on the initial render (e.g., `const [data, setData] = useState(() => computeInitialData());`).
Best Practices
Use descriptive names: Name your state variables and their setters clearly (e.g., `[isOpen, setIsOpen]`, `[products, setProducts]`).
Keep state minimal: Only store data in state that is absolutely necessary for rendering or user interaction. Derived values should be computed from existing state or props.
Prefer primitive types for simple state: If a piece of state only needs to hold a single value (number, string, boolean), use a separate `useState` call for it rather than embedding it in a larger object, unless they are logically grouped.
Use functional updates for complex state logic: When the new state depends on the previous state, always use the functional form of the setter (`setCount(prevCount => prevCount + 1)`). This prevents issues with stale closures.
Practice Exercises
Exercise 1: Text Input Display
Create a functional component that has an input field and a paragraph. As the user types into the input field, the text in the paragraph should update in real-time to reflect the input's value.
Exercise 2: Simple To-Do List Item
Build a component that displays a single to-do item (e.g., 'Learn React Hooks'). Add a button next to it that, when clicked, toggles a 'completed' status. Display ' (Completed)' next to the item when it's completed.
Exercise 3: Color Changer
Create a component with a simple square `div` element. Add three buttons: 'Red', 'Blue', and 'Green'. When a button is clicked, change the background color of the square `div` to the corresponding color.
Mini Project / Task
Build a simple 'Like Button' component. It should display the text 'Like' and a number (initially 0). When the button is clicked, the number should increment by 1. Add a second button labeled 'Dislike' that decrements the number by 1, but the number should not go below 0.
Challenge (Optional)
Extend the 'Like Button' component. Instead of just a number, make it display 'Like' if the count is 0, '1 Like' if the count is 1, and 'X Likes' (where X is the number) if the count is greater than 1. Also, add the ability to 'undo' the last like/dislike action by clicking an 'Undo' button. This will require storing a history of actions, potentially using an array in state.
Rendering Lists
In React, rendering lists of data is a fundamental task for almost any application. Whether you're displaying a list of products, user comments, or navigation links, React provides efficient ways to iterate over collections of data and render corresponding UI elements. This mechanism is crucial because most real-world applications deal with dynamic data that needs to be presented in a structured and repeatable manner. Without a clear way to render lists, developers would be forced to manually create each item, which is impractical and error-prone for dynamic content. React's approach leverages JavaScript's array methods, primarily
map(), to transform data arrays into arrays of React elements.The core idea behind rendering lists in React is to use JavaScript's iterative methods to create a new array of React elements from an existing array of data. This new array of elements is then directly rendered by React. This declarative approach means you describe what the UI should look like for each item, and React takes care of efficiently updating the DOM when the underlying data changes. It's used everywhere from simple to-do lists to complex data tables and feeds.
Core Concepts & Sub-types
The primary concept for rendering lists in React revolves around the
Array.prototype.map() method. While other methods like filter() and reduce() can be used in conjunction for data manipulation, map() is the workhorse for transforming each item in an array into a React component or element.The most critical aspect when rendering lists is the use of keys. Each item in a list should have a unique and stable key. Keys help React identify which items have changed, are added, or are removed. This allows React to efficiently re-render the list, improving performance and preventing potential bugs. Without keys, or with unstable keys, React might re-render more than necessary or exhibit unexpected behavior, especially when list items are reordered or filtered.
Keys must be unique among siblings, meaning within the same array of elements. They don't need to be globally unique across the entire application. The best key is a unique ID from your data (e.g., database ID). If you don't have stable IDs, you can fall back to the item's index in the array, but this is generally discouraged if the list items can be reordered, filtered, or added/removed, as it can lead to performance issues and UI inconsistencies.
Step-by-Step Explanation
1. Prepare your data: Ensure your data is in an array format. Each item in the array should ideally be an object containing properties that you want to display.
2. Use
map(): Inside your React component's return statement, or within JSX, call the map() method on your data array.3. Return JSX for each item: The callback function for
map() should return a React element (or component) for each item in the array.4. Assign a unique
key: To the top-level element returned by the map() callback, assign a unique key prop. This key should be a string or a number.5. Embed in JSX: Place the result of the
map() operation directly into your JSX. React will automatically render the array of elements.Comprehensive Code Examples
Basic example
function SimpleList() {
const fruits = ['Apple', 'Banana', 'Cherry'];
return (
{fruits.map((fruit, index) => (
- {fruit}
))}
);
}
Real-world example
function ProductList({ products }) {
return (
Available Products
{products.length === 0 ? (
No products found.
) : (
{products.map(product => (
{product.name}
Price: ${product.price.toFixed(2)}
Category: {product.category}
))}
)}
);
}
const App = () => {
const myProducts = [
{ id: 'p1', name: 'Laptop', price: 1200, category: 'Electronics' },
{ id: 'p2', name: 'Keyboard', price: 75, category: 'Electronics' },
{ id: 'p3', name: 'Mouse', price: 30, category: 'Electronics' },
];
return ;
};
Advanced usage (Component-based list items)
function TodoItem({ todo, onDelete }) {
return (
{todo.text}
);
}
function TodoList() {
const [todos, setTodos] = React.useState([
{ id: 't1', text: 'Learn React Hooks' },
{ id: 't2', text: 'Build a list component' },
{ id: 't3', text: 'Refactor old code' },
]);
const handleDeleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
return (
My Todo List
{todos.map(todo => (
))}
);
}
Common Mistakes
1. Forgetting the
key prop: React will warn you in the console if you forget keys. This is the most common mistake and can lead to inefficient updates and unexpected UI behavior.Fix: Always provide a unique and stable
key prop to the outermost element rendered inside your map() callback.2. Using array index as
key when list items can reorder/change: While using index as a key might seem convenient, it's problematic if your list items can be reordered, filtered, or added/removed from the middle. React uses keys to track identity, and an index changes if the item's position changes, confusing React.Fix: Use a unique ID from your data source (e.g., a database ID). Only use
index as a key if the list is static and will never change, reorder, or be filtered.3. Placing
map() inside deeply nested JSX without proper wrapping: Sometimes developers try to put map() directly without wrapping it in a parent element or a <> fragment, leading to syntax errors or invalid JSX.Fix: The result of
map() is an array of elements. This array must be placed where React expects a single element or an array of elements (e.g., directly inside a parent element like or , or within a fragment).Best Practices
1. Always provide a unique and stable key: This is paramount for performance and correctness. Use IDs from your data whenever possible.
2. Extract list items into separate components: For complex list items, create a dedicated component (e.g.,
, ). This improves readability, reusability, and makes your components more manageable. Pass the individual item data as props to this component.3. Avoid expensive operations inside
map(): While map() is efficient, avoid performing heavy calculations or side effects within its callback, as it runs for every item on every render.4. Handle empty lists gracefully: Always consider what your UI should look like when the list is empty. Display a message like "No items found" instead of just rendering nothing.
Practice Exercises
1. Create a component that displays a list of your 5 favorite books, including their titles and authors. Use an array of objects for the books.
2. Modify the book list component to include a "Read" button next to each book. (The button doesn't need to do anything yet, just render it).
3. Create a list of numbers from 1 to 10. Render each number inside a
tag.
Mini Project / Task
Build a simple contact list application. The application should display a list of contacts, where each contact has a name and a phone number. Initialize the list with at least three contacts. Each contact should be rendered as a list item.
Challenge (Optional)
Extend the contact list application. Add an input field and a button to allow users to add new contacts to the list. Ensure that each new contact receives a unique ID (you can use a simple counter for this in the absence of a backend). The list should update dynamically as new contacts are added.
Conditional Rendering
Conditional rendering in React means showing different UI output depending on data, state, props, or user actions. In simple terms, your component decides what to display based on a condition. This exists because real applications are rarely static. A dashboard may show a loading spinner while data is being fetched, an e-commerce site may display an “Out of Stock” badge, and a login page may show different content for authenticated and guest users. React makes this natural because components are just JavaScript functions that return JSX, so you can use JavaScript conditions to control what appears on screen.
In practice, conditional rendering is used everywhere: hiding admin-only tools, displaying validation errors, toggling menus, switching between dark and light themes, and rendering empty states when lists have no items. Common patterns include if statements, ternary operators, logical AND &&, early returns, and rendering null when nothing should appear. Each pattern is useful in a different situation. An if block is great when logic is larger. A ternary is helpful when choosing between two values directly in JSX. The && operator is concise when you want to show something only if a condition is true. Returning null is useful when a component should render nothing.
Step-by-Step Explanation
Start with a condition, usually from state or props. Then decide which JSX should appear. For example, if isLoggedIn is true, show a welcome message; otherwise show a login prompt. In React, you cannot write a full if statement directly inside JSX braces, but you can compute values before the return, use a ternary inside JSX, or call helper functions.
Pattern 1: early return. Check a condition at the top of the component and return different JSX immediately.
Pattern 2: ternary operator. Syntax: condition ? trueOutput : falseOutput.
Pattern 3: logical AND. Syntax: condition && jsx. If the condition is true, the JSX appears.
Pattern 4: assign JSX to a variable before returning it. This keeps markup clean when logic gets longer.
Comprehensive Code Examples
function StatusMessage() {
const isLoggedIn = true;
return (
{isLoggedIn ? Welcome back!
: Please log in.
}
);
}function ProductCard() {
const inStock = false;
const name = "Wireless Mouse";
return (
{name}
{inStock && Available now
}
{!inStock && Out of stock
}
);
}function Dashboard({ user, isLoading, isAdmin }) {
if (isLoading) {
return Loading dashboard...
;
}
if (!user) {
return You must sign in to continue.
;
}
return (
Hello, {user.name}
{isAdmin ? Admin tools enabled
: Standard account
}
);
}The first example shows a basic two-way choice with a ternary. The second shows a real-world product status using &&. The third is more advanced because it handles loading, authentication, and role-based content with early returns and a ternary.
Common Mistakes
- Using
ifdirectly inside JSX: write the condition beforereturnor use a ternary. - Rendering
0by accident with&&: values like0can appear on screen. Convert the condition to a boolean, such ascount > 0 && .... - Making nested ternaries too complex: deeply nested conditions are hard to read. Move logic into variables or helper functions.
- Forgetting a false case: if users need an alternative message, use a ternary instead of only
&&.
Best Practices
- Use early returns for loading, error, and empty states.
- Keep JSX readable by moving complex conditions into variables.
- Use ternaries only for simple two-way decisions.
- Prefer descriptive boolean names like
isLoading,hasError, andisAdmin. - Render
nullwhen a component should intentionally display nothing.
Practice Exercises
- Create a component that shows “Day Mode” or “Night Mode” based on a boolean variable.
- Build a message component that shows a loading text until a variable changes to false.
- Create a profile card that displays an “Admin” badge only when
isAdminis true.
Mini Project / Task
Build a simple shopping cart summary that conditionally shows: a loading message, an empty-cart message, or a list header with total items depending on the cart state.
Challenge (Optional)
Create a single component that handles four UI states: loading, error, no data, and successful data display, while keeping the JSX clean and easy to read.
Handling Events
In React, handling events is a fundamental aspect of creating interactive user interfaces. Events are actions that happen in the system you are programming, such as a user clicking a button, hovering over an element, typing into an input field, or submitting a form. React's event system is very similar to the native DOM event system, but with a few key differences and optimizations. It provides a synthetic event system that wraps browser's native events, providing a cross-browser compatible API. This means you don't have to worry about browser inconsistencies when handling events, making your code more robust and maintainable. Event handling is crucial for any dynamic web application, allowing users to interact with the application and trigger specific responses, leading to a much richer user experience. For example, when you click a 'Like' button on a social media feed, submit a search query, or toggle a dark mode theme, you are interacting with events handled by the application's event system.
React's event handlers are named using camelCase, unlike the lowercase HTML attributes. For instance, the HTML `onclick` becomes `onClick` in React. You pass a function as the event handler, rather than a string. This is a significant difference from traditional HTML where you might write `
Forms and Controlled Inputs
Forms are a fundamental part of web applications, allowing users to interact with the application by inputting data. In React, handling form data differs from traditional HTML forms due to React's declarative nature and its concept of 'state'. Instead of letting the browser handle form data directly, React encourages the use of 'controlled components'. A controlled component is an input form element whose value is controlled by React state. This means that the data for the input element is handled by a React component, making the component the 'single source of truth' for that input's data.
Why do we use controlled components? This approach simplifies data flow, makes input values predictable, and allows for immediate validation and manipulation of input data. Imagine a login form: as a user types their username, you might want to check if it meets certain criteria in real-time. With controlled inputs, every keystroke updates the component's state, giving you immediate access to the current value and enabling such dynamic behaviors. This pattern is crucial for building robust and interactive user interfaces where user input needs to be managed efficiently.
Step-by-Step Explanation
To implement controlled inputs in React, you typically follow these steps:
- Declare State for Each Input: For every input field you want to control, declare a corresponding state variable using the
useStatehook. This state variable will hold the current value of the input. - Bind Input's
valueto State: Set thevalueprop of the input element to the state variable you just created. This makes the input a 'controlled component' because its displayed value is now dictated by React state. - Handle Changes with
onChange: Attach anonChangeevent handler to the input. This function will be called every time the input's value changes (e.g., when a user types). - Update State in
onChange: Inside theonChangehandler, update the state variable with the new value from the input. This is typically done usingevent.target.value. - Handle Form Submission: For the form itself, attach an
onSubmitevent handler. Inside this handler, remember to callevent.preventDefault()to stop the browser's default form submission behavior (which would cause a page reload). Then, you can access the collected form data from your component's state and process it (e.g., send it to an API).
Comprehensive Code Examples
Basic Example
A simple controlled text input.
import React, { useState } from 'react';
function ControlledInput() {
const [name, setName] = useState('');
const handleChange = (event) => {
setName(event.target.value);
};
return (
Hello, {name}!
);
}
export default ControlledInput;
Real-world Example
A login form with multiple controlled inputs and submission handling.
import React, { useState } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleUsernameChange = (event) => {
setUsername(event.target.value);
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault(); // Prevent default browser form submission
alert(`Submitting: Username - ${username}, Password - ${password}`);
// In a real app, you'd send this data to an API
setUsername(''); // Clear form after submission
setPassword('');
};
return (
);
}
export default LoginForm;
Advanced Usage
Handling multiple inputs with a single event handler function and a single state object.
import React, { useState } from 'react';
function MultiInputForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
});
const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prevState => ({
...prevState,
[name]: value,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
alert(`Form Data: ${JSON.stringify(formData, null, 2)}`);
setFormData({ firstName: '', lastName: '', email: '' }); // Reset form
};
return (
);
}
export default MultiInputForm;
Common Mistakes
- Forgetting
valueoronChange: If you omit thevalueprop, the input will be 'uncontrolled' and React won't manage its state. If you omitonChange, the input will be read-only because React state never updates. Always provide both for controlled inputs. - Mutating State Directly: Never directly modify the state object (e.g.,
formData.firstName = 'new'). Always use the state setter function (e.g.,setFormData(...)) to ensure React re-renders the component. - Forgetting
event.preventDefault(): In the form'sonSubmithandler, failing to callevent.preventDefault()will cause the browser to perform its default form submission, leading to a page reload and loss of React state.
Best Practices
- Use a Single State Object for Complex Forms: For forms with many inputs, group related input values into a single state object. This makes updating and managing form data more organized.
- Extract Custom Hooks for Form Logic: For very complex forms, consider creating a custom hook (e.g.,
useForm) to encapsulate the state management and change handlers, making your components cleaner and more reusable. - Implement Input Validation: Integrate validation logic within your
onChangehandlers oronSubmithandler to provide immediate feedback to users and ensure data integrity. - Leverage Libraries for Advanced Forms: For highly complex forms with validation, error handling, and submission logic, consider using libraries like Formik or React Hook Form, which abstract away much of the boilerplate.
Practice Exercises
- Exercise 1: Simple Counter Input
Create a component with a number input. The input should be controlled. Display the current value of the input below it. - Exercise 2: Text Area Character Count
Build a controlled
useEffect Hook
The useEffect Hook lets React function components perform side effects after rendering. A side effect is anything that reaches outside the component’s pure UI calculation, such as fetching API data, setting a timer, listening for window events, updating the page title, or syncing local storage. Before Hooks, these tasks were usually handled in class lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. useEffect brings those responsibilities into function components in a cleaner and more organized way.
In real applications, this Hook is used when a product page loads data from a server, when a chat app subscribes to messages, when a form saves draft input to local storage, or when a dashboard updates the browser tab title based on selected reports. The Hook runs after React updates the DOM, so your UI appears first and then the effect logic runs. This makes rendering predictable. There are common patterns to understand: an effect with no dependency array runs after every render, an effect with an empty dependency array runs once after the first render, and an effect with specific dependencies runs whenever those values change. Effects can also return a cleanup function, which React runs before the effect reruns or when the component unmounts. Cleanup is essential for removing listeners, clearing timers, and preventing memory leaks.
Step-by-Step Explanation
Basic syntax: useEffect(() => { /* side effect */ return () => { /* cleanup */ }; }, [dependencies]).
Step 1: import useEffect from React.
Step 2: call it inside your component, not outside and not inside loops or conditions.
Step 3: place side-effect logic inside the callback.
Step 4: provide a dependency array to control when the effect runs.
Step 5: if needed, return a cleanup function.
Think of dependencies as values your effect uses from the component scope. If your effect reads userId, that value usually belongs in the dependency array so React can rerun the effect when it changes.
Comprehensive Code Examples
Basic example
import React, { useEffect } from 'react';
function PageTitle() {
useEffect(() => {
document.title = 'Welcome to React App';
}, []);
return Home Page;
}Real-world example
import React, { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
return user ? {user.name} : Loading...;
}Advanced usage
import React, { useEffect, useState } from 'react';
function WindowWidthTracker() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return Window width: {width};
}Common Mistakes
- Missing dependencies: An effect uses a value but does not list it in the dependency array. Fix: include all referenced reactive values unless you intentionally restructure the logic.
- Forgetting cleanup: Event listeners and timers stay active after unmount. Fix: return a cleanup function.
- Causing infinite loops: Updating state inside an effect that reruns every render. Fix: use the correct dependency array and avoid unnecessary state updates.
Best Practices
- Keep each effect focused on one responsibility, such as data fetching or event subscription.
- Always think carefully about dependencies; they control correctness and performance.
- Use cleanup for subscriptions, timers, and listeners to avoid memory leaks.
- Do not use effects for logic that can be calculated during rendering.
Practice Exercises
- Create a component that changes the browser tab title to the current count whenever a button is clicked.
- Build a component that starts a timer on mount and clears it on unmount.
- Fetch and display a list of posts from a public API when the component loads.
Mini Project / Task
Build a weather search component that fetches weather data whenever the user changes the city name, displays loading text, and cleans up any outdated request logic if needed.
Challenge (Optional)
Create a responsive component that tracks window width, stores the latest width in local storage, and restores that saved value when the component first loads.
useRef Hook
The useRef Hook in React lets you store a mutable value that survives across component re-renders without causing another render when that value changes. This is important because not every piece of data belongs in state. State is great when the UI must update, but sometimes you only need to remember a value, access a DOM element, or keep track of something behind the scenes. In real projects, useRef is often used to focus an input, control media playback, store timer IDs, track previous values, or integrate with browser APIs and third-party libraries.
There are two main ways developers use useRef. First, as a DOM reference: React attaches the ref to an element so you can directly access it through ref.current. Second, as a persistent container for mutable values: you can store something like a counter, interval ID, or previous prop value in ref.current without triggering a re-render. This makes useRef different from useState. Updating state re-renders the component; updating a ref does not. That difference is the key reason this Hook exists.
Step-by-Step Explanation
To use it, import useRef from React and create a ref with const myRef = useRef(initialValue). React returns an object with a single property: current. If the ref is attached to an HTML element like , then after render, myRef.current points to that DOM node. If the ref is used to store a value, then you manually read and write myRef.current.
Beginners should remember three rules. First, refs persist between renders. Second, changing ref.current does not refresh the screen. Third, refs are best for values not meant for rendering. If the UI must immediately show the updated value, use state instead.
Comprehensive Code Examples
Basic example
import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus();
};
return (
);
}
export default FocusInput;Real-world example
import React, { useEffect, useRef } from 'react';
function SearchBox() {
const searchRef = useRef(null);
useEffect(() => {
searchRef.current.focus();
}, []);
return ;
}
export default SearchBox;Advanced usage
import React, { useEffect, useRef, useState } from 'react';
function TimerTracker() {
const intervalRef = useRef(null);
const rendersRef = useRef(0);
const [seconds, setSeconds] = useState(0);
useEffect(() => {
rendersRef.current += 1;
});
const startTimer = () => {
if (!intervalRef.current) {
intervalRef.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
}
};
const stopTimer = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
return (
Seconds: {seconds}
Render count: {rendersRef.current}
);
}
export default TimerTracker;Common Mistakes
- Using a ref for visible UI data: if the screen should update, use state instead of only changing
ref.current. - Accessing
ref.currenttoo early: a DOM ref may benullbefore the element mounts, so check it first or useuseEffect. - Forgetting cleanup: when storing timers or external instances in refs, clear them when no longer needed.
Best Practices
- Use refs for DOM access, timers, previous values, and external library instances.
- Keep refs out of rendering logic unless you understand that changes will not trigger re-renders.
- Name refs clearly, such as
inputRef,videoRef, orintervalRef.
Practice Exercises
- Create a component with an input and a button that focuses the input using
useRef. - Build a text area that automatically receives focus when the component loads.
- Create a timer with start and stop buttons, storing the interval ID in a ref.
Mini Project / Task
Build a simple note-taking input where the text field is automatically focused on page load and a clear button returns focus to the input after clearing.
Challenge (Optional)
Create a component that stores the previous value of a text input in a ref and displays both the current and previous values on the screen.
useContext Hook
The
useContext Hook in React is a powerful tool designed to simplify state management and prop drilling. Before useContext, passing data down through multiple levels of nested components (a process often referred to as "prop drilling") could become cumbersome and lead to less maintainable code. useContext provides a way to share values like user authentication status, theme preferences, or application configuration across the component tree without explicitly passing props at each level. It leverages React's Context API, which allows you to create a global store of data that any component within a certain subtree can access. This eliminates the need for intermediate components to receive and forward props they don't directly use, making your component hierarchy cleaner and more efficient. In real-world applications, useContext is frequently used for managing global state such as the currently logged-in user, dark/light theme toggles, language settings, or even complex application-wide data that needs to be accessible by many different components.The core idea behind
useContext and the Context API is to provide a "teleportation" mechanism for data. Instead of passing props down step-by-step, you define a Context Provider at a higher level in your component tree. This provider makes the value available to all components nested within it, regardless of how deep they are. Then, any descendant component can use the useContext Hook to consume that value directly. This pattern significantly improves code readability and reduces the boilerplate associated with prop passing, especially in large applications with deeply nested component structures. It's important to note that while useContext is excellent for managing global state, it's not a complete replacement for state management libraries like Redux or Zustand for very complex, highly interactive data flows, but it serves as an excellent solution for many common global state needs.Step-by-Step Explanation
To use the
useContext Hook, you typically follow these steps:- Create a Context: First, you need to create a Context object using
React.createContext(). This object comes with a Provider and a Consumer component. We primarily use the Provider. - Provide a Value: Wrap the components that need access to the context in the
Context.Providercomponent. TheProvidertakes avalueprop, which will be the data you want to make available to all its descendants. - Consume the Value: In any descendant component, call the
useContext(YourContext)Hook, passing the Context object you created. This Hook will return the current context value.
The basic syntax looks like this:
// 1. Create a Context
const MyContext = React.createContext(defaultValue);
// 2. Provide a value
function App() {
const [theme, setTheme] = React.useState('light');
return (
);
}
// 3. Consume the value
function Toolbar() {
const theme = React.useContext(MyContext);
return Current theme: {theme};
}Comprehensive Code Examples
Basic Example: Theme Toggle
import React, { createContext, useContext, useState } from 'react';
// 1. Create the Context
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
// 2. Provide the theme value and toggle function
Theme Switcher Example
);
}
function Toolbar() {
// 3. Consume the theme and toggle function
const { theme, toggleTheme } = useContext(ThemeContext);
return (
Current theme: {theme}
);
}
function ThemedButton() {
const { theme } = useContext(ThemeContext);
return (
);
}
export default App;Real-world Example: User Authentication Status
import React, { createContext, useContext, useState } from 'react';
// Context for user authentication
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null); // null means not logged in, object means logged in
const login = (username) => {
// Simulate API call
setTimeout(() => setUser({ username, id: Date.now() }), 500);
};
const logout = () => {
// Simulate API call
setTimeout(() => setUser(null), 300);
};
return (
{children}
);
}
function UserProfile() {
const { user, logout } = useContext(AuthContext);
if (!user) {
return Please log in to view your profile.
;
}
return (
Welcome, {user.username}!
User ID: {user.id}
);
}
function LoginForm() {
const [username, setUsername] = useState('');
const { user, login } = useContext(AuthContext);
const handleSubmit = (e) => {
e.preventDefault();
if (username) {
login(username);
setUsername('');
}
};
if (user) {
return null; // Don't show login form if already logged in
}
return (
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
);
}
function App() {
return (
Authentication Example with useContext
);
}
export default App;Advanced Usage: Multiple Contexts and Custom Hooks
You can combine multiple contexts or create custom hooks to simplify context consumption.
import React, { createContext, useContext, useState } from 'react';
// Context for user authentication
const AuthContext = createContext(null);
// Context for application settings
const SettingsContext = createContext({ language: 'en', notifications: true });
// Custom Hook for AuthContext
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Custom Hook for SettingsContext
function useSettings() {
const context = useContext(SettingsContext);
if (!context) {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
}
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username) => setUser({ username });
const logout = () => setUser(null);
return (
{children}
);
}
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({ language: 'en', notifications: true });
const toggleNotifications = () => setSettings(prev => ({ ...prev, notifications: !prev.notifications }));
return (
{children}
);
}
function UserDashboard() {
const { user, logout } = useAuth();
const { settings, toggleNotifications } = useSettings();
if (!user) return Please log in.
;
return (
Hello, {user.username}!
Language: {settings.language}
Notifications: {settings.notifications ? 'On' : 'Off'}
);
}
function App() {
return (
Multiple Contexts & Custom Hooks Example
);
}
export default App;Common Mistakes
- Forgetting to Wrap Components in Provider: If you call
useContextin a component that is not a descendant of the correspondingContext.Provider, the hook will return thedefaultValueprovided tocreateContext(if any) orundefined/null, leading to unexpected behavior or errors. Always ensure the component consuming the context is within the provider's tree.
Fix: Double-check your component tree and make sure theContext.Providerwraps all components that need access to its value. - Updating Context Value Incorrectly (Performance Issues): If the
valueprop passed toContext.Provideris an object or array that is created inline (e.g.,value={{ data: myData }}) in every render, it will cause all consuming components to re-render, even if the underlying data hasn't logically changed, because the reference to the object changes. This can lead to performance issues.
Fix: Memoize the context value usinguseStateoruseMemoif the value is an object or array and you want to prevent unnecessary re-renders. For example:const contextValue = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]); - Overusing Context for Local State: While
useContextis great for global state, it's not a replacement for local component state (useState) or prop drilling for data that is only relevant to a few closely related components. Overusing context can make your application harder to reason about and debug.
Fix: UseuseStatefor component-specific state and prop drilling for data that only needs to go down one or two levels. ReserveuseContextfor truly global or widely shared data.
Best Practices
- Separate Context Creation and Provider: It's a good practice to create your context and its provider component in a separate file (e.g.,
ThemeContext.js). This makes your code more organized and reusable. - Provide Meaningful Default Values: When creating context with
createContext(defaultValue), provide a default value that makes sense for consumers that might render without a provider, or for testing purposes. - Memoize Context Values: If your context value is an object or array, use
useMemoto prevent unnecessary re-renders of consuming components when the provider re-renders but the actual context value hasn't changed. - Split Large Contexts: If a single context holds many unrelated values, consider splitting it into multiple, smaller contexts. This helps prevent unnecessary re-renders for components that only care about a subset of the context's data. For example, separate
AuthContextfromThemeContext. - Create Custom Hooks for Consumption: Encapsulate the
useContextcall within a custom hook (e.g.,useAuth,useTheme). This provides a cleaner API for consuming components, abstracts away the context details, and allows for adding error handling if the hook is used outside of its provider.
Practice Exercises
- Beginner-friendly: Create a simple message display component. Use
useContextto pass a static message string (e.g., "Hello from Context!") from anAppcomponent down to aDisplayMessagecomponent, and render it. - Intermediate: Build a language switcher. Create a
LanguageContextthat holds the current language string (e.g., 'en', 'es'). Provide buttons in yourAppcomponent to change the language. In a child component, useuseContextto display a greeting in the selected language. - Intermediate: Develop a simple shopping cart item counter. Create a
CartContextthat stores the number of items in a cart. In theAppcomponent, display a button to "Add Item". In a header component, useuseContextto display the current item count.
Mini Project / Task
Build a simple settings panel for a user profile. Create a
SettingsContext that manages two states: fontSize (e.g., 'small', 'medium', 'large') and notificationsEnabled (boolean). Your application should have an App component, a SettingsPanel component (with controls to change font size and toggle notifications), and a TextDisplay component that uses the context to render text with the selected font size and shows an alert if notifications are enabled.Challenge (Optional)
Extend the previous settings panel. Introduce a
UserContext that contains the logged-in user's name. Modify the SettingsPanel so that it only allows changing settings if a user is logged in. If no user is logged in, it should display a "Please log in to change settings" message. You will need to nest one context provider inside another. useReducer Hook
The useReducer Hook is a React state management tool used when component state becomes more complex than a simple value update. While useState works well for small, isolated values, useReducer is better when state has multiple related fields, when updates depend on the previous state, or when many actions can change the same state object. It exists to make state updates predictable by moving logic into a reducer function. In real projects, it is commonly used for counters with multiple actions, form state, shopping carts, filters, task managers, and UI workflows such as loading, success, and error states.
The hook works with two main ideas: state and actions. A reducer is a function that receives the current state and an action, then returns the next state. The action usually contains a type property and sometimes extra data in payload. This pattern is similar to Redux, so learning it also helps you understand larger React architectures. Common action styles include increment/decrement actions, object field updates, reset actions, and async-related status changes like start, success, and failure.
Step-by-Step Explanation
The syntax is const [state, dispatch] = useReducer(reducer, initialState). First, create an initialState object or value. Next, write a reducer function like function reducer(state, action) { ... }. Inside it, use conditional logic or a switch statement to check action.type. Then return a new state object instead of changing the old one directly. Finally, call dispatch({ type: 'ACTION_NAME' }) when the user clicks a button or submits a form. React sends that action to the reducer, calculates the next state, and re-renders the component.
Comprehensive Code Examples
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}import { useReducer } from 'react';
const initialState = { name: '', email: '' };
function formReducer(state, action) {
switch (action.type) {
case 'update_field':
return { ...state, [action.field]: action.value };
case 'reset_form':
return initialState;
default:
return state;
}
}
export default function ContactForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
return (
<form>
<input
value={state.name}
onChange={(e) => dispatch({ type: 'update_field', field: 'name', value: e.target.value })}
placeholder='Name'
/>
<input
value={state.email}
onChange={(e) => dispatch({ type: 'update_field', field: 'email', value: e.target.value })}
placeholder='Email'
/>
<button type='button' onClick={() => dispatch({ type: 'reset_form' })}>Clear</button>
</form>
);
}import { useEffect, useReducer } from 'react';
const initialState = { loading: false, data: [], error: '' };
function reducer(state, action) {
switch (action.type) {
case 'fetch_start':
return { ...state, loading: true, error: '' };
case 'fetch_success':
return { loading: false, data: action.payload, error: '' };
case 'fetch_error':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
export default function Products() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
async function loadData() {
dispatch({ type: 'fetch_start' });
try {
const res = await fetch('/api/products');
const data = await res.json();
dispatch({ type: 'fetch_success', payload: data });
} catch {
dispatch({ type: 'fetch_error', payload: 'Failed to load products' });
}
}
loadData();
}, []);
return <div>{state.loading ? 'Loading...' : state.error || state.data.length}</div>;
}Common Mistakes
Mutating state directly. Fix: always return a new object or array using spread syntax.
Forgetting a default case in the reducer. Fix: return the current
statefor unknown actions.Using
useReducerfor trivial state. Fix: preferuseStatefor very simple values.Sending unclear action names. Fix: use descriptive types such as
add_itemortoggle_theme.
Best Practices
Keep reducers pure: no API calls, timers, or direct DOM work inside the reducer.
Use action objects consistently with
typeand optionalpayload.Group related state in one reducer when updates are connected.
Split large reducers into smaller helper functions if logic grows too much.
Practice Exercises
Create a counter with increment, decrement, and reset actions using
useReducer.Build a login form reducer that updates username and password fields and clears the form.
Create a theme switcher reducer with actions for light mode and dark mode.
Mini Project / Task
Build a shopping cart component with actions to add an item, remove an item, increase quantity, decrease quantity, and clear the cart.
Challenge (Optional)
Build a task manager using useReducer where users can add tasks, mark them complete, edit task text, delete tasks, and filter by status.
useMemo and useCallback
React applications often re-render whenever state or props change. That behavior is useful, but sometimes a component repeats expensive calculations or recreates functions unnecessarily. useMemo and useCallback are optimization hooks that help control this. useMemo stores the result of a calculation and recomputes it only when its dependencies change. useCallback stores a function reference so React can reuse the same function between renders unless dependencies change. In real projects, these hooks are used in dashboards with filtered data, search interfaces, charts, long lists, and parent-child component trees where rerenders can become expensive.
The key difference is simple: useMemo returns a memoized value, while useCallback returns a memoized function. You should not add them everywhere. They are performance tools, not default coding style. If the calculation is cheap or the function is not causing rerender problems, using these hooks may add complexity without measurable benefit.
Step-by-Step Explanation
Basic syntax for useMemo looks like const value = useMemo(() => computeSomething(a, b), [a, b]). React runs the callback during rendering and saves the returned value. On the next render, React checks the dependency array. If a and b did not change, React reuses the old value.
Basic syntax for useCallback looks like const handleClick = useCallback(() => doSomething(id), [id]). React saves the function itself, not the result of calling it. This is especially useful when passing handlers to memoized child components because a new function reference can trigger a rerender even if the logic is the same.
A good beginner rule is this: use useMemo for expensive derived data such as sorted arrays, filtered lists, or computed statistics. Use useCallback when passing callbacks to children wrapped with React.memo or when a hook depends on a stable function reference.
Comprehensive Code Examples
import { useMemo, useState } from 'react';brbrfunction PriceSummary() {br const [items, setItems] = useState([10, 20, 30]);br const total = useMemo(() => {br return items.reduce((sum, price) => sum + price, 0);br }, [items]);brbr return Total: {total};br}import { useCallback, useState, memo } from 'react';brbrconst Button = memo(function Button({ onClick, label }) {br console.log('Button rendered');br return ;br});brbrfunction Counter() {br const [count, setCount] = useState(0);br const handleIncrement = useCallback(() => {br setCount(c => c + 1);br }, []);brbr return import { useMemo, useCallback, useState } from 'react';brbrfunction ProductSearch({ products }) {br const [query, setQuery] = useState('');br const [sortBy, setSortBy] = useState('price');brbr const filteredProducts = useMemo(() => {br const filtered = products.filter(p =>br p.name.toLowerCase().includes(query.toLowerCase())br );br return filtered.sort((a, b) => a[sortBy] - b[sortBy]);br }, [products, query, sortBy]);brbr const clearSearch = useCallback(() => {br setQuery('');br }, []);brbr return (br br setQuery(e.target.value)} />br br br {filteredProducts.map(product => (br - {product.name} - ${product.price}
br ))}br
br br );br}Common Mistakes
- Using these hooks for everything. Fix: apply them only when there is a real rerender or expensive computation problem.
- Forgetting dependencies. Fix: include every reactive value used inside the callback unless you intentionally use a stable updater pattern.
- Using
useCallbackwhen you actually need a computed value. Fix: if you want a result, useuseMemoinstead. - Mutating arrays before memoizing. Fix: create new arrays with methods like
filter,map, or copied sorting patterns.
Best Practices
- Measure performance before optimizing, especially in small components.
- Use
useMemofor costly derived values, not for simple expressions. - Use
useCallbackwith memoized children or effect dependencies that need stable references. - Prefer state updater functions like
setCount(c => c + 1)to reduce unnecessary dependencies.
Practice Exercises
- Create a component that filters a list of names using
useMemobased on a search input. - Build a parent component that passes a click handler to a memoized child using
useCallback. - Make a product list that calculates total inventory value with
useMemoand resets a search box withuseCallback.
Mini Project / Task
Build a searchable and sortable employee directory. Memoize the filtered and sorted employee list with useMemo, and memoize action handlers like reset, select employee, or toggle favorites with useCallback.
Challenge (Optional)
Create a dashboard widget with a large dataset where you compare behavior with and without memoization. Identify which values should use useMemo and which event handlers should use useCallback to reduce unnecessary child rerenders.
Custom Hooks
Custom Hooks are reusable JavaScript functions that let you extract and share stateful logic between React components. React includes built-in Hooks such as useState, useEffect, and useMemo, but real applications often repeat the same logic across multiple components. A custom Hook solves this by wrapping that repeated behavior into one reusable function, usually named with the use prefix such as useWindowWidth or useFetch. This pattern exists to improve readability, reduce duplication, and make components easier to test and maintain. In real life, custom Hooks are used for API requests, form handling, authentication, local storage syncing, media queries, debouncing input, and tracking browser events. There are no special React “types” of custom Hooks, but they are commonly grouped by purpose: state management Hooks, side-effect Hooks, browser API Hooks, data-fetching Hooks, and utility Hooks. The key idea is that a custom Hook does not render UI directly; it only manages logic and returns values or functions that components can use.
Step-by-Step Explanation
To create a custom Hook, define a function whose name starts with use. Inside it, you can call other Hooks just like in a component. Then return the state, derived values, or handler functions needed by the component. For example, a simple Hook can manage a counter. First, create function useCounter(initialValue). Next, call useState(initialValue). Then create helper functions like increment and decrement. Finally, return them in an object or array. Components that call this Hook get isolated state, meaning each component instance has its own separate data. Custom Hooks must follow the Rules of Hooks: call Hooks only at the top level and only inside React function components or other custom Hooks. Do not call them inside loops, conditions, or nested functions.
Comprehensive Code Examples
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
function Counter() {
const { count, increment, decrement, reset } = useCounter(5);
return (
Count: {count}
);
}import { useEffect, useState } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function ThemeToggle() {
const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
return (
);
}import { useEffect, useState } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function load() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Request failed');
const result = await response.json();
if (!cancelled) setData(result);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}Common Mistakes
- Not using the use prefix: React tools expect Hook names like
useSomething. Rename the function properly. - Calling Hooks conditionally: Never place Hook calls inside
ifblocks or loops. Keep them at the top level. - Mixing UI with logic: A custom Hook should manage logic, not return JSX. Return data and handlers instead.
- Forgetting dependencies: In Hooks using
useEffect, include correct dependency values to avoid stale data.
Best Practices
- Keep Hooks focused: One Hook should solve one clear problem.
- Return a clean API: Expose only what components need, using an object for readability.
- Reuse built-in Hooks carefully: Combine
useState,useEffect, and others to hide complexity. - Document expected inputs and outputs: This makes Hooks easier for teams to reuse.
- Handle cleanup: Remove listeners, cancel requests, and avoid memory leaks.
Practice Exercises
- Create a
useToggleHook that switches betweentrueandfalse. - Build a
useInputHook that stores an input value and provides anonChangehandler. - Create a
useDocumentTitleHook that updates the browser tab title when a value changes.
Mini Project / Task
Build a reusable search box feature using a custom Hook that stores the query, updates it from an input field, and clears it with one button.
Challenge (Optional)
Create a useDebounce Hook that delays updating a search term for 500 milliseconds, then use it to reduce unnecessary API calls in a search component.
Component Lifecycle
Component lifecycle describes the stages a React component goes through from the moment it appears on the screen, while it updates, until it is removed. In older class components, lifecycle behavior was handled with methods such as componentDidMount, componentDidUpdate, and componentWillUnmount. In modern React, functional components usually handle lifecycle-related work with hooks, especially useEffect. This concept exists because user interfaces are not static: data loads from APIs, timers start and stop, event listeners are attached, and subscriptions must be cleaned up. In real projects, lifecycle knowledge is used when fetching user profiles after a page loads, updating charts when filters change, or clearing intervals when a component disappears.
The lifecycle can be understood in three main phases: mounting, updating, and unmounting. Mounting happens when a component is first added to the DOM. Updating happens when props or state change and React re-renders the component. Unmounting happens when the component is removed. With hooks, you can model these phases by controlling when an effect runs. An effect with an empty dependency array runs once after mount. An effect with dependencies runs after mount and whenever those values change. A cleanup function returned from the effect runs before the effect re-runs and when the component unmounts.
Step-by-Step Explanation
In a functional component, first import useEffect. Then place lifecycle logic inside useEffect(() => { ... }, dependencies). If the dependency array is empty, the effect runs once after the first render. If you include variables such as userId, the effect runs whenever userId changes. If you return a function, React uses it for cleanup. This is where you remove event listeners, clear timers, or cancel subscriptions.
Class components express the same idea differently. componentDidMount is used after first render, componentDidUpdate reacts to changes, and componentWillUnmount cleans up before removal. Even if you mainly use hooks, knowing these lifecycle categories helps you reason about component behavior clearly.
Comprehensive Code Examples
import { useEffect } from 'react';
function Welcome() {
useEffect(() => {
console.log('Component mounted');
}, []);
return Hello React;
}import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
return user ? {user.name}
: Loading...
;
}import { useEffect, useState } from 'react';
function Clock() {
const [time, setTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return {time}
;
}Common Mistakes
- Forgetting cleanup: Timers and listeners continue running. Return a cleanup function from
useEffect. - Wrong dependency array: Missing dependencies can create stale data. Include values used inside the effect when needed.
- Fetching on every render: Omitting the dependency array may trigger repeated requests. Add proper dependencies.
Best Practices
- Keep effects focused on one responsibility, such as fetching data or managing subscriptions.
- Always clean up intervals, subscriptions, and event listeners.
- Use functional components and hooks for modern React code unless maintaining legacy class components.
- Avoid putting unrelated logic inside one large effect block.
Practice Exercises
- Create a component that logs a message only when it first mounts.
- Build a component that updates displayed text whenever a prop changes using
useEffect. - Create a timer component and properly clean it up when the component unmounts.
Mini Project / Task
Build a weather widget component that fetches weather data when it mounts, reloads data when the city prop changes, and cleans up any polling interval when removed.
Challenge (Optional)
Create a searchable product list where typing a category updates results, and add cleanup logic to prevent outdated asynchronous responses from affecting the UI.
Lifting State Up
Lifting State Up is a fundamental concept in React for managing shared state between multiple sibling or parent-child components. In React, data flows unidirectionally, from parent to child. When two or more components need to share the same state, or when a child component needs to communicate changes back to a parent, direct communication between siblings or child-to-parent is not straightforward. Lifting state up solves this problem by moving the shared state to their closest common ancestor. This ancestor component then passes the state down to its children via props, and also passes down callback functions to allow children to update that shared state.
This pattern is crucial for building robust and maintainable React applications because it centralizes state management, making it easier to debug, understand, and predict how data changes will affect your UI. Without lifting state, you might find yourself duplicating state in multiple components, leading to inconsistencies and complex synchronization issues. It's widely used in almost every React application, from simple form inputs that need to update a display component to complex data tables with filtering and sorting capabilities.
The core idea is to identify the components that need to react to the same state. Instead of each component managing its own version of that state, you move the state up to their nearest common parent. This parent then becomes the 'source of truth' for that particular piece of state. When a child component needs to change the state, it calls a function (passed down as a prop) from the parent, which then updates the state in the parent. The parent's state update triggers a re-render, and the new state is passed down to all relevant children, ensuring they all reflect the latest data.
Step-by-Step Explanation
1. Identify Shared State: Determine which piece of state needs to be shared or synchronized between multiple components.
2. Find Common Ancestor: Locate the closest common parent component of all components that need access to this state.
3. Move State to Ancestor: Cut the state (e.g.,
const [value, setValue] = useState('')) from the child component and paste it into the common ancestor.4. Pass State Down as Props: Pass the state variable (e.g.,
value) from the ancestor to the child components that need to read it, using props.5. Pass Setter Function Down as Props: Pass the state update function (e.g.,
setValue) from the ancestor to the child components that need to modify the state, also using props.6. Child Updates State: In the child component, when an event occurs that should change the state (e.g., an
onChange event on an input), call the setter function received via props.Comprehensive Code Examples
Basic example: Temperature Converter
Let's say we want to build a simple temperature converter where you can enter a temperature in Celsius or Fahrenheit, and the other input automatically updates.
import React, { useState } from 'react';
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
return (
);
}
function Calculator() {&br> const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const handleCelsiusChange = (temperature) => {
setTemperature(temperature);
setScale('c');
};
const handleFahrenheitChange = (temperature) => {
setTemperature(temperature);
setScale('f');
};
const celsius = scale === 'f'
? tryConvert(temperature, toCelsius)
: temperature;
const fahrenheit = scale === 'c'
? tryConvert(temperature, toFahrenheit)
: temperature;
return (
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange} />
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange} />
);
}
export default Calculator;Real-world example: Filterable Product Table
Imagine a product table where users can filter products by a search term. The search input and the product list both need access to the search term state.
import React, { useState } from 'react';
function SearchBar({ filterText, onFilterTextChange }) {
return (
type="text"
placeholder="Search..."
value={filterText}
onChange={(e) => onFilterTextChange(e.target.value)} />
);
}
function ProductTable({ products, filterText }) {
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(filterText.toLowerCase())
);
return (
{filteredProducts.map(product => (
))}
Name Price {product.name} {product.price}
);
}
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
return (
filterText={filterText}
onFilterTextChange={setFilterText} />
products={products}
filterText={filterText} />
);
}
const PRODUCTS = [
{id: 1, name: 'Apple', price: '$1'},
{id: 2, name: 'Banana', price: '$1'},
{id: 3, name: 'Carrot', price: '$2'},
{id: 4, name: 'Dragonfruit', price: '$4'},
];
function App() {
return ;
}
export default App; Advanced usage: Controlled components with multiple inputs
When dealing with forms that have multiple inputs, lifting state up allows the parent to manage all form data centrally.
import React, { useState } from 'react';
function MyInput({ label, value, onChange, name }) {
return (
type="text"
name={name}
value={value}
onChange={onChange} />
);
}
function UserForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted: ${JSON.stringify(formData)}`);
};
return (
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange} />
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange} />
label="Email"
name="email"
value={formData.email}
onChange={handleChange} />
);
}
export default UserForm;Common Mistakes
1. Not identifying the correct common ancestor: Lifting state too high can lead to unnecessary prop drilling. Lifting it too low means components still can't share state effectively.
Fix: Always identify the closest common ancestor that needs to own the state.
2. Forgetting to pass the setter function: Child components receive the state but can't update it, leading to read-only UI elements.
Fix: Ensure both the state value and its corresponding setter function are passed down as props to any child that needs to modify it.
3. Mutating state directly in child components: Attempting to modify a prop received from a parent directly. This doesn't trigger re-renders and violates React's unidirectional data flow.
Fix: Always use the setter function passed down from the parent to update state. Props are read-only for child components.
Best Practices
1. Keep state as local as possible: Only lift state up when absolutely necessary. If a component's state doesn't affect its siblings or parent, keep it within that component.
2. Use descriptive prop names: For state and setter functions, use clear names like
value and onValueChange to indicate their purpose and direction.3. Pass functions, not just values: When a child needs to modify state, pass a function that updates the parent's state, rather than trying to pass the state itself back up.
4. Consider Context API or Redux for global state: For very deep component trees or truly global state, prop drilling can become cumbersome. React's Context API or state management libraries like Redux can be more suitable for such scenarios, but start with lifting state up for local shared state.
5. Immutability: When updating object or array state, always create a new object/array with the changes rather than mutating the existing one. This ensures React detects the change and re-renders correctly.
Practice Exercises
1. Counter Application: Create a parent component
App and two child components: IncrementButton and DisplayCount. Lift the count state to App. The button should increment the count, and the display component should show the current count.2. Text Synchronizer: Build a parent component
TextSync and two child TextInput components. When you type into one TextInput, the text in the other TextInput should update simultaneously.3. Toggle Switch with Text: Create a parent component
ToggleApp and two children: ToggleButton and StatusDisplay. The ToggleButton should toggle an isOn boolean state, and the StatusDisplay should show "On" or "Off" based on this state.Mini Project / Task
Build a simple dashboard with a
Dashboard parent component. This dashboard should contain two child components: InputBox and DisplayArea. The InputBox should be a text input where a user can type a message. As the user types, the DisplayArea component should immediately show the current message. Ensure the message state is lifted to the Dashboard component.Challenge (Optional)
Extend the Temperature Converter example. Add a
BoilingVerdict component that takes the Celsius temperature as a prop. This component should display a message indicating whether the water would boil at that temperature (above or equal to 100 degrees Celsius). Ensure this component also reacts to changes in either the Celsius or Fahrenheit input fields. Component Composition
Component composition is the practice of building complex user interfaces by combining smaller, focused React components. Instead of creating one huge component that handles layout, styling, data, buttons, headers, and lists all at once, React encourages you to split UI into reusable pieces and assemble them like building blocks. This exists because real applications grow quickly, and composition makes code easier to read, test, maintain, and reuse. In real projects, composition is used everywhere: dashboards made from cards and charts, ecommerce pages built from product tiles and filters, and admin panels assembled from tables, forms, modals, and navigation sections.
The core idea is that a parent component can include child components and pass data or UI into them. Common composition patterns include simple nesting, using the special children prop, creating layout wrapper components, and passing components as props for flexible rendering. Simple nesting means one component renders another. The children prop lets you place custom content inside a reusable wrapper. Layout components define structure such as cards, sidebars, or page shells. Component-as-prop patterns let you inject a button, icon, or custom section into a reusable component without changing its internal logic.
Step-by-Step Explanation
Start with a small component that does one job. For example, a Button should display a button, not manage an entire page. Next, create a parent component that uses smaller components together. In JSX, composition looks like normal HTML nesting: <Card><Button /></Card>. If you want the wrapper to display custom inner content, access children in the component parameters. For example, function Card({ children }) means anything placed between <Card> tags becomes available inside that component. For more flexibility, you can pass another component or JSX through props such as header, footer, or icon. This avoids duplication while keeping the API clear for beginners and teams.
Comprehensive Code Examples
function Header() {
return <h1>My App</h1>;
}
function Footer() {
return <p>Copyright 2026</p>;
}
function App() {
return (
<div>
<Header />
<Footer />
</div>
);
}function Card({ children }) {
return <div className="card">{children}</div>;
}
function Profile() {
return (
<Card>
<h2>Ava Patel</h2>
<p>Frontend Developer</p>
</Card>
);
}function Modal({ title, footer, children }) {
return (
<div className="modal">
<h2>{title}</h2>
<div>{children}</div>
<div className="modal-footer">{footer}</div>
</div>
);
}
function App() {
return (
<Modal
title="Delete Item"
footer={<button>Confirm</button>}
>
<p>Are you sure you want to delete this file?</p>
</Modal>
);
}Common Mistakes
- Making components too large: Split UI into smaller parts when a component handles unrelated jobs.
- Forgetting
children: If nested content does not appear, ensure the wrapper renders{children}. - Passing too many props: If a component has a long prop list, consider composing smaller subcomponents instead.
- Mixing layout and business logic everywhere: Keep presentational wrappers focused on structure and appearance.
Best Practices
- Design components around a single responsibility.
- Use
childrenfor flexible wrappers like cards, panels, and modals. - Prefer clear component APIs such as
title,footer, andchildren. - Compose behavior gradually instead of building monolithic components.
- Reuse layout components across pages for consistency.
Practice Exercises
- Create a
Panelcomponent that renders any nested content usingchildren. - Build a
PageLayoutcomponent that composes a header, main content area, and footer. - Make a reusable
AlertBoxthat accepts a custom message and a custom action button through composition.
Mini Project / Task
Build a simple dashboard page composed of reusable components: Navbar, Sidebar, Card, and StatsPanel. Use children so each card can display different content.
Challenge (Optional)
Create a reusable Modal component that supports a title, body content through children, and an optional custom footer passed as a prop, then use it in two different ways without changing the modal code.
React Router Setup
React Router is a crucial library for building single-page applications (SPAs) with React. In traditional multi-page applications, the browser requests a new HTML page from the server for every navigation. SPAs, however, load a single HTML page and dynamically update its content as the user interacts, providing a smoother, more app-like experience. React Router enables this by synchronizing the UI with the URL, allowing you to define different components to render based on the current path in the browser's address bar. It handles routing client-side, without requiring a full page reload, and provides features like nested routes, URL parameters, and programmatic navigation. This is fundamental for almost any non-trivial React application, from e-commerce sites to social media platforms, where different views need to be accessible via distinct URLs.
The core concept behind React Router is declarative routing. Instead of imperatively telling the application to navigate, you declare what components should render for specific paths. It achieves this through several key components: BrowserRouter (or HashRouter for older browsers/static sites), Routes, Route, and navigation components like Link and NavLink. BrowserRouter uses the HTML5 history API to keep your UI in sync with the URL. Routes acts as a container for individual Route components, and it intelligently picks the best match among its children based on the current URL. Each Route component specifies a path and the element (the React component) to render when that path matches. Link and NavLink are used to create navigation links within your application, preventing full page reloads.
Step-by-Step Explanation
Setting up React Router involves a few straightforward steps. First, you need to install the library. Then, you wrap your application's main component with a router, define your routes, and create navigation links.
1. Installation: Open your terminal in your React project directory and run:
npm install react-router-dom or yarn add react-router-dom.2. Wrap your App: In your
src/index.js (or equivalent entry file), import BrowserRouter and wrap your root component with it. This makes routing context available throughout your application.import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);3. Define Routes: In your
App.js (or a dedicated Router.js file), import Routes and Route. Use as a parent for all your components. Each needs a path prop (the URL segment) and an element prop (the component to render).import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';
function App() {
return (
} />
} />
} />
);
}
export default App;4. Create Navigation Links: Use the
component to navigate between routes. It takes a to prop, specifying the target path.import { Link } from 'react-router-dom';
function Navbar() {
return (
);
}
export default Navbar;Comprehensive Code Examples
Basic example
This example sets up a simple navigation bar and three basic pages.
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// App.js
import { Routes, Route, Link } from 'react-router-dom';
const Home = () => Welcome to the Home Page!
;
const About = () => Learn About Us!
;
const Services = () => Our Services
;
const NotFound = () => 404 - Page Not Found
;
function App() {
return (
} />
} />
} />
} /> {/* Catch-all route for 404 */}
);
}
export default App;Real-world example (Nested Routes and URL Parameters)
This demonstrates a simple blog application with a list of posts and individual post pages.
// index.js (Same as basic example)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// App.js
import { Routes, Route, Link, useParams } from 'react-router-dom';
const posts = [
{ id: '1', title: 'First Post', content: 'This is the content of the first post.' },
{ id: '2', title: 'Second Post', content: 'This is the content of the second post.' },
{ id: '3', title: 'Third Post', content: 'This is the content of the third post.' },
];
const Home = () => Welcome to the Blog!
;
const PostList = () => (
All Posts
{posts.map(post => (
{post.title}
))}
);
const PostDetail = () => {
const { postId } = useParams();
const post = posts.find(p => p.id === postId);
if (!post) {
return Post not found!
;
}
return (
{post.title}
{post.content}
Back to Posts
);
};
function App() {
return (
} />
} />
} />
{/* The :postId is a URL parameter that can be accessed via useParams() */}
);
}
export default App;Advanced usage (NavLink for active links)
This example uses
NavLink to automatically apply an 'active' class to the currently active link.// App.js (assuming index.js is set up with BrowserRouter)
import { Routes, Route, NavLink } from 'react-router-dom';
import './App.css'; // For active link styling
const Home = () => Welcome Home!
;
const Dashboard = () => User Dashboard
;
const Settings = () => Application Settings
;
function App() {
return (
} />
} />
} />
);
}
export default App;
// App.css
nav ul {
list-style: none;
padding: 0;
}
nav li {
display: inline-block;
margin-right: 15px;
}
nav a {
text-decoration: none;
color: #333;
font-weight: normal;
}
nav a.active {
color: blue;
font-weight: bold;
border-bottom: 2px solid blue;
}Common Mistakes
- Forgetting
or: Your entire routing setup must be wrapped within a router component for React Router to function. Without it, you'll see errors like "useRoutes() may be used only in the context of a."component
Fix: Ensure your rootcomponent, or at least the part of your application that uses routing, is a child of(usually inindex.js). - Using
instead of: Using standard HTMLtags for internal navigation will cause a full page refresh, defeating the purpose of a single-page application.
Fix: Always use theorcomponents provided by React Router for navigating between routes within your application. - Incorrect
pathmatching or ordering: Especially with dynamic segments (e.g.,/users/:id) or catch-all routes (*), the order ofcomponents withinmatters. If a more general route comes before a more specific one, the general route might match unexpectedly.
Fix: Place more specific routes above more general ones. For instance,/users/newshould come before/users/:id. The*(catch-all for 404) route should always be the last one.
Best Practices
- Centralize Your Routes: For larger applications, consider creating a dedicated file (e.g.,
src/routes.js) to define all your application's routes. This improves maintainability and readability. - Use
NavLinkfor Navigation Menus:NavLinkis specifically designed for navigation links that need to indicate when they are active. ItsclassNameprop (orstyle) accepts a function that gives you anisActiveboolean, allowing you to easily apply active styles. - Implement a 404 Page: Always include a catch-all route (
) as the last route within your} /> component. This provides a user-friendly experience when a user navigates to an invalid URL. - Programmatic Navigation with
useNavigate: When you need to navigate based on an event (e.g., after a form submission or clicking a button that isn't aLink), use theuseNavigatehook.const navigate = useNavigate(); navigate('/dashboard'); - Nested Routes for Layouts: Utilize nested routes for complex layouts where parts of the UI remain consistent while sub-sections change. For example, a user dashboard might have
/dashboard/profileand/dashboard/settings, sharing theDashboardcomponent's layout.
Practice Exercises
- Simple Page Navigation: Create a React application with three pages: Home, About, and Contact. Set up React Router to navigate between these pages using
components in a navigation bar. - Blog Post Viewer: Extend the previous application to include a 'Blog' section. The blog should have a main page listing several post titles. When a user clicks on a title, it should navigate to a dedicated page for that post, using a URL parameter (e.g.,
/blog/1). - Active Link Styling: Modify your navigation bar to use
and apply a distinct CSS style (e.g., background color or bold text) to the link corresponding to the currently active route.
Mini Project / Task
Build a simple e-commerce website structure using React Router. It should have the following routes and components:
/: Home Page (shows a welcome message)/products: Products List Page (displays a list of dummy products)/products/:productId: Product Detail Page (shows details for a specific product, usingproductIdfrom the URL)/cart: Shopping Cart Page- A navigation bar with links to Home, Products, and Cart.
- A 404 Not Found page for any unmatched routes.
Challenge (Optional)
Enhance the e-commerce mini-project by implementing nested routing for the
/products section. The /products route should render a ProductsLayout component that contains a sidebar with product categories (e.g., 'Electronics', 'Books', 'Clothing'). Clicking a category should update the URL to /products/categoryName (e.g., /products/electronics) and display only products belonging to that category within the main content area of the ProductsLayout. Routes and Links
Routes and links are fundamental concepts in building Single Page Applications (SPAs) with React. In a traditional multi-page application, navigating between different sections of a website involves the browser making a full page refresh request to the server. This can lead to a slower, less fluid user experience. SPAs, on the other hand, load a single HTML page and dynamically update its content as the user interacts with the application. Routes define which components should be rendered based on the URL in the browser's address bar, while links provide the interactive elements (like buttons or navigation menus) that allow users to trigger these route changes without a full page reload.
The primary library for handling routing in React is React Router. It enables declarative routing, meaning you define your routes as part of your component tree. This approach makes it easy to understand how different parts of your application are connected and how navigation flows. React Router is crucial for creating applications that feel fast and responsive, providing a superior user experience by mimicking traditional website navigation within a single page.
React Router is used in virtually every moderately complex React application that requires navigation between different views or pages. From e-commerce sites with product listings and detail pages to dashboards with various data visualizations, routing is essential for organizing content and providing a clear user journey. Without it, a React app would essentially be a single, static view, severely limiting its utility.
The core concepts revolve around mapping URLs to components and providing mechanisms to navigate between these URLs.
BrowserRouter: This is the recommended router for web applications. It uses the HTML5 history API (pushState, replaceState, and the popstate event) to keep your UI in sync with the URL. It's typically wrapped around your entire application.
Routes: The `Routes` component (formerly `Switch` in older versions) renders the first `Route` that matches the current URL. This ensures that only one route is active at a time.
Route: The `Route` component is used to define a mapping between a URL path and a component. It takes a `path` prop (the URL to match) and an `element` prop (the component to render).
Link: The `Link` component is used for navigation within the application. It renders an accessible anchor tag with a real `href` but prevents the browser's default behavior of reloading the page. Instead, it uses React Router to update the URL and render the corresponding component.
NavLink: Similar to `Link`, but it automatically applies an active class to the link when its route is active, which is useful for styling navigation menus.
useNavigate: A hook that allows programmatic navigation. You can call `navigate('/some-path')` to change the URL and render a different component, often used after form submissions or button clicks.
useParams: A hook that allows you to access URL parameters (e.g., `id` from `/products/:id`).
Outlet: Used in nested routes to render the child route elements.
Step-by-Step Explanation
To get started with React Router, you first need to install it:
npm install react-router-dom
# or
yarn add react-router-domOnce installed, you'll typically wrap your entire application with `BrowserRouter` in your `index.js` or `App.js` file. This makes routing context available to all components within your app.
Next, you'll define your routes using the `Routes` and `Route` components. The `Routes` component acts as a container for all your `Route` definitions. Each `Route` specifies a `path` (the URL segment) and an `element` (the component to render when that path is matched).
For navigation, you'll use the `Link` component. Instead of standard HTML `About`, you'll use `About`. The `to` prop specifies the destination URL. This prevents a full page reload and allows React Router to handle the view change.
For programmatic navigation, the `useNavigate` hook is essential. You call it inside a functional component to get a `navigate` function, which you can then call with a path string to redirect the user.
Comprehensive Code Examples
Basic Example: Setting up a simple navigation
First, create three simple components: `Home`, `About`, and `Contact`.
// src/components/Home.js
function Home() {
return Welcome to the Home Page!
;
}
export default Home;
// src/components/About.js
function About() {
return Learn more About Us.
;
}
export default About;
// src/components/Contact.js
function Contact() {
return Contact us here.
;
}
export default Contact;Now, set up the routing in your `App.js`:
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
function App() {
return (
} />
} />
} />
);
}
export default App;Real-world Example: Product detail page with dynamic routing
Let's create a `ProductList` and `ProductDetail` component.
// src/components/ProductList.js
import React from 'react';
import { Link } from 'react-router-dom';
const products = [
{ id: '1', name: 'Laptop' },
{ id: '2', name: 'Mouse' },
{ id: '3', name: 'Keyboard' },
];
function ProductList() {
return (
Products
{products.map(product => (
{product.name}
))}
);
}
export default ProductList;
// src/components/ProductDetail.js
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
const allProducts = [
{ id: '1', name: 'Laptop', description: 'Powerful computing machine.' },
{ id: '2', name: 'Mouse', description: 'Ergonomic wireless mouse.' },
{ id: '3', name: 'Keyboard', description: 'Mechanical gaming keyboard.' },
];
function ProductDetail() {
const { productId } = useParams();
const navigate = useNavigate();
const product = allProducts.find(p => p.id === productId);
if (!product) {
return (
Product Not Found
);
}
return (
{product.name}
ID: {product.id}
Description: {product.description}
);
}
export default ProductDetail;Update `App.js` to include these routes:
// src/App.js (partial update)
import ProductList from './components/ProductList';
import ProductDetail from './components/ProductDetail';
// ... inside App component return statement
{/* ... existing routes */}
} />
} />
// ...Advanced Usage: Nested Routes and NavLink for active styling
Let's create a dashboard with nested sections.
// src/components/Dashboard.js
import React from 'react';
import { NavLink, Outlet } from 'react-router-dom';
function Dashboard() {
return (
Dashboard
);
}
export default Dashboard;
// src/components/Profile.js
function Profile() {
return User Profile Content
;
}
export default Profile;
// src/components/Settings.js
function Settings() {
return Application Settings Content
;
}
export default Settings;Update `App.js` with nested routes:
// src/App.js (partial update)
import Dashboard from './components/Dashboard';
import Profile from './components/Profile';
import Settings from './components/Settings';
// ... inside App component return statement
{/* ... existing routes */}
}>
} /> {/* Relative path */}
} /> {/* Relative path */}
Please select a dashboard section} /> {/* Default child route */}
// ...Common Mistakes
- Using `` instead of ``: A common mistake for beginners. Using standard anchor tags will cause a full page reload, defeating the purpose of an SPA and React Router. Always use `` for internal navigation.
- Forgetting `
` wrapper : All React Router components (`Routes`, `Route`, `Link`, `useNavigate`, etc.) must be rendered within a `BrowserRouter` (or another router like `HashRouter`). If you forget this, your application will throw errors about not finding a router context. - Incorrectly configuring `Routes` and `Route`: Placing `Route` components directly outside `Routes` or using `component` prop instead of `element` (in React Router v6) are common errors. Remember `Routes` acts as a container, and `Route` uses the `element` prop. Also, ensure your paths are correctly specified, especially for nested routes (relative vs. absolute).
Best Practices
- Keep `BrowserRouter` at the top level: Wrap your entire application in `
` (usually in `index.js` or `App.js`) to ensure all components have access to routing functionalities. - Use `NavLink` for navigation menus: `NavLink` automatically adds an `active` class (or allows custom styling with a render prop) to the link that matches the current URL, making it easy to highlight the active page in your navigation bar.
- Organize your routes: For larger applications, consider creating a separate `routes.js` file or a dedicated routing component to keep your `App.js` clean and manageable.
- Handle 404 (Not Found) routes: Always include a catch-all `Route` at the end of your `Routes` list (e.g., `
} />`) to gracefully handle invalid URLs. - Utilize nested routes for complex layouts: When parts of your UI change together (e.g., a dashboard with sub-sections), nested routes (`
}> } /> `) along with `Outlet` in the parent component, are highly effective for maintaining clean code and efficient rendering.
Practice Exercises
- Exercise 1 (Basic Navigation): Create a new React application. Implement three pages: Home, Products, and Services. Add a navigation bar with `Link` components to navigate between these pages.
- Exercise 2 (Dynamic Product Display): Extend the Products page from Exercise 1. Display a list of 5 dummy products. Each product name should be a `Link` that, when clicked, navigates to a `ProductDetail` page (e.g., `/products/1`, `/products/2`). On the `ProductDetail` page, use `useParams` to display the product ID.
- Exercise 3 (Admin Dashboard with Nested Routes): Create an `/admin` route. Inside the `AdminDashboard` component, set up nested routes for `/admin/users` and `/admin/reports`. Use `NavLink` for navigation within the dashboard and display a different component for each sub-route.
Mini Project / Task
Build a simple blog application. It should have:
- A Home page.
- A 'Posts' page that lists several blog post titles. Each title should be a link.
- A 'Post Detail' page that displays the full content of a specific post when its title is clicked. Use dynamic routing (e.g., `/posts/:postId`) and `useParams` to fetch and display the correct post content.
- A 'Create New Post' page with a form. After submitting the form, use `useNavigate` to redirect the user back to the 'Posts' list.
Challenge (Optional)
Enhance the blog application. Implement protected routes. Create a simple authentication (`isLoggedIn` boolean in state/context). Only allow access to the 'Create New Post' page and a new '/admin' page if `isLoggedIn` is true. If not logged in, redirect the user to a '/login' page. You'll need to use `Outlet` and potentially a custom `ProtectedRoute` component.
Dynamic Routes
Dynamic routes let a React application display different content based on values inside the URL, such as a product ID, username, blog slug, or category name. Instead of creating a separate route for every possible item, you define a route pattern like /products/:id and React Router matches the changing part at runtime. This exists because real applications often have thousands of records, and manually writing routes like /products/1, /products/2, and so on is impossible to maintain.
In real life, dynamic routes are used in e-commerce product pages, social profile pages, article pages, learning platforms, dashboards, and admin panels. The main idea is simple: the path contains a variable segment, and your component reads that value to fetch or display the correct data. Common forms include route parameters such as :id, slugs like :postSlug, nested dynamic routes such as /users/:userId/orders/:orderId, and optional patterns handled through route structure and component logic.
Step-by-Step Explanation
First, define a route with a placeholder segment. In React Router, a colon marks a dynamic parameter, for example :id. Second, create the component that should render when the route matches. Third, use the useParams() hook to read the current route values. Fourth, use that value to find data locally or request it from an API. Finally, render loading, error, or not-found states when needed.
Syntax breakdown: <Route path="/products/:id" element={<ProductPage />} /> means any URL starting with /products/ followed by one segment will open ProductPage. Inside that component, const { id } = useParams() extracts the route value. If the URL is /products/42, then id becomes "42".
Comprehensive Code Examples
import { BrowserRouter, Routes, Route, useParams } from "react-router-dom";
function ProductPage() {
const { id } = useParams();
return Product ID: {id}
;
}
export default function App() {
return (
} />
);
}import { useParams } from "react-router-dom";
const posts = [
{ slug: "react-basics", title: "React Basics" },
{ slug: "dynamic-routes", title: "Dynamic Routes" }
];
function BlogPost() {
const { slug } = useParams();
const post = posts.find((item) => item.slug === slug);
if (!post) return Post not found.
;
return {post.title}
;
}import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
function UserOrderPage() {
const { userId, orderId } = useParams();
const [order, setOrder] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}/orders/${orderId}`)
.then((res) => res.json())
.then((data) => setOrder(data));
}, [userId, orderId]);
if (!order) return Loading order...
;
return Order #{order.id} for User {userId}
;
}Common Mistakes
- Forgetting the colon in the route path: writing
/products/idcreates a fixed path. Use/products/:id. - Expecting numbers from
useParams(): route params are strings. Convert when needed withNumber(id). - Ignoring missing data: if the param does not match a record, show a not-found message instead of crashing.
- Not updating effects: when fetching with params, include them in the dependency array.
Best Practices
- Use meaningful parameter names like
:productIdor:username. - Validate route values before using them in API calls or rendering logic.
- Provide loading, empty, and error states for better user experience.
- Prefer slugs for readable public URLs and IDs for internal lookups when appropriate.
- Keep route structure consistent across the application.
Practice Exercises
- Create a route
/users/:usernameand display the username on the page. - Build a small article page using
/articles/:slugand load article data from a local array. - Create a nested route like
/teams/:teamId/members/:memberIdand print both values.
Mini Project / Task
Build a product catalog where a list page links to /products/:id, and each product detail page shows the selected product name, price, and description from a local dataset.
Challenge (Optional)
Create a blog system that supports both category and post routes, such as /blog/:category/:slug, and display a custom not-found message when either value is invalid.
Nested Routes
Nested routes in React are a routing pattern where one route lives inside another route. They exist because many real applications have layouts and pages that naturally contain sub-pages. For example, a dashboard may have a sidebar and top navigation that stay visible while the main content changes between Overview, Settings, and Billing. Instead of rebuilding the whole page for every path, nested routes let you keep a parent layout mounted and swap only the child content. This improves code organization, user experience, and reuse. In React Router, nested routes are commonly used for dashboards, admin panels, user profiles, e-commerce account areas, and documentation sites.
The main idea is simple: define a parent route, place child routes inside it, and render an Outlet inside the parent component. That Outlet acts like a placeholder where the matching child route appears. Nested routing also supports index routes, which are default child pages shown when the parent path matches exactly. You can also create dynamic nested routes, such as /users/:id/posts, for data-driven screens.
Step-by-Step Explanation
First, create a parent layout component. This component usually contains shared UI like headers, menus, or sidebars. Inside that component, import and render Outlet from React Router. Second, define routes using createBrowserRouter or Routes and place child route objects or elements inside the parent route. Third, use relative child paths like settings instead of writing the full URL every time. Fourth, add an index route if you want a default child screen. Finally, navigate between nested pages using Link or NavLink.
A nested routing structure usually has these parts: parent route, child routes, index route, dynamic child route, and the Outlet placeholder. If the parent component forgets to render Outlet, child pages will never appear even when the URL matches correctly.
Comprehensive Code Examples
Basic example
import { BrowserRouter, Routes, Route, Link, Outlet } from "react-router-dom";
function Dashboard() {
return (
Dashboard
);
}
function Overview() { return Overview page
; }
function Settings() { return Settings page
; }
export default function App() {
return (
}>
} />
} />
);
}Real-world example
function AccountLayout() {
return (
My Account
);
}
}>
} />
} />
} />
Advanced usage
function UsersLayout() {
return (
Users
);
}
function UserDetails() {
const { id } = useParams();
return Viewing user {id}
;
}
}>
} />
} />
Common Mistakes
- Forgetting
Outlet: Child routes match but nothing renders. Addinside the parent layout. - Using absolute child paths unnecessarily: Writing full paths can make route trees harder to maintain. Prefer relative child paths like
settings. - Missing an index route: The parent page looks empty when visiting the base path. Add
indexfor a default child view.
Best Practices
- Create layout components for sections that share navigation or page structure.
- Use
NavLinkfor active menu styling in nested navigation. - Keep route definitions grouped by feature, such as account, admin, or dashboard.
- Use dynamic nested routes for entity-specific pages like users, products, or orders.
Practice Exercises
- Create a
/dashboardroute with child routes foroverviewandreports. - Add an index route to a parent layout so a default page renders at the parent URL.
- Build a nested
/products/:idroute and add a child route forreviews.
Mini Project / Task
Build a small admin panel with a shared sidebar and nested pages for Home, Users, and Settings. The sidebar must stay visible while only the content area changes.
Challenge (Optional)
Create a nested documentation app where /docs shows a default intro page, and child routes like /docs/hooks and /docs/router load inside the same layout with active navigation links.
Protected Routes
Protected routes are routes that only allow access when a user meets a condition, usually being authenticated. In real applications, they are used for dashboards, account pages, admin panels, checkout flows, and any screen that should not be visible to anonymous users. Without route protection, a user could manually type a URL such as /dashboard and reach a page that should be restricted. In React, protected routes are commonly implemented with React Router and some form of authentication state, such as a context value, Redux store, or token stored in memory or browser storage.
The core idea is simple: before rendering a page component, check whether the user is allowed to enter. If yes, render the page. If not, redirect the user to a safer route like /login. Some applications also have role-based protection, where a normal logged-in user can access one route, but only an admin can access another. So protected routes often appear in two forms: authentication-based protection and authorization-based protection. Authentication checks who the user is, while authorization checks what the user is allowed to do.
Step-by-Step Explanation
Start by creating routes with React Router. Then create a wrapper component, often called ProtectedRoute. This component reads authentication state and decides what to render. If the user is authenticated, it returns the protected content. If not, it returns a redirect using Navigate.
A common beginner pattern is to pass protected page content as children. Another approach is to use nested routes with Outlet. The nested approach is cleaner when several pages share the same protection rule. You may also preserve the previous location so that after login the user returns to the page they originally wanted to visit.
Comprehensive Code Examples
Basic example
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
function ProtectedRoute({ isAuthenticated, children }) {
return isAuthenticated ? children : ;
}
function App() {
const isAuthenticated = true;
return (
Login Page} />
path="/dashboard"
element={
Dashboard
}
/>
);
} Real-world example
import { createContext, useContext, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState({ name: 'Ava' });
return {children} ;
}
function ProtectedRoute({ children }) {
const { user } = useContext(AuthContext);
const location = useLocation();
if (!user) {
return ;
}
return children;
} Advanced usage
import { Outlet, Navigate } from 'react-router-dom';
function RoleProtectedRoute({ user, allowedRoles }) {
if (!user) return ;
if (!allowedRoles.includes(user.role)) return ;
return ;
}
// Example route idea:
// }>
// } />
// Common Mistakes
- Protecting only the UI, not the route: Hiding a link is not enough. Always guard the actual route component too.
- Using local storage as the only source of truth: It can help persist state, but validation should come from trusted auth logic or the server.
- Forgetting loading states: If auth status is still being checked, users may be redirected too early. Show a loading screen first.
- Ignoring roles: Logged-in users are not always equal. Add role checks when needed.
Best Practices
- Keep authentication state in context or a central store for easy access across routes.
- Use reusable wrapper components like
ProtectedRouteandRoleProtectedRoute. - Redirect users back to their intended page after successful login.
- Combine frontend route protection with backend authorization for real security.
- Create clear pages for
/loginand/unauthorizedto improve user experience.
Practice Exercises
- Create a
ProtectedRoutecomponent that redirects unauthenticated users from/profileto/login. - Add a loading state so the route waits before deciding whether to render or redirect.
- Build an admin-only route that allows access to
/adminonly when the user role isadmin.
Mini Project / Task
Build a small React app with three pages: Home, Login, and Dashboard. Make Dashboard accessible only to logged-in users. After login, redirect the user back to Dashboard automatically.
Challenge (Optional)
Extend your protected routing system to support both authentication and multiple roles such as user, editor, and admin, with different access rules for separate pages.
Fetching Data
Fetching data in React means requesting information from an external source such as a REST API, backend server, or third-party service, then displaying that information inside components. This exists because most real applications do not rely only on hardcoded values. A shopping app loads products, a weather app loads forecasts, and a project dashboard loads tasks from a server. In React, data fetching is commonly handled after a component renders, often with the useEffect hook and browser APIs like fetch(). The main idea is simple: start with an initial state, request data asynchronously, update state when the response arrives, and reflect loading or error conditions in the UI.
A beginner should understand several related concepts. First, asynchronous code means the app continues running while waiting for a server response. Second, state stores values such as fetched results, loading flags, and error messages. Third, side effects are actions that interact with the outside world, which is why data fetching belongs in useEffect instead of directly in the component body. Common sub-types include one-time fetch on mount, fetching when a value changes such as a search term or ID, parallel fetching of multiple resources, and conditional fetching based on user interaction. React apps may also use libraries like Axios, React Query, or SWR, but understanding native fetch() first builds strong fundamentals.
Step-by-Step Explanation
Start by creating state variables with useState for the data, loading status, and possible error. Next, use useEffect so the request runs at the correct time. Inside the effect, call fetch(url), convert the response with response.json(), and save the result into state. Before the request starts, set loading to true. If the request succeeds, store the data and clear any old error. If it fails, save an error message. Finally, set loading to false so the UI can stop showing a loading indicator. If your request depends on a prop or state value, place that value in the dependency array so the effect reruns when the value changes.
Comprehensive Code Examples
import { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
})
.then((data) => setUsers(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return Loading...
;
if (error) return Error: {error}
;
return (
{users.map((user) => (
- {user.name}
))}
);
}import { useEffect, useState } from 'react';
function ProductSearch() {
const [query, setQuery] = useState('phone');
const [products, setProducts] = useState([]);
useEffect(() => {
async function loadProducts() {
const response = await fetch(`/api/products?search=${query}`);
const data = await response.json();
setProducts(data);
}
loadProducts();
}, [query]);
return (
Found {products.length} products for {query}
);
}import { useEffect, useState } from 'react';
function Dashboard() {
const [data, setData] = useState({ users: [], posts: [] });
useEffect(() => {
async function loadDashboard() {
const [usersRes, postsRes] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts')
]);
const [users, posts] = await Promise.all([
usersRes.json(),
postsRes.json()
]);
setData({ users, posts });
}
loadDashboard();
}, []);
return Users: {data.users.length} | Posts: {data.posts.length}
;
}Common Mistakes
- Fetching inside the component body: this causes repeated requests on every render. Put it inside
useEffect. - Ignoring loading state: users see empty content and think the app is broken. Show a loading message or spinner.
- Not handling errors: failed requests can crash logic or confuse users. Use try/catch or
.catch()and display feedback. - Missing dependencies: if the fetch depends on a value, add it to the dependency array.
Best Practices
- Keep separate state for data, loading, and error.
- Check
response.okbefore parsing JSON. - Use async functions inside
useEffectfor readability. - Extract fetch logic into reusable custom hooks when multiple components need it.
- Avoid unnecessary refetching by choosing dependencies carefully.
Practice Exercises
- Create a component that fetches and displays a list of post titles from a public API.
- Add loading and error states to an existing fetch example.
- Build a user details component that refetches data when a user ID changes.
Mini Project / Task
Build a simple React page that fetches products from an API and displays product names, prices, a loading message, and an error message if the request fails.
Challenge (Optional)
Create a search-driven component that fetches filtered results whenever the search term changes, while keeping the UI responsive and avoiding unnecessary requests.
Axios and HTTP Requests
Axios is a popular JavaScript library used to send HTTP requests from React applications to servers, APIs, and backend services. In real-world apps, React rarely works alone. It often needs data such as user profiles, products, blog posts, analytics, or form submission results. HTTP requests are the bridge between the frontend and the backend. Axios exists to make this communication easier, cleaner, and more consistent than using low-level browser tools directly. It supports common request methods such as GET, POST, PUT, PATCH, and DELETE. In React, Axios is commonly used inside useEffect for loading data when a component mounts, or inside event handlers when a user submits a form. Important ideas include request methods, response objects, asynchronous code with async/await, error handling, headers, query parameters, and reusable Axios instances. A GET request fetches data, POST sends new data, PUT or PATCH updates existing data, and DELETE removes data. Axios returns a promise, so React developers must handle loading, success, and failure states carefully to give users a smooth experience.
Step-by-Step Explanation
First, install Axios with npm install axios. Next, import it into a component using import axios from 'axios'. To fetch data when a page loads, create state values for data, loading, and error. Then use useEffect and call an async function that awaits axios.get(url). The response object usually contains useful data in response.data. For sending data, use axios.post(url, payload). To include custom headers such as tokens, pass a third configuration object like { headers: { Authorization: 'Bearer token' } }. Query parameters can be added with params, which keeps URLs cleaner and easier to maintain. In React, always update UI state after the request finishes so users can see loading spinners, success messages, or errors. This pattern makes components predictable and easy to debug.
Comprehensive Code Examples
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
setUsers(response.data);
} catch (err) {
setError('Failed to load users');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return Loading...;
if (error) return {error};
return (
{users.map(user => (
- {user.name}
))}
);
}
export default Users;import React, { useState } from 'react';
import axios from 'axios';
function AddPost() {
const [title, setTitle] = useState('');
const [message, setMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await axios.post('https://jsonplaceholder.typicode.com/posts', { title });
setMessage('Post created successfully');
setTitle('');
} catch (error) {
setMessage('Failed to create post');
}
};
return (
setTitle(e.target.value)} />
{message}
);
}
export default AddPost;import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
headers: {
Authorization: 'Bearer my-token'
}
});
export const getProducts = () => api.get('/products', { params: { limit: 10 } });
export const deleteProduct = (id) => api.delete(`/products/${id}`);Common Mistakes
- Forgetting error handling: Always use
try/catchor.catch()so failed requests do not break the UI. - Using response incorrectly: Axios data is usually in
response.data, not directly inresponse. - Triggering infinite requests: Be careful with
useEffectdependencies or the component may fetch repeatedly. - Ignoring loading state: Users need visual feedback while data is being fetched.
Best Practices
- Create a reusable Axios instance for shared base URLs, headers, and configuration.
- Store loading, success, and error states separately for clearer UI behavior.
- Keep API logic in service files when applications grow larger.
- Use
async/awaitfor readable asynchronous code. - Validate and sanitize user input before sending requests.
Practice Exercises
- Create a React component that fetches and displays a list of posts from a public API.
- Build a form that sends a
POSTrequest and shows a success or error message. - Make a component with loading, error, and success states for a
GETrequest.
Mini Project / Task
Build a small user directory that fetches users from an API, displays them in a list, and includes a form to add a new user with Axios.
Challenge (Optional)
Create a reusable API module with an Axios instance and use it across multiple React components for fetching, creating, and deleting resources.
Loading and Error States
Loading and error states are essential parts of any React application that fetches data, submits forms, or waits for asynchronous work to finish. In real applications, data does not arrive instantly. A product list may take a second to load, a profile request may fail because of a network issue, or a form submission may return a server error. Without proper handling, users see empty screens, broken layouts, or confusing behavior. Loading states communicate that work is in progress, while error states explain when something went wrong and often guide the user toward recovery.
In React, these states are usually managed with component state such as isLoading, error, and the final data value. A common pattern is to track three phases: loading, success, and failure. Some apps also include empty states, such as when a request succeeds but returns no results. These patterns appear everywhere: dashboards, search pages, checkout flows, chat apps, and admin panels.
Step-by-Step Explanation
A beginner-friendly way to handle async UI in React is to create state variables. First, store the returned data. Second, create a loading flag set to true before the request starts. Third, store an error message or object if the request fails. In a useEffect, start the fetch, clear old errors, and update the UI when the request completes. Then use conditional rendering to show different content.
Typical flow:
1. Set loading to true.
2. Clear previous error.
3. Start async request.
4. If successful, save data.
5. If failed, save error.
6. Set loading to false.
You can render states in order of priority. If loading is true, show a spinner or loading text. If there is an error, show an error message and possibly a retry button. If there is data, render it. If the data array is empty, show a friendly empty state. This approach keeps the interface predictable and easy to maintain.
Comprehensive Code Examples
Basic example
import { useEffect, useState } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
})
.then((data) => setUsers(data))
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return Loading users...
;
if (error) return Error: {error}
;
return (
{users.map((user) => (
- {user.name}
))}
);
}Real-world example
import { useEffect, useState } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const loadProducts = async () => {
try {
setIsLoading(true);
setError('');
const response = await fetch('/api/products');
if (!response.ok) throw new Error('Could not load products');
const data = await response.json();
setProducts(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadProducts();
}, []);
if (isLoading) return Loading catalog...
;
if (error) return {error}
;
if (products.length === 0) return No products found.
;
return (
{products.map((product) => (
- {product.name} - ${product.price}
))}
);
}Advanced usage
import { useEffect, useState } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [status, setStatus] = useState('idle');
const [error, setError] = useState('');
useEffect(() => {
if (!query) return;
const controller = new AbortController();
const fetchResults = async () => {
try {
setStatus('loading');
setError('');
const response = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
if (!response.ok) throw new Error('Search failed');
const data = await response.json();
setResults(data);
setStatus('success');
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setStatus('error');
}
}
};
fetchResults();
return () => controller.abort();
}, [query]);
if (status === 'loading') return Searching...
;
if (status === 'error') return {error}
;
return {results.map((item) => - {item.title}
)}
;
}Common Mistakes
- Not checking
response.ok: fetch does not throw for HTTP errors automatically. Manually throw an error when the response is not successful. - Leaving loading stuck on true: always reset loading in a
finallyblock or after both success and failure paths. - Rendering data before it exists: guard against
null, empty arrays, or undefined values before mapping or reading properties. - Ignoring request cleanup: when inputs change quickly, use
AbortControllerto prevent outdated responses from updating the UI.
Best Practices
- Use clear, user-friendly loading text or skeleton screens instead of blank space.
- Show actionable error messages and provide a retry option when possible.
- Separate loading, error, empty, and success states for predictable rendering.
- Use a status string like
idle,loading,success, anderrorfor more complex components. - Keep state updates grouped and readable to make async logic easier to debug.
Practice Exercises
- Build a component that fetches a list of posts and shows a loading message before the data appears.
- Add error handling to a user profile component and display an error message if the request fails.
- Create an empty state for a todo list that says there are no tasks when the returned array is empty.
Mini Project / Task
Build a small weather search component that shows a loading message during the request, displays weather data on success, and shows a retryable error message when the API call fails.
Challenge (Optional)
Create a searchable React component that cancels previous requests when the user types quickly and correctly handles loading, error, success, and empty results states.
Context API
Context API is React’s built-in feature for sharing data across many components without passing props manually through every level of the component tree. This problem is often called prop drilling. For example, a logged-in user, theme mode, language setting, or shopping cart summary may be needed by deeply nested components. Instead of sending the same value through parent, child, grandchild, and deeper components, Context lets you place shared data in a central provider and read it wherever needed inside that provider tree.
In real applications, Context API is commonly used for theme switching, authentication state, app settings, notification systems, and small-to-medium global state needs. It is not always a replacement for all state management solutions, but it is excellent when multiple related components need access to the same value. The main parts are createContext, Provider, and consuming the context with useContext or a Consumer component. A context object stores a shared value. The Provider wraps a portion of your app and supplies that value. Any child component inside the Provider can access it directly.
There are two common ways to consume context. The modern approach is useContext(), which is simpler and used in function components. The older approach is Context.Consumer, which still works but is less common today. Context becomes even more useful when paired with state, because the Provider can expose both data and update functions. For instance, a theme context can share the current theme and a function to toggle between light and dark mode.
Step-by-Step Explanation
First, create a context using createContext(). Second, build a Provider component that stores shared state. Third, wrap the relevant part of your app with that Provider. Fourth, consume the context inside child components using useContext().
Syntax flow: define the context, provide a value prop on the Provider, then read that value from descendants. If the value changes, React re-renders consumers that use that context. A good beginner rule is: use context for data needed in many places, but keep local state local when only one component needs it.
Comprehensive Code Examples
Basic example
import React, { createContext, useContext } from 'react';
const MessageContext = createContext();
function App() {
return (
);
}
function Child() {
const message = useContext(MessageContext);
return {message}
;
}Real-world example
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => (t === 'light' ? 'dark' : 'light'));
return (
{children}
);
}
function Toolbar() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
Current theme: {theme}
);
}
export default function App() {
return (
);
}Advanced usage
import React, { createContext, useContext, useMemo, useState } from 'react';
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = name => setUser({ name });
const logout = () => setUser(null);
const value = useMemo(() => ({ user, login, logout }), [user]);
return {children} ;
}
function Profile() {
const { user, login, logout } = useContext(AuthContext);
return user ? (
Welcome, {user.name}
) : (
);
}Common Mistakes
- Using context for every piece of state. Fix: keep component-specific state local.
- Forgetting to wrap components with the Provider. Fix: place the Provider above all consumers.
- Passing a new object value on every render unnecessarily. Fix: memoize complex values with
useMemo()when needed. - Creating one huge context for unrelated data. Fix: split contexts by responsibility, such as AuthContext and ThemeContext.
Best Practices
- Use context for shared cross-component data, not all application state.
- Create custom hooks like
useTheme()for cleaner consumption. - Keep Provider logic focused and easy to test.
- Split contexts to reduce unnecessary re-renders and improve code clarity.
Practice Exercises
- Create a LanguageContext that stores the current language and displays it in two nested components.
- Build a ThemeContext with a button that switches between light and dark mode.
- Create a UserContext that stores a username and shows it in a profile component without prop drilling.
Mini Project / Task
Build a small dashboard with an AuthContext and ThemeContext. Show a login button, user greeting, theme status, and a toggle button that changes the shared theme for all nested components.
Challenge (Optional)
Create separate contexts for user settings and notifications, then design the component tree so only the components that need each context consume it efficiently.
Redux Introduction
Redux is a state management library used to store and control shared application data in one predictable place. In React, local component state works well for isolated features, but as applications grow, passing data through many layers of components becomes difficult. Redux was created to solve this problem by introducing a central store, strict update rules, and a clear data flow. It is commonly used in dashboards, e-commerce apps, authentication systems, analytics tools, and any interface where many components need access to the same data.
At its core, Redux follows a few important ideas. First, the entire shared state is kept inside a single store object. Second, state cannot be changed directly. Instead, you describe what happened by dispatching an action, which is a plain JavaScript object with a type. Third, reducers are functions that receive the current state and an action, then return a new state. This makes updates predictable, testable, and easier to debug. In modern React development, Redux is usually used through Redux Toolkit, which reduces boilerplate and provides simpler patterns for creating slices, reducers, and async logic.
There are also practical sub-types or building blocks you should understand. Store holds the global state. Actions describe events such as adding an item to a cart. Reducers decide how state changes in response to actions. Dispatch sends actions to the store. Selectors read specific values from state. In modern Redux Toolkit, slices group state, reducers, and generated actions together. You may also hear about middleware, which handles side effects like API calls and logging.
Step-by-Step Explanation
To use Redux in React, you typically install @reduxjs/toolkit and react-redux. First, create a slice with an initial state and reducer functions. Redux Toolkit automatically creates action creators for those reducers. Next, configure a store and register your slice reducer. Then, wrap your React app in the Provider component so every child can access the store. Inside components, use useSelector to read values and useDispatch to send actions.
The beginner-friendly flow is: user clicks a button, component dispatches an action, reducer updates state, React re-renders components using that state. This one-way data flow is the main reason Redux stays organized in larger applications.
Comprehensive Code Examples
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; }
}
});
export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });import React from 'react';
import { Provider, useDispatch, useSelector } from 'react-redux';
import { store, increment, decrement } from './store';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
Count: {count}
);
}
export default function App() {
return (
);
}const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});The first example shows the basic store and slice setup. The second shows React integration using hooks. The third reflects a real-world cart feature where many components may need shared access to cart data.
Common Mistakes
- Mutating state outside reducers: Always update Redux state through dispatched actions, not by editing values directly in components.
- Forgetting Provider: If your app is not wrapped with
Provider, Redux hooks will not work. - Using Redux for everything: Small local UI state like modal visibility often belongs in component state, not the global store.
- Writing overly large slices: Split unrelated features into separate slices for better structure.
Best Practices
- Use Redux Toolkit instead of older manual Redux patterns.
- Keep state minimal and store only shared, important data.
- Use selectors to read state cleanly and consistently.
- Name actions clearly, such as
addItemorlogoutUser. - Organize by feature, placing slice files near related components and logic.
Practice Exercises
- Create a Redux counter with increment and decrement buttons.
- Build a theme slice with
lightanddarkmodes and display the current theme in a component. - Create a todo slice that adds and removes tasks using Redux state.
Mini Project / Task
Build a small shopping cart state manager with Redux. Add products, remove products, and show the total number of items from different components.
Challenge (Optional)
Create a Redux-powered notes app with separate slices for notes and filters, then display only notes matching the selected filter.
Redux Toolkit
Redux Toolkit is the official, modern way to write Redux logic in React applications. Redux itself was created to solve a common frontend problem: when many components need to share and update the same data, passing props through many levels becomes hard to manage. Redux Toolkit simplifies this by giving you a central store, safer state updates, and built-in patterns for common tasks like async requests. In real applications, it is used for carts in e-commerce apps, user authentication, theme settings, dashboards, notifications, and API-driven data. Redux Toolkit reduces the older Redux boilerplate by providing configureStore, createSlice, and createAsyncThunk.
The main concepts are simple. A store holds global state. A slice is a focused part of that state, such as auth or cart. Reducers describe how state changes. Actions trigger those changes. Thunks handle async logic like fetching data from an API. Redux Toolkit also uses Immer internally, so you can write update logic that looks like mutation while staying immutable under the hood.
Step-by-Step Explanation
First, install the required packages: @reduxjs/toolkit and react-redux. Next, create a slice with a name, initial state, and reducers. Then create the store using configureStore and register your slice reducer. Wrap your React app with Provider so components can access the store. Inside components, use useSelector to read data and useDispatch to send actions. For API calls, create a thunk with createAsyncThunk and handle loading, success, and error states in extraReducers.
Comprehensive Code Examples
Basic example
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; }
}
});
export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });import { Provider, useDispatch, useSelector } from 'react-redux';
function Counter() {
const value = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (<>
Count: {value}
);
}Real-world example
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addToCart: (state, action) => { state.items.push(action.payload); },
removeFromCart: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});Advanced usage
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
return await res.json();
});
const usersSlice = createSlice({
name: 'users',
initialState: { list: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => { state.loading = true; })
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false; state.list = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false; state.error = action.error.message;
});
}
});Common Mistakes
Forgetting to wrap the app with
Provider. Fix: wrap the root component and pass the store.Mutating state outside reducers. Fix: only update Redux state through slice reducers or thunks.
Using wrong selector paths. Fix: match
useSelectorwith the reducer key used inconfigureStore.
Best Practices
Create one slice per feature, such as auth, cart, or users.
Keep state minimal and derive computed values in selectors.
Use thunks for async work and track loading and error states clearly.
Export actions and reducer from the same slice file for organization.
Practice Exercises
Create a Redux Toolkit counter with increment, decrement, and reset actions.
Build a theme slice that switches between light and dark mode.
Create a users slice that stores loading, data, and error for an API request.
Mini Project / Task
Build a shopping cart feature using Redux Toolkit where users can add items, remove items, and see the total number of products in the cart.
Challenge (Optional)
Create a task manager with two slices: one for tasks and one for filters. Add async loading for initial tasks and display only completed or pending tasks based on the selected filter.
React Query
React Query, now commonly known as TanStack Query, is a library for managing server state in React applications. Server state means data that comes from an external source such as a REST API, GraphQL endpoint, or backend service. In many apps, developers need to fetch data, show loading indicators, handle errors, cache responses, refresh stale data, and update the UI after creating or editing records. Doing all of that manually with useEffect and useState often leads to repetitive code and bugs. React Query exists to solve this problem by giving React developers a powerful and structured way to fetch, cache, synchronize, and update remote data.
In real projects, React Query is used in dashboards, admin panels, e-commerce product lists, user profiles, analytics screens, and any interface that depends on backend data. Its most important ideas include queries for reading data, mutations for changing data, query keys for identifying cached data, and invalidation for refetching outdated information. It also supports loading states, background refetching, retries, pagination, dependent queries, and optimistic updates. This makes applications feel faster and easier to maintain.
The main sub-types you should understand are queries and mutations. A query is used when you want to read data, such as fetching posts or users. A mutation is used when you want to create, update, or delete data, such as submitting a new todo item. Query keys are usually arrays like ['todos'] or ['user', userId] so React Query can cache data correctly. You also work with a QueryClient and QueryClientProvider to enable React Query across your app.
Step-by-Step Explanation
First, install the library and create a QueryClient. Second, wrap your React app in QueryClientProvider. Third, use useQuery to fetch data. It accepts an object with a unique queryKey and a queryFn that returns a promise. The hook gives you values such as data, isLoading, isError, and error. For writing data, use useMutation. After a successful mutation, you often call queryClient.invalidateQueries() so related data is refreshed.
Beginners should think of React Query as a smart data manager. You tell it what data you want and how to get it, and it handles caching and synchronization for you. This reduces boilerplate and improves performance.
Comprehensive Code Examples
Basic example
import React from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const queryClient = new QueryClient();
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
};
function Posts() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
if (isLoading) return Loading...
;
if (isError) return {error.message}
;
return (
{data.slice(0, 5).map(post => (
- {post.title}
))}
);
}
export default function App() {
return (
);
}Real-world example
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
enabled: !!userId
});
if (isLoading) return Loading user...
;
return Welcome, {data.name}
;
}Advanced usage
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
function Todos() {
const queryClient = useQueryClient();
const { data = [] } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos');
return res.json();
}
});
const addTodo = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo)
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
return (
{data.map(todo => - {todo.title}
)}
);
}Common Mistakes
- Forgetting the provider: If you do not wrap the app with
QueryClientProvider, hooks will not work. Always configure it at the root. - Using weak query keys: A vague key like
['data']causes confusion. Use descriptive keys such as['products', categoryId]. - Not handling errors: Beginners often show only loading and success states. Always check
isErrorand display a useful message. - Skipping invalidation: After adding or updating data, the UI may stay stale unless you invalidate or update the cache.
Best Practices
- Keep API logic in reusable functions instead of writing fetch code everywhere.
- Use stable, descriptive query keys to organize cache entries clearly.
- Use
enabledfor dependent queries that should wait for required values. - Prefer React Query for server state and React local state for UI-only values such as modals or form input.
- Configure stale times carefully to avoid unnecessary requests.
Practice Exercises
- Create a component that fetches and displays a list of 10 users with
useQuery. - Build a product details component that uses a dynamic query key based on product ID.
- Create a form that adds a new item using
useMutationand refreshes the list after success.
Mini Project / Task
Build a small task manager that fetches tasks from an API, displays loading and error states, and lets the user add a new task with a mutation that refreshes the task list.
Challenge (Optional)
Create a paginated posts viewer where each page is cached separately, and the interface keeps previous data visible while the next page loads.
Styling in React
Styling in React is the process of controlling how components look, including colors, spacing, layout, typography, hover states, and responsive behavior. React itself focuses on building UI structure and logic, so styling is added using standard web technologies such as CSS, inline style objects, CSS Modules, and external styling libraries. This exists because applications need both behavior and presentation: a button must not only work, it must also look clickable, consistent, and accessible. In real projects, styling in React is used everywhere, from product cards and navigation bars to forms, dashboards, and theme-based interfaces.
There are several common ways to style React components. Traditional CSS files are simple and familiar: you create a CSS file, import it into a component, and use class names with the className attribute. Inline styles use JavaScript objects directly inside JSX, which is useful for dynamic values such as width, color, or conditional spacing. CSS Modules solve naming conflicts by scoping class names locally to a component. Developers also often combine conditional classes with JavaScript expressions to apply different visual states such as active, disabled, or error. Understanding when to use each approach is important for maintainability.
Step-by-Step Explanation
In React, you use className instead of HTML's class because JSX follows JavaScript naming conventions. For external CSS, first create a file like Button.css. Then import it using import './Button.css'. After that, assign the class with className="button".
For inline styling, pass a JavaScript object to the style prop. CSS property names become camelCase, so background-color becomes backgroundColor. Values can be strings or numbers depending on the property. For example, padding: 12 means 12 pixels.
For CSS Modules, create a file such as Card.module.css. Import it as an object, commonly named styles. Then apply classes with className={styles.card}. This prevents clashes with similarly named classes in other components.
Dynamic styling is also common. You can switch classes using conditions such as isActive ? 'tab active' : 'tab'. This makes it easy to reflect application state visually.
Comprehensive Code Examples
// Basic example: external CSS
import './App.css';
export default function App() {
return <h1 className="title">Hello React Styling</h1>;
}/* App.css */
.title {
color: #61dafb;
text-align: center;
}// Real-world example: inline dynamic styles
export default function StatusBadge({ online }) {
const badgeStyle = {
backgroundColor: online ? 'green' : 'gray',
color: 'white',
padding: '8px 12px',
borderRadius: '20px'
};
return <span style={badgeStyle}>{online ? 'Online' : 'Offline'}</span>;
}// Advanced example: CSS Module with conditional class
import styles from './Card.module.css';
export default function ProductCard({ featured, name, price }) {
return (
<div className={`${styles.card} ${featured ? styles.featured : ''}`}>
<h3>{name}</h3>
<p>${price}</p>
</div>
);
}/* Card.module.css */
.card {
padding: 16px;
border: 1px solid #ddd;
border-radius: 10px;
}
.featured {
border-color: orange;
box-shadow: 0 0 10px rgba(255,165,0,0.3);
}Common Mistakes
- Using
classinstead ofclassName. Fix: always useclassNamein JSX. - Writing inline CSS property names with dashes. Fix: use camelCase like
fontSizeandbackgroundColor. - Forgetting to import the CSS file or CSS Module. Fix: import the style file at the top of the component.
- Mixing global CSS carelessly. Fix: prefer scoped naming or CSS Modules for reusable components.
Best Practices
- Use external CSS or CSS Modules for larger components instead of overusing inline styles.
- Use inline styles mainly for values that change dynamically at runtime.
- Keep class names meaningful, such as
card,buttonPrimary, orerrorText. - Organize styles close to components to improve maintenance.
- Design with accessibility in mind, including contrast, focus states, and readable spacing.
Practice Exercises
- Create a heading component and style it with an imported CSS file using custom color, alignment, and font size.
- Build a button component that changes background color using inline styles based on a
primaryprop. - Create a card component with CSS Modules and add a special bordered style when a
featuredprop is true.
Mini Project / Task
Build a small product list interface with three product cards. Use external CSS for layout, inline styles for dynamic stock indicators, and CSS Modules for each card's isolated design.
Challenge (Optional)
Create a theme switcher component that toggles between light and dark mode by applying different class names and a few dynamic inline style values for smooth visual feedback.
Performance Optimization
Performance optimization in React is the process of making applications render faster, use fewer resources, and stay responsive as the UI grows. React already provides efficient DOM updates through its reconciliation system, but real-world apps can still become slow when components re-render too often, heavy calculations run on every update, large lists appear on screen, or bundles grow too large. You will see this topic used in dashboards, ecommerce sites, admin panels, social feeds, and any application where users expect smooth interactions. The goal is not to optimize everything immediately, but to measure bottlenecks and improve the parts that matter most.
The main areas of React performance optimization include render optimization, computation optimization, list optimization, and delivery optimization. Render optimization reduces unnecessary component updates using tools such as React.memo, useMemo, and useCallback. Computation optimization avoids repeating expensive operations during renders. List optimization focuses on efficient handling of large collections through stable keys, pagination, and virtualization. Delivery optimization improves how code reaches the browser by splitting bundles and loading components lazily. A strong rule is this: first identify a real problem with React DevTools Profiler or browser performance tools, then apply the smallest useful fix.
Step-by-Step Explanation
Start by understanding why a component renders. A React component renders when its state changes, its parent renders, or its props change. If a parent re-renders, child components may also render even when visible output does not change. To reduce this, wrap pure child components with React.memo. This tells React to skip rendering when props are unchanged.
Next, use useMemo for expensive calculated values. If sorting, filtering, or transforming data is slow, compute it once and recompute only when dependencies change. Use useCallback when passing functions to memoized children so the function reference stays stable between renders.
For long lists, always provide stable keys such as database IDs, not array indexes when order can change. If the list is very large, render only visible rows with virtualization libraries like react-window. Finally, reduce initial load time with lazy loading. React.lazy and Suspense can load feature-heavy pages only when needed.
Comprehensive Code Examples
Basic example
import React, { useState, memo } from 'react';
const CounterDisplay = memo(function CounterDisplay({ count }) {
console.log('CounterDisplay rendered');
return Count: {count}
;
});
export default function App() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState('light');
return (
<>
);
}Real-world example
import React, { useMemo, useState } from 'react';
export default function ProductSearch({ products }) {
const [query, setQuery] = useState('');
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase())
);
}, [products, query]);
return (
<>
setQuery(e.target.value)} placeholder='Search products' />
{filteredProducts.map(product => (
- {product.name}
))}
);
}Advanced usage
import React, { lazy, Suspense, useCallback, useState } from 'react';
const ReportsPage = lazy(() => import('./ReportsPage'));
function Toolbar({ onRefresh }) {
return ;
}
const MemoToolbar = React.memo(Toolbar);
export default function Dashboard() {
const [showReports, setShowReports] = useState(false);
const handleRefresh = useCallback(() => {
console.log('Refreshing...');
}, []);
return (
<>
{showReports && (
Loading reports...}>
)}
);
}Common Mistakes
- Optimizing too early: Developers add memoization everywhere without measuring. Fix: profile first, then optimize real bottlenecks.
- Using unstable keys: Array indexes can cause incorrect re-renders in changing lists. Fix: use unique stable IDs.
- Overusing useCallback and useMemo: These hooks also have overhead. Fix: use them only when they prevent expensive work or help memoized children.
Best Practices
- Use React DevTools Profiler before and after each optimization.
- Keep state as local as possible to limit large render chains.
- Split large components into focused smaller components.
- Memoize only pure components with stable props.
- Lazy-load heavy routes, charts, editors, and admin features.
Practice Exercises
- Create a parent component with two buttons and a child display. Prevent the child from re-rendering when unrelated state changes.
- Build a searchable user list and optimize the filtering logic with
useMemo. - Create a component that passes a callback to a memoized child and stabilize the callback with
useCallback.
Mini Project / Task
Build a product dashboard with a search box, category filter, and a lazily loaded analytics panel. Optimize repeated filtering and prevent unnecessary child renders.
Challenge (Optional)
Create a very large activity feed and design a strategy to keep scrolling smooth using stable keys, memoized row items, and list virtualization.
Final Project
The final project in a React course is the culmination of all the knowledge and skills you've acquired throughout your learning journey. It's an opportunity to apply theoretical concepts to a practical, real-world scenario, transforming abstract ideas into a functional application. The primary purpose of a final project is to solidify your understanding of React's core principles, such as component-based architecture, state management, routing, and interacting with APIs. It exists to provide a tangible demonstration of your proficiency, serving as a powerful portfolio piece that showcases your abilities to potential employers or collaborators. In real life, projects like these are the backbone of modern web development. Every single-page application, interactive dashboard, or dynamic user interface you encounter on the web likely uses principles and technologies similar to what you'll employ in your final project. From social media platforms to e-commerce sites and data visualization tools, React projects are everywhere, making this skill highly valuable in the industry.
While there aren't 'types' of final projects in the same way there are types of loops, projects can be categorized by their complexity and the features they emphasize. For instance, a project might be highly focused on data visualization, requiring strong command of state management and potentially integrating charting libraries. Another might focus on user authentication and routing, demanding careful consideration of security and navigation flows. A third could be an e-commerce application, integrating a shopping cart, product listings, and payment gateways. The common thread is the application of React's component model and lifecycle methods to build a cohesive user experience.
Step-by-Step Explanation
Building a final project typically follows a structured approach. First, define your project idea and its core features. What problem will it solve? What functionalities will it offer? Sketch out a basic UI/UX flow. Next, set up your React project using Create React App (CRA) or Vite. This provides a boilerplate with essential configurations. Then, break down your application into smaller, manageable components. Think about reusability and separation of concerns. For example, a navigation bar, a product card, or a user profile will each be distinct components. Implement routing using `react-router-dom` to navigate between different views or pages of your application. Manage application state, either locally within components using `useState` and `useReducer`, or globally using Context API or Redux for more complex applications. Integrate with a backend API (real or mocked) to fetch and send data, using `useEffect` for data fetching. Finally, focus on styling, error handling, and deploying your application.Comprehensive Code Examples
Basic example: Initializing a React project and creating a simple component
npx create-react-app my-final-project
cd my-final-project
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return Hello, {name}! Welcome to your project!
;
}
export default Greeting;
// src/App.js
import React from 'react';
import Greeting from './components/Greeting';
function App() {
return (
);
}
export default App;Real-world example: Fetching data from an API and displaying it
// src/components/DataFetcher.js
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(json => {
setData(json);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, []);
if (loading) return Loading data...;
if (error) return Error: {error.message};
return (
Fetched Posts
{data.map(post => (
{post.title}
{post.body}
))}
);
}
export default DataFetcher;Advanced usage: Implementing routing with `react-router-dom`
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
// src/pages/Home.js
function Home() { return Welcome Home!
; }
// src/pages/About.js
function About() { return About Us Page
; }
// src/pages/Contact.js
function Contact() { return Contact Us Page
; }
function App() {
return (
} />
} />
} />
);
}
export default App;Common Mistakes
- Not breaking down the project into smaller components: Beginners often try to build a monolithic `App` component, leading to unmanageable code. Fix: Spend time planning your component hierarchy before coding.
- Ignoring state management best practices: Over-relying on local component state for global data or prop drilling excessively. Fix: Use Context API or Redux for global state; lift state up only when necessary for direct parent-child communication.
- Lack of error handling: Not handling network errors or unexpected API responses. Fix: Always include `try...catch` blocks with `async/await` or `.catch()` with Promises when fetching data, and display user-friendly error messages.
Best Practices
- Plan before you code: Sketch out your UI, define components, and consider data flow. A little planning saves a lot of refactoring.
- Modularize your code: Keep components small, focused, and reusable. Separate concerns (e.g., data fetching logic from rendering logic).
- Meaningful naming conventions: Use clear and consistent names for components, props, and state variables.
- Use functional components and Hooks: Embrace modern React practices. Hooks simplify stateful logic and side effects in functional components.
- Version control (Git): Commit frequently with descriptive messages. This is crucial for tracking changes and collaborating.
- Testing: Even for a final project, consider basic unit or integration tests for critical components.
Practice Exercises
- Create a simple task list application where users can add, delete, and mark tasks as complete. Use `useState` to manage the list of tasks.
- Build a basic weather app that fetches weather data for a hardcoded city from a public API (e.g., OpenWeatherMap – remember to get an API key) and displays it.
- Design a simple navigation bar with three links (`Home`, `Dashboard`, `Settings`) using `react-router-dom`. Each link should render a different component displaying its name.
Mini Project / Task
Build a simple blog application. It should have a home page displaying a list of blog posts. Each post should have a title and a short description. When a user clicks on a post title, it should navigate to a dedicated 'Post Detail' page showing the full content of that post. You can mock the blog post data as a JavaScript array initially, and then consider fetching it from an API.
Challenge (Optional)
Enhance the blog application. Implement the ability to add new blog posts (with title and content) via a form. Store these new posts in your application's state. Additionally, add a feature to delete existing posts. For an extra challenge, try to persist these changes using local storage so they don't disappear on page refresh.