This article combs the new features of React 18

Iterative process of React

The main features of React from v16 to v18 include three changes:

  • v16: Async Mode
  • v17: Concurrent Mode
  • v18: Concurrent Render

The update process of Fiber tree in React is divided into two stages: render stage and commit stage. The render function of the component is called render when it is executed (what changes need to be made in this update), pure js calculation; The process of rendering the result of render to the page is called commit (changing to the real host environment is operating DOM in the browser).

In Sync mode, the render phase is completed at one time; In Concurrent mode, the render phase can be disassembled, and a part of each time slice is executed until the execution is completed. Due to the DOM update in the commit phase, it is impossible to interrupt the DOM update in half. It must be completed at one time.

  • Async Mode: make render asynchronous and interruptible.
  • Concurrent Mode: make the commit concurrent in the user's perception.
  • Concurrent Render: concurrent mode contains breaking change, for example, many libraries are incompatible (mobx, etc.), so v18 proposes Concurrent Render, which reduces the migration cost of developers.

New features of React concurrency

The purpose of concurrent rendering mechanism is to properly adjust the rendering process according to the user's device performance and network speed, so as to ensure that the React application can still maintain interactivity in the long-term rendering process, avoid page jamming or non response, and improve the user experience.

v18 formally introduces the concurrent rendering mechanism, which brings us many new features. These new features are optional concurrent functions. The components that use these new features can trigger concurrent rendering, and their whole subtree will automatically turn on strictMode.

New root API

Before v18, the root node was opaque to users.

import * as ReactDOM from 'react-dom'
import App from './App'
​
const root = document.getElementById('app')
// Method before v18
ReactDOM.render(<App/>,root)

In v18, we can manually create the root node through createRoot Api.

import * as ReactDOM from 'react-dom'
import App from './App'
​
const root = ReactDOM.createRoot(document.getElementById('app'))
// New method of v18
root.render(<App/>,root)

If you want to use other new feature APIs in v18, you must use the new Root API to create the root node.

Batch optimization automatic

Batch processing: React groups multiple state updates into one re render for better performance. (merge multiple setstate events)

Before v18, batch processing was only implemented in the event processing function. In v18, all updates will be automatically batch processed, including promise chain, setTimeout and other asynchronous codes and native event processing functions.

// Before v18
function handleClick () {
  fetchSomething().then(() => {
      // React 17 and earlier versions will not batch the following state s:
      setCount((c) => c + 1) // Re render
      setFlag((f) => !f) // Second re rendering
    })
}
// v18 lower
// 1. promise chain
function handleClick () {
  fetchSomething().then(() => {
      setCount((c) => c + 1)  
      setFlag((f) => !f) // Merge into one re render
    })
}
// 2. In asynchronous code such as setTimeout
setTimeout(() => {
  setCount((c) => c + 1)  
  setFlag((f) => !f) // Merge into one re render
}, 5000)
// 3. In native events
element.addEventListener("click", () => {
setCount((c) => c + 1)  
  setFlag((f) => !f) // Merge into one re render
})

If you want to quit automatic batch processing and update immediately, you can use reactdom Flushsync().

import * as ReactDOM from 'react-dom'
​
function handleClick () {
  // Update now
  ReactDOM.flushSync(() => {
    setCounter(c => c + 1)
  })
  // Update now
  ReactDOM.flushSync(() => {
    setFlag(f => !f)
  })
}

startTransition

Can be used to reduce rendering priority. It is used to wrap the function and value with large amount of calculation, reduce the priority and reduce the number of repeated rendering.

For example: keyword Association of search engine. Generally speaking, the user's input in the input box is expected to be updated in real time. If there are many associative words and they need to be updated in real time, the user's input may get stuck. In this way, the user experience will become worse, which is not the result we want.

We extract the status update of this scene: one is the update input by the user; One is the update of associative words. The urgency of these two updates is obviously greater than that of the latter.

In the past, we can use anti shake operation to filter unnecessary updates, but anti shake has a disadvantage. When we input continuously for a long time (the time interval is less than the anti shake setting time), the page will not respond for a long time. startTransition can specify the rendering priority of UI, which needs real-time update and which needs delayed update. Even if the user inputs for a long time, it will be updated at the latest 5s. The official also provides the hook version of useTransition, which accepts the parameter of one millisecond to modify the latest update time, and returns the pending state of a transition period and the startTransition function.

import * as React from "react";
import "./styles.css";
​
export default function App() {
  const [value, setValue] = React.useState();
  const [searchQuery, setSearchQuery] = React.useState([]);
  const [loading, startTransition] = React.useTransition(2000);
​
  const handleChange = (e) => {
    setValue(e.target.value);
    // Delayed update
    startTransition(() => {
      setSearchQuery(Array(20000).fill(e.target.value));
    });
  };
​
  return (
    <div className="App">
      <input value={value} onChange={handleChange} />
      {loading ? (
        <p>loading...</p>
      ) : (
        searchQuery.map((item, index) => <p key={index}>{item}</p>)
      )}
    </div>
  );
}

All updates in the startTransition callback will be considered as non urgent processing. If there is a more urgent processing (such as user input here), startTransition will interrupt the previous update and only render the latest status update.

The principle of startTransition is to use the priority scheduling model at the bottom of React.

More examples: Real world example: adding startTransition for slow rendering

Suspend component under SSR

Function of suspend: divide the parts of the page that need concurrent rendering.

Hydration: during ssr, the server outputs strings (html). The client (usually the browser) completes the initialization of React according to these strings and the loaded JavaScript. This stage is hydration.

In the SSR before React v18, the client must wait for HTML data to be loaded into the server at one time, and wait for all JavaScript to be loaded before starting hydration, and wait for all components to be hydrated before interaction. That is, the whole process needs to complete the process from obtaining data (server) → rendering to HTML (server) → loading code (client) → hydrate (client). Such SSR can not make our fully interactive faster, but improve the speed of users' perception of static page content.

React v18 supports suspend under SSR. What is the biggest difference?

1. The server does not need to wait for the suspended package component to be loaded, and then it can send HTML. Instead of the suspended package, the component is the content in the fallback, which is generally a placeholder (spinner), marking the location of this HTML with the minimum inline < script > tag. Wait until the data of the components on the server is ready, and then React will send the remaining HTML to the same stream.

2. The process of hydration is step-by-step. There is no need to wait for all js to load before starting hydration, so as to avoid page jamming.

3. React will listen to interactive events (such as mouse clicks) on the page in advance and hydration the priority of the area where the interaction occurs.

https://github.com/reactwg/react-18/discussions/37

useSyncExternalStore

This API can prevent the third-party store from being modified after the task is interrupted in the concurrent mode, and the problem of data inconsistency caused by tearing occurs when the task is restored. Users rarely use it. In most cases, it is provided to state management libraries such as redux. Through useSyncExternalStore, React can keep its state synchronized with the state from Redux in concurrent mode.

import * as React from 'react'
​
// Basic usage: getSnapshot returns a cached value
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot)
​
// According to the data field, use the inline getSnapshot to return the cached data
const selectedField = React.useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField)
  • The first parameter is a subscription function, which will cause the update of the component when the subscription is triggered.
  • The second function returns an immutable snapshot. The return value is the data we want to subscribe to. It needs to be re rendered only when the data changes.

useInsertionEffect

This hook plays a great role in the existing CSS in JS library specially designed for React. It can dynamically generate new rules and insert them into the document together with < style > tags.

Suppose now we want to insert a css and put this operation during rendering.

function css(rule) {
  if (!isInserted.has(rule)) {
    isInserted.add(rule)
    document.head.appendChild(getStyleForRule(rule))
  }
  return rule
}
function Component() {
  return <div className={css('...')} />
}

This will result in that each time the CSS style is modified, react needs to recalculate all CSS rules for all nodes in each rendered frame, which is not the result we want.

Can we insert these css styles before all DOM are generated? At this time, we may think of uselayouteeffect, but the DOM can be accessed in uselayouteeffect. If the layout style of a DOM (such as clientWidth) is accessed in this hook, the information we read will be wrong.

useLayoutEffect ( ( )  =>  { 
  if  ( ref.current.clientWidth  <  100 )  { 
    setCollapsed ( true ) ; 
  } 
} ) ;

useInsertionEffect can help us avoid the above problems, which can meet the requirements of inserting and not accessing DOM before all DOM are generated. Its working principle is roughly the same as uselayouteeffect, except that the reference of DOM node cannot be accessed at this time. We can insert global DOM nodes into this hook, such as < style >, or SVG < defs >.

const useCSS: React.FC = (rule) => {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule)
      document.head.appendChild(getStyleForRule(rule))
    }
  })
  return rule
}
const Component: React.FC = () => {
  let className = useCSS(rule)
  return <div className={className} />
}

https://github.com/reactwg/react-18/discussions/110

useId

React has been developing towards the field of SSR, but SSR rendering must ensure that the HTML structure generated by the client and server matches. We usually use math In front of SSR, random () cannot guarantee the id uniqueness between the client and the server.

In order to solve this problem, React puts forward the hook of useOpaqueIdentifier, but it will produce different results in different environments

  • A string will be generated on the server
  • An object will be generated on the client side and must be passed directly to the DOM attribute

In this way, if you need to generate multiple identifiers on the client side, you need to call this hook multiple times, because it does not support conversion to string, so string splicing cannot be used.

const App: React.FC = () => {
  const tabIdOne = React.unstable_useOpaqueIdentifier();
  const panelIdOne = React.unstable_useOpaqueIdentifier();
  const tabIdTwo = React.unstable_useOpaqueIdentifier();
  const panelIdTwo = React.unstable_useOpaqueIdentifier();
​
  return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={tabIdOne} value="one">
            One
          </Tab>
          <Tab id={tabIdTwo} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={panelIdOne} value="one">
          Content One
        </TabPanel>
        <TabPanel id={panelIdTwo} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  );
}

useId can generate a unique id between the client and the server, and return a string. Such a component can call useId only once and use the result as the identifier basis required by the whole component (such as splicing different strings) to generate a unique id.

const App: React.FC = () => {
  const id = React.useId()
  return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={`${id}tab1`} value="one">
            One
          </Tab>
          <Tab id={`${id}tab2`} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={`${id}panel1`} value="one">
          Content One
        </TabPanel>
        <TabPanel id={`${id}panel2`} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  )
}

useDefferdValue

React can allow variable delay update through useDefferdValue, and accept an optional maximum value of delay update at the same time. React will try to update the delay value as soon as possible. If it fails to complete within the given timeout ms period, it will force the update.

const defferValue = useDeferredValue(value, { timeoutMs: 1000 })

useDefferdValue can well show the characteristics of priority adjustment during concurrent rendering. It can be used to delay the state with complex calculation logic, let other components render I first, and wait for this state to be updated before rendering.

Keywords: Javascript Front-end React

Added by psr540 on Wed, 09 Mar 2022 04:45:01 +0200