Skip to content

Instantly share code, notes, and snippets.

@anshulkapoor018
Last active April 26, 2024 16:50
Show Gist options
  • Save anshulkapoor018/119b6696aa26131fbc429abf015efe15 to your computer and use it in GitHub Desktop.
Save anshulkapoor018/119b6696aa26131fbc429abf015efe15 to your computer and use it in GitHub Desktop.
React JS Notes

React Notes

1. Virtual DOM

  • React uses the virtual DOM to clone a node that already exists in the DOM

  • Subtrees are created in the virtual DOM and then rendered based on state changes

  • When a state change occurs two things happen:

    • React runs a diff to check what changed
    • Then it updates the DOM based on the result of that diff (Referred to as reconciliation)
  • The Virtual DOM is considered the magic behind React. It batches DOM operations to keep everything quick and snappy because DOM manipulation is SLOW

  • Once a state is changed it triggers the diff algorithm to check all components, re-rendering only those that have changed properties.

2. JSX (JavaScript XML)

JSX allows you to write HTML in your JavaScript code, which makes the code more readable and writing it feels like writing HTML. Under the hood, JSX translates into React.createElement() calls.

Example:

const element = <h1>Hello, world!</h1>;

This JSX returns a React element, which React then renders to the DOM.

3. Components

Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.

Class Component Example:

class  Welcome  extends  React.Component {
  render() {
	  return  <h1>Hello, {this.props.name}</h1>;
  }
}

Functional Component Example:

function  Welcome(props) {
   return  <h1>Hello, {props.name}</h1>;
}

Class components have additional features like local state and lifecycle methods, while functional components are simpler and rely on hooks for state management and side effects.

Differences in Class based and Function Based Component

Feature Class Component Functional Component
Syntax More verbose, uses ES6 class syntax. Simpler, uses function syntax (regular or arrow functions).
State Management Uses this.state and this.setState. Uses the useState hook.
Lifecycle Methods Has lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. Uses hooks like useEffect for lifecycle events.
Access to this Yes, this keyword is available. No this keyword, everything is accessed directly from function scope.
Use of Props Accessed via this.props. Accessed directly via function parameters.
Side Effects Managed through lifecycle methods. Managed using the useEffect hook.
Memoization No built-in hook, but can use higher-order components or libraries. Uses useMemo and useCallback hooks for memoization and returning memorized callbacks.
Type of Components Before React 16.8, all components with state or lifecycle methods had to be class components. Encouraged for all components post-React 16.8 due to hooks enabling state and lifecycle features.
Performance Slightly slower due to the nature of classes in JavaScript. Generally lighter and faster due to functions being less resource-intensive than classes.
Deployment More overhead in setup and teardown, affecting performance slightly. Less overhead, better performance in frequent mounting and unmounting scenarios.
Reusability Encourages more structured and object-oriented programming models. Promotes simpler, more reusable code structures using hooks for sharing logic.

4. Props and State

Props and State Props (short for properties) are read-only data passed down from parent components to child components. They allow components to be reusable and configurable. State, on the other hand, is mutable data managed within a component itself. It represents the component's local state and can be updated using the setState method in class components or the useState hook in functional components. Example Using State and Props:

import React, { useState } from 'react';

function Counter({ title }) {
  // useState hook to manage the count state
  const [count, setCount] = useState(0);

  // Function to increment the count
  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>{title}</h1> {/* Using props directly in the functional component */}
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

export default Counter;

In this example, title is a prop passed from a parent component, while count is a state variable managed within the Counter component.

5. Lifecycle Methods

Lifecycle methods are special methods in class components that allow you to hook into different stages of a component's lifecycle.

  • componentDidMount: Called after the component is mounted to the DOM. It's commonly used for fetching data or setting up subscriptions.
  • componentDidUpdate: Called after the component updates. It's used for performing side effects based on prop or state changes.
  • componentWillUnmount: Called before the component is unmounted from the DOM. It's used for cleanup tasks like cancelling subscriptions or timers. Example:
class Timer extends React.Component {
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    console.log('Tick tock');
  }

  render() {
    return <h1>Look at the console every second!</h1>;
  }
}

