React Hooks 源码解析

自我期望:能引起团队成员的共鸣,让团队成员有所收获,加强自身对 react 源码的理解。

大纲

  • React Hooks 出现的背景
  • 如何使用及使用过程中需要注意什么 ? (简单过一下)
  • React Fiber 的结构 (稍微介绍一下)
  • Hooks 是如何工作的 ? (详细讲解)
  • 实现自己的 hooks (没这个必要,梳理好流程自然知道怎么实现了)
  • 附录
    • React 源码调试 版本号为 v17.0.3

React Hooks 出现的背景

  1. 解决在组件间复用状态逻辑困难的问题,可以让我们在无需改动组件结构的情况下复用状态逻辑。
  2. 解决复杂组件变得难以理解的问题,将组件中互相关联的部分拆分成更小的函数,还可以通过 reducer 来管理组件的内部状态,使其更加可预测。
  3. class 中 this 指向难以理解,Hook 让我们在非 class 的情况下可以使用更多的 react 特性。(函数式编程)

Hooks 的使用

useState

  1. import React, { useState } from 'react';
  2. function Example() {
  3. // 声明一个叫 "count" 的 state 变量
  4. const [count, setCount] = useState(0);
  5. return (
  6. <div>
  7. <p>You clicked {count} times</p>
  8. <button onClick={() => setCount(count + 1)}>
  9. Click me
  10. </button>
  11. </div>
  12. );
  13. }
  14. /**
  15. * import {defineComponent, ref} from "vue"
  16. * export default defineComponent({
  17. * setup () {
  18. * const count = ref(0)
  19. * return () => {
  20. * return (
  21. * <div>
  22. * <p>You clicked {count.value} times</p>
  23. * <button onClick={() => count.value++}>
  24. * Click me
  25. * </button>
  26. * </div>
  27. * )
  28. * }
  29. * }
  30. * })
  31. * /

上述代码等价于:

  1. class Example extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. };
  7. }
  8. render() {
  9. return (
  10. <div>
  11. <p>You clicked {this.state.count} times</p>
  12. <button onClick={() => this.setState({ count: this.state.count + 1 })}>
  13. Click me
  14. </button>
  15. </div>
  16. );
  17. }
  18. }

useEffect

  1. import React, { useState, useEffect } from 'react';
  2. function FriendStatus(props) {
  3. const [isOnline, setIsOnline] = useState(null);
  4. useEffect(() => {
  5. function handleStatusChange(status) {
  6. setIsOnline(status.isOnline);
  7. }
  8. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  9. // Specify how to clean up after this effect:
  10. return function cleanup() {
  11. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  12. };
  13. });
  14. if (isOnline === null) {
  15. return 'Loading...';
  16. }
  17. return isOnline ? 'Online' : 'Offline';
  18. }

等价于

  1. class FriendStatus extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { isOnline: null };
  5. this.handleStatusChange = this.handleStatusChange.bind(this);
  6. }
  7. // 等价于 vue 中的 mounted 生命周期
  8. componentDidMount() {
  9. ChatAPI.subscribeToFriendStatus(
  10. this.props.friend.id,
  11. this.handleStatusChange
  12. );
  13. }
  14. // 等价于 vue 中的 unmounted 也就是 destroy
  15. componentWillUnmount() {
  16. ChatAPI.unsubscribeFromFriendStatus(
  17. this.props.friend.id,
  18. this.handleStatusChange
  19. );
  20. }
  21. handleStatusChange(status) {
  22. this.setState({
  23. isOnline: status.isOnline
  24. });
  25. }
  26. render() {
  27. if (this.state.isOnline === null) {
  28. return 'Loading...';
  29. }
  30. return this.state.isOnline ? 'Online' : 'Offline';
  31. }
  32. }

useContext

  1. // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
  2. // 为当前的 theme 创建一个 context(“light”为默认值)。
  3. const ThemeContext = React.createContext('light');
  4. class App extends React.Component {
  5. render() {
  6. // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
  7. // 无论多深,任何组件都能读取这个值。
  8. // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
  9. return (
  10. <ThemeContext.Provider value="dark">
  11. <Toolbar />
  12. </ThemeContext.Provider>
  13. );
  14. }
  15. }
  16. // 中间的组件再也不必指明往下传递 theme 了。
  17. function Toolbar() {
  18. return (
  19. <div>
  20. <ThemedButton />
  21. </div>
  22. );
  23. }
  24. class ThemedButton extends React.Component {
  25. // 指定 contextType 读取当前的 theme context。
  26. // React 会往上找到最近的 theme Provider,然后使用它的值。
  27. // 在这个例子中,当前的 theme 值为 “dark”。
  28. static contextType = ThemeContext;
  29. render() {
  30. return <Button theme={this.context} />;
  31. }
  32. }

useReducer

  1. const initialState = {count: 0};
  2. function reducer(state, action) {
  3. switch (action.type) {
  4. case 'increment':
  5. return {count: state.count + 1};
  6. case 'decrement':
  7. return {count: state.count - 1};
  8. default:
  9. throw new Error();
  10. }
  11. }
  12. function Counter() {
  13. const [state, dispatch] = useReducer(reducer, initialState);
  14. return (
  15. <>
  16. Count: {state.count}
  17. <button onClick={() => dispatch({type: 'decrement'})}>-</button>
  18. <button onClick={() => dispatch({type: 'increment'})}>+</button>
  19. </>
  20. );
  21. }

useRef

useRef() 可以方便地保存任何可变值,当 ref 对象发生改变时,useRef 并不会通知我们,变更 .current 属性不会引发组件重新渲染。如果想要

  1. function TextInputWithFocusButton() {
  2. const inputEl = useRef(null);
  3. const onButtonClick = () => {
  4. // `current` 指向已挂载到 DOM 上的文本输入元素
  5. inputEl.current.focus();
  6. };
  7. return (
  8. <>
  9. <input ref={inputEl} type="text" />
  10. <button onClick={onButtonClick}>Focus the input</button>
  11. </>
  12. );
  13. }

……

Hooks 使用的注意事项

  • 只能在最顶层使用 Hook,不要在循环,条件或者嵌套函数中调用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 Javascript 函数中调用 Hook

React Fiber

为什么要讲 React Fiber,因为 React 源码的实现都围绕 Fiber 的数据结构展开,了解 Fiber 的结构有助于我们理解 Hook 源码。

  Fiber 分为 currentRenderingFiber 及 workInProgressFiber,顾名思义 currentRenderingFiber 就是当前在屏幕中显示的内容对应的 Fiber 树就是 currentRenderingFiber。 workInProgressFiber 是当内容更新时,React 在内存中重新构建的一棵新的 Fiber 树,当其更新完成之后,React 会使用它直接替换 currentRenderingFiber 树达到快速更新 DOM 的目的。

React Fiber 的数据结构

  1. // 相对于 Fiber 还是缺少 nextEffect、firstEffect、lastEffect 等属性的
  2. function FiberNode(
  3. tag: WorkTag,
  4. pendingProps: mixed,
  5. key: null | string,
  6. mode: TypeOfMode,
  7. ) {
  8. // Instance
  9. // 节点类型标记 大概有 24 种 FunctionComponent || classComponent || IndeterminateComponent || HostRoot || ……
  10. this.tag = tag;
  11. // 节点索引信息
  12. this.key = key;
  13. // 节点类型
  14. this.elementType = null;
  15. // 节点类型
  16. this.type = null;
  17. // dom 节点对象,组件实例
  18. this.stateNode = null;
  19. // Fiber
  20. // 父节点的 Fiber 信息
  21. this.return = null;
  22. // 子节点的 Fiber 信息
  23. this.child = null;
  24. // 兄弟节点的 Fiber 信息
  25. this.sibling = null;
  26. // 当前位置,相对于兄弟节点
  27. this.index = 0;
  28. // 存放当前 ref 的信息
  29. this.ref = null;
  30. // 构建中的 props 属性
  31. this.pendingProps = pendingProps;
  32. // 存放的 props 属性
  33. this.memoizedProps = null;
  34. // 更新的队列信息
  35. this.updateQueue = null;
  36. // 当前的节点信息
  37. this.memoizedState = null;
  38. // 记录当前节点的上下文事件依赖关系
  39. this.dependencies = null;
  40. // 标记节点类型
  41. this.mode = mode;
  42. // Effects
  43. // 用于表示 fiber 节点的创建时间
  44. this.flags = NoFlags;
  45. // 同上
  46. this.subtreeFlags = NoFlags;
  47. // 缺失的 Fiber 数组?
  48. this.deletions = null;
  49. // 这个没记错的话应该是任务执行的优先级
  50. this.lanes = NoLanes;
  51. this.childLanes = NoLanes;
  52. // Fiber 备份的数据,用于新旧节点比对的时候使用
  53. this.alternate = null;
  54. if (enableProfilerTimer) {
  55. ...
  56. }
  57. if (__DEV__) {
  58. ...
  59. }
  60. }

  关于 lanes 相关的定义,感兴趣的小伙伴,可以自行了解 /react-debug/src/react/packages/react-reconciler/src/ReactFiberLane.new.js

Hook 如何挂载到 Fiber 上

  • useState 的执行流程 useState -> mountState -> mountWorkInProgressHook

    1. // /react-debug/src/react/packages/react-reconciler/ReactFiberHooks.old.js
    2. HooksDispatcherOnMountInDEV = {
    3. ...,
    4. useState<S>(
    5. initialState: (() => S) | S,
    6. ): [S, Dispatch<BasicStateAction<S>>] {
    7. currentHookNameInDev = 'useState';
    8. mountHookTypesDev();
    9. const prevDispatcher = ReactCurrentDispatcher.current;
    10. ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
    11. try {
    12. return mountState(initialState);
    13. } finally {
    14. ReactCurrentDispatcher.current = prevDispatcher;
    15. }
    16. },
    17. ...
    18. }
    19. // 初始化 state 的值
    20. function mountState<S>(
    21. initialState: (() => S) | S,
    22. ): [S, Dispatch<BasicStateAction<S>>] {
    23. // 这里进行初始化
    24. const hook = mountWorkInProgressHook();
    25. if (typeof initialState === 'function') {
    26. // $FlowFixMe: Flow doesn't like mixed types
    27. initialState = initialState();
    28. }
    29. // 重点注意一下这里,将 hook 相关的值存放到 fiber 的 memoizedState 属性
    30. hook.memoizedState = hook.baseState = initialState;
    31. const queue: UpdateQueue<S, BasicStateAction<S>> = {
    32. pending: null,
    33. interleaved: null,
    34. lanes: NoLanes,
    35. dispatch: null,
    36. lastRenderedReducer: basicStateReducer,
    37. lastRenderedState: (initialState: any),
    38. };
    39. hook.queue = queue;
    40. const dispatch: Dispatch<
    41. BasicStateAction<S>,
    42. > = (queue.dispatch = (dispatchSetState.bind(
    43. null,
    44. currentlyRenderingFiber,
    45. queue,
    46. ): any));
    47. return [hook.memoizedState, dispatch];
    48. }
  • useEffect 的执行流程 useEffect -> mountEffect -> mountEffectImpl -> mountWorkInProgressHook

    1. HooksDispatcherOnMountInDEV = {
    2. ...,
    3. useEffect(
    4. create: () => (() => void) | void,
    5. deps: Array<mixed> | void | null,
    6. ): void {
    7. currentHookNameInDev = 'useEffect';
    8. mountHookTypesDev();
    9. checkDepsAreArrayDev(deps);
    10. return mountEffect(create, deps);
    11. },
    12. ...
    13. }
    14. function mountEffect(
    15. create: () => (() => void) | void,
    16. deps: Array<mixed> | void | null,
    17. ): void {
    18. if (
    19. __DEV__ &&
    20. enableStrictEffects &&
    21. (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
    22. ) {
    23. return mountEffectImpl(
    24. MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
    25. HookPassive,
    26. create,
    27. deps,
    28. );
    29. } else {
    30. return mountEffectImpl(
    31. PassiveEffect | PassiveStaticEffect,
    32. HookPassive,
    33. create,
    34. deps,
    35. );
    36. }
    37. }
    38. function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
    39. const hook = mountWorkInProgressHook();
    40. const nextDeps = deps === undefined ? null : deps;
    41. currentlyRenderingFiber.flags |= fiberFlags;
    42. // 看一下这里,将 useEffect 相关的副作用存放到 fiber 的 memoizedState 属性上
    43. hook.memoizedState = pushEffect(
    44. HookHasEffect | hookFlags,
    45. create,
    46. undefined,
    47. nextDeps,
    48. );
    49. }
  • useContext -> readContext

  • useRef -> mountRef -> mountWorkInProgressHook
  • useReducer -> mountReducer -> mountWorkInProgressHook
  • useMemo -> mountMemo -> mountWorkInProgressHook
  • useCallback -> mountCallback -> mountWorkInProgressHook
  • ……

接下来,看一下 Hook 挂载到 Fiber 上的核心函数:

  1. function mountWorkInProgressHook(): Hook {
  2. const hook: Hook = {
  3. memoizedState: null,
  4. baseState: null,
  5. baseQueue: null,
  6. queue: null,
  7. next: null,
  8. };
  9. if (workInProgressHook === null) {
  10. // This is the first hook in the list
  11. currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  12. } else {
  13. // Append to the end of the list
  14. workInProgressHook = workInProgressHook.next = hook;
  15. }
  16. return workInProgressHook;
  17. }

再来看一段 debug 用的代码

  1. // /react-debug/src/index.js
  2. const container = document.getElementById('root');
  3. const root = ReactDOM.createRoot(container);
  4. root.render(<App />);
  5. // /react-debug/src/app.js
  6. function App() {
  7. const [count, setCount] = useState(1)
  8. const appRef = useRef()
  9. useEffect(() => {
  10. console.log("init")
  11. }, [])
  12. const [username, setUsername] = useState("jq")
  13. return (
  14. <div className="App" key="app" ref={appRef}>
  15. <p>this count value is: {count}</p>
  16. <button onClick={
  17. () => setCount(count+1)
  18. }>+1</button>
  19. </div>
  20. );
  21. }

接下来看看它执行之后的 fiber

React Hooks 源码解析 - 图1

  通过以上信息可以了解到 Hook 是挂载到 Fiber 上的,这也就说明了,为什么不能在普通的 javascript 函数中调用 Hook,因为普通的 javascript 函数中并不存在 Fiber 信息。

  另外通过 Fiber.memoizedState 的链式结构,我们也可以知道 Hook 要写在顶层,是避免指针异常,导致不可预估的错误。

Hooks 更新中的内容发生变化后

state 更新过程

  1. 执行 requestUpdateLane 查找 fiber 更新的优先级
  2. 判断是否为渲染阶段的更新
    • 渲染阶段,执行 enqueueRenderPhaseUpdate 函数更新当前的队列信息
    • 非渲染阶段,执行 enqueueUpdate 函数 -> 判断是否交叉执行
  3. 执行 requestEventTime 方法 获取当前事件的时间
  4. 执行 scheduleUpdateOnFiber 方法 更新 fiber 的优先级,回调事件,时间的执行时间等。
    • scheduleUpdateOnFiber
    • ensureRootIsScheduled
      • cancelCallback
      • scheduleLegacySyncCallback
      • scheduleSyncCallback
      • scheduleMicrotask
      • scheduleCallback
    • Scheduler
      • unstable_scheduleCallback
      • unstable_cancelCallback
      • unstable_shouldYield
      • unstable_requestPaint
      • ……
  5. ……
  1. function dispatchSetState<S, A>(
  2. fiber: Fiber,
  3. queue: UpdateQueue<S, A>,
  4. action: A,
  5. ) {
  6. ...
  7. const lane = requestUpdateLane(fiber);
  8. const update: Update<S, A> = {
  9. lane,
  10. action,
  11. hasEagerState: false,
  12. eagerState: null,
  13. next: (null: any),
  14. };
  15. if (isRenderPhaseUpdate(fiber)) {
  16. enqueueRenderPhaseUpdate(queue, update);
  17. } else {
  18. enqueueUpdate(fiber, queue, update, lane);
  19. const alternate = fiber.alternate;
  20. if (
  21. fiber.lanes === NoLanes &&
  22. (alternate === null || alternate.lanes === NoLanes)
  23. ) {
  24. const lastRenderedReducer = queue.lastRenderedReducer;
  25. if (lastRenderedReducer !== null) {
  26. let prevDispatcher;
  27. if (__DEV__) {
  28. prevDispatcher = ReactCurrentDispatcher.current;
  29. ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
  30. }
  31. try {
  32. const currentState: S = (queue.lastRenderedState: any);
  33. const eagerState = lastRenderedReducer(currentState, action);
  34. // lastRenderedReducer = basicStateReducer 在此处更新 相应的 fiber 节点信息
  35. // function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  36. // // $FlowFixMe: Flow doesn't like mixed types
  37. // return typeof action === 'function' ? action(state) : action;
  38. // }
  39. update.hasEagerState = true;
  40. update.eagerState = eagerState;
  41. if (is(eagerState, currentState)) {
  42. return;
  43. }
  44. } catch (error) {
  45. } finally {
  46. if (__DEV__) {
  47. ReactCurrentDispatcher.current = prevDispatcher;
  48. }
  49. }
  50. }
  51. }
  52. const eventTime = requestEventTime();
  53. const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
  54. if (root !== null) {
  55. entangleTransitionUpdate(root, queue, lane);
  56. }
  57. }
  58. markUpdateInDevTools(fiber, lane, action);
  59. }

渲染阶段更新

  1. function enqueueRenderPhaseUpdate<S, A>(
  2. queue: UpdateQueue<S, A>,
  3. update: Update<S, A>,
  4. ) {
  5. // This is a render phase update. Stash it in a lazily-created map of
  6. // queue -> linked list of updates. After this render pass, we'll restart
  7. // and apply the stashed updates on top of the work-in-progress hook.
  8. didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  9. const pending = queue.pending;
  10. if (pending === null) {
  11. // This is the first update. Create a circular list.
  12. update.next = update;
  13. } else {
  14. update.next = pending.next;
  15. pending.next = update;
  16. }
  17. queue.pending = update;
  18. }

非渲染阶段更新

  1. function enqueueUpdate<S, A>(
  2. fiber: Fiber,
  3. queue: UpdateQueue<S, A>,
  4. update: Update<S, A>,
  5. lane: Lane,
  6. ) {
  7. if (isInterleavedUpdate(fiber, lane)) {
  8. const interleaved = queue.interleaved;
  9. if (interleaved === null) {
  10. // This is the first update. Create a circular list.
  11. update.next = update;
  12. // At the end of the current render, this queue's interleaved updates will
  13. // be transferred to the pending queue.
  14. pushInterleavedQueue(queue);
  15. } else {
  16. update.next = interleaved.next;
  17. interleaved.next = update;
  18. }
  19. queue.interleaved = update;
  20. } else {
  21. const pending = queue.pending;
  22. if (pending === null) {
  23. // This is the first update. Create a circular list.
  24. update.next = update;
  25. } else {
  26. update.next = pending.next;
  27. pending.next = update;
  28. }
  29. queue.pending = update;
  30. }
  31. }

React 源码调试(v17.0.3)

  1. 创建react项目 npx create-react-app react-debug
  2. 切换到项目目录下,弹射出 webpack 的配置文件 npm run eject
  3. 将 react 的项目克隆到 /react-debug/src 目录下 git clone https://github.com/facebook/react.git 网络不好的可以通过镜像克隆 git clone https://github.com.cnpmjs.org/facebook/react.git
  4. 修改配置文件

这里理论上报找不到什么模块,我们就按照提示添加对应的模块即可

  1. // /react-debug/config/webpack.config.js
  2. resolve: {
  3. alias: {
  4. "react-native": "react-native-web",
  5. "react": path.resolve(__dirname, "../src/react/packages/react"),
  6. "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"),
  7. "shared": path.resolve(__dirname, "../src/react/packages/shared"),
  8. "react-reconciler": path.resolve(__dirname, "../src/react/packages/react-reconciler"),
  9. "react-devtools-timeline": path.resolve(__dirname, "../src/react/packages/react-devtools-timeline"),
  10. "react-devtools-shared": path.resolve(__dirname, "../src/react/packages/react-devtools-shared"),
  11. }
  12. }
  1. 修改项目的环境变量
  1. // /react-debug/config/env.js
  2. const stringified = {
  3. "process.env": Object.keys(raw).reduce((env, key) => {
  4. env[key] = JSON.stringify(raw[key])
  5. return env
  6. }, {}),
  7. __DEV__: true,
  8. SharedArrayBuffer: true,
  9. spyOnDev: true,
  10. spyOnDevAndProd: true,
  11. spyOnProd: true,
  12. __PROFILE__: true,
  13. __UMD__: true,
  14. __EXPERIMENTAL__: true,
  15. __VARIANT__: true,
  16. gate: true,
  17. trustedTypes: true
  18. }
  1. 处理一下类型检测报错的问题,简单粗暴一点,整个删了
  1. // webpack.config.js
  2. {
  3. ...,
  4. plugins: [
  5. ...,
  6. - !disableESLintPlugin &&
  7. - new ESLintPlugin({
  8. - // Plugin options
  9. - extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
  10. - formatter: require.resolve('react-dev-utils/eslintFormatter'),
  11. - eslintPath: require.resolve('eslint'),
  12. - failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
  13. - context: paths.appSrc,
  14. - cache: true,
  15. - cacheLocation: path.resolve(
  16. - paths.appNodeModules,
  17. - '.cache/.eslintcache'
  18. - ),
  19. - // ESLint class options
  20. - cwd: paths.appPath,
  21. - resolvePluginsRelativeTo: __dirname,
  22. - baseConfig: {
  23. - extends: [require.resolve('eslint-config-react-app/base')],
  24. - rules: {
  25. - ...(!hasJsxRuntime && {
  26. - 'react/react-in-jsx-scope': 'error',
  27. - }),
  28. - },
  29. - },
  30. - }),
  31. - ],
  32. ...
  33. }
  1. 导出 HostConfig
  1. // /react-debug/src/react/packages/react-reconciler/src/ReactFiberHostConfig.js
  2. + export * from './forks/ReactFiberHostConfig.dom';
  3. - throw new Error('This module must be shimmed by a specific renderer.');
  1. 修改 ReactSharedInternals.js 文件
    ```javascript // /react-debug/src/react/packages/shared/ReactSharedInternals.js
  • const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
  • import ReactSharedInternals from ‘../react/src/ReactSharedInternals’; ```
  1. 关闭 eslint 扩展

    1. // /react-debug/src/react/.eslingrc.js
    2. // 删除 extends
    3. // extends: [
    4. // 'fbjs',
    5. // 'prettier'
    6. //]
  2. 禁止 invariant 报错

    1. // /react-debug/src/react/packages/shared/invariant.js
    2. export default function invariant(condition, format, a, b, c, d, e, f) {
    3. if (condition) return;
    4. throw new Error(
    5. 'Internal React error: invariant() is meant to be replaced at compile ' +
    6. 'time. There is no runtime version.',
    7. );
    8. }
  3. 修改 react react-dom 引入方式
    ```javascript import as React from “react” import as ReactDOM from “react-dom” import ‘./index.css’; import App from ‘./App’; import reportWebVitals from ‘./reportWebVitals’;

  • const container = document.getElementById(‘root’);

// Create a root.

  • const root = ReactDOM.createRoot(container);

  • root.render();

  • ReactDOM.render(
  • ,
  • document.getElementById(‘root’)
  • ); ```