Chapter 4 - Listing, Stylings and Forms

In this blog, we are going to learn a lot, starting with styling, then how to render lists, and finally, form handling. In the end, we will make a small to-do application.
Let's start...
Understanding logic makes you a great developer, but if you can't style, you can't develop frontend applications. So here it is.
There are many ways to style but in this blog we will learn the below ways
Inline Styles
CSS Modules
Styled-components
Using Third-Party Libraries like Material-UI
Tailwind CSS
Shadcn ui (we will learn this in a different blog but the same course)
1. Inline Styles
Inline styles are the simplest way to apply styles in React. Instead of using external stylesheets or CSS files, you define styles directly within your components using JavaScript objects. These objects are passed to the style
prop of a React element.
How It Works:
Keys in the style object are camelCased versions of CSS properties (e.g.,
backgroundColor
instead ofbackground-color
).Values are strings or numbers representing CSS values.
Example:
const MyComponent = () => {
return (
<div
style={{
color: "blue",
backgroundColor: "lightgray",
padding: "10px",
borderRadius: "5px",
fontSize: "16px", // CamelCase for CSS properties
}}
>
Hello, World!
</div>
);
};
Pros:
Simple and Quick: No need for external files or additional setup.
Scoped to Component: Styles are applied only to the specific component.
Dynamic Styling: You can easily compute styles based on component state or props.
Cons:
Limited Functionality: No support for pseudo-classes (
:hover
,:focus
), pseudo-elements, or media queries.Hard to Maintain: Inline styles can become messy in larger applications.
Performance Overhead: React re-renders the style object on every render, which can impact performance.
When to Use:
Small projects or prototypes.
Dynamic styles that depend on component state or props
2. CSS Modules
CSS Modules are a step up from inline styles. They allow you to write traditional CSS files but with the added benefit of locally scoped styles. This means that class names are unique to the component, preventing style conflicts across your application.
How It Works:
Create a
.module.css
file (e.g.,MyComponent.module.css
).Import the CSS Module into your component and use the generated class names.
Example:
- Create a CSS Module file (
MyComponent.module.css
):
.container {
color: blue;
background-color: lightgray;
padding: 10px;
border-radius: 5px;
}
.title {
font-size: 24px;
font-weight: bold;
}
- Import and use the CSS Module in your component:
import styles from './MyComponent.module.css';
const MyComponent = () => {
return (
<div className={styles.container}>
<h1 className={styles.title}>Hello, World!</h1>
</div>
);
};
Pros:
Locally Scoped Styles: Prevents class name collisions.
Familiar Syntax: Uses standard CSS.
Separation of Concerns: Keeps styles and logic separate.
Cons:
Limited Dynamic Styling: Harder to apply styles based on component state or props.
Additional Setup: Requires Webpack or a similar bundler to work.
When to Use:
Medium to large projects where you want to avoid global style conflicts.
Teams comfortable with traditional CSS.
Pros:
Locally Scoped Styles: Prevents class name collisions.
Familiar Syntax: Uses standard CSS.
Separation of Concerns: Keeps styles and logic separate.
Cons:
Limited Dynamic Styling: Harder to apply styles based on component state or props.
Additional Setup: Requires Webpack or a similar bundler to work.
When to Use:
Medium to large projects where you want to avoid global style conflicts.
Teams comfortable with traditional CSS.
3. Styled-components
styled-components
is a CSS-in-JS library that allows you to write CSS directly within your JavaScript code. It enables you to create reusable, styled components with dynamic styling capabilities.
How It Works:
Define styles using template literals.
Styles are scoped to the component and can be dynamically adjusted based on props.
Example:
- Install
styled-components
:
npm install styled-components
- Create a styled component:
import styled from 'styled-components';
const StyledDiv = styled.div`
color: ${(props) => (props.primary ? 'white' : 'blue')};
background-color: ${(props) => (props.primary ? 'blue' : 'lightgray')};
padding: 10px;
border-radius: 5px;
font-size: 16px;
&:hover {
background-color: darkblue;
}
`;
const MyComponent = () => {
return (
<>
<StyledDiv>Hello, World!</StyledDiv>
<StyledDiv primary>Primary Button</StyledDiv>
</>
);
};
Pros:
Dynamic Styling: Easily adjust styles based on props or state.
No Class Name Collisions: Styles are scoped to the component.
Full CSS Power: Supports pseudo-classes, media queries, and animations.
Cons:
Learning Curve: Requires familiarity with CSS-in-JS syntax.
Bundle Size: Adds some overhead to your JavaScript bundle.
When to Use:
Projects requiring dynamic or theme-based styling.
Teams comfortable with CSS-in-JS.
4. Using Third-Party Libraries like Material-UI
Material-UI is a popular React UI framework that provides pre-designed components following Google's Material Design guidelines. It also includes a powerful theming system and styling solution.
How It Works:
Use pre-built components like
Button
,Card
, andTextField
.Customize components using the
sx
prop or themakeStyles
API.
Example:
- Install Material-UI:
npm install @mui/material @emotion/react @emotion/styled
- Use a Material-UI component:
import Button from '@mui/material/Button';
const MyComponent = () => {
return (
<Button variant="contained" color="primary">
Hello, World!
</Button>
);
};
Pros:
Pre-Designed Components: Saves development time.
Consistent Design System: Follows Material Design guidelines.
Theming: Easily customize the look and feel of your app.
Cons:
Bundle Size: Adds significant overhead to your app.
Learning Curve: Requires familiarity with Material-UI's APIs.
When to Use:
Projects requiring a consistent design system.
Teams that prefer pre-built components.
5. Tailwind CSS
Tailwind CSS is a utility-first CSS framework that provides low-level utility classes to build designs directly in your markup. It promotes a functional approach to styling, where you apply small, single-purpose classes to elements.
Key Features:
Utility-First: Small, reusable classes for styling.
Responsive Design: Built-in support for responsive layouts.
Customization: Highly configurable via
tailwind.config.js
.
Installation
// version 4
npx create-react-app my-tailwind-app
cd my-tailwind-app
npm install tailwindcss
// version 3
npx create-react-app my-tailwind-app
cd my-tailwind-app
npm install tailwindcss postcss autoprefixer
npx tailwindcss init
// in version 3 we need to add tailwind.config.js file and few lines of code in index.css
Configure tailwind.config.js
:
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};
Add Tailwind to your CSS:
Create a src/index.css
file and add the following:
@tailwind base;
@tailwind components;
@tailwind utilities;
Example:
<div class="bg-blue-500 text-white p-4 rounded-lg">
Hello, World!
</div>
Pros:
Rapid Prototyping: Quickly build UIs without writing custom CSS.
Consistency: Enforces a consistent design system.
Customizable: Tailor the framework to your project's needs.
Cons:
Verbose HTML: Can lead to cluttered markup.
Learning Curve: Requires familiarity with utility classes.
When to Use:
Projects requiring rapid development.
Teams comfortable with utility-first CSS.
Building Components with Tailwind CSS
Tailwind CSS encourages building reusable components by combining utility classes. You can also extract repeated styles into custom components.
Example:
- Button Component:
const Button = ({ children }) => {
return (
<button className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
{children}
</button>
);
};
const MyComponent = () => {
return <Button>Click Me</Button>;
};
- Card Component:
const Card = ({ title, content }) => {
return (
<div className="bg-white shadow-lg rounded-lg p-6">
<h2 className="text-xl font-bold mb-4">{title}</h2>
<p className="text-gray-700">{content}</p>
</div>
);
};
const MyComponent = () => {
return <Card title="My Card" content="This is a card component built with Tailwind CSS." />;
};
Now a big project -
Let's create a custom component! Why do this? As you advance in development, you'll find yourself building more custom components rather than just using libraries and their components.
We are creating a box component similar to MUI.
The Box
component will accept a style
prop, which is an object containing CSS properties. These styles will be applied to the component using the style
attribute in JSX.
// Import PropTypes for type-checking the component's props
import PropTypes from 'prop-types';
// Define the Box component
const Box = ({ backgroundColor, color, padding, children, ...restStyles }) => {
// Default styles for the Box component
const defaultStyles = {
backgroundColor: backgroundColor || 'lightgray', // Default background color
color: color || 'black', // Default text color
padding: padding || '10px', // Default padding
borderRadius: '5px', // Default border radius
border: '1px solid #ccc', // Default border
textAlign: 'center', // Default text alignment
fontSize: '16px', // Default font size
};
// Merge default styles with any additional styles passed via `restStyles`
const boxStyles = { ...defaultStyles, ...restStyles };
// Render the Box component with the combined styles
return (
<div style={boxStyles}>
{children} {/* Render the children passed to the Box component */}
</div>
);
};
// Define PropTypes for the Box component to ensure type safety
Box.propTypes = {
backgroundColor: PropTypes.string, // Background color of the box
color: PropTypes.string, // Text color of the box
padding: PropTypes.string, // Padding inside the box
children: PropTypes.node.isRequired, // Content inside the box (required)
restStyles: PropTypes.object, // Additional styles passed as an object
};
// Export the Box component for use in other files
export default Box;
// Usage in App.js file
import React from 'react';
import Box from './Box'; // Import the Box component
// Define the App component
const App = () => {
return (
<div>
<h1>Box Component with Rest Styles</h1>
{/* Example usage of the Box component with custom styles */}
<Box
backgroundColor="lightblue" // Custom background color
color="darkblue" // Custom text color
padding="20px" // Custom padding
margin="10px" // Additional style: margin
boxShadow="0 4px 8px rgba(0, 0, 0, 0.2)" // Additional style: box shadow
>
This is a box with custom styles!
</Box>
{/* Another example usage of the Box component with different custom styles */}
<Box
backgroundColor="lightcoral" // Custom background color
color="darkred" // Custom text color
padding="15px" // Custom padding
border="2px dashed darkred" // Additional style: border
fontSize="20px" // Additional style: font size
>
This is another box with custom styles!
</Box>
</div>
);
};
// Export the App component
export default App;
Explanation of Changes
...restStyles
:The
...restStyles
syntax collects all additional props passed to the component that aren't explicitly destructured (e.g.,backgroundColor
,color
,padding
).These additional props are treated as custom styles.
Merging Styles:
The
defaultStyles
object contains the base styles for the component.The
boxStyles
object merges thedefaultStyles
with therestStyles
using the spread operator ({ ...defaultStyles, ...restStyles }
).If there are conflicting properties (e.g.,
backgroundColor
in bothdefaultStyles
andrestStyles
), therestStyles
values will take precedence.
With this, we conclude our Stylings section.
Now to Listings…
Rendering Lists in React
Using map()
to Render Lists
React makes it easy to render lists of data using JavaScript’s map()
function. The map()
function iterates over an array and returns a new array of React elements.
// Define an array of user objects with id, name, and age properties
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 },
];
// Define a functional React component to display a list of users
function UserList() {
return (
<ul>
{/* Iterate over the users array and render each user inside a list item */}
{users.map((user) => (
<li key={user.id}>
{/* Display user's name and age */}
{user.name} (Age: {user.age})
</li>
))}
</ul>
);
}
// Export the component to be used in other parts of the application
export default UserList;
Explanation:
The
map()
function iterates over theusers
array.For each
user
, a<li>
element is returned with the user’sname
andage
.The
key
prop is added to each<li>
element to help React identify individual list items.
The Importance of Keys
Why Keys Are Necessary
Keys are used by React to identify which items in a list have changed, been added, or been removed. They are essential for optimizing performance and ensuring correct behavior.
Why Are Keys Important?
React relies on the concept of a Virtual DOM to efficiently update the UI. When rendering lists, React needs to determine how to update the existing DOM with minimal operations. Keys help React:
Improve Performance: By uniquely identifying elements, React can update only the changed items instead of re-rendering the entire list.
Maintain Component State: Components associated with a key retain their state even if the list order changes.
Prevent UI Bugs: Without keys, React may incorrectly reuse components, leading to unexpected behavior.
Using Keys in React
When rendering a list in React, each element should have a unique key. Let’s look at an example:
import React from "react";
const TodoList = () => {
const todos = [
{ id: 1, task: "Learn React" },
{ id: 2, task: "Practice JavaScript" },
{ id: 3, task: "Build Projects" },
];
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.task}</li>
))}
</ul>
);
};
export default TodoList;
Explanation
Here, each
<li>
element has akey
attribute set totodo.id
.The
id
ensures that React can track each item correctly.When an item is added or removed, React only updates the necessary elements, improving performance.
Common Mistakes with Keys
1. Using Index as a Key
{todos.map((todo, index) => (
<li key={index}>{todo.task}</li>
))}
Why is this problematic?
If the list order changes (e.g., due to sorting or deletion), React may not properly track changes, leading to incorrect UI updates.
This can cause issues with component state retention.
2. Not Providing a Key
{todos.map((todo) => (
<li>{todo.task}</li>
))}
Why is this problematic?
React will show a warning in the console: Each child in a list should have a unique "key" prop.
React won’t efficiently update the list.
Best Practices for Using Keys
Always use a unique identifier (like an
id
) instead of an array index.If no
id
exists, consider generating a unique identifier using libraries likeuuid
.Ensure keys remain consistent across renders.
Arrays in State
import React, { useState } from 'react';
function UserList() {
// Initialize state with an array of user objects
const [users, setUsers] = useState([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
]);
// Function to add a new user to the list
const addUser = () => {
// Create a new user object with a unique ID and a default name
const newUser = { id: users.length + 1, name: 'New User' };
// Update the state by adding the new user to the existing users array
setUsers([...users, newUser]); // Using spread operator to maintain immutability
};
// Function to remove a user from the list by their ID
const removeUser = (id) => {
// Filter out the user with the specified ID and update the state
setUsers(users.filter((user) => user.id !== id)); // Immutable update
};
// Function to update a user's name by their ID
const updateUser = (id, newName) => {
// Map through the users array and update the name of the user with the specified ID
setUsers(
users.map((user) =>
user.id === id ? { ...user, name: newName } : user // Immutable update
)
);
};
return (
<div>
{/* Render the list of users */}
<ul>
{users.map((user) => (
<li key={user.id}>
{/* Display the user's name */}
{user.name}{' '}
{/* Button to update the user's name */}
<button onClick={() => updateUser(user.id, 'Updated Name')}>
Update
</button>{' '}
{/* Button to remove the user */}
<button onClick={() => removeUser(user.id)}>Remove</button>
</li>
))}
</ul>
{/* Button to add a new user */}
<button onClick={addUser}>Add User</button>
</div>
);
}
export default UserList;
Explanation
State Initialization:
- The
useState
hook initializes theusers
state with an array of user objects, each containing anid
and aname
.
- The
addUser
Function:Creates a new user object with a unique
id
(based on the current length of theusers
array) and a default name.Updates the state by spreading the existing
users
array and appending the new user.
removeUser
Function:- Filters out the user with the specified
id
and updates the state with the new array.
- Filters out the user with the specified
updateUser
Function:Maps through the
users
array and updates thename
of the user with the specifiedid
.Uses the spread operator to ensure immutability.
Rendering the List:
The
map()
function iterates over theusers
array and renders each user as a list item (<li>
).Each list item contains:
The user’s name.
An "Update" button to change the user’s name to "Updated Name".
A "Remove" button to delete the user from the list.
"Add User" Button:
- Triggers the
addUser
function to add a new user to the list.
- Triggers the
Key Points
Immutability: The code emphasizes immutability when updating state (using spread operator and
map()
/filter()
).Unique Keys: Each list item has a unique
key
prop (user.id
) to help React efficiently manage the list.Component Structure: The component is cleanly structured with separate functions for adding, removing, and updating users.
Looping Through Object Values
Extracting Specific Values from an Object
If you have an object and want to loop through specific values, you can use Object.keys()
, Object.values()
, or Object.entries()
.
Example: Rendering Object Values
// Define a user object with properties: id, name, age, and email
const user = {
id: 1,
name: 'Alice',
age: 25,
email: 'alice@example.com',
};
// UserDetails component to display the user's details
function UserDetails() {
return (
<ul>
{/* Use Object.entries() to convert the user object into an array of key-value pairs */}
{Object.entries(user).map(([key, value]) => (
// Render each key-value pair as a list item
<li key={key}>
{/* Display the key in bold */}
<strong>{key}:</strong>{' '}
{/* Display the corresponding value */}
{value}
</li>
))}
</ul>
);
}
export default UserDetails;
Explanation:
Object.entries(user)
returns an array of key-value pairs.We use
map()
to iterate over these pairs and render them as list items.
Rendering Nested Object Data
If your object contains nested data, you can use recursion or helper functions to render it.
Example: Rendering Nested Data
// Define a user object with nested properties
const user = {
id: 1,
name: 'Alice',
address: {
city: 'New York',
zip: '10001',
},
};
// UserDetails component to display the user's details, including nested data
function UserDetails() {
// Helper function to recursively render nested data
const renderNestedData = (data) => {
// Convert the data object into an array of key-value pairs using Object.entries()
return Object.entries(data).map(([key, value]) => (
// Render each key-value pair as a list item
<li key={key}>
{/* Display the key in bold */}
<strong>{key}:</strong>{' '}
{/* Check if the value is an object */}
{typeof value === 'object' ? (
// If the value is an object, recursively call renderNestedData to handle nested data
renderNestedData(value)
) : (
// If the value is not an object, display it directly
value
)}
</li>
));
};
// Render the user details inside an unordered list
return <ul>{renderNestedData(user)}</ul>;
}
export default UserDetails;
Explanation:
The
renderNestedData
function recursively renders nested objects.If a value is an object, the function calls itself to render the nested data.
Conditional Rendering in React
Conditional rendering lets you decide which parts of the UI to show based on certain conditions. It's often used in programming to display or hide stuff depending on what the user does, the state of the data, or what's going on in the system. This makes the user experience better by showing only the info that's important at the moment.
Using
if-else
StatementsUsing the Ternary Operator
Using Short-Circuit Evaluation
Using
switch
Statements
1. Using if-else
Statements
The if-else
statement is the most basic and flexible way to implement conditional rendering. It allows you to write clear and explicit logic for determining what to render.
Example:
import React from 'react';
function UserGreeting({ isLoggedIn, username }) {
if (isLoggedIn) {
return <h1>Welcome back, {username}!</h1>;
} else {
return <h1>Please sign up or log in.</h1>;
}
}
export default function App() {
return (
<div>
<UserGreeting isLoggedIn={true} username="JohnDoe" />
<UserGreeting isLoggedIn={false} />
</div>
);
}
Explanation:
The
UserGreeting
component takes two props:isLoggedIn
andusername
.If
isLoggedIn
istrue
, it renders a personalized welcome message.If
isLoggedIn
isfalse
, it renders a generic message asking the user to sign up or log in.In the
App
component, we render theUserGreeting
component twice with different prop values.
When to Use:
Use
if-else
when you have multiple conditions or complex logic that determines what to render.It’s ideal for scenarios where readability and explicitness are important.
2. Using the Ternary Operator
The ternary operator (condition ? expressionIfTrue : expressionIfFalse
) is a concise way to implement conditional rendering. It’s perfect for simple conditions where you need to choose between two options.
Example:
import React from 'react';
function Greeting({ isLoggedIn, username }) {
return (
<div>
{isLoggedIn ? (
<h1>Welcome back, {username}!</h1>
) : (
<h1>Please sign up or log in.</h1>
)}
</div>
);
}
export default function App() {
return (
<div>
<Greeting isLoggedIn={true} username="JaneDoe" />
<Greeting isLoggedIn={false} />
</div>
);
}
Explanation:
The ternary operator checks the condition (
isLoggedIn
).If the condition is
true
, it renders the first expression (<h1>Welcome back, {username}!</h1>
).If the condition is
false
, it renders the second expression (<h1>Please sign up or log in.</h1>
).
When to Use:
Use the ternary operator for simple conditions where you only need to choose between two options.
It’s great for inline rendering and keeping your code concise.
3. Using Short-Circuit Evaluation
Short-circuit evaluation leverages the behavior of the &&
operator in JavaScript. If the left-hand side of the &&
operator is false
, the right-hand side is not evaluated. This makes it useful for rendering a component or element only if a condition is true
.
Example:
import React from 'react';
function Notification({ messages }) {
return (
<div>
{messages.length > 0 && (
<h1>You have {messages.length} new messages!</h1>
)}
</div>
);
}
export default function App() {
const messages = ['Message 1', 'Message 2'];
return (
<div>
<Notification messages={messages} />
<Notification messages={[]} />
</div>
);
}
Explanation:
The
Notification
component checks if themessages
array has any items.If
messages.length > 0
istrue
, it renders the<h1>
element.If
messages.length > 0
isfalse
, nothing is rendered.In the
App
component, we render theNotification
component twice: once with messages and once with an empty array.
When to Use:
Use short-circuit evaluation when you want to render something only if a condition is
true
.It’s ideal for conditional rendering of single elements or components.
4. Using switch
Statements
The switch
statement is useful when you have multiple conditions and want to avoid long chains of if-else
statements. It’s particularly helpful when rendering different components based on a specific value.
Example:
import React from 'react';
function UserRole({ role }) {
switch (role) {
case 'admin':
return <h1>Welcome, Admin!</h1>;
case 'editor':
return <h1>Welcome, Editor!</h1>;
case 'subscriber':
return <h1>Welcome, Subscriber!</h1>;
default:
return <h1>Please log in to access your account.</h1>;
}
}
export default function App() {
return (
<div>
<UserRole role="admin" />
<UserRole role="editor" />
<UserRole role="subscriber" />
<UserRole role="guest" />
</div>
);
}
Explanation:
The
UserRole
component takes arole
prop and uses aswitch
statement to determine what to render.Depending on the value of
role
, it renders a different welcome message.If the
role
doesn’t match any of the cases, it renders a default message.
When to Use:
Use
switch
when you have multiple conditions based on a single variable or value.It’s ideal for scenarios where you want to avoid nested
if-else
statements.
Combining Techniques
In real-world applications, you’ll often need to combine these techniques to handle complex rendering logic. For example, you might use an if-else
statement for the main logic, a ternary operator for smaller conditions, and a switch
statement for multiple cases.
Example:
import React from 'react';
function Dashboard({ user }) {
if (!user) {
return <h1>Please log in to access the dashboard.</h1>;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
{user.isAdmin ? (
<p>You have admin privileges.</p>
) : (
<p>You have standard user privileges.</p>
)}
{user.hasUnreadNotifications && <p>You have unread notifications!</p>}
<UserRole role={user.role} />
</div>
);
}
function UserRole({ role }) {
switch (role) {
case 'admin':
return <p>Role: Administrator</p>;
case 'editor':
return <p>Role: Editor</p>;
case 'subscriber':
return <p>Role: Subscriber</p>;
default:
return <p>Role: Guest</p>;
}
}
export default function App() {
const user = {
name: 'John Doe',
isAdmin: true,
hasUnreadNotifications: true,
role: 'admin',
};
return (
<div>
<Dashboard user={user} />
<Dashboard user={null} />
</div>
);
}
Explanation:
The
Dashboard
component first checks if theuser
object exists. If not, it asks the user to log in.If the
user
exists, it displays a welcome message and checks additional conditions using the ternary operator and short-circuit evaluation.The
UserRole
component uses aswitch
statement to display the user’s role.
Best Practices for Conditional Rendering
Keep it Simple: Avoid overly complex conditional logic. Break it into smaller components or helper functions if necessary.
Use Meaningful Variable Names: Use descriptive variable names for conditions to improve readability.
Avoid Nesting: Deeply nested conditions can make your code hard to read. Use
switch
or helper functions to simplify.Leverage Component Composition: Break your UI into smaller components and use conditional rendering within those components.
Handling Events, Forms
There is no website without forms, and handling them is honestly awesome. So, let's understand how we can do it.
React uses synthetic events to provide a consistent interface for handling browser events across different environments.
In simple terms, React uses something called synthetic events to handle user interactions (like clicks or typing) in a way that works the same across all web browsers. Different browsers may behave slightly differently when handling events, but React takes care of these differences by wrapping the browser’s event in a standardized format. This ensures that events work consistently no matter which browser you’re using.
Here is a simple example of handling events-
import React from 'react';
function App() {
// Function to handle button click event
const handleClick = (e) => {
e.preventDefault(); // Prevents the default action (useful in forms, links, etc.)
console.log('Button clicked!', e); // Logs the event object to the console
};
return (
<div>
{/* Button with an onClick event listener */}
<button onClick={handleClick}>Click Me</button>
</div>
);
}
export default App;
Key Points:
Event Naming: React events are written in camelCase (e.g.,
onClick
,onChange
).Event Object: The event object (
e
) contains useful properties liketarget
,currentTarget
, andpreventDefault()
.Performance: React pools synthetic events for performance optimization, so you should not rely on the event object asynchronously.
Key Features of Synthetic Events:
Cross-browser consistency: React normalizes events so they work the same way in all browsers.
Event pooling: Synthetic events are pooled for performance reasons (more on this later).
Common properties: Synthetic events provide common properties like
target
,currentTarget
,type
, andtimestamp
.
Commonly Used Synthetic Events:
onClick
onChange
onSubmit
onKeyDown
onMouseOver
onFocus
onBlur
Passing Arguments to Event Handlers
You can pass additional arguments to event handlers using arrow functions or the bind
method.
Using Arrow Functions:
function App() {
const handleClick = (message) => {
alert(message);
};
return (
<button onClick={() => handleClick('Button clicked!')}>
Click Me
</button>
);
}
// Using bind:
function App() {
const handleClick = function(message) {
alert(message);
};
return (
<button onClick={handleClick.bind(this, 'Button clicked!')}>
Click Me
</button>
);
}
Event Pooling
React reuses synthetic events for performance reasons. After the event handler is called, the event object is nullified. If you need to access the event properties asynchronously, you must call event.persist()
.
function handleEvent(event) {
event.persist(); // Preserve the event object
setTimeout(() => {
console.log(event.type); // Access event properties asynchronously
}, 100);
}
Conditional Event Handling
You can conditionally handle events based on specific conditions, such as keyboard modifiers or component state.
function App() {
const handleClick = (event) => {
if (event.shiftKey) {
alert('Shift + Click');
} else {
alert('Button clicked!');
}
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
Event Delegation
Event delegation is a pattern where a single event listener is attached to a parent element to manage events for all of its child elements. Instead of attaching individual event listeners to each child element, the parent element listens for events that bubble up from its children. This approach reduces the number of event listeners in the DOM, which improves performance and simplifies event management.
How It Works:
Single Event Listener:
When your React application mounts, React attaches a single event listener (e.g., for
click
,change
, etc.) to the document or root DOM node.This listener captures all events that bubble up from the DOM.
Event Propagation:
When an event occurs (e.g., a button click), it bubbles up through the DOM tree.
The event is captured by React's root event listener.
Event Handling:
React determines which component should handle the event by inspecting the event's
target
property.React then invokes the appropriate event handler (e.g.,
onClick
) that you defined in your component.
Synthetic Events:
- React wraps the native event in a SyntheticEvent object, which provides a consistent API across browsers.
Example:
function App() {
// Function to handle button click event
const handleClick = (event) => {
console.log('Button clicked:', event.target);
// 'event.target' refers to the element that triggered the event
};
return (
<div>
{/* Event delegation: The event listener is attached to the button,
but 'event.target' allows us to access the actual element that was clicked. */}
<button onClick={handleClick}>Click Me</button>
</div>
);
}
export default App;
Benefits of Event Delegation in React
Performance Optimization:
By attaching a single event listener at the root, React minimizes the number of event listeners in the DOM.
This is especially beneficial for large applications with many interactive elements.
Memory Efficiency:
- Fewer event listeners mean less memory usage, which is crucial for long-running applications.
Simplified Event Management:
You don’t need to manually attach or remove event listeners for individual elements.
React handles event listener management automatically.
Consistent Behavior:
- React’s synthetic event system ensures that events behave consistently across all browsers.
Event Delegation vs. Direct Event Handling
Aspect | Event Delegation | Direct Event Handling |
Number of Listeners | Single listener at the root. | One listener per element. |
Performance | More efficient for large applications. | Less efficient for many elements. |
Memory Usage | Lower memory footprint. | Higher memory usage. |
Event Management | Automatic, handled by React. | Manual, requires explicit setup/cleanup. |
Use Case | Ideal for dynamic content or large lists. | Suitable for static content with few elements. |
How React Handles Dynamic Content
Event delegation is particularly useful for handling events on dynamically added or removed elements. Since the event listener is attached at the root, it doesn’t matter if elements are added or removed from the DOM—React will still handle their events correctly.
Example: Dynamic List
import React from 'react';
function App() {
// State to manage the list of items
const [items, setItems] = React.useState(['Item 1', 'Item 2']);
// Function to handle click events on list items
const handleClick = (event) => {
console.log('Clicked:', event.target.textContent);
// Logs the text content of the clicked <li> item
};
// Function to add a new item to the list
const addItem = () => {
setItems([...items, `Item ${items.length + 1}`]);
// Adds a new item dynamically to the state
};
return (
<div>
{/* Button to add a new item to the list */}
<button onClick={addItem}>Add Item</button>
{/* Unordered list (ul) containing dynamically generated list items (li) */}
<ul>
{items.map((item, index) => (
<li key={index} onClick={handleClick}>
{/* Each list item has an event listener for click events */}
{item}
</li>
))}
</ul>
</div>
);
}
export default App;
In this example:
New list items can be added dynamically.
The
handleClick
function works for all list items, even those added after the initial render.
Limitations of Event Delegation in React
Event Bubbling:
Event delegation relies on event bubbling. If an event does not bubble (e.g.,
focus
,blur
), React cannot handle it through delegation.For non-bubbling events, React attaches individual event listeners.
Event Pooling:
- React reuses synthetic events for performance reasons. If you need to access event properties asynchronously, you must call
event.persist()
.
- React reuses synthetic events for performance reasons. If you need to access event properties asynchronously, you must call
Custom Event Handling:
- For custom events or non-React DOM elements, you may need to manually implement event delegation.
Best Practices for Event Delegation in React
Use React’s Built-in Event System: Rely on React’s synthetic events and avoid manually attaching event listeners to DOM elements.
Avoid Inline Event Handlers: Inline arrow functions in JSX can cause unnecessary re-renders. Define event handlers outside the JSX.
Clean Up Listeners: If you manually attach event listeners (e.g., for custom events), clean them up in
componentWillUnmount
or theuseEffect
cleanup function.Leverage Conditional Event Handling: Use conditions within event handlers to handle different scenarios (e.g.,
event.shiftKey
).
Event Propagation: Capturing and Bubbling
Event propagation refers to the process by which an event travels through the DOM tree. There are two main phases of event propagation:
Capturing Phase:
The event starts from the root of the DOM tree and travels down to the target element.
This phase is rarely used in React but is important to understand for advanced use cases.
Bubbling Phase:
After reaching the target element, the event bubbles up from the target element back to the root of the DOM tree.
This is the phase where most event handling occurs in React.
<div id="parent">
<button id="child">Click Me</button>
</div>
When the button is clicked:
The event starts at the
document
and travels down to thebutton
(capturing phase).The event reaches the
button
(target phase).The event bubbles up from the
button
to thedocument
(bubbling phase).
Event Bubbling in React
React’s synthetic event system relies heavily on event bubbling. When an event occurs, React captures it during the bubbling phase and delegates it to the appropriate component.
Example:
function App() {
const handleParentClick = () => {
console.log('Parent clicked');
};
const handleChildClick = () => {
console.log('Child clicked');
};
return (
<div onClick={handleParentClick}>
<button onClick={handleChildClick}>Click Me</button>
</div>
);
}
// output
// Child clicked
// Parent clicked
When the button is clicked:
The
handleChildClick
function is called (child event handler).The event bubbles up to the parent
div
, and thehandleParentClick
function is called (parent event handler).
Stopping Event Propagation
Sometimes, you may want to prevent an event from bubbling up the DOM tree. You can do this using the event.stopPropagation()
method.
Example:
function App() {
const handleParentClick = () => {
console.log('Parent clicked');
};
const handleChildClick = (event) => {
event.stopPropagation(); // Stop event bubbling
console.log('Child clicked');
};
return (
<div onClick={handleParentClick}>
<button onClick={handleChildClick}>Click Me</button>
</div>
);
}
//output
// Child clicked
When the button is clicked:
The
handleChildClick
function is called.The event propagation is stopped, so the
handleParentClick
function is not called.
Event Capturing in React
While React primarily uses event bubbling, you can also handle events during the capturing phase by appending Capture
to the event name (e.g., onClickCapture
).
Example:
function App() {
const handleParentCapture = () => {
console.log('Parent captured');
};
const handleChildClick = () => {
console.log('Child clicked');
};
return (
<div onClickCapture={handleParentCapture}>
<button onClick={handleChildClick}>Click Me</button>
</div>
);
}
// output
// Parent captured
// Child clicked
When the button is clicked:
The
handleParentCapture
function is called during the capturing phase.The
handleChildClick
function is called during the bubbling phase.
Commonly Used Events in React
React supports a wide range of events, including mouse events, keyboard events, form events, and more. Here are some commonly used events:
Mouse Events:
onClick
: Triggered when an element is clicked.onDoubleClick
: Triggered when an element is double-clicked.onMouseEnter
: Triggered when the mouse pointer enters an element.onMouseLeave
: Triggered when the mouse pointer leaves an element.onMouseOver
: Triggered when the mouse pointer is over an element.onMouseOut
: Triggered when the mouse pointer leaves an element.
Keyboard Events:
onKeyDown
: Triggered when a key is pressed down.onKeyUp
: Triggered when a key is released.onKeyPress
: Triggered when a key is pressed (deprecated in modern browsers).
Form Events:
onChange
: Triggered when the value of an input, select, or textarea changes.onSubmit
: Triggered when a form is submitted.onFocus
: Triggered when an element receives focus.onBlur
: Triggered when an element loses focus.
Touch Events:
onTouchStart
: Triggered when a touch point is placed on the screen.onTouchMove
: Triggered when a touch point is moved along the screen.onTouchEnd
: Triggered when a touch point is removed from the screen.
Scroll Events:
onScroll
: Triggered when an element is scrolled.
Preventing Default Behavior
Some events have default behaviors (e.g., form submission, link navigation). You can prevent these behaviors using the event.preventDefault()
method.
Example:
function App() {
const handleSubmit = (event) => {
event.preventDefault(); // Prevent form submission
console.log('Form submitted');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Enter text" />
<button type="submit">Submit</button>
</form>
);
}
Event Pooling in React
React reuses synthetic event objects for performance reasons. After the event handler is called, the event object is nullified. If you need to access event properties asynchronously, you must call event.persist()
.
Example:
function handleEvent(event) {
event.persist(); // Preserve the event object
setTimeout(() => {
console.log(event.type); // Access event properties asynchronously
}, 100);
}
Best Practices for Event Handling in React
Use Functional Components and Hooks:
- Functional components with hooks are simpler and more modern than class components.
Avoid Inline Arrow Functions:
- Inline arrow functions in JSX can cause unnecessary re-renders. Define event handlers outside the JSX.
Clean Up Event Listeners:
- If you manually attach event listeners (e.g., for custom events), clean them up in
componentWillUnmount
or theuseEffect
cleanup function.
- If you manually attach event listeners (e.g., for custom events), clean them up in
Leverage Conditional Event Handling:
- Use conditions within event handlers to handle different scenarios (e.g.,
event.shiftKey
).
- Use conditions within event handlers to handle different scenarios (e.g.,
Test Event Handlers:
- Write unit tests for your event handlers to ensure they behave as expected.
Controlled vs Uncontrolled Components
Controlled Components
In controlled components, the form data is managed by React state. The component re-renders whenever the state changes.
import React, { useState } from 'react';
function ControlledForm() {
// State to store the input value
const [name, setName] = useState('');
// Function to handle form submission
const handleSubmit = (e) => {
e.preventDefault(); // Prevents page reload on form submission
alert(`Name: ${name}`); // Displays the entered name in an alert
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
{/* Input field controlled by state */}
<input
type="text"
value={name} // Input value is controlled by state
onChange={(e) => setName(e.target.value)} // Updates state on input change
/>
</label>
{/* Button to submit the form */}
<button type="submit">Submit</button>
</form>
);
}
export default ControlledForm;
Key Points:
The
value
of the input is tied to the state (value
).The
onChange
handler updates the state whenever the input changes.React controls the input’s value, making it a controlled component.
Advantages of Controlled Components
Single Source of Truth: The state acts as the single source of truth for the input’s value, making it easier to debug and manage.
Real-Time Validation: You can validate input data as the user types, providing immediate feedback.
Dynamic Updates: You can dynamically update other parts of the UI based on the input’s value.
Integration with Form Libraries: Controlled components work seamlessly with libraries like Formik and React Hook Form.
Disadvantages of Controlled Components
Performance Overhead: Each keystroke triggers a re-render, which can be inefficient for large forms or frequent updates.
Boilerplate Code: You need to write more code to manage state and event handlers.
Uncontrolled Components
In uncontrolled components, the form data is managed by the DOM itself. Instead of using React state, you use a ref
to access the input’s value when needed (e.g., during form submission).
import React, { useRef } from 'react';
function UncontrolledForm() {
// Creating a reference for the input field
const inputRef = useRef(null);
// Function to handle form submission
const handleSubmit = (e) => {
e.preventDefault(); // Prevents page reload on form submission
alert(`Name: ${inputRef.current.value}`); // Accesses the input value using the ref
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
{/* Input field using ref instead of state (Uncontrolled Component) */}
<input type="text" ref={inputRef} />
</label>
{/* Button to submit the form */}
<button type="submit">Submit</button>
</form>
);
}
export default UncontrolledForm;
Key Points:
The input’s value is not tied to React state.
You use a
ref
to access the input’s value when needed.The DOM manages the input’s value, making it an uncontrolled component.
Advantages of Uncontrolled Components
Performance: Since the component doesn’t re-render on every keystroke, uncontrolled components can be more performant for large forms.
Simplicity: Less boilerplate code is required since you don’t need to manage state or event handlers.
Integration with Non-React Code: Uncontrolled components are easier to integrate with non-React libraries or legacy code.
Disadvantages of Uncontrolled Components
Limited Control: You don’t have real-time access to the input’s value, making it harder to implement features like real-time validation.
Debugging Challenges: Since the state is managed by the DOM, debugging can be more difficult.
Less Predictable: The component’s behavior is less predictable because it relies on the DOM.
Key Differences Between Controlled and Uncontrolled Components
Feature | Controlled Components | Uncontrolled Components |
State Management | Managed by React state | Managed by the DOM |
Re-Renders | Re-renders on every change | No re-renders on change |
Real-Time Validation | Easy to implement | Harder to implement |
Performance | Can be slower for large forms | More performant for large forms |
Boilerplate Code | Requires more code | Requires less code |
Debugging | Easier to debug | Harder to debug |
Use Cases | Dynamic forms, real-time validation | Simple forms, performance-critical apps |
We have covered pretty much everything so now lets make a todo application
Here, we will use Tailwind CSS along with form handling and a complex use of useState.
https://github.com/vaishdwivedi1/practice-react
https://practice-react-7fyetv1ta-vaishdwivedi1s-projects.vercel.app/
Subscribe to my newsletter
Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by