React.js foundation
Chapter 1: Introduction to React & Setup
This chapter introduces the fundamental ideas behind React and guides you through setting up your first React project.
1.1 What is React?
React is a JavaScript Library: First and foremost, React is a JavaScript library for building user interfaces (UIs). It’s not a full-blown framework like Angular, meaning it focuses primarily on the view layer of your application. You often combine React with other libraries for things like routing or global state management.
Declarative UI: React allows you to write UI in a declarative way. This means you tell React what the UI should look like based on the current data (state), and React takes care of updating the actual browser DOM (Document Object Model) efficiently.
Contrast with Imperative: Traditionally, you might directly manipulate the DOM using JavaScript (e.g., document.getElementById(‘myElement’).innerHTML = ‘New Text’;). This is imperative — you give step-by-step instructions.
React’s Approach: With React, you define components that describe a piece of the UI. When the data associated with that component changes, React automatically re-renders that part of the UI to reflect the new state. You don’t manually change the DOM elements.
Example (Conceptual):
// Imperative Vanilla JS (Manual DOM manipulation)
let isLoggedIn = false;
const loginButton = document.getElementById('loginButton');
const userGreeting = document.getElementById('userGreeting');
function updateUI() {
if (isLoggedIn) {
loginButton.style.display = 'none';
userGreeting.textContent = 'Welcome back!';
userGreeting.style.display = 'block';
} else {
loginButton.style.display = 'block';
userGreeting.style.display = 'none';
}
}
loginButton.addEventListener('click', () => {
isLoggedIn = true;
updateUI();
});
updateUI(); // Initial render
// Declarative React (Conceptual - actual syntax comes later)
function MyAuthComponent() {
const [isLoggedIn, setIsLoggedIn] = useState(false); // React state management
if (isLoggedIn) {
return <p>Welcome back!</p>; // Describe UI when logged in
} else {
return <button onClick={() => setIsLoggedIn(true)}>Log In</button>; // Describe UI when logged out
}
// React handles updating the actual DOM based on isLoggedIn changing
}
- (Don’t worry about the useState or the <p>/<button> syntax yet — we’ll cover those in detail. Just grasp the idea of describing the desired output.)
Component-Based Architecture: React encourages building UIs by breaking them down into smaller, reusable pieces called components. Think of them like Lego bricks. You build individual bricks (components like Button, UserProfile, NavigationBar) and then assemble them to create complex UIs. This makes your code more modular, easier to understand, and maintainable.
- (Imagine a web page: It might have a Header component, a Sidebar component, a Feed component, and each of those might be composed of even smaller components like Logo, NavigationLink, Post, etc.)
1.2 Why use React?
Virtual DOM (VDOM) & Performance: React uses a concept called the Virtual DOM.
- When your application’s data changes, React first creates a lightweight representation of the UI in memory — the Virtual DOM.
- It then compares this new VDOM with the previous VDOM snapshot. This comparison process is called “diffing”.
- React calculates the minimal set of changes required to make the actual browser DOM match the new VDOM.
- It then updates only those specific parts of the real DOM.
Directly manipulating the real DOM can be slow, especially for complex UIs. The VDOM acts as an intermediary, allowing React to batch updates and minimize direct interaction with the browser DOM, often leading to significant performance improvements.
Reusability: Components can be easily reused throughout your application or even in different projects. A Button component, once created, can be used anywhere you need a button, ensuring consistency and saving development time.
Strong Community & Ecosystem: React has a large, active community and a vast ecosystem of libraries, tools, and resources available. This makes finding solutions, libraries for specific tasks (like routing or state management), and learning materials much easier.
1.3 Setting up a React Development Environment
To build and run React applications locally, you need a few tools:
Node.js: React development tools are built using Node.js, which is a JavaScript runtime environment that lets you run JavaScript code outside of a web browser. You don’t typically write server-side Node.js code when just learning React frontend, but you need Node.js installed to use the tools.
npm (Node Package Manager) or yarn: These are package managers that come bundled with Node.js (npm) or can be installed separately (yarn). They are used to install and manage project dependencies (like the React library itself, build tools, etc.).
Check Installation: Open your terminal or command prompt and type:
node -v
npm -v
- If you don’t have them installed, download Node.js (which includes npm) from https://nodejs.org/. We’ll use npm for the commands here, but yarn commands are usually very similar.
Using Vite (Recommended Toolchain):
- Why Vite? Vite is a modern frontend build tool that significantly improves the development experience. It offers near-instant server start-up and lightning-fast Hot Module Replacement (HMR), meaning changes in your code reflect in the browser almost immediately without losing application state. It’s generally much faster than the older Create React App (CRA) during development.
- Create a Project: Open your terminal in the directory where you want to create your project and run:
# Using npm
npm create vite@latest my-react-app --template react
# Or using yarn
# yarn create vite my-react-app --template react
- Replace my-react-app with your desired project name.
- — template react tells Vite to set up a standard React project using JavaScript.
Navigate & Install Dependencies:
cd my-react-app
npm install # Or: yarn install
- This downloads React and other necessary libraries into the node_modules folder.
Start the Development Server:
npm run dev # Or: yarn dev
- This command starts the Vite development server. It will usually print a local URL (like http://localhost:5173) in the terminal. Open this URL in your web browser. You should see the default React starter page!
- (Alternative) Create React App (CRA): You might see older tutorials using CRA. While still functional, it’s generally slower for development than Vite. The command is: npx create-react-app my-react-app. We will proceed using Vite for our examples.
- Project Structure Overview (Vite react template):
After creating the project and opening it in your code editor (like VS Code), you’ll see a structure like this:
my-react-app/
├── node_modules/ # Installed dependencies (managed by npm/yarn)
├── public/ # Static assets (images, fonts, etc.) that are copied directly
│ └── vite.svg # Example asset
├── src/ # Your application's source code!
│ ├── assets/ # Project-specific assets (often processed by build tool)
│ │ └── react.svg
│ ├── App.css # CSS specific to the App component
│ ├── App.jsx # The main application component (root component)
│ ├── index.css # Global CSS styles
│ └── main.jsx # The entry point of your React application
├── .eslintrc.cjs # ESLint configuration (code linting)
├── .gitignore # Files/folders to be ignored by Git
├── index.html # The main HTML file React injects into
├── package.json # Project metadata, dependencies, scripts
├── package-lock.json # Records exact dependency versions
└── vite.config.js # Vite build tool configuration
- index.html: The template HTML file. Your React app will be mounted into the <div id=”root”></div> element within this file.
- src/main.jsx: This is the JavaScript entry point. It imports React, your main App component, and uses ReactDOM to render the App component into the div#root in index.html.
// Example content of src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx' // Import the root component
import './index.css' // Import global styles
// Get the root DOM node
const rootElement = document.getElementById('root');
// Create a React root and render the App component into it
ReactDOM.createRoot(rootElement).render(
<React.StrictMode> {/* Optional: Helps find potential problems */}
<App />
</React.StrictMode>,
)
- src/App.jsx: This is your first React component, often called the “root” or “app shell” component. It’s where you’ll start composing your UI.
// Example initial content of src/App.jsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0) // We'll learn useState later
return (
<> {/* This is a Fragment, we'll learn about it soon */}
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App // Make the component available for import
- (Again, don’t worry about understanding all the code inside App.jsx just yet. Focus on the structure and the idea that this file defines a piece of UI.)
- src/ directory: This is where you’ll spend most of your time, creating components, writing logic, and styling.
- public/ directory: For static files that don’t need processing (like favicon.ico or robots.txt).
Chapter 2: JSX — Writing UI in React
JSX stands for JavaScript XML. It’s a syntax extension recommended for use with React to describe what the UI should look like. While you can write React without JSX, it’s significantly more verbose and less intuitive. JSX allows you to write HTML-like structures directly within your JavaScript code.
2.1 What is JSX?
At first glance, JSX looks very similar to HTML:
// This is JSX
const element = <h1>Hello, world!</h1>;
However, it’s not HTML running inside JavaScript. It’s a syntactic sugar that gets transformed (compiled) into regular JavaScript function calls.
Why use it? React embraces the idea that rendering logic is inherently coupled with UI logic (how events are handled, how state changes over time, how data is prepared for display). JSX makes it easier to visualize the UI structure within the component logic itself, rather than separating markup (HTML) and logic (JS) into different files artificially.
2.2 Embedding JavaScript Expressions (curly braces {})
You can embed any valid JavaScript expression within JSX by wrapping it in curly braces {}.
Expressions vs. Statements: An expression is something that evaluates to a value (e.g., 2 + 2, user.name, myFunction()). A statement performs an action (e.g., if, for, variable assignment let x = 5). You can only embed expressions in JSX.
function UserGreeting(props) {
const userName = "Alice";
const userAge = 30;
const elementId = "main-heading";
// Use {} to embed JavaScript variables and expressions
return (
<div id={elementId}> {/* Embed variable for an attribute */}
<h1>Hello, {userName}!</h1> {/* Embed variable in content */}
<h2>You are {userAge} years old.</h2> {/* Embed variable */}
<p>Next year, you will be {userAge + 1}.</p> {/* Embed calculation */}
<p>Current time: {new Date().toLocaleTimeString()}</p> {/* Embed function call result */}
</div>
);
}
// Example usage (assuming you have a root element in your HTML)
// ReactDOM.createRoot(document.getElementById('root')).render(<UserGreeting />);
Things you cannot directly embed: if/else statements, for loops, variable declarations (let, const). You would typically perform these outside the JSX return block or use alternative methods like ternary operators or map() (which we’ll see later).
2.3 JSX Attributes
JSX uses attributes similar to HTML, but with some key differences, mainly due to JSX being closer to JavaScript than HTML:
camelCase: Since attributes like class and for are reserved keywords in JavaScript, JSX uses camelCase naming conventions for most HTML attributes:
- class becomes className
- for (in labels) becomes htmlFor
- tabindex becomes tabIndex
- onclick becomes onClick (Event handlers are also camelCased — more on this in Chapter 6)
- Other attributes like id, src, alt, href generally remain the same.
Attribute Values:
- String Literals: Use quotes “” for static string values:
const element = <img src="/logo.png" className="app-logo" />;
- JavaScript Expressions: Use curly braces {} to embed dynamic values (variables, function calls, etc.):
const logoUrl = "/logo.png";
const imgClass = "app-logo";
const element = <img src={logoUrl} className={imgClass} alt={"Company Logo"} />;
- Boolean Attributes: If you pass true, you can just include the attribute name. If you pass false, you should omit the attribute or explicitly pass {false}.
<button disabled={true}>Cannot Click</button>
// is equivalent to:
<button disabled>Cannot Click</button>
<button disabled={false}>Can Click</button>
// is usually rendered without the attribute: <button>Can Click</button>
2.4 JSX Represents Objects (Babel compilation overview)
Under the hood, JSX is converted into React.createElement() function calls by a compiler like Babel (which Vite uses). You don’t usually need to interact with this directly, but understanding it helps clarify what JSX is.
This JSX code:
const element = <h1 className="greeting">Hello, world!</h1>;
Is essentially compiled into this JavaScript:
const element = React.createElement(
'h1', // Type of element
{ className: 'greeting' }, // Props object (attributes)
'Hello, world!' // Children (content)
);
And this nested JSX:
const element = (
<div>
<h1>Welcome!</h1>
<p>Enjoy your stay.</p>
</div>
);
Becomes nested React.createElement calls:
const element = React.createElement(
'div',
null, // No props on the div itself
React.createElement('h1', null, 'Welcome!'),
React.createElement('p', null, 'Enjoy your stay.')
);
These React.createElement calls create plain JavaScript objects that React uses to describe the UI structure (part of the Virtual DOM). This is why you need import React from ‘react’ at the top of files using JSX, even if you don’t explicitly call React.createElement yourself (though modern React/tooling sometimes handles this automatically, it’s good practice).
2.5 Rendering Elements
As we saw in Chapter 1, you use ReactDOM to render your React elements (created with JSX) into the actual browser DOM.
// In your main.jsx (or similar entry point)
import React from 'react';
import ReactDOM from 'react-dom/client';
// Define a simple JSX element
const appElement = (
<div>
<h1>My React App</h1>
<p>This is rendered using ReactDOM.</p>
</div>
);
// Get the target DOM node (usually a div with id="root" in index.html)
const rootElement = document.getElementById('root');
// Create a React root and render the element
ReactDOM.createRoot(rootElement).render(appElement);
2.6 Fragments (<React.Fragment> or <>)
JSX expressions must have one single outer element. You cannot return multiple elements side-by-side like this:
// INVALID JSX: Adjacent elements must be wrapped in an enclosing tag
function MyComponent() {
return (
<h1>Title</h1>
<p>Paragraph</p>
);
}
Wrap with <div>: Not recommended
You can wrap your return statement with <div>
to return single outer element.
function MyComponent() {
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
}
Often, you don’t want to add an extra <div> just for the sake of wrapping, as it adds unnecessary nodes to the DOM. React provides Fragments for this purpose.
Long Syntax: React.Fragment
import React from 'react'; // React needs to be in scope
function MyComponent() {
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);
}
Short Syntax (Commonly Used): <> and </>
// No need to import React specifically for the Fragment syntax
function MyComponent() {
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
{/* You can even embed expressions inside fragments */}
{true && <span>Conditional Span</span>}
</>
);
}
Fragments let you group a list of children without adding extra nodes to the DOM, keeping your HTML structure cleaner.
Chapter 3: Components — The Building Blocks
3.1 Functional Components (Modern Standard)
What are they? In modern React, components are typically JavaScript functions. These functions accept an optional input object called props (short for properties — we’ll cover this in Chapter 4) and return React elements (usually written in JSX) that describe what should appear on the screen.
Why functional? They are generally more concise, easier to read and test, and work seamlessly with React Hooks (like useState, useEffect, covered later), which are the modern way to handle state and side effects.
3.2 Creating Your First Component
Let’s create a simple functional component. By convention, component names should always start with a capital letter.
// src/Greeting.jsx (Create a new file for this component)
import React from 'react'; // Still good practice, though sometimes implicit
// Define a simple functional component using an arrow function
const Greeting = () => {
// The component returns JSX to describe its UI
return <h1>Hello from the Greeting component!</h1>;
};
// Export the component so it can be used elsewhere
export default Greeting;content_copydownload
File Naming: It’s common practice to name the file the same as the component (Greeting.jsx). The .jsx extension clearly indicates that the file contains JSX syntax.
Arrow Function: We use ES6 arrow function syntax (() => { … }), which is common in modern JavaScript and React. You could also use the traditional function Greeting() { … } syntax.
Return JSX: The function must return something that React can render. This is typically a JSX structure describing the component’s output. If a component shouldn’t render anything, it can return null.
export default: This makes the Greeting component the primary export from this file, allowing other files to import it easily.
3.3 Composing Components (Parent-Child Relationship)
The real power of components comes from composing them — using components within other components.
Let’s modify our main App.jsx component to use the Greeting component we just created.
// src/App.jsx (Modify the existing file)
import React from 'react'; // Or import { useState } from 'react'; if needed
import Greeting from './Greeting.jsx'; // Import the Greeting component
import './App.css'; // Keep existing imports if needed
function App() {
// The App component now renders the Greeting component
return (
<div className="App"> {/* Keep a wrapping element */}
<h1>Welcome to My App</h1>
<Greeting /> {/* Use the imported component like an HTML tag */}
<Greeting /> {/* You can reuse components */}
<p>This shows component composition.</p>
</div>
);
}
export default App;
import Greeting from ‘./Greeting.jsx’;: We import the component we exported from Greeting.jsx. The ./ indicates it’s a relative path within the same src directory.
<Greeting />: We use the imported component just like a custom HTML tag. React knows that when it encounters <Greeting />, it should execute the Greeting function and render its returned JSX in this position.
Reusability: Notice we used <Greeting /> twice. This demonstrates the reusability aspect — define it once, use it multiple times.
Parent/Child: In this example, App is the parent component, and Greeting is the child component. Data typically flows down from parent to child via props (Chapter 4).
3.4 Exporting and Importing Components (ES6 Modules)
We’ve already seen export default and import. Let’s clarify the two main types of exports/imports:
Default Export/Import:
- A file can have only one default export.
- It’s used for the primary thing the module exports (like a component).
Exporting (in MyComponent.jsx):
const MyComponent = () => { /* ... */ };
export default MyComponent;
Importing (in App.jsx): You can name the import whatever you want, though convention is to use the component’s name.
import AnyNameIWant from './MyComponent.jsx';
// Usage: <AnyNameIWant />
Named Export/Import:
- A file can have multiple named exports.
- Used for exporting secondary helpers, constants, or multiple components from one file (less common for components, more for utility functions).
Exporting (in utils.js):
export const PI = 3.14159;
export const helpfulFunction = () => {
console.log("Helping!");
};
const AnotherComponent = () => <h2>Another!</h2>;
export { AnotherComponent }; // Can also export existing variables/functions
Importing (in App.jsx): You must use the exact exported names within curly braces {}.
import { PI, helpfulFunction, AnotherComponent } from './utils.js';
// Or import everything as an object:
// import * as Utils from './utils.js';
// Usage: Utils.PI, Utils.helpfulFunction()
function App() {
helpfulFunction();
return (
<div>
<p>Value of PI: {PI}</p>
<AnotherComponent />
</div>
);
}
When to use which? Generally, use export default for your main component per file. Use export (named exports) for utility functions, constants, or if you have a compelling reason to group multiple smaller, related components in one file (though separating components into their own files is often cleaner).
3.5 (Brief Mention) Class Components
Before React Hooks were introduced (in React 16.8), state and lifecycle features were only available in Class Components. You might encounter them in older codebases or tutorials.
import React from 'react';
// Class component equivalent of our simple Greeting
class GreetingClass extends React.Component {
render() {
// Must have a render() method that returns JSX
return <h1>Hello from the Greeting Class Component!</h1>;
}
}
export default GreetingClass;
// --- Example with state (just for illustration, we'll use Hooks later) ---
class CounterClass extends React.Component {
constructor(props) {
super(props); // Always call super(props) in constructor
this.state = { count: 0 }; // Initialize state in constructor
}
incrementCount = () => {
// Update state using this.setState
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
</div>
);
}
}
Key Differences:
- Extend React.Component.
- Need a render() method to return JSX.
- Use this.props to access props.
- Use this.state and this.setState() for state management (more complex than useState Hook).
- Have lifecycle methods like componentDidMount, componentDidUpdate (more complex than useEffect Hook).
Why Functional is Preferred Now: Functional components with Hooks are less verbose, avoid confusion around the this keyword, are generally easier to reason about, and encourage better code patterns. We will focus exclusively on Functional Components and Hooks going forward. Understanding that class components exist is mainly for reading older code.
Chapter 4: Props — Passing Data Down
Props (short for “properties”) are the mechanism React uses to pass data from a parent component down to its child components. Think of them like function arguments for your components. Props are read-only within the child component; a component cannot modify the props it receives directly.
4.1 What are Props?
Inputs: Props are arbitrary inputs that you pass to your custom components.
Data Flow: They enable the unidirectional data flow in React (data flows down from parent to child).
Read-Only: A child component should treat its props as immutable. It should not try to change them. If a component needs to react to user input or data changes, it will use state (Chapter 5).
4.2 Passing Props from Parent to Child
You pass props to a child component using attributes in the JSX, similar to how you’d pass attributes to standard HTML elements.
Let’s create a UserProfile component that expects some data, and then pass that data from the App component.
// src/UserProfile.jsx (New component file)
import React from 'react';
// This component expects 'name' and 'role' props
const UserProfile = (props) => {
console.log(props); // Log the props object to see what it contains
return (
<div style={{ border: '1px solid #eee', margin: '10px', padding: '10px' }}>
<h2>User Profile</h2>
<p>
<strong>Name:</strong> {props.name}
</p>
<p>
<strong>Role:</strong> {props.role}
</p>
</div>
);
};
export default UserProfile;
// --- Now, let's use it in App.jsx ---
// src/App.jsx (Modify the existing file)
import React from 'react';
import UserProfile from './UserProfile.jsx'; // Import the child component
import './App.css';
function App() {
const userData1 = {
id: 1,
username: 'Alice',
job: 'Developer',
};
const userData2 = {
id: 2,
username: 'Bob',
job: 'Designer',
};
return (
<div className="App">
<h1>Company Directory</h1>
{/* Pass data to UserProfile using attributes */}
{/* 'name' and 'role' here become keys in the 'props' object */}
<UserProfile name={userData1.username} role={userData1.job} />
{/* Pass data for a different user */}
<UserProfile name={userData2.username} role={userData2.job} />
{/* You can also pass hardcoded strings (use quotes) */}
<UserProfile name="Charlie" role="Manager" />
{/* You can pass other data types too (use curly braces) */}
<UserProfile name="Dana" role="Intern" yearsExperience={0.5} isActive={true} />
</div>
);
}
export default App;
In App.jsx:
- We define userData1 and userData2 objects (this could come from an API later).
- When we render <UserProfile … />, we add attributes like name=”Alice” or role={userData1.job}.
- The attribute name (name, role, yearsExperience, isActive) becomes the key in the props object.
- The attribute value (“Alice”, userData1.job, 0.5, true) becomes the corresponding value in the props object.
- Remember to use {} when passing non-string values (like variables, numbers, booleans, objects, arrays).
4.3 Accessing Props within a Component (props object)
Functional components receive props as the first argument to the function. By convention, this argument is named props. It’s an object containing all the properties passed down from the parent.
// src/UserProfile.jsx (Showing access again)
const UserProfile = (props) => {
// props is an object like:
// For the first instance: { name: 'Alice', role: 'Developer' }
// For the second instance: { name: 'Bob', role: 'Designer' }
// For the third instance: { name: 'Charlie', role: 'Manager' }
// For the fourth instance: { name: 'Dana', role: 'Intern', yearsExperience: 0.5, isActive: true }
return (
<div style={/* ... */}>
<h2>User Profile</h2>
{/* Access values using dot notation */}
<p><strong>Name:</strong> {props.name}</p>
<p><strong>Role:</strong> {props.role}</p>
{/* Conditionally render based on a prop */}
{props.isActive && <p>Status: Active</p>}
{/* Use other props */}
{props.yearsExperience !== undefined && <p>Experience: {props.yearsExperience} years</p>}
</div>
);
};
export default UserProfile;
4.4 Destructuring Props
Accessing props.name, props.role repeatedly can be verbose. JavaScript destructuring assignment syntax makes this cleaner. You can destructure the props object right in the function signature or at the beginning of the function body.
// src/UserProfile.jsx (Using Destructuring)
import React from 'react';
// Destructure 'name' and 'role' directly from the props object argument
const UserProfile = ({ name, role, yearsExperience, isActive }) => {
// Now you can use 'name' and 'role' directly as variables
return (
<div style={{ border: '1px solid #eee', margin: '10px', padding: '10px' }}>
<h2>User Profile</h2>
<p>
<strong>Name:</strong> {name}
</p>
<p>
<strong>Role:</strong> {role}
</p>
{isActive && <p>Status: Active</p>}
{yearsExperience !== undefined && <p>Experience: {yearsExperience} years</p>}
</div>
);
};
/*
// Alternative: Destructuring inside the function body
const UserProfile = (props) => {
const { name, role, yearsExperience, isActive } = props; // Destructure here
// ... rest of the component using name, role etc.
return ( ... );
}
*/
export default UserProfile;
Destructuring in the function signature is the most common and concise approach.
4.5 Default Props
Sometimes, you want a component to have default values for props if the parent doesn’t provide them.
// src/UserProfile.jsx (Adding Default Props)
import React from 'react';
const UserProfile = ({ name = "Guest", role = "Visitor", yearsExperience, isActive = false }) => {
// Default values assigned during destructuring if the prop is undefined
return (
<div style={{ border: '1px solid #eee', margin: '10px', padding: '10px' }}>
<h2>User Profile</h2>
<p>
<strong>Name:</strong> {name}
</p>
<p>
<strong>Role:</strong> {role}
</p>
{isActive && <p>Status: Active</p>}
{!isActive && <p>Status: Inactive</p>}
{yearsExperience !== undefined && <p>Experience: {yearsExperience} years</p>}
</div>
);
};
// --- Usage in App.jsx ---
// <UserProfile /> => Renders with Name: Guest, Role: Visitor, Status: Inactive
// <UserProfile name="Eve" /> => Renders with Name: Eve, Role: Visitor, Status: Inactive
// <UserProfile role="Admin" isActive={true} /> => Renders with Name: Guest, Role: Admin, Status: Active
export default UserProfile;
Using default parameter values in the destructuring assignment (name = “Guest”) is the modern way to set default props in functional components.
4.6 props.children
There’s a special prop called children. It contains whatever content is placed between the opening and closing tags of a component instance in the parent.
Let’s create a Card component that can wrap other content.
// src/Card.jsx (New component file)
import React from 'react';
// This component receives children via props
const Card = ({ children, title }) => {
// Destructuring 'children' from props
return (
<div style={{
border: '1px solid blue',
borderRadius: '8px',
padding: '15px',
margin: '10px',
boxShadow: '2px 2px 5px rgba(0,0,0,0.1)'
}}>
{title && <h3 style={{ marginTop: 0 }}>{title}</h3>} {/* Optional title */}
{children} {/* Render the content passed between the tags */}
</div>
);
};
export default Card;
// --- Usage in App.jsx ---
// src/App.jsx (Modify the existing file)
import React from 'react';
import UserProfile from './UserProfile.jsx';
import Card from './Card.jsx'; // Import Card
import './App.css';
function App() {
return (
<div className="App">
<h1>Using Cards</h1>
{/* Anything between <Card> and </Card> is passed as props.children */}
<Card title="User Info">
<UserProfile name="Frank" role="Engineer" isActive={true}/>
<p>This user profile is inside a Card component.</p>
</Card>
<Card title="Another Section">
<p>This is some plain text content inside another card.</p>
<button>Click Me</button>
</Card>
<Card> {/* Card without a title */}
<em>Content without a title prop.</em>
</Card>
</div>
);
}
export default App;
- The UserProfile component, the <p> tag, the <button>, and the <em> tag in the examples above are all passed into their respective Card components as props.children.
- The Card component simply renders {children} wherever it wants that nested content to appear.
- This is a powerful pattern for creating layout components, modals, panels, etc., that wrap arbitrary content.
4.7 Prop Types (Using prop-types library)
Since we are using plain JavaScript, we don’t have TypeScript’s compile-time type checking. However, React used to have built-in PropTypes, which are now available as a separate package (prop-types). They provide runtime type checking, primarily useful during development to catch bugs where incorrect data types are passed as props.
- Install the library:
npm install prop-types
2. Use it in your component:
// src/UserProfile.jsx (Adding PropTypes)
import React from 'react';
import PropTypes from 'prop-types'; // Import the library
const UserProfile = ({ name = "Guest", role = "Visitor", yearsExperience, isActive = false }) => {
// ... component logic remains the same ...
return (
<div style={{ border: '1px solid #eee', margin: '10px', padding: '10px' }}>
<h2>User Profile</h2>
<p><strong>Name:</strong> {name}</p>
<p><strong>Role:</strong> {role}</p>
{isActive && <p>Status: Active</p>}
{!isActive && <p>Status: Inactive</p>}
{yearsExperience !== undefined && <p>Experience: {yearsExperience} years</p>}
</div>
);
};
// Define the expected prop types after the component definition
UserProfile.propTypes = {
// Define type for each prop
name: PropTypes.string.isRequired, // 'name' must be a string and is required
role: PropTypes.string, // 'role' must be a string, but is optional
yearsExperience: PropTypes.number, // 'yearsExperience' must be a number
isActive: PropTypes.bool // 'isActive' must be a boolean
};
// You can also define default props here (alternative to default parameters)
// UserProfile.defaultProps = {
// name: "Guest",
// role: "Visitor",
// isActive: false
// };
// Note: Using default parameters in destructuring is generally preferred now.
export default UserProfile;
How it works: If you pass a prop with the wrong type (e.g., yearsExperience=”two” instead of yearsExperience={2}) or forget a required prop (.isRequired), React will log a warning in the browser’s developer console during development. This does not stop the application or provide compile-time safety like TypeScript, but it’s a valuable debugging tool.
Common Types: PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.array, PropTypes.object, PropTypes.func, PropTypes.node (anything renderable), PropTypes.element (a single React element). Add .isRequired to make a prop mandatory.
Chapter 5: State — Managing Component Data
While props allow you to pass data down from parent to child, state allows a component to manage its own internal data that can change over time, often in response to user interactions or network requests. When a component’s state changes, React automatically re-renders the component to reflect those changes.
5.1 What is State?
Internal Memory: State is like a component’s private memory. It holds data that is specific to that instance of the component and can change during the component’s lifecycle.
Triggers Re-renders: The key difference from regular JavaScript variables inside a component is that when state is updated using its special updater function, React knows it needs to re-run the component function (re-render) to potentially update the UI. Modifying a normal variable won’t trigger this.
Local: State is typically local or encapsulated within the component that defines it. It’s not directly accessible to parent components or siblings (though you can pass state down as props, or use techniques like “lifting state up”).
5.2 The useState Hook
In functional components, we manage state using the useState Hook. A Hook is a special function that lets you “hook into” React features (like state) from functional components.
- Import: You need to import useState from the ‘react’ library.
import React, { useState } from 'react';
2. Initialization: Call useState at the top level of your functional component. It takes one argument: the initial state value.
const [count, setCount] = useState(0); // Initial state is 0 (a number)
const [name, setName] = useState("Guest"); // Initial state is "Guest" (a string)
const [isActive, setIsActive] = useState(false); // Initial state is false (a boolean)
const [items, setItems] = useState([]); // Initial state is an empty array
const [user, setUser] = useState({ id: null, name: '' }); // Initial state is an object
3. Return Value: useState returns an array containing exactly two elements:
- [0]: The current state value. (e.g., count, name, isActive). You use this value in your JSX or logic.
- [1]: A state updater function. (e.g., setCount, setName, setIsActive). You call this function to change the state value. By convention, this function is named set followed by the capitalized state variable name.
4. Array Destructuring: We use JavaScript’s array destructuring syntax (const [value, setValue] = …) to give meaningful names to the state variable and its updater function.
Example: A Simple Counter
Let’s build a component that uses state to keep track of a count.
// src/Counter.jsx (New component file)
import React, { useState } from 'react'; // Import useState
function Counter() {
// 1. Initialize state: 'count' starts at 0
// 'setCount' is the function to update it
const [count, setCount] = useState(0);
// Event handler function to increment the count
const handleIncrement = () => {
// 3. Update state using the setter function
setCount(count + 1);
// NEVER do this: count = count + 1; (This won't trigger a re-render)
// NEVER do this: count++; (This won't trigger a re-render)
};
// Event handler function to decrement the count
const handleDecrement = () => {
setCount(count - 1);
};
console.log("Counter component rendered. Current count:", count);
return (
<div>
<h2>Simple Counter</h2>
{/* 2. Read state: Display the current 'count' value */}
<p>Current Count: {count}</p>
<button onClick={handleIncrement}>Increment +</button>
<button onClick={handleDecrement}>Decrement -</button>
<button onClick={() => setCount(0)}>Reset</button> {/* Update state directly in handler */}
</div>
);
}
export default Counter;
Following is code to use with App.jsx
// --- Usage in App.jsx ---
import Counter from './Counter.jsx';
function App() {
return (
<div>
<h1>State Demo</h1>
<Counter />
<Counter /> {/* Each Counter instance has its own independent state */}
</div>
);
}
- Initialization: useState(0) sets the initial count to 0.
- Reading: We display the current value using {count} in the <p> tag.
- Updating: When the “Increment +” button is clicked, handleIncrement is called. Inside handleIncrement, setCount(count + 1) is called. This tells React: “The count state needs to be updated to the new value (current count + 1). Please schedule a re-render of the Counter component.” React then re-runs the Counter function, useState provides the new count value (e.g., 1), and the updated value is displayed in the paragraph.
- Independent State: If you render <Counter /> multiple times, each instance will have its own independent count state. Clicking increment on one counter will not affect the others.
5.3 State Updates are Asynchronous (Batched)
When you call a state updater function (like setCount), React does not immediately change the state value within the currently executing code block. Instead, React schedules the state update.
- Batching: For performance reasons, React often batches multiple state updates within the same event handler or effect into a single re-render.
- Implication: You cannot rely on the state variable having its new value immediately after calling the setter function in the same scope.
function CounterBuggy() {
const [count, setCount] = useState(0);
const handleTripleIncrement = () => {
// If count is 0, all these calls use 0 as the value for 'count'
setCount(count + 1); // Schedules update to 0 + 1 = 1
console.log("Called setCount(count + 1). Current count in this render:", count); // Still logs 0
setCount(count + 1); // Schedules update to 0 + 1 = 1 (overwrites previous schedule)
console.log("Called setCount(count + 1). Current count in this render:", count); // Still logs 0
setCount(count + 1); // Schedules update to 0 + 1 = 1 (overwrites again)
console.log("Called setCount(count + 1). Current count in this render:", count); // Still logs 0
// After this function finishes, React processes the scheduled updates.
// The component re-renders, and 'count' becomes 1, NOT 3.
};
return (
<div>
<h2>Buggy Triple Increment</h2>
<p>Count: {count}</p>
<button onClick={handleTripleIncrement}>Add 3 (Buggy)</button>
</div>
);
}
5.4 Updating State Based on Previous State (Functional Updates)
To reliably update state based on its previous value, especially when making multiple updates or dealing with asynchronous operations, you should pass a function to the state updater. This function receives the pending state value as its argument and should return the new state value.
function CounterCorrected() {
const [count, setCount] = useState(0);
const handleTripleIncrementCorrect = () => {
// Pass a function to ensure updates are based on the latest state
setCount(prevCount => {
console.log("Functional update 1. prevCount:", prevCount); // Logs 0
return prevCount + 1; // Return the new value (1)
});
setCount(prevCount => {
console.log("Functional update 2. prevCount:", prevCount); // Logs 1 (uses the result of the previous update)
return prevCount + 1; // Return the new value (2)
});
setCount(prevCount => {
console.log("Functional update 3. prevCount:", prevCount); // Logs 2
return prevCount + 1; // Return the new value (3)
});
// React processes these functional updates sequentially.
// The component re-renders, and 'count' becomes 3.
};
return (
<div>
<h2>Correct Triple Increment</h2>
<p>Count: {count}</p>
<button onClick={handleTripleIncrementCorrect}>Add 3 (Correct)</button>
</div>
);
}
Rule of Thumb: If your new state depends on the previous state value, always use the functional update form: setState(prevState => newState).
5.5 State vs. Props
5.6 Lifting State Up
What happens when multiple components need to reflect the same changing data, or when a child component needs to change data owned by a parent (or ancestor)? The answer is Lifting State Up.
Scenario: Imagine two input fields that should always display the same temperature, one in Celsius and one in Fahrenheit. Changing one should update the other. Neither input can manage the single source of truth alone.
Solution:
- Find the nearest common ancestor component of all components that need the shared state.
- Define the state (useState) in that common ancestor.
- Pass the state value down as props to the child components that need to display it.
- If a child needs to update the state, pass an event handler function (that calls the setState in the parent) down as a prop to that child.
Example: Synchronized Inputs
// src/SharedInput.jsx (Child Component)
import React from 'react';
// This component displays a value and calls a function when changed
function SharedInput({ label, value, onValueChange }) {
const handleChange = (event) => {
// Call the function passed down from the parent
onValueChange(event.target.value);
};
return (
<div>
<label>{label}: </label>
<input type="text" value={value} onChange={handleChange} />
</div>
);
}
export default SharedInput;
// src/SynchronizedInputsContainer.jsx (Parent Component)
import React, { useState } from 'react';
import SharedInput from './SharedInput';
function SynchronizedInputsContainer() {
// 1. State is defined in the common ancestor
const [textValue, setTextValue] = useState('');
// 4. Event handler function defined in the parent
const handleTextChange = (newValue) => {
setTextValue(newValue); // Updates the parent's state
};
return (
<div>
<h2>Synchronized Inputs</h2>
<p>Type in either box:</p>
{/* 3. Pass state down as 'value' prop */}
{/* 4. Pass handler down as 'onValueChange' prop */}
<SharedInput
label="Input 1"
value={textValue}
onValueChange={handleTextChange}
/>
{/* Pass the SAME state and handler to the second input */}
<SharedInput
label="Input 2"
value={textValue}
onValueChange={handleTextChange}
/>
<p>Current Value: {textValue}</p>
</div>
);
}
export default SynchronizedInputsContainer;
Following is example of App.jsx.
// --- Usage in App.jsx ---
import SynchronizedInputsContainer from './SynchronizedInputsContainer.jsx';
function App() {
return (
<div>
<h1>Lifting State Up Demo</h1>
<SynchronizedInputsContainer />
</div>
);
}
In this example, SynchronizedInputsContainer owns the textValue state. It passes textValue down to both SharedInput components as the value prop. It also passes the handleTextChange function down as the onValueChange prop. When you type in either SharedInput, its onChange event triggers handleChange (inside SharedInput), which calls the handleTextChange function passed down from the parent. handleTextChange then updates the textValue state in the parent. This triggers a re-render of SynchronizedInputsContainer, which then passes the new textValue down to both SharedInput components, keeping them synchronized.
Chapter 6: Handling Events
6.1 Basic Event Handling (onClick, onChange, onSubmit, etc.)
Handling events with React elements involves these key points:
camelCase Naming: React event names are written in camelCase, rather than lowercase like in HTML.
- HTML: onclick => React: onClick
- HTML: onchange => React: onChange
- HTML: onsubmit => React: onSubmit
- HTML: onmouseover => React: onMouseOver
Passing Functions: Instead of passing a string containing JavaScript code (like in old HTML onclick=”alert(‘hi’)”), you pass a function (or a reference to a function) as the event handler.
// Example: Button click
import React, { useState } from 'react';
function ClickButton() {
const [message, setMessage] = useState("Click the button!");
// This is the event handler function
const handleClick = () => {
setMessage("Button Clicked!");
console.log("Button was clicked!");
};
return (
<div>
<p>{message}</p>
{/* Pass the handleClick function reference to the onClick prop */}
<button onClick={handleClick}>
Click Me
</button>
</div>
);
}
export default ClickButton;
- In this example, when the button is clicked, the handleClick function is executed, updating the state and logging a message.
6.2 Event Handler Functions (Inline vs. Defined Methods)
You can define your event handler functions in a couple of ways:
- Defined within the Component: (As shown in the ClickButton example above) This is the most common and generally preferred approach for handlers that have more than one line of logic or are reused. It keeps the JSX cleaner.
function MyComponent() {
const handleSomething = () => {
// ... logic ...
console.log("Handling something!");
};
return <div onClick={handleSomething}>Clickable Div</div>;
}
function MyComponent() { const handleSomething = () => { // ... logic ... console.log("Handling something!"); }; return <div onClick={handleSomething}>Clickable Div</div>; }
2. Inline Arrow Function: For very simple handlers, you can define an arrow function directly within the JSX.
function SimpleCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
{/* Inline arrow function for onClick */}
<button onClick={() => setCount(count + 1)}>
Increment
</button>
{/* Be careful with inline functions if passing arguments is complex */}
</div>
);
}
- Potential Gotcha: If you define an inline arrow function like onClick={() => doSomething()}, a new function instance is created on every render of the component. For simple cases, this is usually fine. However, if this inline function is passed down as a prop to a child component that is optimized (e.g., using React.memo, covered later), it can cause unnecessary re-renders of the child. Defining the handler outside the JSX avoids this potential issue.
6.3 Passing Arguments to Event Handlers
Often, you need to pass specific data to your event handler when the event occurs (e.g., the ID of the item that was clicked).
Problem: If you just write onClick={myHandler(itemId)}, this will call myHandler immediately when the component renders, not when the button is clicked.
Solution: Use an inline arrow function to wrap the call to your handler. The arrow function itself becomes the event handler, and it only calls your target function (myHandler) when the event (e.g., click) actually happens.
import React from 'react';
function ItemList() {
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
];
// Event handler that accepts an argument (the item name)
const handleItemClick = (itemName) => {
alert(`You clicked on ${itemName}`);
};
// Separate handler for a specific button
const handleSpecialButtonClick = (message) => {
console.log(`Special button says: ${message}`);
}
return (
<div>
<h2>Item List</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* Use an inline arrow function to pass item.name to the handler */}
<button onClick={() => handleItemClick(item.name)} style={{ marginLeft: '10px' }}>
Info
</button>
</li>
))}
</ul>
{/* Pass a specific string argument */}
<button onClick={() => handleSpecialButtonClick("Hello There!")}>
Special Button
</button>
</div>
);
}
export default ItemList;
- Inside the map, onClick={() => handleItemClick(item.name)} creates a function that, when called (on click), executes handleItemClick with the specific item.name for that list item.
6.4 Understanding the SyntheticEvent Object
When you define an event handler, React passes a SyntheticEvent object as the first argument to your handler function (even if you don’t explicitly declare it in your function signature). This object is a cross-browser wrapper around the browser’s native event object.
Why? It provides a consistent API across different browsers, smoothing out inconsistencies in native browser event implementations.
Accessing Native Event: You can access the underlying native browser event via event.nativeEvent.
Common Properties/Methods:
- event.preventDefault(): Prevents the browser’s default behavior for the event (e.g., preventing a form submission from reloading the page).
- event.stopPropagation(): Stops the event from bubbling up the DOM tree to parent element handlers.
- event.target: Refers to the DOM element that triggered the event (e.g., the button that was clicked).
- event.target.value: Commonly used with input fields (onChange) to get the current value.
import React, { useState } from 'react';
function SimpleForm() {
const [inputValue, setInputValue] = useState('');
// Event handler for the input field's onChange event
const handleChange = (event) => {
// 'event' is the SyntheticEvent object
console.log('Event type:', event.type); // "change"
console.log('Target element:', event.target); // The <input> DOM node
console.log('Input value:', event.target.value); // The current text in the input
// Update state with the new value from the input
setInputValue(event.target.value);
};
// Event handler for the form's onSubmit event
const handleSubmit = (event) => {
// 'event' is the SyntheticEvent object
event.preventDefault(); // VERY IMPORTANT: Prevent default form submission (page reload)
alert(`Form submitted with value: ${inputValue}`);
// Here you would typically send the data to a server or process it
};
return (
<form onSubmit={handleSubmit}> {/* Attach onSubmit to the form element */}
<h2>Simple Form</h2>
<label>
Name:
<input
type="text"
value={inputValue} // Controlled component (value tied to state)
onChange={handleChange} // Update state when input changes
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
export default SimpleForm;
- In handleChange, we use event.target.value to get the text the user typed into the input field.
- In handleSubmit, event.preventDefault() is crucial to stop the browser from performing its default form submission behavior, allowing our React code to handle it instead.
Handling events is key to making your applications interactive. Remember the camelCase naming, passing functions, using inline arrow functions for arguments, and leveraging the SyntheticEvent object (especially preventDefault for forms).
Chapter 7: Conditional Rendering
7.1 Using if Statements
JavaScript if statements are a fundamental way to conditionally execute code, and you can use them to conditionally render JSX. Since you cannot embed if statements directly inside JSX curly braces { }, you typically use them outside the main JSX return statement to determine which elements or components to render.
import React, { useState } from 'react';
function LoginStatusMessage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// --- Using if/else outside the return statement ---
let messageElement; // Declare a variable to hold the JSX
if (isLoggedIn) {
messageElement = <p>Welcome back, User!</p>;
} else {
messageElement = <p>Please log in.</p>;
}
// --- End of if/else block ---
const toggleLogin = () => setIsLoggedIn(!isLoggedIn);
return (
<div>
<h2>Login Status (using if/else)</h2>
{/* Render the element determined by the if/else logic */}
{messageElement}
<button onClick={toggleLogin}>
{isLoggedIn ? 'Log Out' : 'Log In'} {/* Using ternary for button text */}
</button>
</div>
);
}
// You can also return different JSX blocks entirely
function LoginStatusMessageAlternate() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const toggleLogin = () => setIsLoggedIn(!isLoggedIn);
// --- Returning different JSX directly ---
if (isLoggedIn) {
return (
<div>
<h2>Login Status (Alternate - Logged In)</h2>
<p>Welcome back, User!</p>
<button onClick={toggleLogin}>Log Out</button>
</div>
);
} else {
return (
<div>
<h2>Login Status (Alternate - Logged Out)</h2>
<p>Please log in.</p>
<button onClick={toggleLogin}>Log In</button>
</div>
);
}
// --- End of conditional returns ---
}
export { LoginStatusMessage, LoginStatusMessageAlternate }; // Export both for demonstration
// --- Usage in App.jsx ---
// import { LoginStatusMessage, LoginStatusMessageAlternate } from './LoginStatusMessage';
// function App() {
// return (
// <div>
// <LoginStatusMessage />
// <hr />
// <LoginStatusMessageAlternate />
// </div>
// );
// }
Approach 1: Declare a variable (messageElement) and assign the appropriate JSX to it within the if/else blocks. Then, render that variable within the main return statement’s JSX using {messageElement}.
Approach 2: Return different JSX structures entirely from within the if/else blocks. This is useful if the overall structure changes significantly based on the condition.
7.2 Using the Ternary Conditional Operator (condition ? true : false)
The conditional (ternary) operator is a concise way to handle simple if/else logic directly within JSX, because it’s an expression that evaluates to a value.
Syntax: condition ? expressionIfTrue : expressionIfFalse
import React, { useState } from 'react';
function UserGreeting() {
const [isLoggedIn, setIsLoggedIn] = useState(true);
const [userName, setUserName] = useState('Alice'); // Example user name
const toggleLogin = () => setIsLoggedIn(!isLoggedIn);
return (
<div>
<h2>User Greeting (using ternary)</h2>
{/* Use ternary directly inside JSX */}
{isLoggedIn
? <p>Hello, {userName}! Glad to see you.</p> // Rendered if isLoggedIn is true
: <p>Please sign in to continue.</p> // Rendered if isLoggedIn is false
}
<button onClick={toggleLogin}>
{/* Also common for simple text/attribute changes */}
{isLoggedIn ? 'Log Out' : 'Sign In'}
</button>
</div>
);
}
export default UserGreeting;
Pros: Very concise for simple conditions, usable directly within JSX.
Cons: Can become hard to read if nested or if the expressions being returned are very complex. For complex logic, if/else outside the JSX might be clearer.
7.3 Using Logical && Operator (Short-Circuiting)
You can use the JavaScript logical AND (&&) operator for situations where you want to render something only if a condition is true, and render nothing otherwise. This works because of “short-circuiting”:
- In true && expression, the expression is always evaluated (and returned).
- In false && expression, the expression is never evaluated, and false is returned. React renders nothing for false.
import React, { useState } from 'react';
function Mailbox() {
const [unreadMessages, setUnreadMessages] = useState(['React Basics', 'Props vs State']);
// Try setting it to: const [unreadMessages, setUnreadMessages] = useState([]);
const messageCount = unreadMessages.length;
return (
<div>
<h2>Mailbox (using &&)</h2>
{/* Only render the <h1> if messageCount > 0 */}
{messageCount > 0 &&
<h1>You have {messageCount} unread message(s).</h1>
}
{/* Example: Show a warning only if condition is met */}
{messageCount === 0 &&
<p>No new messages. All caught up!</p>
}
<button onClick={() => setUnreadMessages([])}>Mark all as read</button>
<button onClick={() => setUnreadMessages(['New Email 1', 'New Email 2', 'New Email 3'])}>
Receive 3 Messages
</button>
</div>
);
}
export default Mailbox;
Use Case: Ideal for conditionally including an element or component. If messageCount > 0 is true, the <h1> element after && is rendered. If it’s false, React skips the <h1> and renders nothing in its place.
Important: Avoid using numbers on the left side of && if the number could be 0, because 0 && expression evaluates to 0, and React will render 0 in the DOM, which is usually not what you want. Use a boolean conversion (count > 0 && … or Boolean(count) && …) instead.
7.4 Preventing Components from Rendering (return null)
Sometimes, you want a component to render nothing at all under certain conditions. A component function can return null to achieve this.
import React from 'react';
// This component only renders if the 'warn' prop is true
function WarningBanner({ warn }) { // Destructure the 'warn' prop
// If the 'warn' prop is false or not provided, render nothing
if (!warn) {
return null; // Returning null prevents rendering
}
// Otherwise, render the warning message
return (
<div style={{ backgroundColor: 'yellow', padding: '10px', border: '1px solid orange' }}>
Warning! Something needs your attention.
</div>
);
}
// Parent component that controls the WarningBanner
function PageWithWarning() {
const [showWarning, setShowWarning] = useState(false);
const toggleWarning = () => {
setShowWarning(prevShow => !prevShow);
};
return (
<div>
<h2>Page with Optional Warning (return null)</h2>
{/* Pass the state down as the 'warn' prop */}
<WarningBanner warn={showWarning} />
<p>This is the main content of the page.</p>
<button onClick={toggleWarning}>
{showWarning ? 'Hide' : 'Show'} Warning
</button>
</div>
);
}
export default PageWithWarning;
- The WarningBanner component checks its warn prop. If warn is falsy (e.g., false, undefined), the component returns null, and nothing appears in the DOM where <WarningBanner /> was used. If warn is true, it returns the warning div.
- Returning null does not affect the firing of the component’s lifecycle methods or Hooks (like useEffect), though this is more relevant when we discuss useEffect later.
These four methods (if/else, ternary operator, logical &&, returning null) provide a complete toolkit for controlling what your React application renders based on dynamic conditions. Choosing the right method often depends on the complexity of the condition and where the logic fits best (inside or outside the JSX).
Chapter 8: Lists and Keys
8.1 Rendering Collections of Data (Arrays)
You’ll frequently encounter scenarios where you need to display multiple similar items based on data stored in an array. For example, rendering a list of users, products, notifications, or to-do items.
React leverages standard JavaScript array methods, particularly map(), to achieve this efficiently.
8.2 Using map() to Create Lists of Elements
The .map() method creates a new array by calling a provided function on every element in the original array. In React, we use it to transform an array of data into an array of React elements (like <li>, <div>, or custom components).
import React from 'react';
function SimpleTodoList() {
// 1. Sample data array
const todos = [
{ id: 't1', text: 'Learn React Basics' },
{ id: 't2', text: 'Understand JSX' },
{ id: 't3', text: 'Master Components and Props' },
{ id: 't4', text: 'Explore State with useState' }
];
// 2. Use map() to transform the data array into an array of <li> elements
const todoListItems = todos.map((todo) => {
// For each 'todo' object in the 'todos' array...
// ...return an <li> element representing that todo.
return <li key={todo.id}>{todo.text}</li>;
// ^^ We'll explain 'key' next!
});
console.log(todoListItems); // Inspect the array of React elements in the console
// 3. Render the resulting array of elements within JSX
return (
<div>
<h2>Simple Todo List (using map)</h2>
<ul>
{/* Embed the array of <li> elements directly */}
{todoListItems}
</ul>
</div>
);
}
// --- More Concise Version (Inline map) ---
function SimpleTodoListInline() {
const todos = [
{ id: 't1', text: 'Learn React Basics' },
{ id: 't2', text: 'Understand JSX' },
{ id: 't3', text: 'Master Components and Props' },
{ id: 't4', text: 'Explore State with useState' }
];
return (
<div>
<h2>Simple Todo List (inline map)</h2>
<ul>
{/* Use map() directly inside the curly braces */}
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{/* Using parentheses () instead of curly braces {} for the arrow
function implicitly returns the single expression (the <li>) */}
</ul>
</div>
);
}
export { SimpleTodoList, SimpleTodoListInline };
// --- Usage in App.jsx ---
// import { SimpleTodoList, SimpleTodoListInline } from './SimpleTodoList';
// function App() {
// return (
// <div>
// <SimpleTodoList />
// <hr />
// <SimpleTodoListInline />
// </div>
// );
// }
- We start with an array of todos objects.
- We call todos.map(). The function passed to map receives each todo object one by one.
- Inside the map function, we return a JSX <li> element for each todo. It’s crucial that this function returns a React element.
- map() collects all the returned <li> elements into a new array (todoListItems).
- We embed this array {todoListItems} (or the direct result of the inline map) inside the <ul> tags. React knows how to render an array of elements sequentially.
8.3 The Importance of key Props
If you run the code above, you’ll likely see a warning in your browser’s developer console similar to: “Warning: Each child in a list should have a unique ‘key’ prop.”
Why Keys? React uses keys to identify which items in a list have changed, been added, or been removed. When a list re-renders, React compares the keys of the new elements with the keys of the previous elements. This allows React’s diffing algorithm (part of the Virtual DOM process) to efficiently update the UI.
- Without keys, React might have to re-render the entire list or make incorrect assumptions about which elements correspond to which data, leading to potential bugs and performance issues, especially with stateful list items or animations.
- Keys help React maintain the identity of elements across updates.
Where to Put Keys: The key prop needs to be specified on the outermost element being returned directly inside the map() callback. In our example, it’s on the <li> element. If you were mapping to custom components like <TodoItem />, you’d put the key on the <TodoItem /> itself:
{todos.map((todo) => (
<TodoItem key={todo.id} text={todo.text} />
))}
- Note: The key prop is used internally by React and is not passed down as a regular prop to the component (TodoItem in this case would not receive key in its props object). If you need the ID inside the TodoItem component, you must pass it as a separate prop (e.g., <TodoItem key={todo.id} id={todo.id} text={todo.text} />).
8.4 Choosing the Right Key
A key must satisfy two conditions:
- Unique Among Siblings: Keys only need to be unique among the direct children of the parent element where the list is being rendered. They don’t need to be globally unique across your entire application. In our todos example, each todo.id (t1, t2, t3, t4) is unique within that specific <ul>.
- Stable: The key for a specific data item should not change between renders. If the data item representing “Learn React Basics” always has the ID t1, then t1 is a stable key. Using something volatile like Math.random() or the array index (see below) is generally bad because the key wouldn’t consistently identify the same piece of data if the list order changes.
3. Ideal Keys: The best keys are usually unique identifiers that come from your data, such as:
- Database IDs (e.g., user.id, product.sku)
- UUIDs generated for items
- Content hashes (if the content itself is guaranteed unique and stable)
Example Revisited with Keys (Correct):
// This version correctly includes the key prop
function SimpleTodoListCorrect() {
const todos = [
{ id: 't1-abc', text: 'Learn React Basics' }, // Using string IDs from data
{ id: 't2-def', text: 'Understand JSX' },
{ id: 't3-ghi', text: 'Master Components and Props' },
{ id: 't4-jkl', text: 'Explore State with useState' }
];
return (
<div>
<h2>Simple Todo List (with Correct Keys)</h2>
<ul>
{todos.map((todo) => (
// Use the stable, unique ID from the data as the key
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
export default SimpleTodoListCorrect;
Why Using Array Index as a Key (key={index}) is Often Problematic
You might be tempted to use the second argument provided by map() (the index) as the key:
// ANTI-PATTERN: Using index as key (generally discouraged)
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
Why this is bad:
- If the order of items changes: If you insert an item at the beginning of the items array, or sort the array, the indices of the subsequent items will change. React will associate the existing components with the new data at that index, potentially leading to incorrect state updates or rendering issues, because the key (index) no longer identifies the same logical item.
- If items are added/removed: Similar issues arise when items are added or removed from the middle of the list. React might end up unnecessarily destroying and recreating components instead of just moving them.
When might index be okay (use with caution)?
- The list and items are static and will never change order or be filtered.
- The items have no IDs.
- The list will never be re-ordered or filtered.
Even in these cases, if you can generate a stable ID (e.g., hashing the content if it’s unique), it’s usually preferable. Defaulting to using a proper ID from your data is the best practice.
Rendering lists with map() and providing stable, unique key props is essential for building dynamic and efficient interfaces in React.
Chapter 9: Forms — Handling User Input
9.1 Controlled Components Pattern
In HTML, form elements like <input>, <textarea>, and <select> typically maintain their own state internally and update it based on user input. In React, a Controlled Component is one where the form element’s value is controlled by React state.
The Flow:
- A piece of state is initialized using useState to hold the value of the input.
- The value prop of the form element (e.g., <input value={/* state value */} />) is set to this state variable. This makes React the “source of truth” for the input’s value.
- An onChange event handler is attached to the form element.
- When the user types or changes the selection, the onChange handler fires.
- Inside the handler, the state updater function (from useState) is called with the new value (usually obtained from event.target.value).
- The state update triggers a re-render, and the form element is rendered with the new value from the updated state.
This ensures that the React state and the input element’s display are always synchronized.
Example: A Simple Controlled Input
// src/NameForm.jsx (New component file)
import React, { useState } from 'react';
function NameForm() {
// 1. Initialize state to hold the input value
const [name, setName] = useState(''); // Start with an empty string
// 4. onChange handler
const handleChange = (event) => {
console.log('Input changed:', event.target.value);
// 5. Update state with the input's current value
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault(); // Prevent default page reload on form submit
alert(`A name was submitted: ${name}`);
// Here you would typically send 'name' to an API or process it
setName(''); // Optionally clear the input after submission
};
return (
// Use the onSubmit handler on the form element
<form onSubmit={handleSubmit}>
<h2>Name Form (Controlled Input)</h2>
<label>
Name:
<input
type="text"
// 2. Set the input's value from state
value={name}
// 3. Attach the onChange handler
onChange={handleChange}
/>
</label>
<p>Current value in state: {name}</p>
<button type="submit">Submit</button>
</form>
);
}
export default NameForm;
// --- Usage in App.jsx ---
// import NameForm from './NameForm.jsx';
// function App() {
// return (
// <div>
// <NameForm />
// </div>
// );
// }
- The input’s value is always driven by the name state variable.
- Typing in the input triggers handleChange, which calls setName, updating the state.
- The state update causes a re-render, showing the new character in the input because its value prop is now bound to the updated name state.
9.2 Handling onChange for Different Input Types
The onChange handler works similarly for various form elements, but how you get the value might differ slightly.
<input type=”text”>, <input type=”password”>, <input type=”email”>, etc., <textarea>:
- Use event.target.value to get the current text content.
// Textarea Example
function EssayForm() {
const [essay, setEssay] = useState('Please write an essay about your favorite DOM element.');
const handleEssayChange = (event) => {
setEssay(event.target.value);
};
// ... submit handler ...
return (
<form /* onSubmit={handleSubmit} */>
<label>
Essay:
<textarea value={essay} onChange={handleEssayChange} />
</label>
{/* ... submit button ... */}
</form>
);
}
<select>:
- Use event.target.value to get the value attribute of the selected <option>. Attach value and onChange to the <select> tag itself.
// Select Example
function FlavorForm() {
const [selectedFlavor, setSelectedFlavor] = useState('coconut'); // Default selected value
const handleFlavorChange = (event) => {
setSelectedFlavor(event.target.value);
};
// ... submit handler ...
return (
<form /* onSubmit={handleSubmit} */>
<label>
Pick your favorite flavor:
<select value={selectedFlavor} onChange={handleFlavorChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
{/* ... submit button ... */}
</form>
);
}
<input type=”checkbox”>:
- Use event.target.checked (a boolean) instead of value.
// Checkbox Example
function AgreeForm() {
const [isAgreed, setIsAgreed] = useState(false);
const handleAgreeChange = (event) => {
setIsAgreed(event.target.checked); // Use .checked
};
// ... submit handler ...
return (
<form /* onSubmit={handleSubmit} */>
<label>
<input
type="checkbox"
checked={isAgreed} // Use 'checked' prop, not 'value'
onChange={handleAgreeChange}
/>
I agree to the terms.
</label>
<p>Agreed: {isAgreed ? 'Yes' : 'No'}</p>
{/* ... submit button ... */}
</form>
);
}
<input type=”radio”>:
- All radio buttons in a group share the same name attribute.
- Use event.target.value to get the value of the selected radio button.
- Use the checked prop, comparing the state variable to the specific radio button’s value.
// Radio Button Example
function ContactMethodForm() {
const [contactMethod, setContactMethod] = useState('email'); // State holds the value of the selected radio
const handleContactChange = (event) => {
setContactMethod(event.target.value);
};
// ... submit handler ...
return (
<form /* onSubmit={handleSubmit} */>
<fieldset> {/* Good practice for grouping radio buttons */}
<legend>Preferred Contact Method:</legend>
<label>
<input
type="radio"
name="contactMethod" // Same name for the group
value="email"
checked={contactMethod === 'email'} // Check if state matches this radio's value
onChange={handleContactChange}
/> Email
</label>
<label>
<input
type="radio"
name="contactMethod"
value="phone"
checked={contactMethod === 'phone'}
onChange={handleContactChange}
/> Phone
</label>
<label>
<input
type="radio"
name="contactMethod"
value="mail"
checked={contactMethod === 'mail'}
onChange={handleContactChange}
/> Mail
</label>
</fieldset>
<p>Selected: {contactMethod}</p>
{/* ... submit button ... */}
</form>
);
}
9.3 Handling Form Submission (onSubmit)
- Attach an onSubmit event handler to the <form> element itself, not the submit button.
- Crucially: Call event.preventDefault() inside the onSubmit handler to prevent the browser’s default behavior of sending a request and reloading the page.
- Inside the handler, you can access all the form data stored in your component’s state variables and perform actions like sending it to an API, updating application state, etc.
// From NameForm example earlier:
const handleSubmit = (event) => {
event.preventDefault(); // Prevent page reload!
alert(`Submitting data: ${name}`);
// Example: fetch('/api/users', { method: 'POST', body: JSON.stringify({ userName: name }) });
setName(''); // Clear form after successful submission
};
// ... in JSX ...
<form onSubmit={handleSubmit}>
{/* ... inputs ... */}
<button type="submit">Submit</button>
</form>
9.4 Handling Multiple Inputs
When you have multiple controlled inputs in a form, creating a separate state variable and handler function for each one can become tedious. A common pattern is to use a single state object and a single handler function, identifying the input using its name attribute.
- Use a single useState hook with an object to hold all form values.
- Give each form input a unique name attribute that matches the corresponding key in your state object.
- Create a single handleChange function. Inside it:
- Get the name and value (or checked and type for checkboxes) from event.target.
- Use the state setter function with the functional update form (setState(prevState => newState)).
- Use the computed property name syntax ([name]: value) to update the specific key in the state object based on the input’s name.
// src/ReservationForm.jsx (New component file)
import React, { useState } from 'react';
function ReservationForm() {
// 1. Single state object
const [formData, setFormData] = useState({
guestName: '',
numberOfGuests: 2,
isGoing: true,
specialRequests: ''
});
// 3. Single handler function
const handleChange = (event) => {
const target = event.target; // Get the input element
const value = target.type === 'checkbox' ? target.checked : target.value; // Get value or checked status
const name = target.name; // Get the input's name attribute
// 4. Update state using functional update and computed property name
setFormData(prevFormData => ({
...prevFormData, // Copy existing state
[name]: value // Update the specific key based on input's name
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log("Submitting reservation:", formData);
alert(`Reservation for ${formData.guestName} (${formData.numberOfGuests} guests) submitted.`);
// Reset form (optional)
setFormData({
guestName: '',
numberOfGuests: 2,
isGoing: true,
specialRequests: ''
});
};
return (
<form onSubmit={handleSubmit}>
<h2>Reservation Form (Multiple Inputs)</h2>
<label>
Guest Name:
<input
type="text"
name="guestName" // 2. name attribute matches state key
value={formData.guestName}
onChange={handleChange}
/>
</label>
<br />
<label>
Number of Guests:
<input
type="number"
name="numberOfGuests"
value={formData.numberOfGuests}
onChange={handleChange}
/>
</label>
<br />
<label>
Is going?
<input
type="checkbox"
name="isGoing"
checked={formData.isGoing}
onChange={handleChange}
/>
</label>
<br />
<label>
Special Requests:
<textarea
name="specialRequests"
value={formData.specialRequests}
onChange={handleChange}
/>
</label>
<br />
<button type="submit">Submit Reservation</button>
<pre style={{marginTop: '1em', background:'#eee', padding:'0.5em'}}>
Current Form Data State: {JSON.stringify(formData, null, 2)}
</pre>
</form>
);
}
export default ReservationForm;
This pattern significantly reduces boilerplate code when dealing with forms containing many fields.
Controlled components are the standard way to handle forms in React, giving you full control over form data and behavior through state management.
Chapter 10: Hooks Deep Dive (Beyond useState)
10.1 The useEffect Hook (Handling Side Effects)
Functional components are primarily meant to calculate and return JSX based on props and state. However, applications often need to interact with the “outside world” — performing actions that aren’t directly related to rendering the UI. These are called side effects.
Examples of side effects include:
- Fetching data from an API
- Setting up or tearing down subscriptions (e.g., to timers, WebSockets, browser events)
- Manually changing the DOM (less common in React, but sometimes necessary)
- Logging
The useEffect Hook provides a way to handle these side effects within functional components.
- Import:
import React, { useState, useEffect } from 'react';
2. Basic Usage: useEffect accepts a function (the “effect function”) as its first argument. This function will be executed after React renders the component to the DOM.
function SimpleEffectLogger() {
const [count, setCount] = useState(0);
// This effect runs AFTER EVERY render (initial render + updates)
useEffect(() => {
// This is the effect function
console.log(`Effect ran! Count is: ${count}`);
document.title = `Count: ${count}`; // Example side effect: updating document title
}); // No dependency array provided
console.log("SimpleEffectLogger component rendered"); // Runs BEFORE the effect
return (
<div>
<h2>Effect Logger</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
- If you run this, you’ll see “SimpleEffectLogger component rendered” logged first, then the component appears on screen, and then “Effect ran!” is logged. Every time you click the button and the component re-renders, the effect function runs again after the render.
3. Controlling When Effects Run: The Dependency Array
Running the effect after every render is often inefficient or incorrect (e.g., re-fetching data on every minor UI update). useEffect takes an optional second argument: an array of dependencies.
- Empty Array []: The effect runs only once after the initial render (component “mounts”). It does not run again on subsequent re-renders. This is ideal for one-time setup like initial data fetching or setting up subscriptions.
function DataFetcher() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Effect runs only ONCE after initial render
useEffect(() => {
console.log("Effect: Fetching data...");
setIsLoading(true);
// Simulate API call
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(jsonData => {
console.log("Effect: Data fetched!", jsonData);
setData(jsonData);
setIsLoading(false);
})
.catch(error => {
console.error("Effect: Fetch error:", error);
setIsLoading(false);
});
}, []); // <-- Empty dependency array
console.log("DataFetcher component rendered");
if (isLoading) return <p>Loading data...</p>;
if (!data) return <p>No data found.</p>;
return (
<div>
<h2>Data Fetcher (useEffect with [])</h2>
<p>Todo Title: {data.title}</p>
<p>Completed: {data.completed ? 'Yes' : 'No'}</p>
</div>
);
}
- Array with Values [prop1, stateValue]: The effect runs after the initial render AND only runs again if any of the values listed in the dependency array have changed since the last render. React performs a shallow comparison of the dependency values.
function UserProfileFetcher({ userId }) { // Receives userId as a prop
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// Effect runs initially AND whenever 'userId' prop changes
useEffect(() => {
if (!userId) return; // Don't fetch if no userId
console.log(`Effect: Fetching data for user ${userId}...`);
setLoading(true);
setUser(null); // Clear previous user data
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.ok ? res.json() : Promise.reject('User not found'))
.then(userData => {
console.log(`Effect: Fetched user ${userId}`, userData);
setUser(userData);
setLoading(false);
})
.catch(error => {
console.error(`Effect: Fetch error for user ${userId}:`, error);
setUser(null); // Ensure no stale data is shown
setLoading(false);
});
}, [userId]); // <-- Dependency array includes userId
console.log(`UserProfileFetcher rendered for user ${userId}`);
if (loading) return <p>Loading user profile...</p>;
if (!user) return <p>Select a user ID or user not found.</p>;
return (
<div>
<h2>User Profile ({userId})</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Website: {user.website}</p>
</div>
);
}
// --- Example Parent Component to control UserProfileFetcher ---
function UserSelector() {
const [selectedUserId, setSelectedUserId] = useState(1);
return (
<div>
<h2>Select User (Parent)</h2>
<button onClick={() => setSelectedUserId(1)}>User 1</button>
<button onClick={() => setSelectedUserId(2)}>User 2</button>
<button onClick={() => setSelectedUserId(5)}>User 5</button>
<button onClick={() => setSelectedUserId(999)}>User 999 (Not Found)</button>
<hr />
<UserProfileFetcher userId={selectedUserId} /> {/* Pass state down as prop */}
</div>
)
}
- In this example, the fetch request inside useEffect only re-runs when the userId prop changes. Clicking the buttons in UserSelector changes the selectedUserId state, which causes UserProfileFetcher to re-render with a new userId prop, triggering the effect.
- Omitting the Array: (As in the first example) The effect runs after every render. Use this sparingly, usually when the effect truly depends on something that might change on any render and doesn’t have specific dependencies.
4. Cleanup Function: The function passed to useEffect can optionally return another function. This returned function is the cleanup function. React runs the cleanup function:
- Before the component unmounts: When the component is removed from the DOM.
- Before the effect runs again: If the dependencies change and the effect is scheduled to re-run, React first runs the cleanup function from the previous effect execution, then runs the new effect.
This is crucial for preventing memory leaks and unwanted behavior by cleaning up resources like timers, subscriptions, or event listeners.
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log("Effect: Setting up interval timer...");
// Setup: Start an interval timer
const intervalId = setInterval(() => {
console.log("Timer tick!");
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000); // Update every second
// Return the cleanup function
return () => {
console.log("Cleanup: Clearing interval timer", intervalId);
clearInterval(intervalId); // Clear the interval when cleaning up
};
}, []); // Empty array: setup runs once on mount, cleanup runs once on unmount
console.log("TimerComponent rendered");
return (
<div>
<h2>Timer (useEffect with Cleanup)</h2>
<p>Seconds Elapsed: {seconds}</p>
</div>
);
}
// --- Example Parent to Mount/Unmount TimerComponent ---
function TimerToggler() {
const [showTimer, setShowTimer] = useState(true);
return (
<div>
<h2>Timer Toggler (Parent)</h2>
<button onClick={() => setShowTimer(s => !s)}>
{showTimer ? 'Hide' : 'Show'} Timer
</button>
<hr />
{/* Conditionally render the TimerComponent */}
{showTimer && <TimerComponent />}
</div>
)
}
When TimerToggler first renders, TimerComponent mounts, the effect runs, the interval starts. When you click “Hide Timer”, TimerComponent unmounts, React runs the cleanup function (() => clearInterval(intervalId)), stopping the timer and preventing errors or memory leaks.
10.2 Rules of Hooks
React relies on a consistent call order for Hooks between renders. To ensure this works correctly, there are two main rules:
- Only Call Hooks at the Top Level:
- Do NOT call Hooks inside loops, conditional (if/else) statements, or nested functions.
- Always call Hooks unconditionally at the very beginning of your functional component or custom Hook.
// BAD: Hook inside condition
function BadComponent({ shouldShow }) {
if (shouldShow) {
// DON'T DO THIS! Hook call order changes based on 'shouldShow'
const [data, setData] = useState(null);
useEffect(() => { /* ... */ }, []);
}
// ...
}
// GOOD: Call Hooks unconditionally, use condition inside logic/JSX
function GoodComponent({ shouldShow }) {
// Hooks called at top level
const [data, setData] = useState(null);
useEffect(() => {
if (shouldShow) { // Condition *inside* the effect is okay
// ... fetch data ...
}
}, [shouldShow]); // Include condition variable in dependencies if effect depends on it
// Conditionally render JSX
return (
<div>
{shouldShow && data ? <p>Data: {JSON.stringify(data)}</p> : null}
</div>
);
}
2. Only Call Hooks from React Functions:
- Call Hooks only from within React functional components.
- Call Hooks only from within custom Hooks (see below).
- Do NOT call Hooks from regular JavaScript functions.
Why these rules? React identifies Hooks based on the order they are called during rendering. If the order changes (due to conditions, loops, etc.), React can’t correctly associate state and effects with the component, leading to bugs. Linters (like ESLint with the eslint-plugin-react-hooks package, usually included in CRA/Vite setups) help enforce these rules.
10.3 Creating Custom Hooks (Reusing Stateful Logic)
If you find yourself writing the same stateful logic (using useState, useEffect, or other Hooks) in multiple components, you can extract that logic into a Custom Hook.
- Definition: A custom Hook is simply a JavaScript function whose name starts with use (e.g., useFetch, useFormInput, useWindowWidth) and that calls other Hooks internally.
- Purpose: To share stateful logic between components without resorting to more complex patterns like render props or higher-order components. Custom Hooks make reusable logic cleaner and easier to integrate.
Example: A Custom Hook to Fetch Data
Let’s create a useFetch Hook to encapsulate the data fetching, loading, and error state logic we saw earlier.
// src/useFetch.js (New file for the custom Hook)
import { useState, useEffect } from 'react';
// Custom Hook definition - name starts with 'use'
function useFetch(url) {
// State managed internally by the Hook
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Effect to perform the fetch when the URL changes
useEffect(() => {
// Ignore empty/null URLs
if (!url) {
setLoading(false);
setData(null);
setError(null);
return;
}
console.log(`useFetch: Fetching from ${url}`);
setLoading(true);
setData(null); // Reset data on new fetch
setError(null); // Reset error on new fetch
let isCancelled = false; // Flag to handle component unmounting during fetch
fetch(url)
.then(response => {
if (!response.ok) {
// Throw an error for bad responses (4xx, 5xx)
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(jsonData => {
if (!isCancelled) { // Only update state if component is still mounted
console.log(`useFetch: Data received from ${url}`, jsonData);
setData(jsonData);
setError(null);
}
})
.catch(fetchError => {
if (!isCancelled) {
console.error(`useFetch: Error fetching ${url}`, fetchError);
setError(fetchError);
setData(null);
}
})
.finally(() => {
if (!isCancelled) {
setLoading(false);
}
});
// Cleanup function: Set flag if component unmounts before fetch completes
return () => {
console.log(`useFetch: Cleanup for ${url}`);
isCancelled = true;
};
}, [url]); // Re-run effect if the URL changes
// Return the state values for the consuming component to use
// Often returned as an object or an array
return { data, loading, error };
}
export default useFetch;
// --- Using the custom Hook in a component ---
// src/TodoDisplay.jsx (New component file)
import React, { useState } from 'react';
import useFetch from './useFetch.js'; // Import the custom hook
function TodoDisplay() {
const [todoId, setTodoId] = useState(1);
// Use the custom Hook, passing the URL
// Destructure the returned state values
const { data: todo, loading, error } = useFetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}` // Construct URL dynamically
);
console.log("TodoDisplay rendered");
const handleNext = () => setTodoId(id => id + 1);
const handlePrev = () => setTodoId(id => (id > 1 ? id - 1 : 1));
return (
<div>
<h2>Todo Display (using useFetch Hook)</h2>
<div>
<button onClick={handlePrev} disabled={todoId <= 1 || loading}>Previous</button>
<span> Current Todo ID: {todoId} </span>
<button onClick={handleNext} disabled={loading}>Next</button>
</div>
{loading && <p>Loading todo...</p>}
{error && <p style={{ color: 'red' }}>Error fetching todo: {error.message}</p>}
{todo && !loading && !error && (
<div>
<h3>{todo.title}</h3>
<p>User ID: {todo.userId}</p>
<p>Completed: {todo.completed ? 'Yes' : 'No'}</p>
</div>
)}
</div>
);
}
export default TodoDisplay;
- The useFetch hook encapsulates all the useState and useEffect calls related to fetching data.
- The TodoDisplay component becomes much simpler. It just calls useFetch(url) and receives the data, loading, and error states.
- This useFetch Hook can now be reused in any other component that needs to fetch data from a URL, promoting code reuse and separation of concerns.
useEffect is powerful for managing side effects, and custom Hooks provide an excellent way to abstract and reuse stateful logic. Understanding the dependency array and cleanup functions for useEffect, along with the Rules of Hooks, is critical for writing correct and efficient React applications.
Chapter 11: Component Lifecycle & Effects (Conceptual)
Think of a component’s life on the screen as having three main phases:
- Mounting: The component instance is being created and inserted into the DOM for the first time.
- Updating: The component is re-rendered because its state or props have changed.
- Unmounting: The component instance is being removed from the DOM.
Let’s see how useState and useEffect relate to these phases.
11.1 Understanding Mounting, Updating, Unmounting Phases
Mounting:
- The component function runs for the first time.
- React creates the DOM nodes based on the returned JSX.
- The DOM nodes are inserted into the page.
- Think: Birth and first appearance.
Updating:
- Triggered by a change in the component’s state (via the setter function from useState) or its props (passed down from a re-rendering parent).
- The component function runs again with the new state/props.
- React calculates the differences between the new JSX output and the previous one (Virtual DOM diffing).
- React efficiently updates only the necessary parts of the actual DOM.
- Think: Responding to changes, growing, reacting to the environment.
Unmounting:
- Triggered when the component is removed from the UI (e.g., due to conditional rendering in the parent ({showComponent && <MyComponent />} where showComponent becomes false), navigating away in a router, etc.).
- React removes the component’s DOM nodes from the page.
- Opportunity to clean up any resources (timers, subscriptions, event listeners) set up during mounting or updating.
- Think: Removal, cleanup before disappearing.
11.2 How useState and useEffect Relate to These Phases
useState:
- Initialization: The initial value passed to useState(initialValue) is used during the Mounting phase.
- Updates: Calling the state setter function (e.g., setState(newValue)) schedules an Update. React will then re-run the component function with the new state value, triggering the update phase.
- useEffect: This Hook is designed to manage side effects in relation to these lifecycle phases, controlled primarily by its dependency array.
Mounting (componentDidMount Equivalent):
- An useEffect with an empty dependency array [] runs its effect function only once, right after the initial Mounting phase is complete.
useEffect(() => {
// Runs ONCE after initial render (Mounting)
console.log("Component has mounted!");
// Example: Initial data fetch, setup subscriptions
}, []);
Updating (componentDidUpdate Equivalent):
- An useEffect with dependencies [dep1, dep2] runs its effect function after the initial mount AND after any subsequent Update where the value of dep1 or dep2 has changed. This is often more efficient than the old componentDidUpdate because it only runs when specific data changes.
useEffect(() => {
// Runs AFTER initial render AND
// Runs AFTER updates where 'someProp' or 'someState' changed
console.log("Component updated due to prop/state change!");
// Example: Re-fetch data when an ID prop changes
}, [someProp, someState]);
- An useEffect with no dependency array runs its effect function after the initial mount AND after every Update. Use this carefully as it can be inefficient.
useEffect(() => {
// Runs AFTER initial render AND AFTER EVERY update
console.log("Component rendered or re-rendered");
// Example: Logging that might need to happen on every render
}); // No dependency array
Unmounting (componentWillUnmount Equivalent):
- The cleanup function (the function returned from the useEffect callback) runs when the component is about to Unmount.
- If the effect also runs on updates (due to dependencies), the cleanup function also runs before the effect runs again on an update, ensuring cleanup from the previous effect’s execution.
useEffect(() => {
// Setup logic (runs on mount, maybe updates)
const timerId = setInterval(() => { /* ... */ }, 1000);
console.log("Effect setup: Timer started", timerId);
// Return the cleanup function
return () => {
// Runs on UNMOUNT
// Also runs BEFORE the effect runs again if dependencies change
console.log("Cleanup: Clearing timer", timerId);
clearInterval(timerId);
};
}, [/* dependencies, if any */]); // Empty [] means cleanup runs only on unmount
Shift in Thinking: Instead of thinking “What code should run when the component mounts/updates/unmounts?”, Hooks encourage you to think “What effect needs to happen (and be cleaned up) based on which state or props?”. useEffect lets you co-locate the setup and cleanup logic for a specific side effect and define precisely when it should synchronize based on its dependencies. It unifies the handling of logic previously spread across componentDidMount, componentDidUpdate, and componentWillUnmount.
This conceptual understanding helps bridge the gap if you encounter older class component code or tutorials discussing lifecycle methods. For modern React development with Hooks, focusing on useState for managing state and useEffect (with careful dependency management and cleanup) for managing side effects is the way forward.
Chapter 12: Styling React Components
12.1 Inline Styles (JavaScript Objects)
You can apply styles directly to an element using the style prop. However, instead of passing a string like in HTML, you pass a JavaScript object.
Syntax:
- The style prop accepts a JavaScript object {}.
- CSS property names are written in camelCase (e.g., backgroundColor instead of background-color, fontSize instead of font-size).
- Style values are typically strings (e.g., ‘10px’, ‘#FFF’, ‘center’). Numbers are usually assumed to be pixels (px) for certain properties (like fontSize, margin, padding, width, height), but it’s often safer and clearer to use strings like ‘10px’.
import React from 'react';
// Define style objects outside the component for better organization
const cardStyle = {
border: '1px solid #ccc',
borderRadius: '8px',
padding: '16px',
margin: '10px 0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
backgroundColor: '#f9f9f9' // camelCase for background-color
};
const headerStyle = {
color: 'navy', // String value for color
fontSize: '20px', // String value with 'px' (or just 20 might work for fontSize)
marginBottom: '10px' // camelCase for margin-bottom
};
function StyledCardInline({ title, children }) {
// You can also define styles directly inline, but it can be less readable
const dynamicStyle = {
borderLeft: title === 'Important' ? '5px solid red' : '1px solid #ccc'
};
// Merge styles if needed (be careful with overriding)
const combinedCardStyle = { ...cardStyle, ...dynamicStyle };
return (
// Apply the style object to the style prop
<div style={combinedCardStyle}>
<h3 style={headerStyle}>{title}</h3>
<div>{children}</div>
</div>
);
}
// --- Usage Example ---
function AppWithInlineStyles() {
return (
<div>
<StyledCardInline title="Regular Card">
This card uses inline styles defined as JavaScript objects.
</StyledCardInline>
<StyledCardInline title="Important">
This card has a dynamic left border style.
</StyledCardInline>
</div>
);
}
export default AppWithInlineStyles; // Assuming this is the main export for demo
Pros:
- Styles are locally scoped to the element; no global namespace collisions.
- Easy to make styles dynamic based on component props or state (as seen with dynamicStyle).
Cons:
- Can become verbose for complex styling.
- Limited CSS features (no pseudo-classes like :hover, :focus, no pseudo-elements like ::before, no media queries directly within the object — though logic can simulate some).
- Performance might be slightly less optimal than static CSS files in some scenarios (though often negligible).
- Writing CSS properties in camelCase can be cumbersome.
12.2 Plain CSS Stylesheets (Importing .css)
This is the traditional approach, familiar to web developers. You write standard CSS in a .css file and import it into your component file.
- Create a CSS file: (e.g., src/Button.css)
/* src/Button.css */
.simple-button {
background-color: dodgerblue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s ease; /* Add transition for hover effect */
}
/* Use standard CSS pseudo-classes */
.simple-button:hover {
background-color: royalblue;
}
.simple-button:disabled {
background-color: lightgray;
cursor: not-allowed;
}
2. Import the CSS file: In your component file (e.g., src/Button.jsx), import the CSS file directly.
// src/Button.jsx
import React from 'react';
import './Button.css'; // Import the CSS file
function Button({ label, onClick, disabled = false }) {
return (
<button
className="simple-button" // Use className to apply the CSS class
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
export default Button;
3. Use className: Apply the CSS classes defined in your stylesheet using the className prop in your JSX (remember, class is a reserved word in JavaScript).
Pros:
- Uses standard, familiar CSS syntax.
- Supports all CSS features (pseudo-classes, media queries, etc.).
- Often better performance as CSS can be optimized and cached by the browser.
- Separation of concerns (styles in .css, component logic in .jsx).
Cons:
- Global Scope: By default, styles imported this way are global. If another component imports a different CSS file that also defines a .simple-button class, the styles could clash and override each other unpredictably depending on import order. This is the main drawback for component-based architectures.
12.3 CSS Modules (Scoped CSS)
CSS Modules offer a way to use standard CSS files but automatically scope the class names locally to the component importing them, avoiding global namespace collisions.
- Create a CSS Module file: Name your CSS file with the .module.css extension (e.g., src/FancyButton.module.css).
/* src/FancyButton.module.css */
/* Class names here will be automatically scoped */
.fancyBtn {
background-image: linear-gradient(to right, #ff8177 0%, #ff867a 0%, #ff8c7f 21%, #f99185 52%, #cf556c 78%, #b12a5b 100%);
color: white;
padding: 12px 25px;
border: none;
border-radius: 25px; /* Rounded corners */
font-weight: bold;
cursor: pointer;
transition: transform 0.2s ease-out;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.fancyBtn:hover {
transform: scale(1.05); /* Add a subtle scale effect on hover */
}
.fancyBtn:active {
transform: scale(0.98); /* Add press effect */
}
/* You can define multiple classes */
.primary {
/* Base styles are already in .fancyBtn */
/* Add modifications or keep it empty if base is enough */
}
.secondary {
background-image: linear-gradient(to top, #d5d4d0 0%, #d5d4d0 1%, #eeeeec 31%, #efeeec 75%, #e9e9e7 100%);
color: #333;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
2. Import the CSS Module: Import the .module.css file as an object. Conventionally, this object is named styles.
// src/FancyButton.jsx
import React from 'react';
// Import CSS Module - 'styles' will be an object
import styles from './FancyButton.module.css';
function FancyButton({ label, type = 'primary', onClick }) {
// 1. Access the base class name via the styles object
let buttonClass = styles.fancyBtn;
// 2. Conditionally add other classes from the module
if (type === 'secondary') {
buttonClass += ` ${styles.secondary}`; // Append the scoped secondary class
} else {
buttonClass += ` ${styles.primary}`; // Append the scoped primary class
}
// Log the generated class name to see the scoping in action
// It will look something like: "FancyButton_fancyBtn__aBcDe FancyButton_primary__fGhIj"
console.log('Generated className:', buttonClass);
return (
<button className={buttonClass} onClick={onClick}>
{label}
</button>
);
}
export default FancyButton;
3. Use Class Names: Access the class names as properties of the imported styles object (e.g., styles.fancyBtn, styles.primary). The build tool (like Vite or CRA’s Webpack setup) transforms these class names into unique strings (often including the filename, class name, and a hash) to ensure they don’t clash globally. You then apply these generated unique class names to the className prop.
Pros:
- Local Scope by Default: Solves the global namespace problem of plain CSS. Styles are scoped to the component.
- Uses standard CSS syntax and features within the .module.css file.
- Clear dependency: You know exactly which styles belong to which component.
Cons:
- Requires the .module.css naming convention.
- Accessing class names via the styles object (styles.myClass) is slightly different from standard HTML/CSS. Composing multiple classes requires string concatenation or helper libraries.
- Can sometimes be slightly harder to debug in the browser inspector as class names are generated (though source maps usually help).
12.4 (Brief Mention) CSS-in-JS Libraries
Another popular approach involves writing CSS styles directly within your JavaScript component files using tagged template literals or object notation, often provided by libraries like Styled Components or Emotion.
General Idea:
- You define components that have styles attached to them.
- These libraries generate actual CSS rules (often scoped) and inject them into the <head> of the document.
- They allow easy dynamic styling based on props.
Example (Conceptual — using Styled Components syntax):
// Using a CSS-in-JS library (like styled-components) - Requires installation
import styled from 'styled-components';
// Define a styled component - Button is now a React component with styles
const StyledButton = styled.button`
background-color: ${props => props.primary ? 'palevioletred' : 'white'};
color: ${props => props.primary ? 'white' : 'palevioletred'};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
cursor: pointer;
&:hover { // Pseudo-classes work directly
background-color: ${props => props.primary ? 'darkred' : '#eee'};
}
`;
// Usage
function AppWithStyledComponents() {
return (
<div>
<StyledButton>Normal Button</StyledButton>
<StyledButton primary>Primary Button</StyledButton> {/* Pass prop for dynamic style */}
</div>
);
}
Pros: Component-scoped styles, easy dynamic styling based on props, co-location of component logic and styles.
Cons: Requires adding library dependencies, can have a slight runtime overhead compared to static CSS, syntax is library-specific.
Which to Choose?
- Inline Styles: Good for highly dynamic styles or very simple, element-specific overrides. Not ideal for main component styling.
- Plain CSS: Familiar, but risky for larger apps due to global scope. Can be okay for very global styles (like base typography, resets) defined once at the top level.
- CSS Modules: Often considered the best balance for component-based React apps. Provides local scoping with standard CSS syntax. A solid default choice.
- CSS-in-JS: Powerful, especially for design systems or apps with heavy dynamic styling needs. Adds dependencies and a different way of thinking about styles.
For learning core React, CSS Modules or even starting with plain CSS (while being aware of its global nature) are excellent choices.
Chapter 13: Context API — Global State Management
13.1 The Problem: Prop Drilling
As your application grows, you might encounter situations where certain data (like UI theme, user authentication status, preferred language) needs to be accessed by components deep down in the component tree.
Passing this data down solely through props can become cumbersome and verbose. You might have to pass the prop through many intermediate components that don’t actually use the prop themselves, just to get it to the component that needs it. This process is often called “prop drilling”.
Example of Prop Drilling:
// App owns the theme state
function App() {
const [theme, setTheme] = useState('light');
// ... toggleTheme function ...
// App passes theme down to Layout
return <Layout theme={theme} />;
}
// Layout doesn't use theme, just passes it down
function Layout({ theme }) {
return (
<div>
<Header theme={theme} />
<MainContent />
</div>
);
}
// Header doesn't use theme, just passes it down to UserMenu
function Header({ theme }) {
return (
<header>
<Logo />
<UserMenu theme={theme} /> {/* Drilling theme down */}
</header>
);
}
// UserMenu FINALLY uses the theme prop
function UserMenu({ theme }) {
const style = theme === 'light' ? { background: '#eee', color: '#333'} : { background: '#333', color: '#eee'};
return <div style={style}>User Menu</div>;
}
Prop drilling makes components less reusable and harder to refactor, as intermediate components become coupled to props they don’t care about.
13.2 React.createContext
The first step in using the Context API is to create a Context object.
- React.createContext(defaultValue): This function creates a Context object.
- defaultValue: This value is used by a consuming component only when it does not find a matching Provider higher up in the tree. It can be useful for testing components in isolation or providing a fallback, but often the value provided by the Provider is what you’ll primarily use.
// src/contexts/ThemeContext.js (Create a new file for the context)
import React from 'react';
// 1. Create the context object
// The default value 'light' will only be used if a component tries to consume
// this context without a ThemeContext.Provider above it in the tree.
const ThemeContext = React.createContext('light'); // Default value is 'light'
export default ThemeContext;
// --- You might also create contexts for other global state ---
// src/contexts/AuthContext.js
// import React from 'react';
// const AuthContext = React.createContext({ isLoggedIn: false, user: null });
// export default AuthContext;
It’s common practice to keep context definitions in separate files (e.g., within a src/contexts/ directory) for better organization.
13.3 Context.Provider
Every Context object comes with a Provider React component. This component allows consuming components descended from it to subscribe to context changes.
Purpose: The Provider component wraps the part of your component tree where you want the context value to be available.
value Prop: The Provider component accepts a value prop. This value is the data that will be passed down to all consuming components within this Provider. This is the most important prop.
Dynamic Values: Typically, the value passed to the Provider comes from the state of an ancestor component, allowing the context value to change dynamically.
// src/App.jsx (or a suitable parent component)
import React, { useState } from 'react';
import ThemeContext from './contexts/ThemeContext'; // Import the context
import Toolbar from './Toolbar'; // Assume Toolbar is a component that might consume the context
function App() {
// State to hold the current theme value
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// The value passed to the Provider can be anything (string, object, array)
// Often it's an object containing both the state value and update functions
const themeContextValue = {
currentTheme: theme,
toggleTheme: toggleTheme // Pass the function down too!
};
return (
// 2. Wrap the part of the tree that needs the context with the Provider
// 3. Pass the current context data via the 'value' prop
<ThemeContext.Provider value={themeContextValue}>
<div className={`app theme-${theme}`}> {/* Apply theme class for global styles maybe */}
<h1>Context API Demo</h1>
<Toolbar /> {/* Any component inside Provider can potentially access the context */}
<hr/>
<p>Some other content in the App.</p>
{/* Button to change the context value */}
{/* <button onClick={toggleTheme}>Toggle Theme (from App)</button> */}
{/* Note: Better to have the toggle button inside the provider consume the context */}
</div>
</ThemeContext.Provider>
);
}
export default App;
- Any component rendered inside <ThemeContext.Provider> (like Toolbar and its children) can now access the themeContextValue object.
- When the theme state in App changes (via toggleTheme), the themeContextValue object passed to the Provider updates, and React will automatically re-render all consuming components subscribed to this context.
13.4 useContext Hook
To access the context value within a child component (functional component), you use the useContext Hook.
- useContext(MyContext): This hook accepts the Context object itself (the one returned by React.createContext) as an argument.
- Return Value: It returns the current context value for that context, determined by the value prop of the nearest matching Provider up the component tree.
- Re-rendering: If the value of the Provider changes, the component calling useContext will automatically re-render with the new context value.
// src/ThemedButton.jsx (A component deep in the tree)
import React, { useContext } from 'react';
import ThemeContext from './contexts/ThemeContext'; // Import the SAME context object
function ThemedButton({ children }) {
// 4. Consume the context value using the useContext hook
const themeData = useContext(ThemeContext);
// Access the data provided by the nearest ThemeContext.Provider's 'value' prop
const currentTheme = themeData.currentTheme;
const toggleTheme = themeData.toggleTheme; // Can access functions too!
console.log("ThemedButton rendered. Current theme:", currentTheme);
const buttonStyle = {
backgroundColor: currentTheme === 'light' ? '#eee' : '#555',
color: currentTheme === 'light' ? '#333' : '#fff',
padding: '10px 15px',
border: '1px solid #888',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
};
return (
<button style={buttonStyle} onClick={toggleTheme}>
{children} (Toggle Theme)
</button>
);
}
export default ThemedButton;
// src/Toolbar.jsx (An intermediate component)
// This component doesn't NEED to know about the theme itself,
// but it renders components that do. Context avoids prop drilling here.
import React from 'react';
import ThemedButton from './ThemedButton';
function Toolbar() {
return (
<div>
<ThemedButton>Click Me</ThemedButton>
<span>Some text in the toolbar</span>
<ThemedButton>Another Button</ThemedButton>
</div>
);
}
export default Toolbar;
Now, ThemedButton can directly access the themeData (including currentTheme and toggleTheme) without App, Layout, Header, or Toolbar having to explicitly pass theme props down.
13.5 When to Use Context
Context is powerful, but it’s not meant to replace all prop passing or state management solutions.
Use Context For:
- Global Data: Data that many components at different nesting levels need, like UI theme, current authenticated user, locale/language preferences.
- Low-Frequency Updates: Context is generally best suited for data that doesn’t change very frequently. Every component consuming the context will re-render when the context value changes.
Consider Alternatives When:
- High-Frequency Updates: If the state changes very often (e.g., mouse coordinates, rapidly changing form input), putting it in Context can cause performance issues because it triggers re-renders in potentially many consuming components. Component state (useState) or more optimized state management libraries might be better.
- Avoiding Prop Drilling Only: If only a few levels of drilling are involved, simply passing props might still be simpler and more explicit than setting up Context.
- Complex State Logic: Context itself doesn’t provide tools for managing complex state transitions or logic. For that, you might combine Context with useReducer (another React Hook for more complex state) or look into dedicated state management libraries (like Redux, Zustand, Jotai — see Chapter 16).
Performance Considerations:
- When the value prop of a Provider changes, all components consuming that context via useContext re-render, even if they only use a small part of the value object that didn’t actually change.
Optimization: You can sometimes mitigate this by splitting contexts (having separate contexts for data that changes at different rates) or by using memoization techniques (React.memo, useMemo — see Chapter 15) on the consuming components or the context value itself, but this adds complexity.
Context API provides a clean way to pass data through the component tree without manual prop drilling, making it ideal for sharing global-like data.
Chapter 14: Basic Routing with React Router
Most web applications aren’t just a single view; they consist of multiple “pages” or sections (e.g., Home, About, Products, Contact). In a traditional multi-page application, clicking a link requests a new HTML page from the server. In a Single Page Application (SPA) built with React, the browser initially loads a single HTML file, and React dynamically updates the UI as the user interacts with the app, without requesting new pages from the server for every navigation.
To manage navigation and display different components based on the URL within an SPA, we need a client-side routing library. The most popular one for React is React Router.
14.1 Setting up React Router (react-router-dom)
First, you need to add the React Router library to your project.
- Installation: Open your terminal in your project directory (my-react-app or similar) and run:
npm install react-router-dom
or if using yarn:
yarn add react-router-dom
yarn add react-router-dom
14.2 Core Components (<BrowserRouter>, <Routes>, <Route>)
React Router provides several components to set up routing. The main ones for web applications are:
<BrowserRouter>:
- This component should wrap your entire application (or at least the parts that need routing).
- It uses the HTML5 History API (pushState, replaceState, popstate events) to keep your UI in sync with the URL in the browser’s address bar. It provides clean URLs without hash symbols (#).
- Typically, you’ll wrap your main App component with <BrowserRouter> in your entry file (src/main.jsx).
<Routes>:
- This component acts as a container for one or more <Route> components.
- It intelligently looks through its child <Route> elements and renders the first one whose path matches the current URL.
<Route>:
- This is the core component for defining a route mapping.
- It takes two main props:
- path: A string representing the URL path segment to match (e.g., /, /about, /products/:productId).
- element: The React element (usually a component instance like <HomePage /> or <AboutPage />) to render when the path matches the current URL.
Example Setup:
// src/main.jsx (Entry point)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; // Import BrowserRouter
import App from './App.jsx';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* 1. Wrap the entire App with BrowserRouter */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// src/App.jsx (Main application component)
import React from 'react';
import { Routes, Route } from 'react-router-dom'; // Import Routes and Route
import HomePage from './pages/HomePage'; // Import page components
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
import Layout from './components/Layout'; // Optional: A layout component
function App() {
return (
// Optional: Wrap Routes in a Layout component for shared UI (navbar, footer)
<Layout>
{/* 2. Define route mappings inside <Routes> */}
<Routes>
{/* 3. Define individual routes */}
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
{/* Add more routes as needed */}
{/* <Route path="/products" element={<ProductsPage />} /> */}
{/* Catch-all route for 404 Not Found */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Layout>
);
}
export default App;
// --- Example Page Components ---
// src/pages/HomePage.jsx
import React from 'react';
import { Link } from 'react-router-dom'; // Import Link for navigation
function HomePage() {
return (
<div>
<h2>Home Page</h2>
<p>Welcome to our website!</p>
<p>Check out the <Link to="/about">About Page</Link>.</p>
</div>
);
}
export default HomePage;
// src/pages/AboutPage.jsx
import React from 'react';
function AboutPage() {
return (
<div>
<h2>About Page</h2>
<p>This is the about section.</p>
</div>
);
}
export default AboutPage;
// src/pages/NotFoundPage.jsx
import React from 'react';
import { Link } from 'react-router-dom';
function NotFoundPage() {
return (
<div>
<h2>404 - Page Not Found</h2>
<p>Oops! The page you are looking for does not exist.</p>
<Link to="/">Go back to Home</Link>
</div>
);
}
export default NotFoundPage;
// --- Optional Layout Component ---
// src/components/Layout.jsx
import React from 'react';
import { Link } from 'react-router-dom'; // Use Link for navigation
function Layout({ children }) {
const navStyle = {
backgroundColor: '#f0f0f0',
padding: '10px',
marginBottom: '20px',
borderBottom: '1px solid #ccc'
};
const linkStyle = {
margin: '0 10px',
textDecoration: 'none',
color: 'blue'
};
return (
<div>
{/* Shared Navigation Bar */}
<nav style={navStyle}>
<Link style={linkStyle} to="/">Home</Link>
<Link style={linkStyle} to="/about">About</Link>
{/* Add other links: <Link style={linkStyle} to="/products">Products</Link> */}
</nav>
{/* Main content area where routed components will be rendered */}
<main style={{ padding: '0 20px' }}>
{children} {/* Renders the matched <Route>'s element */}
</main>
{/* Optional Shared Footer */}
<footer style={{ marginTop: '30px', paddingTop: '10px', borderTop: '1px solid #ccc', textAlign: 'center', fontSize: '0.9em' }}>
My React App © {new Date().getFullYear()}
</footer>
</div>
);
}
export default Layout;
- Structure: We wrap App in BrowserRouter in main.jsx. Inside App, we use <Routes> to contain our <Route> definitions. Each <Route> maps a URL path to a page component element.
- Layout Component: Using a Layout component (like the example Layout.jsx) is a common pattern. It wraps the <Routes> (or is placed directly inside App to wrap the <Routes> element) and contains shared UI elements like navigation bars, sidebars, or footers. The children prop within the Layout component will render the matched route’s element.
- path=”*”: This special path acts as a wildcard and will match any URL that hasn’t been matched by previous routes. It’s commonly used for rendering a “404 Not Found” page.
14.3 Linking Between Pages (<Link>)
To navigate between routes without causing a full page reload (which would defeat the purpose of an SPA), React Router provides the <Link> component.
- Usage: Use <Link> instead of standard <a> tags for internal navigation within your React app.
- to Prop: Instead of href, <Link> uses the to prop to specify the target URL path.
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* Use Link for client-side navigation */}
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User Profile (Example)</Link> {/* Example with parameter */}
{/* Use standard <a> for external links */}
<a href="https://reactrouter.com" target="_blank" rel="noopener noreferrer">
React Router Docs (External)
</a>
</nav>
);
}
When you click a <Link>, React Router intercepts the navigation, updates the URL in the address bar using the History API, and re-renders the appropriate component matched by <Routes> and <Route>, all without requesting a new page from the server.
14.4 Basic Route Parameters (useParams)
Often, you need routes that capture dynamic segments from the URL, like user IDs, product slugs, etc. React Router allows this using route parameters in the path definition.
- Define the Route with a Parameter: Use the colon (:) prefix in the path prop of your <Route> to denote a parameter.
// In App.jsx or wherever Routes are defined
import UserProfilePage from './pages/UserProfilePage';
<Routes>
{/* ... other routes ... */}
<Route path="/users/:userId" element={<UserProfilePage />} />
{/* ':userId' is the parameter name */}
{/* This will match /users/1, /users/alice, /users/abc-123, etc. */}
</Routes>
2. Access the Parameter with useParams Hook: Inside the component rendered by that route (e.g., UserProfilePage), use the useParams hook provided by react-router-dom to access the values captured by the parameters.
// src/pages/UserProfilePage.jsx
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom'; // Import useParams
function UserProfilePage() {
// useParams returns an object where keys are parameter names
// and values are the actual values from the URL
const params = useParams();
const userId = params.userId; // Access the 'userId' parameter value
// Example: Fetch user data based on the userId from the URL
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.ok ? res.json() : Promise.reject('User not found'))
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
console.error(err);
setUser(null); // Clear user if fetch fails
setLoading(false);
});
}, [userId]); // Re-fetch if userId changes (e.g., navigating from /users/1 to /users/2)
if (loading) return <p>Loading user profile for ID: {userId}...</p>;
return (
<div>
<h2>User Profile Page</h2>
<p>Displaying profile for User ID: <strong>{userId}</strong></p>
{user ? (
<div>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Website:</strong> {user.website}</p>
</div>
) : (
<p>User data not found.</p>
)}
<hr />
<Link to="/users/1">View User 1</Link> |{' '}
<Link to="/users/2">View User 2</Link> |{' '}
<Link to="/">Back to Home</Link>
</div>
);
}
export default UserProfilePage;
- useParams() returns an object like { userId: ‘123’ } if the current URL is /users/123.
- You can then use this extracted userId to fetch data specific to that user or perform other logic.
React Router provides the foundation for navigation in React SPAs. These core concepts (BrowserRouter, Routes, Route, Link, useParams) allow you to create multi-view applications with clean URLs and efficient client-side transitions. There are more advanced features (nested routes, navigation hooks, search parameters, etc.), but this covers the basics effectively.
Chapter 15: Performance Optimizations (Introduction)
While React is generally fast due to its Virtual DOM and diffing algorithm, sometimes you might encounter performance bottlenecks, especially in large or complex applications with frequent updates. React provides tools to help optimize rendering behavior.
Important Note: Optimization adds complexity. Don’t optimize prematurely. Use tools like the React DevTools Profiler to identify actual performance problems before applying these techniques. Often, the default behavior is fast enough.
The primary goal of these optimizations is often to prevent unnecessary re-renders of components. By default, when a parent component re-renders (due to its own state or prop changes), React will re-render all of its child components as well, even if the props passed to those children haven’t changed. Memoization techniques help skip these unnecessary re-renders.
15.1 React.memo (Memoizing Functional Components)
React.memo is a Higher-Order Component (HOC). It wraps your functional component and memoizes the result. This means React will skip rendering the component if its props have not changed (using a shallow comparison).
- How it works: React.memo performs a shallow comparison of the component’s previous props and next props. If they are the same, React reuses the last rendered result, skipping the re-render.
- Shallow Comparison: Checks only the top-level properties. For objects or arrays passed as props, it checks if the reference is the same, not if the content inside the object/array has changed.
Usage: Wrap the component export with React.memo.
// src/components/UserInfoDisplay.jsx
import React from 'react';
// A potentially "expensive" component or one that re-renders often unnecessarily
function UserInfoDisplay({ user }) {
console.log(`Rendering UserInfoDisplay for ${user.name}`); // Log renders
// Imagine some complex calculation or rendering based on user props
// const processedData = expensiveCalculation(user.details);
return (
<div style={{ border: '1px solid green', margin: '10px', padding: '10px' }}>
<h4>User Info (Memoized)</h4>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
{/* <p>Details: {processedData}</p> */}
</div>
);
}
// Wrap the component with React.memo before exporting
// Now, this component will only re-render if the 'user' prop changes (shallowly)
export default React.memo(UserInfoDisplay);
// --- Parent Component ---
// src/UserDashboard.jsx
import React, { useState } from 'react';
import UserInfoDisplay from './components/UserInfoDisplay';
function UserDashboard() {
const [user, setUser] = useState({ id: 1, name: 'Alice', email: 'alice@example.com' });
const [counter, setCounter] = useState(0); // Some other state in the parent
console.log('Rendering UserDashboard');
const updateUser = () => {
// Correctly update state by creating a NEW object
setUser({ id: 1, name: 'Alice Smith', email: 'alice.smith@example.com' });
};
const incrementCounter = () => {
setCounter(c => c + 1); // This state change causes UserDashboard to re-render
};
return (
<div>
<h2>User Dashboard (Parent)</h2>
<button onClick={updateUser}>Update User Name</button>
<button onClick={incrementCounter}>Increment Counter: {counter}</button>
<hr />
{/* UserInfoDisplay receives the 'user' object as a prop */}
<UserInfoDisplay user={user} />
</div>
);
}
export default UserDashboard;
- In this example, without React.memo, UserInfoDisplay would re-render every time you click “Increment Counter” because its parent (UserDashboard) re-renders.
- With React.memo, clicking “Increment Counter” only re-renders UserDashboard. UserInfoDisplay’s user prop hasn’t changed, so React skips re-rendering it (you won’t see its console.log message).
- Clicking “Update User Name” does cause UserInfoDisplay to re-render because the user prop has changed (it’s a new object reference).
- Custom Comparison: React.memo accepts an optional second argument: a custom comparison function (prevProps, nextProps) => boolean. If you provide this, React uses your function instead of shallow comparison. Return true if props are equal (skip render), false otherwise. Use this rarely and with caution.
15.2 useCallback (Memoizing Functions)
React.memo often breaks if you pass functions (like event handlers) as props. This is because functions are typically redefined on every render of the parent component, creating a new function reference each time. Even if the function body is identical, the reference changes, causing the shallow prop comparison in React.memo to fail.
useCallback is a Hook that returns a memoized version of the callback function. This memoized function reference only changes if one of its dependencies has changed.
Usage: Wrap your function definition in useCallback.
// src/components/MemoizedButton.jsx
import React from 'react';
// Assume this button is expensive or we want to optimize its parent
function MemoizedButton({ onClick, label }) {
console.log(`Rendering MemoizedButton: ${label}`);
return <button onClick={onClick}>{label}</button>;
}
// Wrap with React.memo so it only re-renders if props change
export default React.memo(MemoizedButton);
// --- Parent Component ---
// src/ActionPanel.jsx
import React, { useState, useCallback } from 'react'; // Import useCallback
import MemoizedButton from './components/MemoizedButton';
function ActionPanel() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(''); // Other state
console.log('Rendering ActionPanel');
// WITHOUT useCallback: This function is recreated on every ActionPanel render
// const handleIncrement = () => {
// setCount(c => c + 1);
// };
// WITH useCallback: Memoizes the handleIncrement function.
// The returned function reference only changes if dependencies change (none here).
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
console.log('handleIncrement called');
}, []); // Empty dependency array: function is created once
// Example with dependency:
const handleValueLog = useCallback(() => {
console.log('Current value:', value);
}, [value]); // Recreates function only if 'value' state changes
return (
<div>
<h2>Action Panel (Parent using useCallback)</h2>
<p>Count: {count}</p>
<input type="text" value={value} onChange={e => setValue(e.target.value)} placeholder="Type something"/>
{/* Pass the memoized callback down */}
<MemoizedButton onClick={handleIncrement} label="Increment Count" />
<MemoizedButton onClick={handleValueLog} label="Log Value" />
{/* This button uses an inline function - its 'onClick' prop changes on every render */}
{/* Even if MemoizedButton is memoized, it WILL re-render because the onClick prop changes */}
<MemoizedButton onClick={() => console.log("Inline handler!")} label="Inline Handler Button" />
</div>
);
}
export default ActionPanel;
- In ActionPanel, handleIncrement is wrapped in useCallback with an empty dependency array ([]). This means useCallback returns the exact same function reference across all renders of ActionPanel.
- When ActionPanel re-renders (e.g., because value state changes), the handleIncrement prop passed to the first MemoizedButton remains the same reference. Because MemoizedButton is wrapped in React.memo and its onClick prop didn’t change, it skips re-rendering.
- handleValueLog depends on value. It will return the same function reference unless the value state changes, in which case a new memoized function referencing the updated value is created.
- The “Inline Handler Button” re-renders every time because the inline arrow function () => console.log(…) creates a new function reference on every render, breaking the memoization.
- When to use: Primarily when passing callbacks to optimized child components (React.memo) or when functions are dependencies in useEffect hooks (to prevent the effect from running unnecessarily). Don’t wrap every function in useCallback — only where needed for optimization.
15.3 useMemo (Memoizing Values)
useMemo is similar to useCallback, but instead of memoizing a function, it memoizes the result of a function call (a value). It recomputes the memoized value only when one of its dependencies has changed.
- Purpose: Useful for optimizing expensive calculations that shouldn’t run on every render, or for memoizing objects/arrays passed as props to memoized components (to ensure referential stability).
Usage: Wrap the calculation/value creation in useMemo.
import React, { useState, useMemo } from 'react';
import UserInfoDisplay from './components/UserInfoDisplay'; // Assume this is memoized
// Simulate an expensive calculation
function calculateHighScores(users) {
console.log("Calculating high scores... (Expensive!)");
// Imagine complex filtering, sorting, mapping...
return users
.filter(u => u.score > 100)
.sort((a, b) => b.score - a.score)
.map(u => u.name);
}
function ScoreBoard() {
const [users, setUsers] = useState([
{ id: 1, name: 'Alice', score: 150 },
{ id: 2, name: 'Bob', score: 90 },
{ id: 3, name: 'Charlie', score: 120 },
]);
const [filterText, setFilterText] = useState(''); // Some unrelated state
console.log("Rendering ScoreBoard");
// WITHOUT useMemo: calculateHighScores runs on *every* ScoreBoard render
// const highScorers = calculateHighScores(users);
// WITH useMemo: calculation runs only when 'users' array changes
const highScorers = useMemo(() => {
return calculateHighScores(users);
}, [users]); // Dependency array includes 'users'
// Example: Memoizing an object prop for a memoized child
const userForDisplay = useMemo(() => ({
name: users[0].name,
email: `${users[0].name.toLowerCase()}@example.com`
}), [users]); // Only recreate if users array changes
return (
<div>
<h2>Score Board (useMemo)</h2>
<input
type="text"
placeholder="Filter (just causes re-render)"
value={filterText}
onChange={e => setFilterText(e.target.value)}
/>
{/* This button changes the 'users' dependency, triggering recalculation */}
<button onClick={() => setUsers([...users, {id: Date.now(), name: 'Newbie', score: 101}])}>Add User</button>
<h3>High Scorers:</h3>
<ul>
{highScorers.map(name => <li key={name}>{name}</li>)}
</ul>
<hr />
{/* Pass the memoized object to the memoized child */}
{/* If userForDisplay wasn't memoized, UserInfoDisplay would re-render on every ScoreBoard render */}
{/* even if the user data content was the same, because a new object {} is created each time */}
<UserInfoDisplay user={userForDisplay} />
</div>
);
}
export default ScoreBoard;
- In ScoreBoard, typing in the filter input causes the component to re-render. Without useMemo, calculateHighScores would run every time you type a character, even though the users data hasn’t changed.
- With useMemo, calculateHighScores is only called when the component first mounts and subsequently only if the users state array changes (e.g., when “Add User” is clicked). Typing in the filter input does not trigger the expensive calculation.
- userForDisplay uses useMemo to ensure that the object passed to the memoized UserInfoDisplay component maintains the same reference unless the underlying users data changes, preventing unnecessary re-renders of the child.
15.4 When and Why to Optimize
- Profile First: Always use the React DevTools Profiler to measure your component rendering times and identify bottlenecks before optimizing.
- Target High-Frequency Updates: Focus optimization efforts on components that re-render very frequently or components involved in complex interactions (like drag-and-drop, animations).
- Target Expensive Renders: Optimize components whose render function involves genuinely expensive calculations or generates very large/complex JSX trees.
- Memoize Stable Props: Use React.memo on components that often receive the same props but are forced to re-render due to parent updates. Use useCallback and useMemo in the parent to stabilize function/object/array props passed to these memoized children.
- Avoid Premature Optimization: Adding memo, useCallback, and useMemo everywhere can make code harder to read and maintain, and might even slightly worsen performance if used unnecessarily due to the overhead of memoization itself. Apply them strategically where measurements show a benefit.
These tools (React.memo, useCallback, useMemo) provide powerful ways to control and optimize rendering performance in React when needed.
This post is based on interaction with https://aistudio.corp.google.com/.
Happy learning!!!