In this example, the componentDidMount method sets up an interval timer, and the componentWillUnmount method cleans it up when the component is unmounted.

In modern React development, the emphasis has increasingly shifted towards using functional components with hooks, which offer a more concise and intuitive way to handle side effects and state management compared to class components. Here’s how the lifecycle methods typically used in class components map to hooks in functional components:

React introduced Hooks in version 16.8, which allow you to use state and other React features without writing a class. Here’s how you can replicate the behavior of lifecycle methods in functional components:

Using useEffect for componentDidMount, componentDidUpdate, and componentWillUnmount:

import React, { useState, useEffect } from 'react';

function Timer() {
    // Using useState to maintain timer status
    const [tick, setTick] = useState(0);

    useEffect(() => {
        // Equivalent to componentDidMount and componentDidUpdate:
        const timerID = setInterval(() => {
            console.log('Tick tock');
            setTick(tick => tick + 1); // Update tick count to re-render component and see the change
        }, 1000);

        // Equivalent to componentWillUnmount:
        return () => {
            clearInterval(timerID);
        };
    }, []); // The empty dependency array makes this useEffect act like componentDidMount

    return <h1>Look at the console every second! Tick count: {tick}</h1>;
}
export default Timer;

Explanation

  • useEffect Hook: This hook is used to perform side effects in function components. It can be configured to run under different conditions:
    • No dependencies ([]): Pass an empty array to useEffect to emulate componentDidMount, ensuring the effect runs only once after the initial render.
    • No dependency array: Omitting the dependency array causes the effect to run after every render, similar to componentDidUpdate.
    • Cleanup function: Returning a function from useEffect is the way to perform cleanup, mimicking componentWillUnmount. This cleanup function runs when the component is about to unmount, or before the effect runs again.

Using hooks like useEffect not only simplifies the code but also aligns with functional programming principles, enhancing code readability and maintainability. It reduces the boilerplate associated with class components and provides a more direct API to react to changes in props and state. This shift in paradigm encourages developers to think of side effects as part of the rendering results, leading to a more declarative approach to component behavior.

6. Hooks

Hooks are functions that allow you to add state and lifecycle features to functional components. They provide a way to reuse stateful logic across components.

  • useState: Allows you to add state to functional components.

  • useEffect: Allows you to perform side effects, such as fetching data or subscribing to events.

  • useContext: Allows you to consume context in functional components.

  • useReducer: Provides a way to manage complex state logic with a reducer function.

  • useMemo: Allows you to memoize expensive computations.

  • useCallback: Allows you to memoize callback functions.

Rules of Hooks:

  • Only call hooks at the top level of functional components or other custom hooks.
  • Don't call hooks inside loops, conditions, or nested functions.
  • Always follow the same order when calling multiple hooks.

useState

useState lets you add state to function components. You declare a state variable and a function to update it.

Example:

  
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect

useEffect lets you perform side effects in function components. It can be used for data fetching, subscriptions, or manually changing the DOM.

Example:

function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Failed to fetch user:', error);
        // Optionally handle the error by setting state or showing an error message
      }
    };

    fetchUser();
  }, [userId]); // Only re-run the effect if userId changes

  return (
    <div>
      {user ? <p>Profile: {user.name}</p> : <p>Loading...</p>}
    </div>
  );
}

useContext

useContext lets you subscribe to React context without introducing nesting.

Example:

Context Creation and Provider Setup

import React, { createContext, useContext, useState } from 'react';

// Create a Context for the theme
const ThemeContext = createContext();

function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');

    // Toggle function to change the theme
    const toggleTheme = () => {
        setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
    };

    // The context value that will be supplied to any descendants of this provider.
    const value = {
        theme,
        toggleTheme
    };

    return (
        <ThemeContext.Provider value={value}>
            {children}
        </ThemeContext.Provider>
    );
}

Now, Consuming the Context in another Component

function ThemedButton() {
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
        <button
            onClick={toggleTheme}
            style={{ background: theme === 'dark' ? 'black' : 'white', color: theme === 'dark' ? 'white' : 'black' }}
        >
            Click me to toggle theme
        </button>
    );
}

useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Example:

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

useMemo and useCallback

These hooks help you optimise performance by memoizing values and functions.

  • useMemo returns a memoized value.
  • useCallback returns a memoized callback function.

Example:

import React, { useState, useMemo, useCallback } from 'react';

function ExpensiveComponent({ compute }) {
  // useMemo is used here to memorize the result of the compute function
  // It only recalculates if dependencies change—in this case, only if compute changes.
  const result = useMemo(() => compute(), [compute]);

  // This component renders the result of the compute function
  return <div>{result}</div>;
}

function App() {
  // useState is used to keep track of the count state with its initial value set to 0
  const [count, setCount] = useState(0);

  // useCallback is used to memorize the compute function
  // It ensures that compute doesn't change unless its dependencies change
  // Since there are no dependencies in the dependency array, it never changes
  const compute = useCallback(() => {
    // This represents an expensive computation
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
      sum += i;
    }
    return sum;
  }, []);

  return (
    <div>
      <p>Count: {count}</p> {/* Display the current count */}
      {/* Button to increment the count, triggers a re-render of App, but not compute recalculating */}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* ExpensiveComponent is passed the compute function. */}
      {/* Due to useCallback, compute function isn't recreated needlessly. */}
      <ExpensiveComponent compute={compute} />
    </div>
  );
}

7. Context API

The Context API allows React apps to produce global variables that can be passed around. This is the alternative to "prop drilling" or moving props from grandparent to child to parent, and so on.

Use Cases

  • Theme Switching: Passing a theme object down an app component tree.
  • User Authentication: Keeping user authentication status and information accessible.
  • Localization: Maintaining language settings for internationalisation.

Pros

  • Avoids prop drilling.
  • Makes data accessible by many components at different nesting levels.
  • Good for global data (e.g., user settings, UI themes).

Cons

  • Overuse can make components difficult to reuse if they rely heavily on context.
  • Changes in Context trigger re-renders of all consuming components, which might impact performance if not handled correctly.

Example:

import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? 'black' : 'white' }}>I am styled by theme context!</button>;
}

In this example, the ThemeContext is created using createContext. The App component provides the context value using the ThemeContext.Provider. The ThemedButton component consumes the context value using the useContext hook.

8. Higher-Order Components (HOCs)

Higher-Order Components are a pattern where a function takes a component and returns a new component, enhancing the original with additional data or behavior. They are used for reusing component logic.

Example:

import React from 'react';

// Create a Higher-Order Component that takes a component and returns a new component
function withUser(WrappedComponent) {
  return function (props) {
    // useState hook to manage user state in the HOC
    const [user] = React.useState('John Doe');

    // Render the WrappedComponent with the user state and any props passed down to this HOC
    return <WrappedComponent user={user} {...props} />;
  };
}

// Functional component that displays user information
function UserInfo(props) {
  return <div>User: {props.user}</div>;
}

// Apply the HOC to UserInfo
const UserInfoWithUser = withUser(UserInfo);

// App component that renders the UserInfoWithUser component
function App() {
  return <UserInfoWithUser />;
}

export default App;

withUser HOC: This function takes a component (WrappedComponent) and returns a new functional component.

Within this functional component:

  • The useState hook initializes the user state with "John Doe".
  • The WrappedComponent is rendered with the user prop and any other props that the component receives (...props). This setup ensures that the WrappedComponent has access to the user data along with any other props passed down.
  • UserInfo: This is a simple functional component that receives props and renders user information. It takes props.user and displays it in a div.
  • UserInfoWithUser: By applying withUser to UserInfo, you create a new component that has the user context built-in, thanks to the HOC. This means whenever UserInfoWithUser is used, it automatically receives the user prop.
  • App: This component serves as the entry point for the component hierarchy and renders UserInfoWithUser. Since UserInfoWithUser is wrapped with the withUser HOC, it displays the user information automatically.

Limitations of HOCs:

  • Name collisions can occur when using multiple HOCs on the same component.

  • Refs are not automatically forwarded to the wrapped component and need to be manually handled.

9. Routing

