React 18 RC.3 has been released, and the API has been stabilized. For now, there are some BUG fixes, but we expect a release soon. The React team is cautiously exploring new features, and it’s been three years since version 16.8, and the full concurrency mode has finally arrived. Today we explore some of the problems and new features of upgrading React 17 to 18 from a user perspective.

upgrade

Use YARN to install the latest React 18 RC

yarn add react@rc react-dom@rc
Copy the code

change

React 18 does not support IE 11. Use React 17 if it is compatible with IE.

createRoot

React 18 provides two Root apis, which we call the Legacy Root API and New Root API.

  • Legacy root API: that is,ReactDOM.render. This creates one that runs in “legacy” moderoot, its working mode andReact 17Exactly the same. Using this API comes with a warning indicating that it has been deprecated and switched toNew Root API.
  • New Root API: that is,createRoot. This will create an inReact 18In the operation of theroot, it addsReact 18All of the improvements and allow the use of concurrency capabilities.

We started the project with Vite + TS as scaffolding. When the project starts, you will see a warning in the console:

This means you can upgrade your project directly to React 18 without causing a break change. This is because it only comes with a warning and will be available and compatible throughout version 18, maintaining the features of the React 17 version.

Why do you do that? Because only for the project upgrade is more efficient, meet a place to change a place, no historical burden. However, the React component ecosystem is very large, and many components use reactdom. render directly, such as modal. confirm API in common UI libraries. In this case, a version cycle is required to upgrade these ecological components.

// React 17
import ReactDOM from 'react-dom';
const container = document.getElementById('app');
/ / loading
ReactDOM.render(<App tab="home" />, container);
/ / unloading
ReactDOM.unmountComponentAtNode(container);

// React 18
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
/ / loading
root.render(<App tab="home" />);
/ / unloading
root.unmount();
Copy the code

It must also be said that the createRoot API is identical to Vue3’s createApp form.

FAQ: In TypeScript, the createRoot parameter container accepts HTMLElement but cannot be empty. Use either assertion or judgment

Server side rendering

hydrateRoot

Upgrade Hydrate to hydrateRoot if your application uses server rendering with water injection

const root = hydrateRoot(container, <App tab="home" />);
Root.render is not required here
Copy the code

In this release, the react-dom/serverAPI has also been improved to fully support Suspense and streaming SSR on servers. As part of these changes, the old Node streaming API, which does not support incremental Suspense streaming on servers, will be deprecated.

  • renderToNodeStream= >renderToPipeableStream
  • newrenderToReadableStreamTo support theDeno
  • Continue to userenderToString(toSuspenseLimited support)
  • Continue to userenderToStaticMarkup(toSuspenseLimited support)

SetState Synchronous/asynchronous

This is the most disruptive update to React and is not retro-compatible.

React batching is simply the merging of multiple state updates into a single re-render for better performance. Prior to React 18, React could only be batched in component lifecycle functions or composited event functions. By default, promises, setTimeout, and native events are not batched. If you want to keep batch processing, you can do this using unstable_batchedUpdates, but it’s not a formal API.

React 18 before:

function handleClick() {
  setCount(1);
  setFlag(true);
  // Batch: will merge into a render
}

async function handleClick() {
  await setCount(2);
  setFlag(false);
  // Synchronous mode: render is executed twice
  // After setCount and before setFlag, the latest count value can be obtained by Ref
}
Copy the code

The second example above React 18 will only have one render because all updates will be automatically batched. This undoubtedly improves the overall performance of the application.

flushSync

What do I do if I want to exit batch in React 18? The official API flushSync is provided.

FlushSync

(fn: () => R): R It takes a function as an argument and allows a return value.

function handleClick() {
  flushSync(() = > {
    setCount(3);
  });
  // setFlag is executed after setCount and render
  setFlag(true);
}
Copy the code

FlushSync uses a function as its scope. The setstates in the flushSync function are still batch updates. This allows precise control of unnecessary batch updates:

function handleClick() {
  flushSync(() = > {
    setCount(3);
    setFlag(true);
  });
  // setCount and setFlag are batch updates
  setLoading(false);
  // This method fires render twice
}
Copy the code

This approach gives rerender more elegant granularity control than React 17 and its predecessors.

FlushSync is useful in situations like clicking the save button in a form, triggering the subform to close, synchronizing to global state, and calling the save method after the state update:

The child form:

export default function ChildForm({ storeTo }) {
  const [form] = Form.useForm();

  // The value of the subform is synchronized globally when the current component is unloaded
  // To trigger the parent to synchronize setState, useLayoutEffect must be used
  useLayoutEffect(() = > {
    return () = >{ storeTo(form.getFieldsValue()); }; } []);return (
    <Form form={form}>
      <Form.Item name="email">
        <Input />
      </Form.Item>
    </Form>
  );
}
Copy the code

External container:

<div
  onClick={() = > {
    // Triggers the subform unload closure
    flushSync(() = > setVisible(false));
    // After the subform value is updated globally, the save method is triggered to ensure that onSave gets the newly filled form valueonSave(); }} > Save </div><div>{visible && <ChildForm storeTo={updateState} />}</div>
Copy the code

Unstable_batchedUpdates will remain in React 18 as many open source libraries use it.

Uninstalled components update status warning

In normal development, we can’t avoid the following errors:

This warning is widely misunderstood and somewhat misleading. It was intended for the following scenarios:

useEffect(() = > {
  function handleChange() {
    setState(store.getState());
  }
  store.subscribe(handleChange);
  return () = >store.unsubscribe(handleChange); } []);Copy the code

If you forget the call in unsubscribe effect cleanup, a memory leak occurs. In practice, this is not often the case. This is more common in our code:

async function handleSubmit() {
  setLoading(true);
  // The component may unload while we wait
  await post('/some-api');
  setLoading(false);
}
Copy the code

Here, too, a warning is triggered. But, in this case, the warning is misleading.

There is no actual memory leak, and the Promise will resolve quickly, after which it can be garbage collected. To suppress this warning, we might write a lot of isMounted useless judgments, which would make the code even more complex.

React 18 has removed this warning.

Component returns NULL

In React 17, if the component returns undefined in render, React will throw an error at runtime:

function Demo() {
  return undefined;
}
Copy the code

Here we can replace undefined with null and the program will continue to run. The purpose of this behavior is to help users find common problems with accidentally forgetting return statements. Suspense fallback for React 18 does not report errors with undefined undefined causing inconsistencies.

Now that type systems and Eslint are robust enough to avoid such low-level errors, React 18 no longer checks for crashes caused by returning undefined.

StrictMode

Starting with React 17, React automatically modifies console methods, such as console.log() to mute logs the second time a lifecycle function is called. However, in some cases where workarounds are available, it can lead to bad behavior.

This behavior has been removed in React 18. If React DevTools > 4.18.0 is installed, the logs during the second rendering will now appear in the console in soft colors.

The new API

useSyncExternalStore

UseSyncExternalStore has undergone a change from unstable_useMutableSource to subscribe to external data sources. Mainly help developers with external store requirements to solve the tear problem.

A simple example of a hook that listens to innerWidth changes:

import { useMemo, useSyncExternalStore } from 'react';

function useInnerWidth() :number {
  // Maintain subscribe fixed reference to avoid resize listener repeated execution
  const [subscribe, getSnapshot] = useMemo(() = > {
    return [
      (notify: () => void) = > {
        // Throttling is used in real situations
        window.addEventListener('resize', notify);
        return () = > {
          window.removeEventListener('resize', notify);
        };
      },
      // Returns the snapshot required after resize
      () = > window.innerWidth, ]; } []);return useSyncExternalStore(subscribe, getSnapshot);
}
Copy the code
function WindowInnerWidthExample() {
  const width = useInnerWidth();

  return <p>Width: {width}</p>;
}
Copy the code

The Demo address: codesandbox. IO/s/usesyncex…

React’s own state has natively solved the tear problem with concurrency. UseSyncExternalStore is mainly used by framework developers such as Redux. Instead of using the React state directly, useSyncExternalStore maintains a store object externally. React cannot automatically resolve the tear problem. React therefore provides such an API.

React-redux 8.0 is implemented based on useSyncExternalStore.

useInsertionEffect

UseInsertionEffect works in much the same way as useLayoutEffect, except that references to DOM nodes cannot be accessed.

So the recommended solution is to use this Hook to insert stylesheets (or reference them if you need to delete them) :

function useCSS(rule) {
  useInsertionEffect(() = > {
    if(! isInserted.has(rule)) { isInserted.add(rule);document.head.appendChild(getStyleForRule(rule)); }});return rule;
}
function Component() {
  let className = useCSS(rule);
  return <div className={className} />;
}
Copy the code

useId

UseId is an API for generating unique ids on both clients and servers while avoiding hydration mismatches. Example:

function Checkbox() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>Select box</label>
      <input type="checkbox" name="sex" id={id} />
    </div>
  );
}
Copy the code

Concurrent mode

Concurrent mode, a new set of React features that help applications stay responsive and adjust appropriately to the user’s device performance and network speed, fixes blocking rendering limitations by making rendering interruptible. In Concurrent mode, React can update multiple states simultaneously.

Normally, when we update state, we expect these changes to be reflected on the screen immediately. It is common sense to expect applications to continuously respond to user input. However, sometimes we will expect to update the delayed response on the screen. Implementing this feature in React was previously difficult. The Concurrent model provides a series of new tools to make this possible.

Transition

React 18 introduced a new API, startTransition, designed to keep the UI responsive even with a large number of tasks. This new API can significantly improve user interaction by marking specific updates as “transitions.”

The issue:

import { startTransition } from 'react';

// Emergency: displays the input
setInputValue(input);

// Mark updates within the callback function as non-urgent
startTransition(() = > {
  setSearchQuery(input);
});
Copy the code

In simple terms, renderings triggered by setState wrapped by the startTransition callback are marked as non-emergency renderings, and they may be preempted by other emergency renderings.

In general, we need to notify the user that the background is working. A useTransition with an isPending transition flag is provided for this purpose, and React will provide visual feedback during the state transition and keep the browser responsive while the transition occurs.

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();
Copy the code

The isPending value is true when the conversion is suspended, and a loader can be placed on the page.

Under normal circumstances:

Using useTransition

The Demo address: codesandbox. IO/s/starttran…

We can use startTransition to wrap any updates to move into the background. In general, these types of updates fall into two categories:

  1. Rendering slowly: These updates take time becauseReactIt takes a lot of work to transform the UI to display the results
  2. The network is slow: These updates take time becauseReactWaiting for some data from the network. This approach is related toSuspenseClose integration

Network slow scene: a list page, when we click “next page”, the existing list immediately disappears, and then we see the entire page has only one loading prompt. This is an “undesirable” loading state. It would be better if we could “skip” this process and wait until the content loads before transitioning to a new page.

In Suspense we do loading boundary processing:

import React, { useState, useTransition, Suspense } from 'react';
import { fetchMockData, MockItem } from './utils';
import styles from './DemoList.module.less';

const mockResource = fetchMockData(1);

export default function DemoList() {
  const [resource, setResource] = useState(mockResource);
  const [isPending, startTransition] = useTransition();

  return (
    <Suspense fallback="Loading">
      <UserList resource={resource} />
      <button
        className={styles.button}
        type="button"
        onClick={()= >startTransition(() => { setResource(fetchMockData(2)); })} > Next</button>
      {isPending && <div className={styles.loading}>In the load</div>}
    </Suspense>
  );
}

function UserList({ resource }: UserListProps) {
  const mockList = resource.read();
  return (
    <div className={styles.list}>
      {mockList.map((item) => (
        <div key={item.id} className={styles.row}>
          <div className={styles.col}>{item.id}</div>
          <div className={styles.col}>{item.name}</div>
          <div className={styles.col}>{item. Age}</div>
        </div>
      ))}
    </div>
  );
}
Copy the code

Results show:

The Demo address: codesandbox. IO/s/usetransi…

theTransitionIntegrated into the application design system

UseTransition is a very common requirement. Almost any click or interaction that can cause a component to hang requires useTransition to avoid accidentally hiding what the user is interacting with.

This can lead to a lot of repetitive code in the component. It is often recommended to incorporate useTransition into the design system components of your application. For example, we can extract the useTransition logic into our own

function Button({ children, onClick }) {
  const [startTransition, isPending] = useTransition();

  function handleClick() {
    startTransition(() = > {
      onClick();
    });
  }

  return (
    <button onClick={handleClick} disabled={isPending}>{children} {isPending ? 'Loading' : null}</button>
  );
}
Copy the code

UseTransition has an optional parameter, you can set the timeout time timeoutMs, but the current TS type is not enabled.

useDeferredValue

Returns the value of a delayed response, which is typically used to keep the interface responsive when you have content that is rendered immediately based on user input, and content that needs to wait for data to be retrieved.

import { useDeferredValue } from 'react';

const deferredValue = useDeferredValue(value);
Copy the code

Does useDeferredValue feel similar to useTransition?

  • The same:useDeferredValueIntrinsically and internally implemented withuseTransitionBoth are marked as deferred update tasks.
  • Different:useTransitionIs to change the update task to a delayed update task, anduseDeferredValueIs to generate a new value as the delayed state.

So what’s the difference between it and Debounce?

Debounce (setTimeout) will always have a fixed delay, while useDeferredValue will only lag for the time it takes to render, which will be less on a good machine and longer if not.

conclusion

This is a summary of the React update, which focuses on the concurrency mode. Get ready in advance to release the official version of the upgrade ~

React-photo-view is fully compatible with React 18.

Learn how to upgrade React 18.

Articles exported this year

  • Great work in 2022: an ultra-refined image preview component
  • Vite is the best practice of reductive routing, a cool way to implement it
  • Vite plug-in development practices: resource handling of microfront-end
  • Vite micro front-end practice, to achieve a componentized scheme
  • JS immutable data trams, immer is not the ultimate way out, high-performance scenarios need to be implemented by themselves