The React iteration process

React features from v16 to V18 include three changes:

  • V16: Async Mode
  • V17: Concurrent Mode
  • V18: Concurrent Render

The Fiber tree update process in React is divided into the Render phase and the COMMIT phase. The component’s render function is executed as render (which changes need to be made in this update) and is purely JS computed; The process of rendering the results of render to the page is called commit (changing to the real host environment, which in the browser is DOM manipulation).

In Sync mode, the Render phase is executed all at once; In Concurrent mode, the Render phase can be disassembled and executed in each time slice until the execution is complete. Because DOM updates occur during the COMMIT phase, it is not possible to interrupt DOM updates in the middle and must be performed in one go.

  • Async Mode: enables render to be asynchronous and interruptible.
  • Concurrent Mode: Allows the commit to be perceived as Concurrent by the user.
  • Concurrent Render: A Concurrent Mode contains breaking change, such as library incompatibations (Mobx, etc.), so V18 introduces Concurrent Render to reduce migration costs for developers.

React Concurrency is new

The purpose of Concurrent rendering is to properly adjust the rendering process based on the user’s device performance and network speed to ensure the React application remains interactive during the long rendering process and avoid page stutter or unresponsiveness, thus improving the user experience.

V18 officially introduced concurrent rendering and brought us a lot of new features based on it. These new features are optional concurrency features, and components that use these new features can trigger concurrent rendering, and strictMode is automatically enabled for their entire subtree.

The 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')
// The method before v18
ReactDOM.render(<App/>,root)
Copy the code

In V18 you can manually create the root node using the createRoot Api.

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

To use the other new V18 feature apis, you need to use the new Root API to create the Root node.

Automatic batching Automatic batching optimization

Batch processing: React groups multiple status updates into a single re-render for better performance. (Merge multiple setState events)

Prior to V18, batching was only implemented in event handlers, where all updates are automatically batched, including promise chains, asynchronous code such as setTimeout, and native event handlers.

/ / v18 before
function handleClick () {
  fetchSomething().then(() = > {
      React 17 and previous versions do not batch the following states:
      setCount((c) = > c + 1) // re-render
      setFlag((f) = >! f)// Re-render})}/ / v18
// 1
function handleClick () {
  fetchSomething().then(() = > {
      setCount((c) = > c + 1)  
      setFlag((f) = >! f)// merge into a re-render})}// 2, setTimeout, etc
setTimeout(() = > {
  setCount((c) = > c + 1)  
  setFlag((f) = >! f)// merge into a re-render
}, 5000)
// 3
element.addEventListener("click".() = > {
setCount((c) = > c + 1)  
  setFlag((f) = >! f)// merge into a re-render
})
Copy the code

If you want to exit automatic batching and update immediately, you can use reactdom.flushsync () to wrap.

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

startTransition

Can be used to reduce rendering priority. They are used to wrap functions and values that require a lot of calculation, respectively, to reduce priority and repeat rendering times.

Take, for example, keyword association in search engines. Generally speaking, for users in the input box input is expected to be updated in real time, if there are more association words at this time also want to update in real time, this may lead to the user’s input will be delayed. It makes the user experience worse, which is not what we want.

We extract the status updates for this scenario: one is user-input updates; One is the updating of associative words. The urgency of these two updates is obviously greater than the former.

We used to be able to filter out unnecessary updates using the shaker action, but the downside of shaker is that when we type continuously for a long time (the interval is shorter than the shaker setting), the page will remain unresponsive for a long time. StartTransition, on the other hand, can specify UI rendering priorities, which should be updated in real time, and which should be updated with delay. UseTransition will be updated even if the user input 5s at the latest for a long time. The hook version of useTransition accepts a millisecond parameter to change the latest update time, and returns a pending state and startTransition function for the transition period.

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);
    // Delay the 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>
  );
}

Copy the code

Anything updated in the startTransition callback is considered non-urgent, and if more urgent processing occurs (such as user input here), startTransition interrupts the previous update and renders only the most recent status update.

StartTransition utilizes the priority scheduling model at the bottom of React.

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

Suspense component under SSR

SuspensePartitioned the parts of a page that need to be rendered concurrently.

Hydration [hydration] : SSR output when the server is a string (HTML), the client browser (usually) according to these strings and combined with loading the JavaScript to complete the initialization of the React in this stage of hydration.

Before React V18, the CLIENT had to wait once for HTML data to load on the server and wait for all JavaScript to load before starting to encounter, and wait for all components to interact with Chocolate. That is, the whole process needs to be completed from obtaining data (server), rendering to HTML (server), loading code (client), and hydrate (client). SSR doesn’t make us fully interactive any faster, it just increases the speed at which users perceive static page content.

React V18 supports Suspense with SSR. What are the biggest differences?

The server does not need to wait for components wrapped in Suspense to finish loading to send HTML. Components wrapped in Suspense instead are content in Fallback, usually a spinner that marks the location of the HTML with a minimum of inline

2. The process of hydration is gradual, and one does not need to wait for all JS to finish loading before he begins to encounter chocolate, thus avoiding the lag of the page.

3. React monitors interaction events (such as mouse clicks) on the page in advance, and reveals the priorities of the areas where interactions occur.

Github.com/reactwg/rea…

useSyncExternalStore

This API prevents in concurrent mode a third party store from being modified after a task has been disconnected and replicated causing tearing inconsistencies. UseSyncExternalStore allows React to keep its state in sync with the state from Redux in concurrent mode.

import * as React from 'react'For basic usage, getSnapshot returns a cached value
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot)
​
// Return cached data based on the data field using inline getSnapshot
const selectedField = React.useSyncExternalStore(store.subscribe, () = > store.getSnapshot().selectedField)
Copy the code
  • The first parameter is a subscription function that causes the component to update when the subscription is triggered.
  • The second function returns an IMmutable snapshot of the data that we want to subscribe to, and only needs to be rerendered if the data changes.

useInsertionEffect

This hook makes great use of the existing CSS-in-JS library designed for React, which dynamically generates new rules to be inserted into documents along with

Suppose we now want to insert a piece of CSS and do it 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('.')} / >
}
Copy the code

This would result in react having to recalculate all CSS rules for all nodes in every frame rendered every time the CSS style is changed, which is not what we want.

So can we insert these CSS styles before all the DOM is generated, and we might think of useLayoutEffect, but useLayoutEffect has access to the DOM, If we access a DOM layout style (such as clientWidth) in this hook, we will read the wrong information.

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

The useInsertionEffect can help us avoid this problem by inserting and not accessing the DOM until all DOM is generated. It works in much the same way as useLayoutEffect, except that you don’t have access to references to DOM nodes. We can insert global DOM nodes in this hook, such as

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} />
}
Copy the code

Github.com/reactwg/rea…

useId

React has been moving toward SSR, but SSR rendering must ensure that the client matches the HTML structure generated by the server. What we normally use, such as math.random (), is no guarantee of id uniqueness between client and server in SSR.

React uses the useOpaqueIdentifier hook to solve this problem, but it will produce different results in different environments.

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

This way, if you need to generate multiple identifiers on the client side, you need to call the hook multiple times, because it does not support conversion to a string, so you cannot use string concatenation.

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>
  );
}
Copy the code

UseId generates a unique ID between the client and server and returns a string. Such a component can call useId just once and use the result as the basis for the identifiers needed for the entire component (such as concatenating 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>)}Copy the code

useDefferdValue

React allows variable delays with useDefferdValue while accepting an optional maximum of delayed updates. React will try to update the delay value as soon as possible, and if it doesn’t complete within a given timeoutMs period, it will force the update.

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

UseDefferdValue can be used to delay the calculation of states with complicated logic, so that other components can render I first and wait for the state to be updated before rendering.