React 18 Deconstructed

React 18 Deconstructed

In March this year, The React Team released React 18 on npm. This introduced a great set of tools to add to your tool box. The majority of these features are built on top of their new concurrent renderer. I won’t go into depth but I strongly recommend reading their blog post about React 18 and “What is Concurrent React?

💥 Important Breaking Changes

There are not too many breaking changes here that affect people outside of library development, however here are two you should be aware of:

The useEffect hook is stricter

useEffect can no longer returning anything unless it is the cleanup function. By good practice, you were hopefully doing this already. However you might have had issues if you were trying to write it in a shorthand method like so:

useEffect(() => myEffectFunction(), []);

If myEffectFunction returned something (e.g. a boolean) it would break the hook. The solution is simple, stop using the shorthand method and write it out in full:

useEffect(() => {
  myEffectFunction();
}, []);

The only thing returned should be a function you want to execute on unmounting.

useEffect(() => {
  doSetup(); // subscribe to some events!
  
  return () => {
    doCleanup(); // unsubscribe to those events!  
  };
}, []);

TypeScript types drop children

The typescript types no longer include children by default. Previously children used to be an include prop for us to use. Take this example:

// Input.tsx

const Input: React.FC = ({ children }) => <div>{children}</div>;
//                         ^^^^^^^^ Property 'children' does not exist
//                                  on type '{}'.

export default Input
// Example.tsx

import Input from './Input';

const Example = () => {
  return ( 
    <>
      ... blah blah ...
      <Input></Input>
//    ^^^^^^^^ Type '{ children: string; }' has no properties in common
//             with type 'IntrinsicAttributes & {}'

      ... blah blah
    </>
  )
}

There are a few ways to handle this, you can add your own interface

import React, { ReactNode } from 'react';

interface InputProps {
  children?: ReactNode;
}

const Input: React.FC<InputProps> = ({ children }) => <div>{children}</div>;

Or use PropsWithChildren:

import { PropsWithChildren } from 'react';

interface InputProps extends PropsWithChildren<{}> {
  niceWayToKeepItCleanAndAddYourOtherPropsHere: string;
}

Your choice is personal preference, or if you want to customise how children works by either loosening and tightening the restrictions on the children prop.

🎁 React's gift to you, some exciting updates

Automatic Batching

React 18 automatically batches multiple state updates into a single re-render for better performance.

So, if you have several setState calls in a row, React 18 will batch these into a single update, reducing the number of renders.

New Suspense Features

Suspense now supports more than just code-splitting. It can manage loading states across your app, so now you can use Suspense to handle data fetching, showing fallback content while waiting for the data.

If you're feeling game and want a bit of a learning lesson, you can even try to implement it yourself. Many libraries have it implemented already.

New APIs

Lets dive through some of the new APIs.

startTransition

This API helps in managing priority levels of state updates. It's particularly useful for distinguishing between urgent updates (like typing in an input) and non-urgent updates (like filtering a large list based on that input).

import { startTransition } from 'react';

const onSearchInputChange = (input) => {
  startTransition(() => {
    setSearchQuery(input);
  });
};

In this example, startTransition is used to update the search query. This allows the UI to remain responsive, as React will treat this update as lower priority. 🔥

useId

A hook that generates unique IDs that are stable across the server and client, helping with SSR (Server-Side Rendering) and stabilising accessibility requirements.

import { useId } from 'react';

const LabeledInput = () => {
  const inputId = useId();
  const labelId = useId();

  return (
    <div>
      <label id={labelId} htmlFor={inputId}>Your Name:</label>
      <input id={inputId} aria-labelledby={labelId} type="text" />
    </div>
  );
};

useDeferredValue

Simply put, this hook allows you to defer re-rendering of non-urgent parts of the component tree. This is a great way to play nice with the DOM

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

const LargeListFilter = ({ items }) => {
  const [filter, setFilter] = useState('');

  // the useDeferredValue here will stop our DOM from having chaotic consequences
  const deferredFilter = useDeferredValue(filter);

  const filteredItems = items.filter(item => 
    item.toLowerCase().includes(deferredFilter.toLowerCase())
  );

  return (
    <div>
      <input 
        type="text" 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
        placeholder="Type to filter...."
      />
      {filteredItems.length > 0 ? (
        <ul>
          {filteredItems.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      ) : (
        <p>No items found (sadface).</p>
      )}
    </div>
  );
};

useSyncExternalStore

A hook for subscribing to external stores in a way that's compatible with React's concurrent rendering. This really opens up to hooking into anything outside of react world.

Here is a bit of a hacked together example, but you get the idea!

export class MyExternalStore {
  constructor(initialState) {
    this.state = initialState;
    this.listeners = new Set();
  }

  getState() {
    return this.state;
  }

  notifyListeners() {
    this.listeners.forEach(listener => listener());
  }

  setState(newState) {
    this.state = newState;

    this.notifyListeners();
  }

  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

// Our external store instance
export const myStore = new MyExternalStore({ count: 0 });


// .... and somewhere else .... //


import React from 'react';
import { useSyncExternalStore } from 'react';

const CounterComponent = () => {
  const state = useSyncExternalStore(
    myStore.subscribe,
    myStore.getState
  );

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => myStore.setState({ count: state.count + 1 })}>
        Increment
      </button>
    </div>
  );
};

export default CounterComponent;

Concurrent Rendering

This is a fundamental change in how React manages rendering. It allows React to work on multiple tasks at the same time, improving performance and responsiveness. Do note, it's a fundamental shift in how React thinks about rendering UIs.

In the past React would block the main thread with rendering tasks, making the user experience feel sluggish during those complex updates. Concurrent Rendering changes this by breaking up the work into smaller, bite size tasks

Concurrent Rendering introduces a few brand new key concepts:

  • Interruptible Rendering: React can start rendering changes, but if something more important comes up (like user input), it can stop and switch to the more critical task. Once done, it can resume where it left off.
  • Priority-Based Rendering: Not all updates are created equal. React now understands which updates are much more urgent and which can wait, optimising the user experience by focusing on what matters most.
  • Suspense: This feature, which is enhanced by Concurrent Rendering, allows components to wait for something before rendering. It’s like telling React, "Hold up, don’t render me just yet; I’m waiting for my data." This makes managing loading states and asynchronous much nicer.

Concurrent Rendering means more control over the user experience, as React can better manage background tasks without interrupting the critical flow of the application you wi

Streamlined Server-Side Rendering

React 18 isn't just about making things work well on the client-side; it's also a lot about giving the server-side some love. With the new concurrent features and automatic batching, SSR in React has become more powerful and, dare I say, simpler to implement.

React 18 enhances SSR with several key features:

  • Enhanced Performance and Efficiency: Leveraging concurrent features, server-side rendering now breaks up work into smaller, non-blocking tasks. This approach significantly reduces the time to get fully rendered HTML out, speeding up the server's response time.
  • Streaming HTML: React 18 introduces HTML streaming from server to browser, allowing parts of the page to be sent and displayed incrementally. This means users see content faster, enhancing the overall user experience.
  • Automatic Batching: This also extends to SSR, automatic batching groups multiple updates into a singular render. This efficiency reduces server load and accelerates content delivery, especially in dynamic applications.
  • Suspense for Data Fetching: Suspense now supports server-side data fetching, allowing components to declare their data needs upfront. React waits for all necessary data before rendering, streamlining the handling of loading states and asynchronous data.

I'll dig deeper into the server-side rendering portion another time. But now developers can create faster, and more efficient applications, resulting in an awesome user experience.

React 18 gives some great confidence towards the react developer team and I'm looking forward to seeing a lot of other new features around SSR, and hopefully React Forget!