React Router is a library that enables routing in React applications. It allows you to handle navigation from one view to another without the page refreshing, which is ideal for single-page applications (SPAs).

Example:

import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

function Home() {
  return <h1>Home Page</h1>;
}

function About() {
  return <h1>About Page</h1>;
}

function App() {
  return (
    <Router>
      <div>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/about">About</Link>
        </nav>
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

In this example, the BrowserRouter component is used to enable routing. The Link component is used for navigation, and the Route component is used to define the mappings between URLs and components.

Advanced Routing Concepts:

  • Nested Routes: Allows you to define routes within other routes for more complex application structures.
  • Route Parameters: Allows you to pass dynamic values through the URL.
  • Programmatic Navigation: Allows you to navigate programmatically using the history object.

10. Performance Optimization

Optimising performance in React involves several strategies such as preventing unnecessary re-renders, using lazy loading, and memoizing components. Optimising performance is crucial for building efficient React applications. Here are some techniques for performance optimization:

  • React.memo: Memoizes functional components to prevent unnecessary rerender when props remain the same.
  • useMemo: Memoizes expensive computations to avoid unnecessary recalculations.
  • useCallback: Memoizes callback functions to prevent unnecessary re-creation on each render.
  • Lazy Loading: Allows you to split your application into smaller chunks and load them on-demand using React.lazy and Suspense.

Example Preventing unnecessary re-renders with React.memo:

import React, { useState, useMemo, useCallback } from 'react';

function ExpensiveComponent({ compute }) {
  const result = useMemo(() => compute(), [compute]);
  return <div>{result}</div>;
}

function App() {
  const [count, setCount] = useState(0);

  const compute = useCallback(() => {
    // Expensive computation
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
      sum += i;
    }
    return sum;
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveComponent compute={compute} />
    </div>
  );
}

In this example, the ExpensiveComponent uses useMemo to memoize the expensive computation passed through the compute prop. The App component uses useCallback to memoize the compute function, preventing unnecessary re-creation on each render.

11. Testing

Testing is an essential part of building robust React applications. Popular tools for testing React components include Jest and React Testing Library.

Example:

Component Code

import React, { useState } from 'react';

function Counter() {
  // State to keep track of the count
  const [count, setCount] = useState(0);

  // Function to increment the count
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>  {/* Display the current count */}
      <button onClick={increment}>Increment</button> {/* Button to trigger increment */}
    </div>
  );
}

export default Counter;

TestingCode

import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments counter when button is clicked', () => {
  render(<Counter />);
  const buttonElement = screen.getByText('Increment');
  fireEvent.click(buttonElement);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

In this example, the render function is used to render the Counter component. The screen object provides methods to query the rendered output. The fireEvent function is used to simulate user interactions, such as clicking a button. Finally, assertions are made using the expect function.

Advanced Testing Concepts:

Mocking Dependencies: Allows you to replace real dependencies with mock implementations for better control and isolation in tests.

Component Code

import React, { useState } from 'react';
import { logCount } from './analyticsService'; // Assume this is an external module.

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => {
      const newCount = prevCount + 1;
      logCount(newCount); // Logging the count to an external service.
      return newCount;
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

Testing Code

import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
import * as AnalyticsService from './analyticsService';

test('increments counter when button is clicked and logs to analytics', () => {
  // Mock the logCount function in the analyticsService module
  const mockLogCount = jest.spyOn(AnalyticsService, 'logCount').mockImplementation(() => {});

  render(<Counter />);
  const buttonElement = screen.getByText('Increment');
  fireEvent.click(buttonElement);

  expect(screen.getByText('Count: 1')).toBeInTheDocument();
  expect(mockLogCount).toHaveBeenCalledWith(1);

  // Clean up the mock
  mockLogCount.mockRestore();
});

Asynchronous Testing: Allows you to test components that involve asynchronous operations, such as API calls or timeouts.

Component Code

import React, { useState, useEffect } from 'react';

function Counter({ fetchInitialCount }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const fetchCount = async () => {
      const initialCount = await fetchInitialCount();
      setCount(initialCount);
    };

    fetchCount();
  }, [fetchInitialCount]);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;

Testing Code

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Counter from './Counter';

test('increments counter when button is clicked after fetching initial count', async () => {
  const mockFetchInitialCount = jest.fn().mockResolvedValue(5);

  render(<Counter fetchInitialCount={mockFetchInitialCount} />);
  await waitFor(() => expect(screen.getByText('Count: 5')).toBeInTheDocument());

  const buttonElement = screen.getByText('Increment');
  fireEvent.click(buttonElement);
  expect(screen.getByText('Count: 6')).toBeInTheDocument();
});

Integration Testing: Allows you to test the interaction between multiple components or modules to ensure they work together correctly.

Component Code

// Counter.js
import React, { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <button onClick={increment}>Increment</button>
      <DisplayCounter count={count} />
    </div>
  );
}

