跳到主要内容

组件

构建组件的方式有哪些

  1. Class Components(类组件):使用ES6的类语法来定义组件。类组件继承自React.Component,并通过render方法返回需要渲染的React元素。
class MyComponent extends React.Component {
render () {
return <div>Hello</div>
}
}
  1. Function Components(函数组件):使用函数来定义组件,函数接收props作为参数,并返回需要渲染的React元素。
function MyComponent (props) {
return <div>Hello</div>
}
  1. Higher-Order Components(高阶组件):高阶组件是一个函数,接收一个组件作为参数,并返回一个新的增强组件。它用于在不修改原始组件的情况下,添加额外的功能或逻辑。
function withLogger (WrappedComponent) {
return class extends React.Component {
componentDidMount () {
console.log('Component did mount!')
}

render () {
return <WrappedComponent {...this.props} />
}
}
}

const EnhancedComponent = withLogger(MyComponent)
  1. Function as Children(函数作为子组件):将函数作为子组件传递给父组件,并通过父组件的props传递数据给子组件。
function MyComponent (props) {
return <div>{props.children('Hello')}</div>
}

<MyComponent>
{(message) => <p>{message}</p>}
</MyComponent>

这些是React中常见的构建组件的方式。每种方式都适用于不同的场景,你可以根据自己的需求选择合适的方式来构建组件。

  1. React.cloneElementReact.cloneElement是一个函数,用于克隆并返回一个新的React元素。它可以用于修改现有元素的props,或者在将父组件的props传递给子组件时进行一些额外的操作。
const parentElement = <div>Hello</div>;
const clonedElement = React.cloneElement(parentElement, { className: 'greeting' });

// Result: <div className="greeting">Hello</div>
  1. React.createElementReact.createElement是一个函数,用于创建并返回一个新的React元素。它接收一个类型(组件、HTML标签等)、props和子元素,并返回一个React元素。
const element = React.createElement('div', { className: 'greeting' }, 'Hello');

// Result: <div className="greeting">Hello</div>

React.createElementReact.cloneElement通常在一些特殊的场景下使用,例如在高阶组件中对组件进行包装或修改。它们不是常规的组件构建方式,但是在某些情况下是非常有用的。非常抱歉之前的遗漏,希望这次能够更全面地回答您的问题。

为什么 react 组件, 都必须要申明一个 import React from 'react';

首先要知道一个事情: JSX 是无法直接运行在浏览器环境

原因

JSX 语法不能直接被浏览器解析和运行,因此需要插件 @babel/plugin-transform-react-jsx 来转换语法,使之能够在浏览器或任何 JavaScript 环境中执行。

所以 React 组件需要引入React的一个主要原因是:在组件中使用 JSX 时,JSX 语法最终会被 Babel 编译成使用React.createElement方法的 JavaScript 代码。也就是说,任何使用 JSX 的 React 组件的背后都隐含了React.createElement的调用。

例如,当你编写如下的 JSX 代码:

const MyComponent = () => {
return <div>Hello, World!</div>;
};

Babel 会将这段 JSX 编译为如下的 JavaScript 代码:

const MyComponent = () => {
return React.createElement('div', null, 'Hello, World!')
}

由于编译后的代码调用了React.createElement,因此你需要在文件顶部导入React对象才能使用它。即使你在组件中并没有直接使用React对象,编译后的代码依赖于React的运行时。

Babel 7.0+ / React 17+ , 可以不再需要 import React

在 Babel 7.0 版本之后,@babel/plugin-transform-react-jsx 插件还支持一个自动模式,它可以自动引入 JSX 转换所需的React包,无需手动在每个文件中添加 import React from 'react'

注意,随着 React 17 的新 JSX 变换,它们引入了一个新的 JSX 转换方式,这在新的 Babel 插件 @babel/plugin-transform-react-jsx@babel/preset-react 中得到了支持。这意味着在写 JSX 时,你不再需要导入 React。这个插件现在接收一个 { runtime: 'automatic' } 选项来启用这一特性。

举个例子,在使用新的 JSX 转换之后,编译器将会自动引入 JSX 的运行时库,而不是 React,例如对于一个使用了新转换的MyComponent的组件:

// React 17+ 及支持新JSX转换的环境,可以不需要显式写这行
// import React from 'react';

const MyComponent = () => {
return <div>Hello, World!</div>;
};

在新的转换下,你会看到类似import { jsx as _jsx } from 'react/jsx-runtime'的东西或者类似的别名,被自动插入到转译后的文件中,而不再是直接的React.createElement调用。这就是为什么在新版本的 React 中,你可能不再需要手动导入 React 了。

