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

  1. Inline Styles

  2. CSS Modules

  3. Styled-components

  4. Using Third-Party Libraries like Material-UI

  5. Tailwind CSS

  6. 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 of background-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:

  1. 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;
}
  1. 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:

  1. Install styled-components:
npm install styled-components
  1. 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, and TextField.

  • Customize components using the sx prop or the makeStyles API.

Example:

  1. Install Material-UI:
npm install @mui/material @emotion/react @emotion/styled
  1. 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:

  1. 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>;
};
  1. 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

  1. ...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.

  2. Merging Styles:

    • The defaultStyles object contains the base styles for the component.

    • The boxStyles object merges the defaultStyles with the restStyles using the spread operator ({ ...defaultStyles, ...restStyles }).

    • If there are conflicting properties (e.g., backgroundColor in both defaultStyles and restStyles), the restStyles 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 the users array.

  • For each user, a <li> element is returned with the user’s name and age.

  • 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:

  1. Improve Performance: By uniquely identifying elements, React can update only the changed items instead of re-rendering the entire list.

  2. Maintain Component State: Components associated with a key retain their state even if the list order changes.

  3. 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 a key attribute set to todo.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 like uuid.

  • 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

  1. State Initialization:

    • The useState hook initializes the users state with an array of user objects, each containing an id and a name.
  2. addUser Function:

    • Creates a new user object with a unique id (based on the current length of the users array) and a default name.

    • Updates the state by spreading the existing users array and appending the new user.

  3. removeUser Function:

    • Filters out the user with the specified id and updates the state with the new array.
  4. updateUser Function:

    • Maps through the users array and updates the name of the user with the specified id.

    • Uses the spread operator to ensure immutability.

  5. Rendering the List:

    • The map() function iterates over the users 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.

  6. "Add User" Button:

    • Triggers the addUser function to add a new user to the list.

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.

  1. Using if-else Statements

  2. Using the Ternary Operator

  3. Using Short-Circuit Evaluation

  4. 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 and username.

  • If isLoggedIn is true, it renders a personalized welcome message.

  • If isLoggedIn is false, it renders a generic message asking the user to sign up or log in.

  • In the App component, we render the UserGreeting 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 the messages array has any items.

  • If messages.length > 0 is true, it renders the <h1> element.

  • If messages.length > 0 is false, nothing is rendered.

  • In the App component, we render the Notification 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 a role prop and uses a switch 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 the user 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 a switch statement to display the user’s role.

Best Practices for Conditional Rendering

  1. Keep it Simple: Avoid overly complex conditional logic. Break it into smaller components or helper functions if necessary.

  2. Use Meaningful Variable Names: Use descriptive variable names for conditions to improve readability.

  3. Avoid Nesting: Deeply nested conditions can make your code hard to read. Use switch or helper functions to simplify.

  4. 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 like target, currentTarget, and preventDefault().

  • 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, and timestamp.

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. 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.

  2. Memory Efficiency:

    • Fewer event listeners mean less memory usage, which is crucial for long-running applications.
  3. Simplified Event Management:

    • You don’t need to manually attach or remove event listeners for individual elements.

    • React handles event listener management automatically.

  4. Consistent Behavior:

    • React’s synthetic event system ensures that events behave consistently across all browsers.

Event Delegation vs. Direct Event Handling

AspectEvent DelegationDirect Event Handling
Number of ListenersSingle listener at the root.One listener per element.
PerformanceMore efficient for large applications.Less efficient for many elements.
Memory UsageLower memory footprint.Higher memory usage.
Event ManagementAutomatic, handled by React.Manual, requires explicit setup/cleanup.
Use CaseIdeal 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

  1. 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.

  2. Event Pooling:

    • React reuses synthetic events for performance reasons. If you need to access event properties asynchronously, you must call event.persist().
  3. 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

  1. Use React’s Built-in Event System: Rely on React’s synthetic events and avoid manually attaching event listeners to DOM elements.

  2. Avoid Inline Event Handlers: Inline arrow functions in JSX can cause unnecessary re-renders. Define event handlers outside the JSX.

  3. Clean Up Listeners: If you manually attach event listeners (e.g., for custom events), clean them up in componentWillUnmount or the useEffect cleanup function.

  4. 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:

  1. 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.

  2. 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:

  1. The event starts at the document and travels down to the button (capturing phase).

  2. The event reaches the button (target phase).

  3. The event bubbles up from the button to the document (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:

  1. The handleChildClick function is called (child event handler).

  2. The event bubbles up to the parent div, and the handleParentClick 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:

  1. The handleChildClick function is called.

  2. 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:

  1. The handleParentCapture function is called during the capturing phase.

  2. 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

  1. Use Functional Components and Hooks:

    • Functional components with hooks are simpler and more modern than class components.
  2. Avoid Inline Arrow Functions:

    • Inline arrow functions in JSX can cause unnecessary re-renders. Define event handlers outside the JSX.
  3. Clean Up Event Listeners:

    • If you manually attach event listeners (e.g., for custom events), clean them up in componentWillUnmount or the useEffect cleanup function.
  4. Leverage Conditional Event Handling:

    • Use conditions within event handlers to handle different scenarios (e.g., event.shiftKey).
  5. 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

  1. 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.

  2. Real-Time Validation: You can validate input data as the user types, providing immediate feedback.

  3. Dynamic Updates: You can dynamically update other parts of the UI based on the input’s value.

  4. Integration with Form Libraries: Controlled components work seamlessly with libraries like Formik and React Hook Form.

Disadvantages of Controlled Components

  1. Performance Overhead: Each keystroke triggers a re-render, which can be inefficient for large forms or frequent updates.

  2. 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

  1. Performance: Since the component doesn’t re-render on every keystroke, uncontrolled components can be more performant for large forms.

  2. Simplicity: Less boilerplate code is required since you don’t need to manage state or event handlers.

  3. Integration with Non-React Code: Uncontrolled components are easier to integrate with non-React libraries or legacy code.

Disadvantages of Uncontrolled Components

  1. Limited Control: You don’t have real-time access to the input’s value, making it harder to implement features like real-time validation.

  2. Debugging Challenges: Since the state is managed by the DOM, debugging can be more difficult.

  3. Less Predictable: The component’s behavior is less predictable because it relies on the DOM.

Key Differences Between Controlled and Uncontrolled Components

FeatureControlled ComponentsUncontrolled Components
State ManagementManaged by React stateManaged by the DOM
Re-RendersRe-renders on every changeNo re-renders on change
Real-Time ValidationEasy to implementHarder to implement
PerformanceCan be slower for large formsMore performant for large forms
Boilerplate CodeRequires more codeRequires less code
DebuggingEasier to debugHarder to debug
Use CasesDynamic forms, real-time validationSimple 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/

0
Subscribe to my newsletter

Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaishnavi Dwivedi
Vaishnavi Dwivedi