// DisplayCounter.js
export function DisplayCounter({ count }) {
  return <p>Count: {count}</p>;
}

Testing Code

import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';

test('increments counter when button is clicked, and DisplayCounter shows updated count', () => {
  render(<Counter />);
  const buttonElement = screen.getByText('Increment');
  fireEvent.click(buttonElement);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

12. Type Checking

Type checking can be done in React using PropTypes or TypeScript, which help ensure components use the correct data types and structure.

Using PropTypes Example:

import PropTypes from 'prop-types';

function User({ name, age }) {
  return <div>{name} is {age} years old</div>;
}

User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired
};

Using TypeScript:

interface UserProps {
  name: string;
  age: number;
}

function User({ name, age }: UserProps) {
  return <div>{name} is {age} years old</div>;
}

13. Advanced Patterns

Advanced component patterns like Render Props and Compound Components give you additional flexibility and power.

Render Props Example: Render props refers to a technique in React for sharing code between components using a prop whose value is a function. This function returns React elements, effectively allowing complex rendering and behavior to be encapsulated in a reusable component. The main idea is to pass a function to a component that will return React elements based on some internal state or functionality of the component that receives the function.

How Render Props Work? Component Structure: A component (often called a container component) defines some logic or state that it needs to share with other components. This could be anything like handling mouse movements, fetching data, or managing focus.

Prop Function: This container component receives a function as a prop, commonly referred to as a "render prop". This function typically takes some arguments (the state or data the container component manages) and returns a React element.

Calling the Render Prop: Inside the container component’s render method (or return statement for functional components), it calls this render prop function, passing it the necessary internal data.

Implementation by Consumers: Other components (consumers) that use this container component will provide a function as the render prop. This function will return JSX based on the inputs provided, effectively customizing the rendering based on the container’s state or behavior.

Example let's break down how it uses the render props pattern:

MouseTracker Component:

import React, { useState } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

Here, MouseTracker uses a render prop. It manages the mouse position state and passes this state to the render prop function, which is responsible for rendering something based on the mouse's position.

App Component Using MouseTracker:

function App() {
  return (
    <MouseTracker render={({ x, y }) => (
      <h1>Mouse is at ({x}, {y})</h1>
    )} />
  );
}

In this usage, App provides a function to MouseTracker that takes the mouse position and renders an h1 element displaying the coordinates.

Benefits of Render Props

  • Flexibility: Components using this pattern can be highly flexible, adapting to where and how they are used simply by the rendering logic provided to them.
  • Reusability: Logic like data fetching, mouse tracking, or any other dynamic interaction can be encapsulated in a single component and reused wherever needed.
  • Composition: Render props allow for better component composition and can be an alternative to using React context or higher-order components for sharing behavior.

This technique is particularly useful in scenarios where you need to manipulate children components based on the state or props of a parent component, without tightly coupling the components together. It encourages a clean separation of concerns where the logic and the rendering are decoupled, yet can interact seamlessly.

Compound Components Example:

In this example, the compound components pattern is used to create a reusable tab component. The Tabs component manages the state of the active tab, and the TabList, TabPanels, and TabPanel components work together to render the tab headers and content. The TabContext is used to share the active tab state between the components.

import React, { createContext, useContext, useState } from 'react';

const TabContext = createContext();

function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <TabContext.Provider value={{ activeIndex, setActiveIndex }}>
      {children}
    </TabContext.Provider>
  );
}

