Interpretation of new features of React18 & full version upgrade guide

Notice

React 18We have given up ie11support for and will 2022年6月15日stop supporting it ie. If you need compatibility, you need to roll back to React 17version.

React 18 中引入的新特性是使用现代浏览器的特性构建的,在IE中无法充分polyfill,比如micro-tasks 

upgrade

  • New project: Use directly npmor yarninstall the latest version of dependencies (if it is js, you do not need to install the types declaration file)
npm i react react-dom --save
    
npm i @types/react @types/react-dom -D 
  • Old project: First change the version number in the dependency to the latest, then delete node_modulesthe folder and reinstall:
npm i 

new features

1. Render API

For better management root节点, React 18a new one has been introduced root API. The new root APIalso supports new concurrent renderer(concurrent mode rendering), which allows you to enter concurrent mode(concurrent mode).

// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.render(<App />, root);

// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.createRoot(root).render(<App />); 

At the same time, when uninstalling the component, we also need to unmountComponentAtNodeupgrade to root.unmount:

// React 17
ReactDOM.unmountComponentAtNode(root);

// React 18
root.unmount(); 

Tips: If we React 18use the old one in render api, you will see a warning in the console after the project is started:

11.jpgThis means you can upgrade your project directly to React 18version without causing a direct hit break change. If you need to maintain React 17version features, you can ignore this error because it 18is compatible across versions.

In addition to this, it has been removed React 18from the method because it generally does not have the expected results when used.render回调函数Suspense

In the new version, if you need to renderuse a callback function in the method, we can implement it in the component through useEffect:

// React 17
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root, () => {
  console.log('渲染完成');
});

// React 18
const AppWithCallback: React.FC = () => {
  useEffect(() => {
    console.log('渲染完成');
  }, []);
  return <App />;
};
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<AppWithCallback />); 

Finally, if your project uses ssrserver-side rendering, you need to hydrationupgrade to hydrateRoot:

// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.hydrate(<App />, root);

// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.hydrateRoot(root, <App />); 

In addition, you also need to update TypeScriptthe type definition. If your project uses it TypeScript, the most noteworthy change is that now when defining propsthe type, if you need to get the subcomponent children, then you need to 显式的定义它, for example:

// React 17
interface MyButtonProps {
  color: string;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
  // 在 React 17 的 FC 中,默认携带了 children 属性
  return <div>{children}</div>;
};

export default MyButton;

// React 18
interface MyButtonProps {
  color: string;
  children?: React.ReactNode;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
  // 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
  return <div>{children}</div>;
};

export default MyButton; 

2. setState automatic batch processing

React 18Performance improvements are implemented out of the box by performing batch processing by default.

Batch processing means that in order to obtain better performance, 多个状态更新batch processing is merged into batches at the data layer 一次更新(and 多个渲染merged into batches at the view layer 一次渲染).

1. Before React 18:

Now React 18 之前, we only React 事件处理函数do batch updates in . By default, updates in promise, setTimeout, 原生事件处理函数in , or 任何其它事件内are not batched:

Scenario 1: React event handler function

import React, { useState } from 'react';

// React 18 之前
const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <button
      onClick={() => {
        setCount1(count => count + 1);
        setCount2(count => count + 1);
        // 在React事件中被批处理
      }}
    >
      {`count1 is ${count1}, count2 is ${count2}`}
    </button>
  );
};

export default App; 

Click the button to print console.log:

animation2.gif

As you can see, the number of renderings and the number of updates are the same. Even if we update two states, the component is only rendered once each time it is updated.

However, if we put the status update inside promiseor setTimeout:

Case 2: setTimeout

import React, { useState } from 'react';

// React 18 之前
const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div
      onClick={() => {
        setTimeout(() => {
          setCount1(count => count + 1);
          setCount2(count => count + 1);
        });
        // 在 setTimeout 中不会进行批处理
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App; 

Click the button to reprint console.log:

Animation 3.gif

As you can see, every time you click to update the two states, the component will be rendered twice and batch updates will not be performed.

Scenario 3: Native js events

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

// React 18 之前
const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  useEffect(() => {
    document.body.addEventListener('click', () => {
      setCount1(count => count + 1);
      setCount2(count => count + 1);
    });
    // 在原生js事件中不会进行批处理
  }, []);
  return (
    <>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </>
  );
};

export default App; 

Click the button to reprint console.log:

Animation 3.gif

It can be seen that in the native js event, the result is the same as the second situation. Every time you click to update the two states, the component will be rendered twice, and batch updates will not be performed.

2. In React 18:

In React 18the three examples above this will only happen once renderas all updates will be automatically batched. This undoubtedly greatly improves the overall performance of the application.

However, the following example will React 18execute render twice in :

import React, { useState } from 'react';

// React 18
const App: React.FC = () => {
  console.log('App组件渲染了!');
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div
      onClick={async () => {
        await setCount1(count => count + 1);
        setCount2(count => count + 1);
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App; 

Summarize:

  • Before 18, batch processing would be automatically executed only in the react event processing function, and in other cases it would be updated multiple times.
  • After 18, batch processing will be automatically performed in any case, and multiple updates will always be merged into one

3. flushSync

Batch processing is a 破坏性改动, if you want to exit batch update, you can use flushSync:

import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const App: React.FC = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <div
      onClick={() => {
        flushSync(() => {
          setCount1(count => count + 1);
        });
        // 第一次更新
        flushSync(() => {
          setCount2(count => count + 1);
        });
        // 第二次更新
      }}
    >
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
    </div>
  );
};

export default App; 

Note: flushSyncMultiple inside the function setStateare still updated in batches , so that unnecessary batch updates can be precisely controlled.

For more information about 批处理and flushSync, you can refer to React’s official Automatic batching deep dive .

4. Update status warning when uninstalling components

When we are developing, we occasionally encounter the following errors:

QQ screenshot 20220503200708.jpg

This error means: Unable to perform a status update on an unmounted (unmounted) component. This is an invalid operation and indicates a memory leak in our code.

In fact, this error is rare, and in previous versions this warning was widely misunderstood and somewhat misleading.

The original intention of this error was to target some special scenarios, such 你在useEffect里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记return一个函数清除副作用,则会发生内存泄漏……as

But in actual development, more scenarios are, 我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件. In this scenario, the warning will also be triggered. However, in this case, there is no memory leak inside the component because the async function has been garbage collected, and at this point, the warning is misleading .

Regarding this point, React also has an official explanation:

222.jpg

To sum up the above reasons, in React 18, this error was officially deleted.

For more information about this error, you can refer to React’s official instructions, click here to view.

5. About the return value of React components

  • In React only allows returning React 17one if you need to . If you return explicitly , the console will throw an error at runtime.空组件nullundefined
  • In React 18, undefinedcrashes caused by returning are no longer checked. It can both return nulland return undefined(but the file React 18of dtswill still be checked and only return is allowed null. You can ignore this type error).

Official explanation about component return value: github.com/reactwg/rea…

6. Strict Mode

Console logs are no longer suppressed:

严格模式React works with each component as you use it 两次渲染, allowing you to observe some unexpected results. In React 17, 其中一次渲染console logging has been suppressed to make the logs easier to read.

To address community confusion about this issue, in React 18, this restriction was officially lifted. If you have it installed React DevTools, log messages for the second render will be grayed out and displayed in the console in a soft manner.

QQ screenshot 20220503205553.jpg

Official explanation about Strict Mode: github.com/reactwg/rea…

7. Suspense no longer requires fallback to capture

In the component React 18of Suspense, officials 空的fallbackhave made changes to the way attributes are handled: no longer skip the boundaries of 缺失值or . Instead, the boundary is captured and looked outside, and if not found, will be rendered as .值为nullfallbackSuspensefallbacknull

Before update:

Previously, if your Suspensecomponent didn't provide fallbacka property, React would silently skip it and continue searching up to the next boundary:

// React 17
const App = () => {
  return (
    <Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
      <Suspense>                      // <--- 这个边界被跳过,没有 fallback 属性
        <Page />
      </Suspense>
    </Suspense>
  );
};

export default App; 

The React team found that this could lead to confusing, difficult-to-debug situations. For example, if you are debugging a problem and you test a problem by throwing a bound in a component without a fallbackproperty Suspense, it may bring some unexpected results and 不会警告say it 没有fallbackhas a property.

Updated:

Now, React will use the current component's Suspenseas the bounds, even if Suspensethe value of the current component's nullis or undefined:

// React 18
const App = () => {
  return (
    <Suspense fallback={<Loading />}> // <--- 不使用
      <Suspense>                      // <--- 这个边界被使用,将 fallback 渲染为 null
        <Page />
      </Suspense>
    </Suspense>
  );
};

export default App; 

This update means us 不再跨越边界组件. Instead, we'll capture at the boundary and render fallbackas if you provided a nullcomponent that returns a . This means that the suspended Suspensecomponent will behave as expected, and fallbackthere will be no problem if you forget to provide the attribute.

Official explanation about Suspense: github.com/reactwg/rea…

New API

1. useId

const id = useId(); 

Supports the same component to generate the same unique ID on the client and server to avoid hydrationincompatibility. This solves the existing problems in React 17and below versions. 17Because our server provides HTMLYes when rendering 无序的, useIdthe principle is that each idrepresents the hierarchical structure of the component in the component tree.

For more information about useId, see useId post in the working group .

二、useSyncExternalStore

useSyncExternalStoreIt is a new API that has undergone a modification and was useMutableSourcechanged from. It is mainly used to solve the problem of external data tearing.

useSyncExternalStore allows React components to safely and effectively read external data sources under CM by forcing synchronous update data. In Concurrent Mode, a React rendering will be executed in slices (in fiber units), and higher priority updates may be interspersed in the middle. If public data (such as data in redux) is changed in a high-priority update, the previous low-priority rendering must be restarted, otherwise the state will be inconsistent.

useSyncExternalStoreIt is generally used by third-party state management libraries, and we do not need to pay attention to it in daily business. Because Reactit useStatehas natively solved tear(撕裂)the problems under the concurrency feature. useSyncExternalStoreMainly for framework developers, for example , it may not be used reduxdirectly when controlling the state. Instead, it maintains an external object to implement data updates. Without the management of , it cannot automatically solve the tearing problem. . Therefore, such an API is provided to the outside world.Reactstatestore发布订阅模式ReactReactReact

Currently React-Redux 8.0it has been useSyncExternalStoreimplemented based on .

For more information about useSyncExternalStore, see the useSyncExternalStore overview post and useSyncExternalStore API details .

3. useInsertionEffect

const useCSS = rule => {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
};

const App: React.FC = () => {
  const className = useCSS(rule);
  return <div className={className} />;
};

export default App; 

This Hooks is only recommended css-in-jsfor use by libraries. The execution time of this Hooks is DOMafter and before generation useLayoutEffect. Its working principle is roughly the useLayoutEffectsame as that of , except that DOMthe reference of the node cannot be accessed at this time. It is generally used to inject <style>scripts in advance.

For more information about useInsertionEffect, see the Library Upgrade Guide for<style> .

Concurrent Mode

Concurrent Mode (hereinafter referred to as CM) is translated as concurrent mode. We may have heard this concept many times. In fact, this concept has become very mature last year and React 17can be 试验性turned on through some APIs in CM.

CM 本身并不是一个功能,而是一个底层设计 

Concurrent mode helps apps stay responsive and adjust appropriately based on the user's device capabilities and network speed, fixing limitations by making rendering interruptible 阻塞渲染. In Concurrentmode, Reactmultiple states can be updated simultaneously.

It may be too complicated to say, but it can be summed up in one sentence:

React 17The difference between and React 18is: from 同步不可中断更新changed to 异步可中断更新.

Here comes the important point, please don’t skip the following part:

We mentioned at the beginning of the article: In React 18, new ones are provided root api. We only need to renderupgrade createRoot(root).render(<App />)to enable concurrent mode.

So at this time, some students may ask: Does turning on 并发模式mean turning on 并发更新?

NO! In React 17some experimental features in , turning on 并发模式means turning on 并发更新, but React 18after the official version was released, due to official policy adjustments, React no longer relies on 并发模式turning 并发更新on.

In other words: Just because it’s turned on 并发模式doesn’t necessarily mean it’s turned on 并发更新!

One sentence summary: In 18, there are no longer multiple models, but based 是否使用并发特性on 是否开启并发更新actions .

From the oldest version to the current one v18, how many versions are there on the market React?

It can be summarized from an architectural perspective. There are currently two architectures:

  • 递归Updated in a non-interruptible manner Stack Reconciler(old architecture)
  • 遍历Updated in an interruptible manner Fiber Reconciler(new architecture)

You can choose whether to enable the new architecture , so there are four situations 并发更新for all versions currently on the market :React

  1. Old architecture (v15 and previous versions)
  2. New architecture, concurrent updates are not enabled, and the behavior is consistent with case 1 (v16 and v17 fall into this case by default)
  3. New architecture, concurrent updates are not enabled, but concurrent mode and some new features are enabled (for example Automatic Batching, this is the case in v18 by default)
  4. New architecture, enable concurrent mode, enable concurrent updates

并发特性Refers to 并发更新features that can only be used after being turned on, such as:

  • useDeferredValue
  • useTransition

relation chart:

stateDiagram-v2
[*] --> React18
React18 --> ReactDOM.render
React18 --> ReactDOM.createRoot
ReactDOM.render --> 未开启并发模式
ReactDOM.createRoot --> 开启并发模式
未开启并发模式 --> 未开启自动批处理
开启并发模式 --> 开启自动批处理
未开启自动批处理 --> 未开启并发更新
开启自动批处理 --> 未使用并发特性
开启自动批处理 --> 使用并发特性
未使用并发特性 --> 未启并发更新
使用并发特性 --> 开启并发更新 

After understanding their relationship clearly, we can continue to explore 并发更新:

Concurrency features:

1. startTransition

Run the following code in v18:

import React, { useState, useEffect, useTransition } from 'react';

const App: React.FC = () => {
  const [list, setList] = useState<any[]>([]);
  const [isPending, startTransition] = useTransition();
  useEffect(() => {
    // 使用了并发特性,开启并发更新
    startTransition(() => {
      setList(new Array(10000).fill(null));
    });
  }, []);
  return (
    <>
      {list.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};

export default App; 

Since it is executed (used ) in the callback function of , it setListwill trigger .startTransition并发特性setList并发更新

startTransition, mainly to keep the UI responsive under a large number of tasks. This new API can “过渡”significantly improve user interaction by marking specific updates as urgent. In simple terms, renderings triggered by startTransitioncallbacks setStateare marked as non-urgent renderings, and these renderings may be preempted by others 紧急渲染.

二、useDeferredValue

Returning a delayed response value allows a statedelay to take effect. The value will become the latest value only when there is no urgent update. useDeferredValueLike startTransition, both are marked as a non-urgent update.

From the introduction point of view useDeferredValue, useTransitiondoes it feel very similar to ?

  • Same: useDeferredValueEssentially the same as the internal implementation useTransition, it is marked as 延迟更新a task.
  • The difference useTransitionis that the update task is turned into a delayed update task, and useDeferredValuea new value is generated, which is used as the delayed status. (One is used to wrap methods and one is used to wrap values)

Therefore, startTransitionwe can also use the above example useDeferredValueto implement:

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

const App: React.FC = () => {
  const [list, setList] = useState<any[]>([]);
  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);
  // 使用了并发特性,开启并发更新
  const deferredList = useDeferredValue(list);
  return (
    <>
      {deferredList.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};

export default App; 

Then start the project and check the printed execution stack diagram:

QQ screenshot 20220505072516.jpg

At this time, our tasks are split into different frames for each frame task, and JS脚本the execution time is roughly around 10% 5ms. This way, the browser has remaining time to perform style layout and style drawing , reducing the possibility of dropped frames.

3. Ordinary situations

We can turn off the concurrency feature and run the project in a normal environment:

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

const App: React.FC = () => {
  const [list, setList] = useState<any[]>([]);
  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);
  return (
    <>
      {list.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );
};

export default App; 

Start the project and check the printed execution stack diagram:

999.jpg

You can see from the printed execution stack diagram that due to the large number of components (10,000), the JS execution time is, which 500msmeans that without the concurrency feature: when 10,000 tags are rendered at one time, the page will It will block for a while 0.5秒, causing lag, but if concurrent updates are turned on, there will be no such problem.

 这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice) 

in conclusion

  • The meaning of concurrent updates is 交替执行different tasks. When the reserved time is not enough, Reactthe thread control is returned to the browser, waiting for the next frame time to arrive, and then continuing the interrupted work
  • 并发模式It is 并发更新the basic premise for the realization of
  • 时间切片It is 并发更新a specific means of realizing

Conclusion

The above is the general content of this Reactupgrade. If there are any errors, please correct them.

Guess you like

Origin blog.csdn.net/web2022050901/article/details/125215382#comments_26623274
Recommended