补充一个细节知识点: plugin-transform-react-jsx@babel/preset-react` 是啥关系

它们是包含关系@babel/preset-react 包括了 @babel/plugin-transform-react-jsx

@babel/plugin-transform-react-jsx@babel/preset-react 都是 Babel 插件,它们在处理 React 项目中的 JSX 代码方面有关联,但它们的用途和包含的内容有所不同。

  1. @babel/plugin-transform-react-jsx: 这是一个特定的 Babel 插件,它的功能就是将 JSX 语法转换为React.createElement 调用。随着 React 17 的更新,它还允许使用新的 JSX 转换,无需导入 React 就可以使用 JSX。这意味着,在文件中不再需要 import React from 'react' 语句了,就可以使用 JSX。

这个插件通常用于开发者想要精细控制某个具体转换功能时。如果你只需要转换 JSX 语法,但不需要处理其他与 React 相关的转换或优化,你可能会单独使用这个插件。

  1. @babel/preset-react: 这是一个 Babel 预设,它是一组 Babel 插件的集合,旨在为 React 项目提供所需的全部 Babel 插件。@babel/preset-react 包括了 @babel/plugin-transform-react-jsx,但它还包含了其他一些插件,如处理 React 的显示名称的 @babel/plugin-transform-react-display-name,以及为开发模式和生产模式添加/删除某些代码的插件。

预设的好处是简化了配置过程。开发者可以在 Babel 的配置中一次性添加 @babel/preset-react,而不是单独添加每一个与 React 相关的 Babel 插件。此外,预设将维护这些插件的正确版本和顺序,这有助于避免潜在的配置错误。

在实践中,大多数开发 React 应用的开发者会使用 @babel/preset-react 因为它提供了一个即插即用的 Babel 环境,无需担心各个插件的具体细节。但是也有些情况下,为了更细致的优化和控制,开发者可能会选择手动添加特定的插件,包括 @babel/plugin-transform-react-jsx

react 中如何引入样式

Class Components 和 Function Components 的区别?

Class组件是使用ES6的类语法定义的组件,它是继承自React.Component的一个子类。Class组件有自己的状态和生命周期方法,可以通过this.state来管理状态,并通过this.setState()来更新状态。

class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

函数组件是使用函数来定义的组件,在React 16.8版本引入的Hooks之后,函数组件可以拥有自己的状态和副作用,可以使用useState和其他Hooks来管理状态。

import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count + 1);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

函数组件通常比Class组件更简洁和易于理解,尤其是在处理简单的逻辑和状态时。然而,Class组件仍然在一些特定情况下有它们的优势,例如需要使用生命周期方法、引入Ref或者需要更多的精确控制和性能优化时。

细节对比

方面Class组件函数组件
语法使用ES6类语法定义组件使用函数语法定义组件
继承继承自React.Component类无需继承任何类
状态管理可通过this.state和this.setState来管理状态可使用useState Hook来管理状态
生命周期方法可使用生命周期方法,如componentDidMount、componentDidUpdate等可使用Effect Hook来处理副作用
Props可通过this.props来访问父组件传递的props可通过函数参数来访问父组件传递的props
状态更新使用this.setState来更新状态使用对应的Hook来更新状态
内部引用可以通过Ref引用组件实例或DOM元素可以使用Ref Hook引用组件实例或DOM元素
性能优化可以使用shouldComponentUpdate来控制组件是否重新渲染可以使用React.memo或useMemo Hook来控制组件是否重新渲染
访问上下文可以使用this.context来访问上下文可以使用useContext Hook来访问上下文

需要注意的是,这只是一些常见的区别,并不是所有的区别。在实际开发中,具体的区别可能还会根据需求和使用的React版本而有所变化。

关键词:React函数组件对比类组件、React函数组件对比类组件性能、React函数组件对比类组件状态管理、React函数组件与类组件

函数组件和类组件是React中两种定义组件的方式,它们有以下区别:

  1. 语法:函数组件是使用函数声明的方式定义组件,而类组件是使用ES6的class语法定义组件。

  2. 写法和简洁性:函数组件更为简洁,没有类组件中的繁琐的生命周期方法和this关键字。函数组件只是一个纯粹的JavaScript函数,可以直接返回JSX元素。

  3. 状态管理:在React的早期版本中,函数组件是无法拥有自己的状态(state)和生命周期方法的。但是从React 16.8开始,React引入了Hooks(钩子)机制,使得函数组件也能够拥有状态和使用生命周期方法。

  4. 性能:由于函数组件不拥有实例化的过程,相较于类组件,它的性能会稍微高一些。但是在React 16.6之后,通过React.memo和PureComponent的优化,类组件也能够具备相对较好的性能表现。

总体来说,函数组件更加简洁、易读,适合用于无需复杂逻辑和生命周期方法的场景,而类组件适合于需要较多逻辑处理和生命周期控制的场景。另外,使用Hooks后,函数组件也能够拥有与类组件类似的能力,因此在开发中可以更加灵活地选择使用哪种方式来定义组件。

状态管理方面做对比

从状态管理的角度来看,函数组件和类组件在React中的区别主要体现在以下几个方面:

  1. 类组件中的状态管理:类组件通过使用state属性来存储和管理组件的状态。state是一个对象,可以通过this.state进行访问和修改。类组件可以使用setState方法来更新状态,并通过this.setState来触发组件的重新渲染。在类组件中,状态的更新是异步的,React会将多次的状态更新合并为一次更新,以提高性能。

  2. 函数组件中的状态管理:在React之前的版本中,函数组件是没有自己的状态的,只能通过父组件通过props传递数据给它。但是从React 16.8版本开始,通过引入Hooks机制,函数组件也可以使用useState钩子来定义和管理自己的状态。useState返回一个状态值和一个更新该状态值的函数,通过解构赋值的方式进行使用。每次调用状态更新函数,都会触发组件的重新渲染。

  3. 类组件的生命周期方法:类组件有很多生命周期方法,例如componentDidMountcomponentDidUpdatecomponentWillUnmount等等。这些生命周期方法可以用来在不同的阶段执行特定的逻辑,例如在componentDidMount中进行数据的初始化,在componentDidUpdate中处理状态或属性的变化等等。通过这些生命周期方法,类组件可以对组件的状态进行更加细粒度的控制。

  4. 函数组件中的副作用处理:在函数组件中,可以使用useEffect钩子来处理副作用逻辑,例如数据获取、订阅事件、DOM操作等。useEffect接收一个回调函数和一个依赖数组,可以在回调函数中执行副作用逻辑,依赖数组用于控制副作用的执行时机。函数组件的副作用处理与类组件的生命周期方法类似,但是可以更灵活地控制执行时机。

函数组件和类组件在状态管理方面的主要区别是函数组件通过使用Hooks机制来定义和管理状态,而类组件通过state属性来存储和管理状态。 函数组件中使用useState来定义和更新状态,而类组件则使用setState方法。 另外,函数组件也可以使用useEffect来处理副作用逻辑,类似于类组件的生命周期方法。通过使用Hooks,函数组件在状态管理方面的能力得到了大幅度的提升和扩展。

性能方面做对比

在性能方面,函数组件和类组件的表现也有一些区别。

  1. 初始渲染性能:函数组件相对于类组件来说,在初始渲染时具有更好的性能。这是因为函数组件本身的实现比类组件更加简单,不需要进行实例化和维护额外的实例属性。函数组件在渲染时更轻量化,因此在初始渲染时更快。

  2. 更新性能:当组件的状态或属性发生变化时,React会触发组件的重新渲染。在类组件中,由于状态的更新是异步的,React会将多次的状态更新合并为一次更新,以提高性能。而在函数组件中,由于每次状态更新都会触发组件的重新渲染,可能会导致性能略低于类组件。但是,通过使用React的memo或useMemo、useCallback等优化技术,可以在函数组件中避免不必要的重新渲染,从而提高性能。

  3. 代码拆分和懒加载:由于函数组件本身的实现比类组件更加简单,所以在进行代码拆分和懒加载时,函数组件相对于类组件更容易实现。React的Suspense和lazy技术可以在函数组件中实现组件的按需加载,从而提高应用的性能。

函数组件相对于类组件在初始渲染和代码拆分方面具有优势,在更新性能方面可能稍逊一筹。然而,React的优化技术可以在函数组件中应用,以提高性能并减少不必要的渲染。此外,性能的差异在实际应用中可能并不明显,因此在选择使用函数组件还是类组件时,应根据具体场景和需求进行综合考量。

组件生命周期

答案

参考 如何理解 React 里面的生命周期

  1. 挂载阶段(Mounting)
    • constructor:组件实例化时执行,用于初始化 state 绑定事件等操作
    • getDerivedStateFromProps:在render方法执行之前调用,用于根据props设置state。
    • render 渲染组件
    • componentDidMount:组件挂载到DOM后执行,用于执行一些需要DOM的操作,如获取数据。
  2. 更新阶段(Updating)
  3. 卸载阶段(Unmounting)
  4. 异常流程会触发如下钩子

此外还废弃了如下钩子

参考 class lifecycle

对于函数组件, 重点钩子映射如下

  • useLayoutEffect ,在 DOM 更新后立即执行,模拟 componentDidMount 和 componentDidUpdate 行为
  • useEffect 副作用钩子,每次挂载和更新后都会触发,通过返回的函数来清理副作用模拟

参考 react-hooks-lifecycle

进一步细节参考 react-hook-component-timeline

示例代码

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React 生命周期演示</title>
    <style>
      .lifecycle-demo {
        max-width: 600px;
        margin: 20px auto;
        padding: 20px;
        font-family: Arial, sans-serif;
      }
      .controls {
        margin: 20px 0;
      }
      .controls button {
        margin-right: 10px;
        padding: 8px 16px;
      }
      .class-component {
        padding: 20px;
        border: 1px solid #eee;
        border-radius: 4px;
        margin: 20px 0;
      }
      .lifecycle-phase {
        margin: 10px 0;
        padding: 10px;
        border: 1px solid #eee;
        border-radius: 4px;
      }
      .lifecycle-phase h4 {
        margin: 0 0 10px 0;
        color: #333;
      }
      .lifecycle-phase ol {
        margin: 0;
        padding-left: 20px;
      }
      .lifecycle-phase li {
        margin: 5px 0;
        color: #666;
      }
      .component-status {
        margin-top: 20px;
        padding: 10px;
        background-color: #f5f5f5;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script type="text/babel" data-type="module">
      import React, { Component } from "https://esm.sh/react@19";
      import ReactDOM from "https://esm.sh/react-dom@19/client";

      // 内部类组件 - 用于演示生命周期
      class ClassComponent extends Component {
        // 添加默认属性
        static defaultProps = {
          defaultTime: new Date().toLocaleTimeString()
        };

        // 添加上下文类型
        static contextType = React.createContext('light');

        constructor(props) {
          super(props);
          console.log("Constructor: 初始化状态");
        }

        static getDerivedStateFromProps(nextProps, prevState) {
          console.log("getDerivedStateFromProps: 从props获取state", {nextProps, prevState});
          return null;
        }

        shouldComponentUpdate(nextProps, nextState, nextContext) {
          console.log("shouldComponentUpdate: 判断是否需要重新渲染", {
            nextProps,
            nextState,
            nextContext
          });
          return true;
        }

        getSnapshotBeforeUpdate(prevProps, prevState) {
          console.log("getSnapshotBeforeUpdate: 获取更新前的快照", {prevProps, prevState});
          return { scrollPos: 0 }; // 示例返回值
        }

        componentDidMount() {
          console.log("componentDidMount: 组件已挂载");
        }

        componentDidUpdate(prevProps, prevState, snapshot) {
          console.log("componentDidUpdate: 组件已更新", {
            prevProps,
            prevState,
            snapshot
          });
        }

        componentWillUnmount() {
          console.log("componentWillUnmount: 组件即将卸载");
        }

        static getDerivedStateFromError(error) {
          console.log("getDerivedStateFromError: 从错误中派生状态", error);
          return { error: error.message };
        }

        componentDidCatch(error, info) {
          console.log("componentDidCatch: 捕获到错误", {
            error,
            info
          });
        }

        render() {
          console.log("render: 渲染组件", {
            props: this.props,
          });

          return (
            <div className="class-component">
              <h3>类组件生命周期</h3>
              <div className="component-status">
                <p>当前状态: {this.props.mounted ? "已挂载" : "未挂载"}</p>
                <p>最后更新: {this.props.updateTime}</p>
              </div>
            </div>
          );
        }
      }

      // 外层容器组件 - 控制类组件的生命周期
      class LifecycleDemo extends Component {
        constructor(props) {
          super(props);
          this.state = {
            mounted: false,
            updateTime: new Date().toLocaleTimeString(),
          };
        }

        handleMount = () => {
          if (!this.state.mounted) {
            console.log('%c=== 挂载阶段 ===', 'color: green; font-weight: bold;'); 
            this.setState({
              mounted: true,
              updateTime: new Date().toLocaleTimeString(),
            });
          }
        };

        handleUpdate = () => {
          if (this.state.mounted) {
            console.log('%c=== 更新阶段 ===', 'color: blue; font-weight: bold;');
            this.setState({
              updateTime: new Date().toLocaleTimeString(),
            });
          }
        };

        handleUnmount = () => {
          if (this.state.mounted) {
            console.clear();
            console.log('%c=== 卸载阶段 ===', 'color: red; font-weight: bold;');
            this.setState({ mounted: false });
          }
        };

        render() {
          return (
            <div className="lifecycle-demo">
              <h2>React 生命周期演示</h2>
              <div className="controls">
                <button onClick={this.handleMount}>Mount</button>
                <button onClick={this.handleUpdate}>Update</button>
                <button onClick={this.handleUnmount}>Unmount</button>
              </div>

              {this.state.mounted && (
                <ClassComponent
                  {...this.state}
                />
              )}

              <div className="note">
                <p>请打开控制台查看生命周期钩子的触发顺序</p>
              </div>
            </div>
          );
        }
      }

      // 渲染组件
      const container = document.getElementById("root");
      const root = ReactDOM.createRoot(container);
      root.render(<LifecycleDemo />);
    </script>
  </body>
</html>

延伸阅读

父组件调用子组件的方法

在React中,我们经常在子组件中调用父组件的方法,一般用props回调即可。但是有时候也需要在父组件中调用子组件的方法,通过这种方法实现高内聚。有多种方法,请按需服用。

类组件中

React.createRef()

  • 优点:通俗易懂,用ref指向。

  • 缺点:使用了HOC的子组件不可用,无法指向真是子组件

比如一些常用的写法,mobx的@observer包裹的子组件就不适用此方法。

import React, { Component } from 'react'

class Sub extends Component {
callback () {
console.log('执行回调')
}

render () {
return <div>子组件</div>
}
}

class Super extends Component {
constructor (props) {
super(props)
this.sub = React.createRef()
}

handleOnClick () {
this.sub.callback()
}

render () {
return (
<div>
<Sub ref={this.sub}></Sub>
</div>
)
}
}

ref的函数式声明

  • 优点:ref写法简洁
  • 缺点:使用了HOC的子组件不可用,无法指向真是子组件(同上)

使用方法和上述的一样,就是定义ref的方式不同。

...

<Sub ref={ref => this.sub = ref}></Sub>

...


使用props自定义onRef属性

  • 优点:假如子组件是嵌套了HOC,也可以指向真实子组件。
  • 缺点:需要自定义props属性
import React, { Component } from 'react'
import { observer } from 'mobx-react'

@observer
class Sub extends Component {
componentDidMount () {
// 将子组件指向父组件的变量
this.props.onRef && this.props.onRef(this)
}

callback () {
console.log('执行我')
}

render () {
return (<div>子组件</div>)
}
}

class Super extends Component {
handleOnClick () {
// 可以调用子组件方法
this.Sub.callback()
}

render () {
return (
<div>
<div onClick={this.handleOnClick}>click</div>
{/* eslint-disable-next-line */}
<Sub onRef={ node => this.Sub = node }></Sub>
</div>)
}
}

函数组件、Hook组件

useImperativeHandle

  • 优点: 1、写法简单易懂 2、假如子组件嵌套了HOC,也可以指向真实子组件
  • 缺点: 1、需要自定义props属性 2、需要自定义暴露的方法
import React, { useImperativeHandle } from 'react'
import { observer } from 'mobx-react'

const Parent = () => {
const ChildRef = React.createRef()

function handleOnClick () {
ChildRef.current.func()
}

return (
<div>
<button onClick={handleOnClick}>click</button>
<Child onRef={ChildRef} />
</div>
)
}

const Child = observer(props => {
// 用useImperativeHandle暴露一些外部ref能访问的属性
useImperativeHandle(props.onRef, () => {
// 需要将暴露的接口返回出去
return {
func
}
})
function func () {
console.log('执行我')
}
return <div>子组件</div>
})

export default Parent

forwardRef

使用forwardRef抛出子组件的ref

这个方法其实更适合自定义HOC。但问题是,withRouter、connect、Form.create等方法并不能抛出ref,假如Child本身就需要嵌套这些方法,那基本就不能混着用了。forwardRef本身也是用来抛出子元素,如input等原生元素的ref的,并不适合做组件ref抛出,因为组件的使用场景太复杂了。

import React, {  useRef, useImperativeHandle } from 'react'
import ReactDOM from 'react-dom'
import { observer } from 'mobx-react'

const FancyInput = React.forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))

return <input ref={inputRef} type="text" />
})

const Sub = observer(FancyInput)

const App = props => {
const fancyInputRef = useRef()

return (
<div>
<FancyInput ref={fancyInputRef} />
<button
onClick={() => fancyInputRef.current.focus()}
>父组件调用子组件的 focus</button>
</div>
)
}

export default App

总结

父组件调子组件函数有两种情况

  • 子组件无HOC嵌套:推荐使用ref直接调用
  • 有HOC嵌套:推荐使用自定义props的方式

react 组件通信方式

答案

默认情况下 react 推荐采用单向数据流,父组件通过 props 向子组件传递数据,子组件通过回调函数向父组件传递数据。除了标准的 props 传递外,还有如下几种方式可以实现组件之间的通信。

  1. props drilling:通过 props 从父组件传递数据到子组件,逐层传递数据。这种方式简单直接,但是当组件层级较深时,会导致 props 传递过程繁琐和组件耦合度增加。
  2. context:使用 React 的 context API,可以实现跨层级的数据传递,避免 props drilling 的问题。通过创建 context 对象,可以在组件树中传递数据,任何一个组件都可以访问到这个数据。但是 context 会使组件的复用性降低,因为组件和 context 之间会产生耦合。
  3. 事件总线:通过事件总线的方式,可以实现任意组件之间的通信。事件总线是一个全局的事件系统,组件可以通过订阅和发布事件来进行通信。但是事件总线会使组件之间的关系变得不明确,不易维护。
  4. Redux/Mobx:使用状态管理库,如 Redux 或 Mobx,可以实现全局状态管理,任意组件都可以访问和修改全局状态。这种方式适用于大型应用,但是引入了额外的复杂性和学习成本。

本质上组件之间的通信就是信息交换的过程,在脱离框架范式的逻辑上,状态的管理可以不仅局限于内存模式,本地存储、URL、远程都可以作为状态的存储介质。

延伸阅读

请求在哪个阶段发出,如何取消请求

受控和非受控组件区别?

答案

参考官方文档 受控和非受控组件

  • 受控组件 React 控制元素状态为受控组件
  • 非受控组件 不受 react 控制的元素
实时编辑器
// 受控组件
function ControlledForm () {
  const [value, setValue] = useState('')
  // react 控制组件的状态
  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  )
}
结果
Loading...

什么是高阶组件 (Higher-Order Components HOC) ?

答案

参考官方文档, 高阶组件 是一个函数,接受一个组件作为参数返回新的组件。const EnhancedComponent = higherOrderComponent(WrappedComponent); 可以采用此模式来对原始组件进行增强、拦截等各种操作。示例如下

import { useState, useEffect } from 'react'

// Higher Order Component
const withLoading = (WrappedComponent, fetchData) => {
  return function WithLoadingComponent (props) {
    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(true)
    const [error, setError] = useState(null)

    useEffect(() => {
      const loadData = async () => {
        try {
          setLoading(true)
          const result = await fetchData()
          setData(result)
        } catch (err) {
          setError(err.message)
        } finally {
          setLoading(false)
        }
      }

      loadData()
    }, [])

    if (loading) return <div>Loading...</div>
    if (error) return <div>Error: {error}</div>
    return <WrappedComponent data={data} {...props} />
  }
}

// Example component that displays user data
const UserList = ({ data }) => {
  return (
      <ul>
         {data.map(user => (
            <li key={user.id}>{user.name}</li>
         ))}
      </ul>
  )
}

// Mock API call
const fetchUsers = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' },
        { id: 3, name: 'Bob Johnson' }
      ])
    }, 1000)
  })
}

const UserListWithLoading = withLoading(UserList, fetchUsers)

const App = () => {
  return (
      <div>
         <h1>Users</h1>
         <UserListWithLoading />
      </div>
  )
}

export default App

消费高阶组件时需要注意如下问题

  1. 不要改变原始组件 高阶组件不应该修改传入的组件,而是使用组合的方式将其包裹起来。详见 不要修改原始组件
  2. 不要在 render 中使用高阶组件 高阶组件不应该在 render 方法中创建,render 需要保持为纯函数。详见 不要在 render 中使用高阶组件
  3. 注意 ref 和 static 丢失的问题 高阶组件可能会导致 ref 和 static 丢失,详见 ref 和 static 丢失

错误边界组件如何使用

jsx 返回 null undefined false 区别

React.Children.map 和 props.children 的区别

state 和 props 区别

setState() 是同步还是异步??

答案

参考 你真的理解setState吗?

注意下列概念只对 react17 之前有效,react18 及以后默认均采用 concurrent 模式,所以 setState 为异步

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 等非 react 控制的流程中是同步。
  2. setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone"></script>
<div id="root"></div>
<script type="text/babel">
   let { Component } = React;

   class Counter extends React.Component {
      constructor(props) {
         super(props);
         this.state = { count: 0 };
         this.refElem = React.createRef();
      }

      componentDidMount() {
         this.setState({ count: this.state.count + 1 });
         console.log("钩子中,setState 是异步,返回历史状态:", this.state.count);
         
         this.refElem.current.addEventListener(
            "click",
            this.nativeIncrement,
            false
         );

         setTimeout(() => {
            // 原生事件中 setState 是同步
            this.setState({ count: this.state.count + 1 });
            console.log("setTimeout,setState 是同步,返回最新状态:", this.state.count);
         }, 1000);
      }

      increment = () => {
         this.setState({ count: this.state.count + 1 });
         this.setState({ count: this.state.count + 1 });
         console.log("React 合成事件中,setState 是异步,返回历史状态:", this.state.count);
      };

      nativeIncrement = () => {
         this.setState({ count: this.state.count + 1 });
         this.setState({ count: this.state.count + 1 });
         console.log("原生事件中,setState 是同步,返回最新状态:", this.state.count);
      };

      fetchRemote = () => {
         fetch("https://jsonplaceholder.typicode.com/todos/1")
            .then((response) => response.json())
            .then((data) => {
               this.setState({ count: this.state.count + 1 });
               console.log("fetch setState 是同步,返回最新状态:", this.state.count);
            });
      };

      componentWillUnmount() {
         this.refElem.current.removeEventListener(
            "click",
            this.nativeIncrement,
            false
         );
      }

      render() {
         return (
            <div>
               {/* React 合成事件 setState 异步 */}
               <button onClick={this.increment}>react add</button>
               
               {/* 原生事件 setState 同步 */}
               <button ref={this.refElem}>dom add</button>
               {/* Fetch 事件 setState 同步 */}
               <button onClick={this.fetchRemote}>fetch add</button>
               <div>count: {this.state.count}</div>
            </div>
         );
      }
   }
   ReactDOM.render(<Counter />, document.getElementById("root"));
</script>

关联问题

延伸阅读

react 中的 key 有什么作用

答案

key 是 React 中用于标识列表项的特殊属性,通过 key 来比对列表元素的差异,实现高效的更新和重用。key 应该是每个列表项的唯一标识,通常使用列表项的 id 或其他唯一标识作为 key。

延伸阅读

createPortal 了解多少?

答案

createPortal 解决需要把组件绑定在特定 DOM 节点上的问题,通常用于模态框、提示框等场景。

import { useState } from 'react'
import { createPortal } from 'react-dom'

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null

  return createPortal(
      <div className="modal-overlay" onClick={onClose}>
         <div className="modal-content" onClick={e => e.stopPropagation()}>
            {children}
            <button onClick={onClose}>Close</button>
         </div>
      </div>,
      document.body
  )
}

const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false)

  return (
      <div className="app">
         <h1>Modal Demo</h1>
         <button onClick={() => setIsModalOpen(true)}>Open Modal</button>

         <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
            <h2>Modal Content</h2>
            <p>This is rendered using createPortal</p>
         </Modal>

         <style>{`
            .modal-overlay {
               position: fixed;
               top: 0;
               left: 0;
               right: 0;
               bottom: 0;
               background-color: rgba(0, 0, 0, 0.5);
               display: flex;
               justify-content: center;
               align-items: center;
            }

            .modal-content {
               background: white;
               padding: 20px;
               border-radius: 4px;
               max-width: 500px;
               width: 90%;
            }
         `}</style>
      </div>
  )
}

export default App

延伸阅读

Portals 作用是什么, 有哪些使用场景?

React Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方式。通常,组件的渲染输出会被插入到其在组件树中的父组件下,但是 Portals 提供了一种穿透组件层次结构直接渲染到任意 DOM 节点的方法。

React Portals 的作用

  1. 父子结构逃逸:React Portals 允许你将子组件渲染到其父组件 DOM 结构之外的地方,这在视觉和位置上「逃逸」了它们的父组件。

  2. 样式继承独立:使用 Portal 的组件通常可以避免父组件样式的影响,易于控制和自定义样式。

  3. 事件冒泡正常:尽管 Portal 可以渲染到 DOM 树中的任何位置,但是事件冒泡会按照 React 组件树而不是 DOM 树来进行。所以,尽管组件可能被渲染到 DOM 树的不同部分,它的行为仍然像常规的 React 子组件一样。

React Portals 的使用场景

  1. 模态框:最常见的场景之一就是模态对话框,这时候对话框需要覆盖应用程序的其余部分(包括可能存在的其他元素如遮罩层),而且往往模态框的样式不应该受到其它 DOM 元素的影响。

  2. 浮动菜单:对于那些需要覆盖其它元素的浮动菜单或下拉式组件,React Portal 可以使这些组件渲染在最外层,避免被其他 DOM 元素的样式或结构干扰。

  3. 提示/通知:用于在界面上创建提示信息,如 Toasts 或 Snackbars,这些通常会浮动在内容之上并在固定位置显示。

  4. 全屏组件:对于需要全屏显示而不受现有 DOM 层级影响的组件(如图片库的全屏视图、视频播放或者游戏界面)。

  5. 第三方库的集成:有时候需要将 React 组件嵌入由非 React 库管理的 DOM 结构中,此时 Portal 可以非常有用。

总之,Portals 提供了一种灵活的方式来逃离父组件的限制,帮助开发者更加自由和方便地进行 UI 布局,同时也有助于维护组件结构的整洁和一致性。

代码使用举例

假设我们想创建一个模态框(Modal)组件,我们会希望这个模态框在 DOM 中是在最顶层的,但在 React 组件树中它应该在逻辑上保持在其父组件下。使用 React Portals 可以很容易地实现这一点。

首先,我们在 public/index.html 中,添加一个新的 DOM 节点,作为 Portal 的容器:

<!-- index.html -->
<div id="app-root"></div>
<!-- React App 将会挂载在这里 -->
<div id="modal-root"></div>
<!-- Modal 元素将会挂载在这里 -->

接着,我们创建一个 Modal 组件,它会使用 ReactDOM.createPortal 来渲染其子元素到 #modal-root

// Modal.js
import React from 'react'
import ReactDOM from 'react-dom'

class Modal extends React.Component {
render () {
// 使用 ReactDOM.createPortal 将子元素渲染到 modal-root 中
return ReactDOM.createPortal(
// 任何有效的 React 孩子元素
this.props.children,
// 一个 DOM 元素
document.getElementById('modal-root')
)
}
}

export default Modal

现在,我们可以在应用程序的任何其他组件中使用这个 Modal 组件了,不论它们在 DOM 树中的位置如何:

// App.js
import React from 'react'
import Modal from './Modal'

class App extends React.Component {
constructor (props) {
super(props)
this.state = { showModal: false }
}

handleShow () {
this.setState({ showModal: true })
}

handleClose () {
this.setState({ showModal: false })
}

render () {
return (
<div className="App">
<button onClick={this.handleShow}>显示模态框</button>

{this.state.showModal
? (
<Modal>
<div className="modal">
<div className="modal-content">
<h2>我是一个模态框!</h2>
<button onClick={this.handleClose}>关闭</button>
</div>
</div>
</Modal>
)
: null}
</div>
)
}
}

export default App

在以上代码中,无论 Modal 组件在 App 组件中的位置如何,模态框的渲染位置总是在 #modal-root 中,这是一个典型的使用 React Portals 的例子。上述代码中的模态框在视觉上会覆盖整个应用程序的位置,但在组件层次结构中它仍然是 App 组件的子组件。

createElement

createElementcloneElement 有什么区别?

React 中的 createElementcloneElement 都可以用来创建元素,但它们用法有所不同。

createElement 用于在 React 中动态地创建一个新的元素,并返回一个 React 元素对象。它的用法如下:

React.createElement(type, [props], [...children]);

其中,type 是指要创建的元素的类型,可以是一个 HTML 标签名(如 divspan 等),也可以是一个 React 组件类(如自定义的组件),props 是一个包含该元素需要设置的属性信息的对象,children 是一个包含其子元素的数组。createElement 会以这些参数为基础创建并返回一个 React 元素对象,React 将使用它来构建真正的 DOM 元素。

cloneElement 用于复制一个已有的元素,并返回一个新的 React 元素,同时可以修改它的一些属性。它的用法如下:

React.cloneElement(element, [props], [...children]);

其中,element 是指要复制的 React 元素对象,props 是一个包含需要覆盖或添加的属性的对象,children 是一个包含其修改后的子元素的数组。cloneElement 会以这些参数为基础复制该元素,并返回一个新的 React 元素对象。

在实际使用中,createElement 通常用于创建新的元素(如动态生成列表),而 cloneElement 更适用于用于修改已有的元素,例如在一个组件内部使用 cloneElement 来修改传递进来的子组件的属性。

cloneElement 有哪些应用场景

React 中的 cloneElement 主要适用于以下场景:

  1. 修改 props

cloneElement 可以用于复制一个已有的元素并覆盖或添加一些新的属性。例如,可以复制一个带有默认属性的组件并传递新的属性,达到修改属性的目的。

// 假设有这样一个组件
function MyComponent(props) {
// ...
}

// 在另一个组件中使用 cloneElement 修改 MyComponent 的 props
function AnotherComponent() {
return React.cloneElement(<MyComponent />, { color: 'red' });
}
  1. 渲染列表

在渲染列表时,可以使用 Array.map() 生成一系列的元素数组,也可以使用 React.Children.map() 遍历子元素并返回一系列的元素数组,同时使用 cloneElement 复制元素并传入新的 key 和 props。

// 使用 Children.map() 遍历子元素并复制元素
function MyList({ children, color }) {
return (
<ul>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, { key: index, color })
)}
</ul>
);
}

// 在组件中使用 MyList 渲染列表元素
function MyPage() {
return (
<MyList color="red">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</MyList>
);
}
  1. 修改子元素

使用 cloneElement 也可以在一个组件内部修改传递进来的子组件的属性,例如修改按钮的样式。

function ButtonGroup({ children, style }) {
return (
<div style={style}>
{React.Children.map(children, (child) =>
React.cloneElement(child, { style: { color: 'red' } })
)}
</div>
);
}

function MyPage() {
return (
<ButtonGroup style={{ display: 'flex' }}>
<button>Save</button>
<button>Cancel</button>
</ButtonGroup>
);
}

总之,cloneElement 可以方便地复制已有的 React 元素并修改其属性,适用于许多场景,例如修改 props、渲染列表和修改子元素等。

cloneElement 的使用

答案

通过 cloneElement 来复制 Element,目前官方不推荐使用,可以通过 render props 等方式实现 Element 复制。

延伸阅读

forwardRef 的使用

答案

forwardRef 是 React 提供的一个 API,用于在函数组件中获取子组件的 ref。解决函数组件无法直接使用 ref 的问题。 目前官方不推荐使用,可以通过 props 的方式传递 ref。

import { useRef, forwardRef } from 'react'

const MyInput = forwardRef(function MyInput (props, ref) {
  return (
   <div>
      <label>My Input</label>
      <input type="text" ref={ref} {...props} />
   </div>
  )
})

function Parent () {
  const ref = useRef()
  return (
   <div>
      <MyInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
   </div>
  )
}

export default Parent

延伸阅读

react 类组件中哪些要保持为纯函数,为什么

答案
22%