function TabList({ children }) {
  const { setActiveIndex } = useContext(TabContext);
  return React.Children.map(children, (child, index) =>
    React.cloneElement(child, { onClick: () => setActiveIndex(index) })
  );
}

function TabPanels({ children }) {
  const { activeIndex } = useContext(TabContext);
  return children[activeIndex];
}

function TabPanel({ children }) {
  return <div>{children}</div>;
}

function App() {
  return (
    <Tabs>
      <TabList>
        <button>Tab 1</button>
        <button>Tab 2</button>
      </TabList>
      <TabPanels>
        <TabPanel>Content 1</TabPanel>
        <TabPanel>Content 2</TabPanel>
      </TabPanels>
    </Tabs>
  );
}

14. Accessibility in React

  1. Accessibility It refers to the practice of designing and developing applications that can be used by people with disabilities. It ensures that everyone, regardless of their abilities, can access and interact with your application. In the context of React, accessibility involves following best practices and guidelines to make your components and overall application accessible.

    1.1 Why Accessibility is Important:

    • Inclusion: Accessibility ensures that people with disabilities can use and benefit from your application, promoting inclusivity and equal access.
    • Legal Compliance: Many countries have laws and regulations that require digital products to be accessible, such as the Americans with Disabilities Act (ADA) in the United States and the Web Content Accessibility Guidelines (WCAG) internationally.
    • Improved Usability: Accessible applications often have better usability for all users, not just those with disabilities. Accessible features like keyboard navigation and clear labeling can enhance the user experience for everyone.

    1.2 Achieving Accessibility in React:

    1. Use Semantic HTML:

      • Use appropriate HTML elements for their intended purpose (e.g., <button> for buttons, <a> for links).
      • Use heading elements (<h1> to <h6>) to structure content hierarchically.
      • Use landmarks like <main>, <nav>, <article>, <section>, etc., to provide a clear structure and navigation for screen reader users.
    2. Provide Alternative Text for Images:

      • Use the alt attribute to describe images that convey meaningful information.
      • For decorative images, use an empty alt attribute (alt="").
    3. Manage Focus:

      • Ensure that all interactive elements (buttons, links, form controls) are reachable and operable using the keyboard.
      • Use the tabIndex attribute judiciously to manage the focus order when necessary.
      • Provide a visible focus indicator for keyboard users.
    4. Use ARIA (Accessible Rich Internet Applications) when needed:

      • Use ARIA roles, states, and properties to enhance the semantics of custom components that don't have native HTML equivalents.
      • Examples include role="button", aria-label, aria-expanded, aria-hidden, etc.
      • Be cautious not to overuse ARIA and only apply it when necessary.
    5. Manage Dynamic Updates:

      • Use the aria-live attribute to announce dynamic changes to the page, such as form validation messages or dynamic content updates.
      • Ensure that dynamically updated content is properly associated with its corresponding controls.
    6. Test for Accessibility:

      • Perform manual testing using keyboard navigation and assistive technologies like screen readers.
      • Use accessibility auditing tools like aXe, WAVE, or Lighthouse to identify potential accessibility issues.
      • Conduct user testing with people who have disabilities to get real-world feedback.

    Example:

    function AccessibleButton({ onClick, children }) {
      return (
        <button onClick={onClick} aria-label="Click me">
          {children}
        </button>
      );
    }
    
    function AccessibleImage({ src, alt }) {
      return <img src={src} alt={alt} />;
    }
    
    function App() {
      return (
        <div>
          <h1>Accessible React Application</h1>
          <AccessibleButton onClick={() => alert('Button clicked!')}>
            Click me
          </AccessibleButton>
          <AccessibleImage
            src="path/to/image.jpg"
            alt="Description of the image"
          />
        </div>
      );
    }

In this example, the AccessibleButton component uses a semantic <button> element and provides an aria-label for clear labeling. The AccessibleImage component includes the alt attribute to describe the image for screen reader users.

By following these accessibility practices and guidelines, you can create React applications that are inclusive and usable by a wider range of users, including those with disabilities. It's important to consider accessibility from the start and make it an integral part of your development process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment