ReactJs
Advanced ReactJs

Advanced ReactJs

Refs and the DOM

Concept: The useRef hook in React allows you to persist values across renders without causing re-renders. It provides a way to access and interact with DOM elements directly, similar to using refs in class components. Unlike state, updating a ref does not trigger a re-render of the component, making it ideal for managing DOM elements and storing mutable values.

Usage Example:

1. Accessing and Manipulating DOM Elements

Suppose you want to programmatically focus an input field when a button is clicked. You can achieve this using useRef:

import React, { useRef } from 'react';
 
const FocusInput = () => {
  // Create a ref using useRef
  const inputRef = useRef(null);
 
  // Function to focus the input element
  const handleFocus = () => {
    inputRef.current.focus(); // Directly accesses the DOM node
  };
 
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click button to focus" />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
};
 
export default FocusInput;

Explanation:

  • useRef(null) initializes a ref object with current set to null.
  • The inputRef is assigned to the ref attribute of the <input> element.
  • inputRef.current gives direct access to the DOM node, allowing you to call methods like focus().

2. Persisting Values Across Renders

You can also use useRef to keep a mutable value that doesn’t cause re-renders when updated:

import React, { useRef } from 'react';
 
const TimerComponent = () => {
  const countRef = useRef(0);
 
  const handleClick = () => {
    countRef.current += 1;
    console.log('Current count:', countRef.current);
  };
 
  return (
    <div>
      <button onClick={handleClick}>Increment Count</button>
      <p>Check console for current count</p>
    </div>
  );
};
 
export default TimerComponent;

Explanation:

  • countRef.current holds the mutable value count.
  • Updating countRef.current does not trigger a re-render, so the component's UI remains unaffected by changes to this ref.

3. Using Refs with Third-Party Libraries

useRef is useful for integrating React components with third-party libraries that require direct DOM manipulation:

import React, { useEffect, useRef } from 'react';
import { Chart } from 'chart.js'; // Example third-party library
 
const ChartComponent = () => {
  const chartRef = useRef(null);
 
  useEffect(() => {
    const ctx = chartRef.current.getContext('2d');
    new Chart(ctx, {
      type: 'line',
      data: {
        labels: ['January', 'February', 'March'],
        datasets: [{
          label: 'My Dataset',
          data: [10, 20, 30],
        }],
      },
    });
  }, []);
 
  return <canvas ref={chartRef} width="400" height="200" />;
};
 
export default ChartComponent;

Explanation:

  • chartRef is assigned to a <canvas> element.
  • useEffect ensures that the chart is initialized once the component mounts.

Benefits:

  • Provides direct access to DOM elements, enabling fine-grained control over rendering and behavior.
  • Avoids unnecessary re-renders when updating mutable values or interacting with third-party libraries.
  • Ideal for managing non-React code or integrating with external libraries.

By utilizing useRef, you can efficiently manage DOM elements, maintain mutable values across renders, and integrate with external libraries, all while keeping your React components performant and responsive.

React Portals

Concept: React Portals provide a way to render child components into a DOM node that exists outside the parent component hierarchy. This feature is particularly useful for scenarios where you need to render elements such as modals, tooltips, or pop-ups that should be visually separated from their parent components but still belong logically to the component tree.

Portals help manage complex UI elements by rendering them at different parts of the DOM while maintaining React's declarative rendering approach. This means you can have components that visually appear outside their parent container, but still retain their position within the React component tree.

Usage Example:

1. Basic Modal Implementation

Suppose you want to create a modal dialog that is rendered outside the main content flow. This is how you can achieve it using React Portals:

Step 1: Create a Modal Component

import React from 'react';
import ReactDOM from 'react-dom';
import './Modal.css'; // Assume you have CSS styles for your modal
 
const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null; // Do not render if the modal is not open
 
  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.getElementById('modal-root') // Target DOM node outside of the main app
  );
};
 
export default Modal;

Step 2: Use the Modal Component

import React, { useState } from 'react';
import Modal from './Modal';
 
const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
 
  const openModal = () => setIsModalOpen(true);
  const closeModal = () => setIsModalOpen(false);
 
  return (
    <div>
      <h1>Main Application</h1>
      <button onClick={openModal}>Open Modal</button>
      <Modal isOpen={isModalOpen} onClose={closeModal}>
        <h2>Modal Content</h2>
        <p>This is a modal dialog rendered outside the main DOM hierarchy.</p>
      </Modal>
    </div>
  );
};
 
export default App;

Step 3: Update Your HTML

Ensure that you have an element in your HTML file to serve as the portal target:

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
  <div id="modal-root"></div> <!-- Portal target element -->
</body>
</html>

Explanation:

  • ReactDOM.createPortal is used to render the modal content into the modal-root DOM node, which is outside of the main React #root element.
  • onClick handlers ensure that clicking on the overlay closes the modal, but clicking inside the modal content does not.
  • stopPropagation prevents the click event from propagating to the overlay when interacting with the modal content.

2. Using Portals for Tooltips

Here's how you might use Portals to render a tooltip component:

Step 1: Create a Tooltip Component

import React from 'react';
import ReactDOM from 'react-dom';
import './Tooltip.css'; // Assume you have CSS styles for your tooltip
 
const Tooltip = ({ children, text }) => {
  return ReactDOM.createPortal(
    <div className="tooltip">{text}</div>,
    document.body // Render tooltip directly into the body
  );
};
 
export default Tooltip;

Step 2: Use the Tooltip Component

import React, { useState } from 'react';
import Tooltip from './Tooltip';
 
const ButtonWithTooltip = () => {
  const [showTooltip, setShowTooltip] = useState(false);
 
  return (
    <div>
      <button
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        Hover me
      </button>
      {showTooltip && <Tooltip text="This is a tooltip" />}
    </div>
  );
};
 
export default ButtonWithTooltip;

Explanation:

  • Tooltip Component: Renders its content into the document.body, so it can appear on top of other content.
  • ButtonWithTooltip Component: Shows the tooltip when hovering over the button.

Benefits:

  • Visual Separation: Helps in rendering elements like modals or tooltips that should appear outside the normal content flow while keeping the React component structure.
  • CSS Management: Makes it easier to manage CSS styles for floating elements without affecting other parts of the UI.
  • Avoids Overflow Issues: Useful for UI elements that might be cut off due to overflow settings on parent containers.

React Portals provide a powerful way to manage complex UI scenarios, allowing you to render components where they visually belong while maintaining their logical position within the React component hierarchy.

Error Boundaries in React

Concept: Error Boundaries are a React feature used to catch and handle errors in the component tree. While traditionally implemented in class components, you can manage errors in functional components using hooks and higher-order components.

Implementation:

1. Creating an Error Boundary with a Functional Component

Define a Custom Hook for Error Handling:

Since React’s functional components don’t have lifecycle methods like componentDidCatch, you’ll need to use a combination of hooks and higher-order components to achieve similar behavior.

Step 1: Define a Hook to Track Error State

import { useState, useEffect } from 'react';
 
const useErrorBoundary = () => {
  const [hasError, setHasError] = useState(false);
 
  useEffect(() => {
    const errorHandler = (error) => {
      setHasError(true);
      console.error('Caught an error:', error);
    };
 
    window.addEventListener('error', errorHandler);
 
    return () => {
      window.removeEventListener('error', errorHandler);
    };
  }, []);
 
  return hasError;
};
 
export default useErrorBoundary;

Step 2: Create the Error Boundary Component

import React from 'react';
import useErrorBoundary from './useErrorBoundary';
 
const ErrorBoundary = ({ children }) => {
  const hasError = useErrorBoundary();
 
  if (hasError) {
    return <h1>Something went wrong.</h1>;
  }
 
  return <>{children}</>;
};
 
export default ErrorBoundary;

Explanation:

  • useErrorBoundary Hook: Listens for global errors and updates the error state.
  • ErrorBoundary Component: Uses the hook to decide whether to show the fallback UI.

Step 3: Use the Error Boundary Component

import React from 'react';
import ErrorBoundary from './ErrorBoundary';
 
const BuggyComponent = () => {
  throw new Error('Broken component!');
  return <div>Not rendered</div>;
};
 
const App = () => (
  <ErrorBoundary>
    <BuggyComponent />
  </ErrorBoundary>
);
 
export default App;

Explanation:

  • ErrorBoundary: Wraps components to handle errors thrown by them.

2. Advanced Example with Error Recovery

Define a Functional Error Boundary with Retry Capability:

Step 1: Create the Error Boundary Component with Retry

import React, { useState, useCallback } from 'react';
import useErrorBoundary from './useErrorBoundary';
 
const ErrorBoundary = ({ children }) => {
  const [hasError, setHasError] = useState(false);
  const [retry, setRetry] = useState(false);
 
  const handleError = useCallback(() => {
    setHasError(true);
  }, []);
 
  const handleRetry = () => {
    setHasError(false);
    setRetry(!retry); // Force a re-render
  };
 
  useErrorBoundary(); // Custom hook to catch global errors
 
  if (hasError) {
    return (
      <div>
        <h1>Something went wrong.</h1>
        <button onClick={handleRetry}>Try Again</button>
      </div>
    );
  }
 
  return <>{children}</>;
};
 
export default ErrorBoundary;

Step 2: Use the Enhanced Error Boundary

import React from 'react';
import ErrorBoundary from './ErrorBoundary';
 
const BuggyComponent = () => {
  throw new Error('Broken component!');
  return <div>Not rendered</div>;
};
 
const App = () => (
  <ErrorBoundary>
    <BuggyComponent />
  </ErrorBoundary>
);
 
export default App;

Explanation:

  • Retry Mechanism: The handleRetry function allows users to retry rendering the component.

Benefits:

  • Graceful Error Handling: Provides fallback UI and recovery options.
  • Error Tracking: Captures global errors and displays relevant messages.
  • Functional Components: Leverages hooks for handling errors in functional components.

Use Cases:

  • UI Components: Use to wrap parts of your app where errors might occur.
  • Global Errors: Effective for catching and handling global errors in functional component setups.

By utilizing hooks and higher-order components, you can effectively manage errors in functional components and enhance user experience by gracefully handling issues and providing recovery options.

Webpack

Concept: Webpack is a powerful and flexible module bundler for JavaScript applications. It manages dependencies and assets, transforming and bundling them into optimized files. This is crucial for modern web development, particularly in complex React projects, where it helps in organizing and optimizing code and assets for better performance.

Key Features and Components:

  1. Entry and Output:

    • Entry: Defines the starting point(s) of your application. Webpack uses this as a base to build the dependency graph.
    • Output: Specifies where the bundled files should be saved and their filenames.

    Example:

    // webpack.config.js
    module.exports = {
      entry: './src/index.js', // Entry point of the application
      output: {
        filename: 'bundle.js', // Output bundle file
        path: __dirname + '/dist', // Directory for the output files
      },
    };
  2. Loaders:

    • Loaders are used to preprocess files before bundling. For instance, babel-loader transpiles modern JavaScript (ES6+) into a version compatible with older browsers.

    Example:

    // webpack.config.js
    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/, // Apply the loader to .js files
            exclude: /node_modules/, // Exclude files in node_modules
            use: 'babel-loader', // Use Babel to transpile JavaScript
          },
        ],
      },
    };
  3. Plugins:

    • Plugins are used to perform a wider range of tasks like optimization, minification, and asset management. They extend Webpack’s functionality.

    Example:

    // webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin');
     
    module.exports = {
      plugins: [
        new HtmlWebpackPlugin({
          template: './src/index.html', // HTML template file
        }),
      ],
    };
  4. Code Splitting:

    • Code splitting helps in splitting the code into smaller chunks, which can be loaded on demand. This improves the performance of the application by reducing the initial load time.

    Example:

    // Import a module only when it's needed
    import(/* webpackChunkName: "my-chunk" */ './module').then(module => {
      // Use the dynamically imported module
      module.doSomething();
    });
  5. DevServer:

    • Webpack DevServer provides a development server with features like live reloading and hot module replacement (HMR).

    Example:

    // webpack.config.js
    module.exports = {
      devServer: {
        contentBase: './dist', // Serve files from the 'dist' directory
        hot: true, // Enable hot module replacement
      },
    };
  6. Optimization:

    • Webpack offers various optimization techniques like minification, tree shaking, and asset management to ensure that the final bundle is as efficient as possible.

    Example:

    // webpack.config.js
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all', // Split all chunks into separate files
        },
        minimize: true, // Minify the output files
      },
    };

Benefits:

  • Efficient Bundling: Combines multiple files into a single bundle to reduce the number of HTTP requests.
  • Asset Management: Handles JavaScript, CSS, images, and other assets.
  • Performance Optimization: Includes features like code splitting and minification to improve load times.
  • Customizable: Highly configurable with loaders, plugins, and optimization settings.

Use Cases:

  • Single Page Applications (SPAs): Manages dependencies and optimizes assets for SPAs like React applications.
  • Complex Projects: Handles complex dependency graphs and asset management for larger applications.
  • Development and Production Builds: Configures different settings for development (e.g., hot reloading) and production (e.g., minification).

Webpack is an essential tool in modern web development, particularly for React projects, where managing and optimizing assets and dependencies is crucial for performance and maintainability.

Babel

Concept: Babel is a JavaScript compiler that converts modern JavaScript (ES6+) code into a version compatible with older browsers. It allows developers to use the latest JavaScript features while ensuring their code runs in a variety of environments, including those that do not support newer syntax.

Key Features and Components:

  1. Presets:

    • Presets are collections of Babel plugins that enable transformations for specific JavaScript features. The most common preset is @babel/preset-env, which includes transformations for ES6+ features based on the target environments.

    Example:

    // babel.config.js
    module.exports = {
      presets: ['@babel/preset-env'],
    };
  2. Plugins:

    • Plugins are used to transform specific syntax or features that are not covered by presets. For instance, @babel/plugin-transform-arrow-functions converts arrow functions into traditional function expressions.

    Example:

    // babel.config.js
    module.exports = {
      plugins: ['@babel/plugin-transform-arrow-functions'],
    };
  3. Polyfills:

    • Polyfills provide implementations for new JavaScript features that are not available in older environments. Babel can include polyfills to ensure compatibility with these features.

    Example:

    // babel.config.js
    module.exports = {
      presets: [
        [
          '@babel/preset-env',
          {
            useBuiltIns: 'entry', // Include polyfills based on usage
            corejs: 3, // Specify the version of core-js
          },
        ],
      ],
    };
  4. Configuration Files:

    • Babel can be configured using various files like .babelrc, babel.config.js, or package.json. This configuration determines which presets and plugins are applied during the transformation process.

    Example:

    // .babelrc
    {
      "presets": ["@babel/preset-env"],
      "plugins": ["@babel/plugin-transform-arrow-functions"]
    }
  5. Transformations:

    • Babel transforms modern JavaScript syntax into a version that is compatible with older browsers. This includes features like async/await, destructuring, and template literals.

    Example:

    // Modern JavaScript
    const add = (a, b) => a + b;
    // Transformed code
    var add = function (a, b) {
      return a + b;
    };

Benefits:

  • Modern JavaScript Features: Allows developers to use the latest JavaScript syntax and features without worrying about browser compatibility.
  • Backward Compatibility: Ensures code runs in older environments by transforming it into compatible syntax.
  • Customizable: Configurable with presets and plugins to meet specific project needs.
  • Polyfills: Includes necessary polyfills to support new features.

Use Cases:

  • React Development: Enables the use of modern React features and JSX syntax, which are not natively supported in all browsers.
  • Cross-Browser Compatibility: Ensures that code works across various browsers and environments by transpiling newer syntax.
  • Experimental Features: Allows experimentation with new JavaScript features before they become standard.

Babel is a crucial tool in modern JavaScript development, especially when working with React or other libraries that use advanced features. It ensures that your codebase remains compatible with a wide range of browsers while allowing you to take advantage of the latest language enhancements.

How Web URLs Work in a Browser

1. Entering the URL:

  • What Happens: When you type a web address (URL) like https://www.example.com/page into your browser's address bar and press Enter, the browser initiates a series of steps to fetch and display the webpage.

2. DNS Lookup (Domain Name System):

  • Purpose: The Domain Name System (DNS) translates human-readable domain names into IP addresses, which are used by computers to locate each other on the network.
  • Process:
    • Request: Your browser sends a query to a DNS resolver (usually provided by your ISP or a public DNS service like Google DNS).
    • Resolution: The resolver checks its cache for the IP address. If not found, it queries other DNS servers to find the address. For example, www.example.com might be resolved to 192.0.2.1.

Example:

  • Domain: www.example.com
  • Resolved IP Address: 192.0.2.1

3. Establishing a Connection:

  • Protocol: The browser uses the protocol specified in the URL (http or https). For https, an encrypted connection is established to secure data transmission.
  • Process:
    • TCP Handshake: The browser and server perform a TCP three-way handshake to establish a connection:
      1. SYN: Browser sends a Synchronize request.
      2. SYN-ACK: Server acknowledges with a Synchronize-Acknowledge.
      3. ACK: Browser sends an Acknowledge, completing the connection setup.
    • TLS Handshake (for HTTPS): If using HTTPS, a TLS handshake is performed to encrypt the connection:
      1. Client Hello: Browser sends a request to establish a secure connection.
      2. Server Hello: Server responds with encryption details.
      3. Key Exchange: Both parties exchange keys to encrypt data.
      4. Finished: The secure connection is established.

4. Sending an HTTP Request:

  • Purpose: The browser needs to request the specific resource from the server.
  • Request Formation:
    • Method: Typically GET for retrieving data.
    • Headers: Include metadata like the browser type, accepted content types, etc.
    • Example Request:
      GET /page HTTP/1.1
      Host: www.example.com
      User-Agent: Mozilla/5.0
      Accept: text/html,application/xhtml+xml

5. Server Processing:

  • Action: The server receives the request, processes it, and prepares a response. This might involve:
    • Fetching Data: Retrieving content from a database or file system.
    • Processing: Executing server-side code (e.g., PHP, Node.js) to generate dynamic content.
  • Response Formation:
    • Status Code: Indicates the result of the request (e.g., 200 OK, 404 Not Found).
    • Headers: Provide additional information (e.g., content type, caching rules).
    • Body: Contains the requested content (e.g., HTML, JSON).

6. Receiving and Rendering the Response:

  • Action: The browser receives the server’s response, which includes HTML content.
  • Processing:
    • HTML Parsing: The browser parses the HTML to build the DOM (Document Object Model), a hierarchical tree representing the webpage structure.
    • CSS Parsing: Stylesheets are parsed and applied to the HTML elements.
    • JavaScript Execution: Any embedded or linked JavaScript is executed to add interactivity or manipulate the DOM.

7. Handling Additional Resources:

  • Action: The HTML may reference additional resources like images, stylesheets, or scripts.
  • Fetching: The browser sends additional HTTP requests to retrieve these resources. This is done in parallel to improve performance.
  • Example:
    • Image Request: GET /images/logo.png HTTP/1.1
    • Stylesheet Request: GET /styles/main.css HTTP/1.1

8. Final Display:

  • Rendering: The browser combines the DOM, CSS, and JavaScript to render the final webpage.
  • User Interaction: Once the page is displayed, the user can interact with it, and any dynamic content or interactivity provided by JavaScript will function.

Summary of the Process:

  1. Enter URL: Type the address into the browser.
  2. DNS Lookup: Translate the domain name into an IP address.
  3. Connection: Establish a TCP/IP connection and, if necessary, a secure HTTPS connection.
  4. HTTP Request: Send a request to the server for the webpage.
  5. Server Response: Server processes the request and sends back the content.
  6. Rendering: Browser processes HTML, CSS, and JavaScript to display the page.
  7. Load Extras: Fetch additional resources like images and scripts.
  8. Display: Render the complete page and allow user interaction.

This detailed process ensures that the user sees the correct content efficiently, with multiple steps working together to make the web experience smooth and fast.

Latest React 19 Feature

React 19 introduces several important features that enhance performance, state management, and ease of development. Here’s an in-depth explanation of each feature with examples:

1. React Server Components

React Server Components (RSC) enable components to be rendered on the server, enhancing page load time and SEO. This approach offloads part of the rendering process to the server, meaning that large or heavy components can be processed server-side and sent as HTML to the client. Server components are typically used for fetching data and can reduce the size of the client bundle.

Example:

"use server";
export async function getData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
}
 
export default function ServerComponent() {
  const data = getData(); // This will be fetched on the server
  return (
    <div>
      {data.map(post => <p key={post.id}>{post.title}</p>)}
    </div>
  );
}

This code demonstrates how to use server components by specifying "use server". The component fetches data on the server side, which improves client performance.

2. Actions and useActionState

Actions are a new way to handle state changes in React 19. The useActionState hook is especially useful for managing asynchronous tasks, such as form submission, with built-in handling for pending states and errors. It simplifies the process of working with forms that need to update state based on user interaction.

Example:

function UpdateEmailForm() {
  const [state, submitAction, isPending, error] = useActionState(
    async (previousState, formData) => {
      const email = formData.get('email');
      const response = await updateEmail(email);
      if (response.error) return response.error;
      return email; // Update the state with the new email
    },
    "example@example.com"
  );
 
  return (
    <form onSubmit={submitAction}>
      <input type="email" name="email" defaultValue={state} />
      <button type="submit" disabled={isPending}>
        {isPending ? "Updating..." : "Update"}
      </button>
      {error && <p>Error: {error}</p>}
    </form>
  );
}

In this example, useActionState simplifies handling the various states (pending, error) during an asynchronous form submission, streamlining form handling logic.

3. Document Metadata Management

React 19 introduces native support for managing document metadata (like title, meta tags, etc.) directly within the component, eliminating the need for third-party libraries like react-helmet. This is useful for improving SEO and accessibility by allowing the page metadata to be defined within the component itself.

Example:

const HomePage = () => {
  return (
    <>
      <title>Welcome to React 19</title>
      <meta name="description" content="Learn about the latest features in React 19" />
      <h1>Welcome to React 19</h1>
    </>
  );
}

This example shows how to manage the document’s title and meta description within a component, improving SEO and streamlining metadata management.

4. Optimized Asset Loading

React 19 optimizes asset loading by loading images, scripts, and other large assets in the background. This helps in preloading assets before the user navigates to a new page, reducing the initial load time when users interact with other parts of the app.

Example:

function ImageLoader() {
  return (
    <div>
      <img src="large-image.jpg" alt="Large asset" loading="lazy" />
    </div>
  );
}

Using the loading="lazy" attribute, this example ensures that large assets like images are loaded asynchronously in the background, improving page load performance.

5. New Hooks (use(), useOptimistic, useFormStatus)

React 19 introduces several new hooks that enhance developer experience, including:

  • use(): This hook allows you to manage asynchronous state directly within the render function without using useEffect. You can pass promises or contexts directly into this hook.

  • useOptimistic: Enables optimistic updates, allowing the UI to immediately reflect changes while the data is still being fetched, enhancing user experience.

  • useFormStatus: Provides information about the status of a form, such as whether it's being submitted or whether there are any errors, without the need to pass this information down via props.

Example: Using use() hook:

async function fetchUser() {
  return await fetch('https://api.example.com/user');
}
 
export default function UserComponent() {
  const user = use(fetchUser());
  return (
    <div>
      <h1>{user.name}</h1>
    </div>
  );
}

This example uses the use() hook to fetch data directly inside the render function without needing additional effect hooks.

6. Support for Stylesheets and Async Scripts

React 19 provides enhanced support for loading stylesheets and asynchronous scripts directly within components. This improves performance by ensuring that styles are loaded in the correct order and reduces conflicts between different stylesheets.

Example:

function StyleExample() {
  return (
    <>
      <link rel="stylesheet" href="styles.css" />
      <h1 className="styled-text">This text is styled!</h1>
    </>
  );
}

React 19 ensures that styles defined in <link> and <style> tags are applied correctly and efficiently, reducing the chance of conflicts.

7. Async Script Loading

Scripts in React 19 can be loaded asynchronously, improving performance by allowing them to be loaded only when required, without blocking other parts of the app.

Example:

function AsyncScriptExample() {
  return (
    <>
      <script async src="https://example.com/analytics.js"></script>
      <h1>Analytics Script Loaded Asynchronously</h1>
    </>
  );
}

In this case, the external script is loaded asynchronously, improving page performance and load times.


React 19’s new features, especially server components and enhanced state handling through actions and hooks, make it an essential upgrade for building high-performance, scalable, and maintainable applications.

React Query

React Query is a powerful library that simplifies fetching, caching, and synchronizing server-side data in React applications. Here’s a step-by-step guide on how to use it effectively with optimal configurations and real-world examples.

  1. Install React Query

Before using React Query, install the library along with its required dependencies.

npm install react-query

You’ll also need to install react-query-devtools for debugging.

npm install @tanstack/react-query-devtools
  1. Setting Up Query Client

To begin using React Query, set up a QueryClient in your application. The QueryClient is the core instance where caching and other behaviors are configured.

import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 
// Create a client with custom config for caching and staleTime
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // Data remains fresh for 5 minutes
      cacheTime: 10 * 60 * 1000, // Cache data for 10 minutes
      refetchOnWindowFocus: false, // Prevent auto refetch on window focus
    },
  },
});
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your application components */}
      <ReactQueryDevtools initialIsOpen={false} /> {/* Optional: debugging tool */}
    </QueryClientProvider>
  );
}
 
export default App;
  1. Basic Query Example: Fetching User Data

Let’s fetch user data from an API using the useQuery hook. This hook handles data fetching, caching, and synchronization automatically.

import { useQuery } from 'react-query';
 
const fetchUser = async () => {
  const response = await fetch('/api/user');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};
 
function UserProfile() {
  // Fetch user data using useQuery
  const { data, error, isLoading, isError } = useQuery('user', fetchUser);
 
  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error: {error.message}</p>;
 
  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {data.name}</p>
      <p>Email: {data.email}</p>
    </div>
  );
}

Key points:

  • useQuery('user', fetchUser) – This creates a query with a unique key (user) and defines how to fetch data.
  • It automatically handles loading states and caching.
  • Cached data is reused unless it's stale or manually invalidated.
  1. Optimizing with Configuration Options

You can further optimize queries with configurations that control refetching behavior, caching, and background syncing.

Important Configurations:

  • staleTime: The time until data is considered stale. In the example, we set staleTime to 5 minutes (300,000 ms).
  • cacheTime: How long unused data stays in the cache before it is garbage collected (e.g., 10 minutes).
  • refetchOnWindowFocus: Whether to automatically refetch data when the window is focused. Disabling this can prevent unnecessary network requests.

Here’s how you can apply these options to individual queries:

const { data, error, isLoading } = useQuery('user', fetchUser, {
  staleTime: 10000,   // Data remains fresh for 10 seconds
  cacheTime: 300000,  // Cache data for 5 minutes
  refetchOnWindowFocus: true, // Automatically refetch data on window focus
});
  1. Mutations: Handling Data Updates

Use mutations for data modifications (e.g., creating, updating, or deleting). Here’s an example of submitting a new user and updating the query data automatically.

import { useMutation, useQueryClient } from 'react-query';
 
const addUser = async (newUser) => {
  const response = await fetch('/api/user', {
    method: 'POST',
    body: JSON.stringify(newUser),
  });
  return response.json();
};
 
function AddUserForm() {
  const queryClient = useQueryClient();  // To invalidate queries after mutation
  const mutation = useMutation(addUser, {
    onSuccess: () => {
      // Invalidate and refetch the user data after the mutation
      queryClient.invalidateQueries('user');
    },
  });
 
  const handleSubmit = (user) => {
    mutation.mutate(user);
  };
 
  return (
    <button onClick={() => handleSubmit({ name: 'New User' })}>
      Add User
    </button>
  );
}

Key points:

  • useMutation is used to modify server-side data.
  • After a successful mutation, the onSuccess callback invalidates the user query, which triggers a refetch to update the UI.
  1. Optimistic Updates: Enhancing UX

Optimistic updates improve user experience by updating the UI before the mutation is confirmed by the server.

const mutation = useMutation(addUser, {
  // Optimistically update UI before mutation completes
  onMutate: async (newUser) => {
    // Cancel any outgoing refetches for 'user'
    await queryClient.cancelQueries('user');
 
    // Snapshot the current user data
    const previousUser = queryClient.getQueryData('user');
 
    // Optimistically update the UI with the new user
    queryClient.setQueryData('user', (old) => ({
      ...old,
      name: newUser.name,
    }));
 
    // Return the context with the previous user
    return { previousUser };
  },
  onError: (err, newUser, context) => {
    // Rollback to the previous user data if mutation fails
    queryClient.setQueryData('user', context.previousUser);
  },
  onSettled: () => {
    // Invalidate and refetch after mutation
    queryClient.invalidateQueries('user');
  },
});
  1. Pagination and Infinite Scrolling

React Query makes it easy to implement pagination and infinite scrolling.

import { useInfiniteQuery } from 'react-query';
 
const fetchUsers = async ({ pageParam = 1 }) => {
  const response = await fetch(`/api/users?page=${pageParam}`);
  return response.json();
};
 
function UserList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery('users', fetchUsers, {
    getNextPageParam: (lastPage, pages) => {
      return lastPage.nextPage ?? false;
    },
  });
 
  return (
    <div>
      {data.pages.map((page) => (
        <Fragment key={page.nextPage}>
          {page.users.map((user) => (
            <div key={user.id}>{user.name}</div>
          ))}
        </Fragment>
      ))}
 
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading more...' : 'Load More'}
      </button>
    </div>
  );
}

Key points:

  • useInfiniteQuery helps you manage infinite scrolling.
  • getNextPageParam specifies how to determine the next page based on the API response.
  1. Background Fetching & Automatic Refetching

React Query automatically refetches data in the background when a query becomes stale or when the browser window regains focus. This ensures the data is always fresh without the need for manual refreshes.

For example, data will refetch when the user returns to the app after leaving:

const { data } = useQuery('user', fetchUser, {
  refetchOnWindowFocus: true,  // Automatically refetch when window is focused
});
  1. Handling Errors and Retries

React Query automatically retries failed requests, with exponential backoff by default. You can configure how retries are handled:

const { data, error, isError } = useQuery('user', fetchUser, {
  retry: 3,  // Retry failed requests 3 times
  retryDelay: 1000,  // Wait 1 second between retries
});
  1. DevTools for Debugging

React Query comes with DevTools to make debugging easier. It provides a visual interface for monitoring queries and their states (loading, fetching, success, error).

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 
<ReactQueryDevtools initialIsOpen={false} />;

Conclusion

React Query significantly simplifies the complexity of managing server-side data in React applications. With powerful features like caching, background refetching, pagination, and optimistic updates, it ensures better performance, user experience, and clean code architecture. By using optimal configurations and features tailored to your app’s needs, you can enhance both the development process and the efficiency of your React applications.

RESTful API vs. GraphQL

AspectRESTful APIGraphQL
Endpoint StructureMultiple endpoints, each corresponding to a resource (e.g., /users, /posts)Single universal endpoint (/graphql) for all queries and mutations
Data FetchingFixed data structure per endpoint, leading to potential over-fetching (getting more data than needed) or under-fetching (requiring multiple requests)Clients specify exactly the fields they need, avoiding over-fetching and under-fetching. Fetches only what is explicitly requested.
HTTP MethodsUses HTTP methods: GET (read), POST (create), PUT/PATCH (update), DELETE (delete)Uses queries (for read operations) and mutations (for write operations). All operations use POST requests to the /graphql endpoint.
SchemaNo strict schema. Endpoints return predefined data structures that may vary in format.Strongly typed schema defines types, queries, and mutations, allowing clients to know exactly what data and operations are available.
VersioningAPI versioning is often required for breaking changes (e.g., /v1/users).No versioning needed. Schema evolves without requiring changes to client queries, as backward compatibility is prioritized.
Nested and Related DataMultiple requests needed to fetch related data (e.g., user’s posts and comments may require separate API calls).Supports nested queries to retrieve related data in a single request, reducing round trips to the server.
Real-time DataRequires additional technologies (e.g., WebSockets, Server-Sent Events) for real-time updates.Built-in support for subscriptions, allowing real-time data updates out of the box.
Request EfficiencyMultiple requests for different resources. Inefficient for complex data needs, especially with deeply nested relationships.Efficient data fetching in a single request, even for complex, nested data. Queries can be batched.
Error HandlingHTTP status codes (e.g., 200, 404, 500) used to indicate success or failure.Errors are returned as part of the response along with partial data, making it easier to debug issues without missing valid data.
CachingCaching is straightforward using HTTP caching mechanisms (e.g., caching GET requests).Requires more advanced client-side caching strategies since GraphQL responses are highly customizable and do not follow standard HTTP caching rules.
Tooling and EcosystemWidely adopted and supported across many tools and frameworks. Simple to implement and understand for basic use cases.Growing ecosystem with powerful tools like Apollo and Relay that provide advanced capabilities such as caching, state management, and real-time updates.
ScalabilityScalability can become an issue when fetching large datasets across multiple endpoints, especially with complex resources.More scalable in terms of fetching only required data in one request, but can become complex to manage if queries are not optimized.
Over-fetching/Under-fetchingCommon issue where endpoints return more data than needed or require multiple calls for complete data.Eliminates over-fetching/under-fetching by allowing clients to request only the data they need.
Learning CurveFamiliar and easy to learn with basic REST concepts; widely used in the industry.Requires learning GraphQL syntax and schema design, but provides more flexibility once mastered.

Summary:

  • REST: Best suited for simpler APIs with well-defined resources, where HTTP methods and caching are critical. However, it may lead to inefficiencies when clients need to make multiple requests or get excess data.

  • GraphQL: Ideal for scenarios where flexible data queries are required, especially in complex applications with deeply nested data. While it eliminates over-fetching and under-fetching, it introduces more complexity in managing real-time updates, caching, and optimizing queries.

GraphQL is powerful for modern applications with dynamic data needs, while REST is simpler and works well for traditional, resource-based APIs.