GAZAR

Principal Engineer | Mentor

Reviewing React 18 Features: A TypeScript Guide

Reviewing React 18 Features: A TypeScript Guide

With the release of React 18, TypeScript developers are poised to unlock a new realm of possibilities in building dynamic and performant web applications. This groundbreaking update introduces a suite of powerful features designed to streamline development workflows, enhance user experiences, and optimize performance. From automatic batching to suspenseful loading states, React 18 empowers developers to harness the full potential of TypeScript in crafting responsive and resilient applications. In this article, we'll delve into the exciting new features of React 18, exploring their implementation in TypeScript and showcasing how they elevate the development landscape.

Automatic Batching

One of the most significant new features in React 18 is automatic batching. This feature reduces the number of re-renders and improves performance by batching multiple updates together. To take advantage of automatic batching, you can use the useTransition hook, which is a new addition to React 18.

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);

Transitions

Urgent updates like typing, clicking, or pressing, need immediate response to match our intuitions about how physical objects behave. Otherwise they feel “wrong”. However, transitions are different because the user doesn’t expect to see every intermediate value on screen.

import { useTransition } from 'react';

function MyComponent() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);

  function selectTab(nextTab) {
    startTransition(() => {
      setCount(count+1);
    });
  }

  return <div>Count: {count}</div>;
}

useTransition is a Hook, so it can only be called inside components or custom Hooks. If you need to start a Transition somewhere else (for example, from a data library), call the standalone startTransition instead

import { startTransition } from 'react';

Suspense

Suspense is another exciting new feature in React 18 that allows you to handle loading states and errors in a more robust way. You can use the useSuspense hook to create a suspenseful state.

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

createRoot

The createRoot function is used to create a root React element that can be rendered into a container element in the DOM. It returns a root object that provides the render method for rendering React elements into the specified container. Here's how createRoot can be utilized:

import { createRoot } from 'react-dom';
import App from './App';

// Create a root React element
const root = createRoot(document.getElementById('root'));
// Render the App component into the specified container
root.render(<App />);

hydrateRoot

The hydrateRoot function is used for server-side rendering (SSR) or rehydration of a server-rendered React tree on the client side. It takes two arguments: the container element to hydrate into and the React element representing the server-rendered content. Here's how hydrateRoot can be used:

import { hydrateRoot } from 'react-dom';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);

useDeferredValue

The useDeferredValue hook is a new addition to React 18, designed to optimize the rendering performance of applications by deferring the update of certain values. This hook is particularly useful for scenarios where rendering updates are not immediately necessary or where delaying updates can improve user experience, such as animations or loading indicators.

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

const AnimatedList: React.FC<{ items: string[] }> = ({ items }) => {
  const [selectedItem, setSelectedItem] = useState<string | null>(null);
  const deferredSelectedItem = useDeferredValue(selectedItem);

  const handleItemClick = (item: string) => {
    setSelectedItem(item);
  };

  return (
    <div>
      <h2>Animated List</h2>
      <ul>
        {items.map((item) => (
          <li
            key={item}
            onClick={() => handleItemClick(item)}
            style={{
              fontWeight: deferredSelectedItem === item ? 'bold' : 'normal',
            }}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AnimatedList;

useId

The useId hook generates a unique identifier string and ensures its consistency across re-renders. This is crucial for cases where components need to generate unique IDs for HTML elements, such as form inputs or ARIA attributes. By using useId, developers can avoid potential issues with duplicate IDs and ensure accessibility guidelines are met.

import { useId } from 'react';

const FormComponent: React.FC = () => {
  // Generate unique IDs for form inputs
  const usernameId = useId();
  const passwordId = useId();

  return (
    <form>
      <label htmlFor={usernameId}>Username:</label>
      <input type="text" id={usernameId} />

      <label htmlFor={passwordId}>Password:</label>
      <input type="password" id={passwordId} />

      <button type="submit">Submit</button>
    </form>
  );
};

export default FormComponent;

Profiler

The Profiler component is part of the React Developer Tools package and is used to wrap around components that need to be profiled. It takes two props: id, which is a string identifier for the profiler, and onRender, a callback function that gets called whenever the wrapped component re-renders. Within the onRender callback, developers can measure the duration of the render and perform any necessary actions, such as logging or analyzing performance metrics

  const handleRender = (
    id: string,
    phase: 'mount' | 'update',
    actualDuration: number,
    baseDuration: number,
    startTime: number,
    commitTime: number,
    interactions: any[]
  ) => {
    console.log(
      `Profiler ID: ${id}, Phase: ${phase}, Duration: ${actualDuration}ms`
    );
  };

  return (
    <Profiler id="complex-component" onRender={handleRender}>
      <ComplexComponent />
    </Profiler>
  );

Lazy:

React.lazy is a function that takes a function that calls a dynamic import and returns a Promise for the React component. This dynamic import function should return a Promise that resolves to a module containing a default exported React component. When the component needs to be rendered, React will automatically load the module and render the component. This feature is particularly useful for large applications with many components, where code-splitting can significantly improve loading times.

const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App: React.FC = () => {
  return (
    <div>
      <h1>Lazy Loading Example</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
};

useDebugValue

The useDebugValue hook accepts a value and an optional formatting function as arguments. It is typically used inside custom hooks to provide helpful labels or values that are displayed in React DevTools. When a custom hook using useDebugValue is inspected in React DevTools, the label or formatted value will be displayed alongside the hook, providing additional context for debugging.

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

const useCounter = () => {
  const [count, setCount] = useState<number>(0);

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

  useEffect(() => {
    // Update the label in React DevTools
    useDebugValue(count);
  }, [count]);

  return { count, increment };
};
export default useCounter;

forwardRef

forwardRef is a higher-order function provided by React that allows you to forward a ref from a parent component to a child component. It enables you to create components that are ref-aware without exposing the implementation details of the child component. When a parent component uses forwardRef with a child component, the child component receives the ref as a second parameter, allowing it to forward the ref to a DOM element or another component.

import React, { useRef } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <ChildComponent ref={inputRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
};

export default ParentComponent;

And the child

import React, { forwardRef, useRef } from 'react';

interface ChildProps {
  // Define props for the ChildComponent
}

// Define props for the ChildComponent
const ChildComponent = forwardRef<HTMLInputElement, ChildProps>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  // Forward the ref to the input element
  React.useImperativeHandle(ref, () => inputRef.current);

  return (
    <div>
      <input type="text" ref={inputRef} />
    </div>
  );
});

export default ChildComponent;

useImperativeHandle

The useImperativeHandle hook is used inside a child component to customize the value that is exposed when the component is used with ref and forwardRef in a parent component. It accepts a ref object and a function that returns a value or an object with properties. The returned value or object will be accessible from the parent component via the ref. This allows child components to expose specific methods or properties to their parent components, enabling more direct communication and control.

const ChildComponent = forwardRef<HTMLInputElement, ChildProps>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);
  // Expose the focus method to the parent component
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    }
  }));

  return (
    <div>
      <input type="text" ref={inputRef} />
    </div>
  );
});

Conclusion:

React 18 brings a plethora of new features and enhancements that empower TypeScript developers to build more responsive and efficient applications. From Suspense and automatic batching to startTransition, useTransition, createRoot, and hydrateRoot, React 18 provides a comprehensive toolkit for building modern web applications with ease. By embracing these features, TypeScript developers can elevate their development workflow and deliver exceptional user experiences on the web.