React 声明组件的几种方式及注意事项,你知道吗?| 创作者训练营第二期

我心飞翔 分类:javascript

俺作为 Reac 初学者时,总是对组件声明的几种方式及其暗坑云里雾里!React 中高阶组件是什么?高阶组件使用有什么缺点? render props又是什么? 都有有哪些使用场景? 为什么又要出来一个函数式 Hooks 组件? 今天将它们总结一下,方便你,方便我,方便他!

class 组件

涉及 React 的生命周期方法(尤其是componentDidCatch)的时候可以使用类组件

React.Component创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。

下面是一个用于捕获 React 错误的组件

import type { ErrorInfo, ReactNode } from "react";
import React from "react";

interface Props {
  children: ReactNode;
}

interface State {
  error: Error | null;
  errorInfo: ErrorInfo | null;
}
class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      error: null,
      errorInfo: null,
    };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // 捕获组件包裹下的所有子组件的渲染错误
    this.setState({
      error,
      errorInfo,
    });
    // 你还可以在这里调用后端接口,把错误信息存储到数据库
  }

  render() {
    if (this.state.errorInfo) {
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: "pre-wrap" }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      );
    }
    // 没有错误,则使用 render prop 正常渲染
    return this.props.children;
  }
}

export default ErrorBoundary;
 

使用示例

import React from "react";
import ReactDOM from "react-dom";

import ErrorBoundary from "./ErrorBoundary";

const Index = () => {
  // ErrorBoundary 组件会捕获到 a未定义的错误
  console.log(a);

  return <h1>hello world</h1>;
};

ReactDOM.render(
  <ErrorBoundary>
    <Index />
  </ErrorBoundary>,
  document.getElementById("root"),
);
 

类组件问题:成员函数不会自动绑定 this

React.Component创建的组件,其成员函数不会自动绑定 this,需要开发者手动绑定,否则 this 不能获取当前组件实例对象

错误示例:

import React from "react";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };
  }
  handleClick() {
    // Cannot read property 'state' of undefined
    console.log(this.state);
  }
  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}
export default Index;
 

解决方案一:使用函数式组件(推荐)

因为函数式组件没有this,因此根本不需要担心这些问题

import React, { useState } from "react";

const Index = () => {
  const [text, setText] = useState("hello world");

  const handleClick = () => {
    // 'hello world'
    console.log(text);
  };

  return <button onClick={handleClick}>点击我</button>;
};

export default Index;
 

解决方案二: 在 render 外部的事件中使用箭头函数

箭头函数在封闭作用域中使用 bind绑定this,(换句话说,this 不会随作用域改变而改变)

import React from "react";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };
  }
  // 在这里使用箭头函数
  handleClick = () => {
    // { text: 'hello world'}
    console.log(this.state);
  };
  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

export default Index;
 

解决方案之三:在 render 中使用 bind

import React from "react";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };
  }
  handleClick() {
    console.log(this.state);
  }
  render() {
    return (
      // 使用bind来绑定
      <button onClick={this.handleClick.bind(this)}>点击我</button>
    );
  }
}

export default Index;
 

每次 renderhandleClick函数在都会重新生成,所以有性能影响。 这听起来是个大问题,但是在大多数应用程序中,这种方法的性能影响微乎其微。

总之:
如果你遇到性能问题,请避免在 render 中使用 bind箭头函数。参考链接

解决方案之四: 在 render 中使用箭头函数

import React from "react";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };
  }
  handleClick() {
    console.log(this.state);
  }
  render() {
    return (
      // 使用arrow function来绑定
      <button onClick={() => this.handleClick()}>点击我</button>
    );
  }
}

export default Index;
 

与方案三有同样的问题

解决方案之五: 在构造函数中 bind

import React from "react";

class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };

    // 构造函数中绑定
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log(this.state);
  }
  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

export default Index;
 

每次声明一个事件,必须在构造函数中做绑定, 可读性和维护性非常不好

组件的 propTypes/defaultProps

随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 TypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。

但即使你不使用TypeScript,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes 属性:

React 官方文档-PropTypes

React.Component在创建组件时配置propTypes,defaultProps这两个对应信息时,他们是作为组件类的属性,不是组件实例的属性,也就是所谓的类的静态属性来配置的。对应配置如下:

import React from "react";
import PropTypes from "prop-types";

class MyButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "hello world",
    };
    // 构造函数中绑定
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this.state);
  }
  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

MyButton.propTypes = {
  // 类的静态属性类型
  name: PropTypes.string.isRequired,
};

MyButton.defaultProps = {
  // 类的静态属性默认值
  // name: ''
};

const Index = () => {
  return (
    <React.StrictMode>
      <MyButton />
    </React.StrictMode>
  );
};

export default Index;
 

设置了isRequired后,如果没有传递对应 prop,控制则会出现警告

react-propTypes

在此示例中,我们使用的是 class 组件,但是同样的功能也可用于函数组件,或者是由 React.memo/React.forwardRef 创建的组件。

不过嘛! 我们现在都用 Typescript, 以上知识点仅作了解哈

高阶组件(HOC)

一般组件是将 props/state 转换为 UI,而高阶组件是将组件转换为另一个组件。

简单来说,高阶组件(HOC)是参数为组件,返回值为新组件的函数

官方文档-HOC

HOC 本质其实是设计模式中的装饰器模式

HOC 在 React 的第三方库中很常见,例如 Redux 的 connect

HOC 的优点:降低原始组件代码逻辑复杂度,将通用逻辑抽离到高阶组件中去,最终实现组件逻辑复用

  • 获取原始组件的实例 ref
  • 抽离原始组件的 state/props

适用场景:

比如两个页面 UI 几乎一样,功能几乎相同,仅仅几个操作不太一样,却写了两个耦合很多的页面级组件。由于它的耦合性过多,经常会添加一个功能(这两个组件都要添加),去改完第一个的时候,还要改第二个。

所以加新功能的时候,写一个高阶组件,往 HOC 里添加方法,把那组件包装一下,这样新代码就不会再出现耦合,旧的逻辑并不会改变

下面是一个复用 Table 数据请求的高阶组件的示例

20210419173453

详细代码请查看在线 Demo

高阶组件的问题

无法获取原始组件的 ref

refs 将不会透传给被包裹的组件。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

详细请参考在高阶组件中转发 refs

如何处理呢?

使用 React.forwardRef API 明确地将 refs 转发到内部的组件。React.forwardRef 接受一个渲染函数,其接收 propsref 参数并返回一个 React 节点。例如:

// hocLog.jsx

import React from "react";

// 一个打印组件 props 的日志-高阶组件
function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      if (prevProps.label !== this.props.label) {
        console.log("old props:", prevProps);
        console.log("new props:", this.props);
      }
    }

    render() {
      const { forwardedRef, ...rest } = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

export default logProps;
 
// index.jsx
import React, { useState } from "react";

import hocLog from "./hocLog";

class MyButton extends React.Component {
  getData = () => {
    console.log("我被调用了");
  };

  render() {
    const { label, handleClick } = this.props;
    return (
      <>
        <button onClick={handleClick}>{label}</button>
      </>
    );
  }
}

const NewMyButton = hocLog(MyButton);

const Index = () => {
  const buttonRef = React.createRef();

  const [label, setLable] = useState("请点击我");

  // 调用子组件内部的成员
  const handleClick = () => {
    buttonRef.current.getData();

    setLable("我已经被点击过了");
  };

  return (
    <NewMyButton label={label} handleClick={handleClick} ref={buttonRef} />
  );
};

export default Index;
 

无法获取原始 class 组件的静态方法

当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法

为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上

官方文档-HOC 务必复制静态方法

render props

“render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

React 官方文档-render prop

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

import React, { useState } from "react";

interface Props {
  header: () => React.ReactNode;
  footer: () => React.ReactNode;
}

// React.VFC参考:  https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components
const MyButton: React.VFC<Props> = ({ header, footer }) => {
  const [text, setText] = useState("hello world");

  const handleClick = () => {
    // 'hello world'
    console.log(text);
  };

  return (
    <>
      {header()}
      <button onClick={handleClick}>点击我</button>
      {footer()}
    </>
  );
};

const Index = () => {
  return (
    <MyButton
      header={() => <h1>未来组成头部</h1>}
      footer={() => <h1>未来组成脚</h1>}
    />
  );
};

export default Index;
 

props.children 是一个特殊的render prop

那么如何给 props.children 传递属性呢?

在 React 中, props.children 是一个特殊的render prop,表示组件的所有节点。

props.children 的值存在三种可能性:

  • 如果当前组件没有子节点,props.childrenundefined
  • 如果当前组件只有一个子节点,props.childrenobject
  • 如果当前组件有多个子节点,props.children 就为 array

我们要怎么样将父组件的 doSomething 方法传递给{props.children}呢?也就是怎么样在父组件中对不确定的子组件进行 props 传递呢?

React 提供的React.Children给了我们很方便的操作。其中:

React.cloneElement 的作用是克隆并返回一个新的 ReactElement (内部子元素也会跟着克隆),新返回的元素会保留有旧元素的 propsrefkey,也会集成新的 props(只要在第二个参数中有定义)。

React.Children.map 来遍历子节点,而不用担心 props.children 的数据类型是 undefined 还是 object

然后,我们直接在子组件中调用 this.props.doSomething()就可以了。

import React, { useEffect } from "react";

interface ChildProps {
  doSomething: () => void;
  name: string;
  age: number;
}

const Child: React.VFC<ChildProps> = ({ doSomething, name, age }) => {
  useEffect(() => {
    console.log("子组件接收name,name:", name, age);
  }, [name, age]);

  return <button onClick={doSomething}>点击我触发外部事件</button>;
};

// -------------
interface Item {
  key: string;
  name: string;
  age: number;
}

interface ParentProps {
  items: Item[];
  renderItem: (items: Item[]) => React.ReactNode;
  label?: React.ReactNode;
  children: React.ReactElement;
}

const Parent: React.VFC<ParentProps> = ({
  children,
  label,
  items,
  renderItem,
}) => {
  const selfProps = { name: "a", age: 10 };

  const doSomething = () => {
    console.log("doSomething被触发了");
  };

  // 通过这里将 一些props 传递给props.children
  const childrenWithProps = React.Children.map(children, (child) =>
    React.cloneElement(child, { doSomething, ...selfProps }),
  );

  return (
    <>
      {label && <h2>{label}</h2>}
      // 这是一个render prop
      {renderItem(items)}
      <div style={{ border: "1px solid black", margin: 10 }}>
        <h3>子组件</h3>
        // 这是一个特殊的render prop
        {childrenWithProps}
      </div>
    </>
  );
};

const Index = () => {
  const items = [
    { key: "1", name: "老王", age: 10 },
    { key: "2", name: "老李", age: 20 },
  ];
  return (
    <Parent
      label='父组件的标题'
      items={items}
      renderItem={() =>
        items.map((item) => (
          <div key={item.key}>
            姓名:{item.name},年龄{item.age}
          </div>
        ))
      }
    >
      <Child />
    </Parent>
  );
};

export default Index;
 

更多详细讨论可以参考:Render props 有什么用?

将组件作为 Prop 传递

function PassThrough(props: { as: React.ElementType<any> }) {
  const { as: Component } = props;

  return <Component />;
}
 

无状态函数组件/纯函数组件

一般推荐纯函数的写法

纯函数:函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用;

  • 函数的结果只受参数的影响;
  • 一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的;

常见副作用(一个函数在执行过程中还有很多方式产生外部可观察的变化);

  • 修改外部的变量;
  • 调用 DOM API 修改页面;
  • Ajax 请求;
  • window.reload 刷新浏览器;
  • console.log 往控制台打印数据也是副作用;

函数组件还有以下几个显著的特点:

  1. 组件不会被实例化,整体渲染性能得到提升

    因为组件被精简成一个 render 方法的函数来实现的,不会有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。

  2. 组件不能访问this对象

    函数组件由于没有实例化过程,所以无法访问组件 this 中的对象,例如:this.refthis.state等均不能访问。若想访问就不能使用这种形式来创建组件

  3. 无状态组件只能访问输入的 props,同样的 props 会得到同样的渲染结果,不会有副作用

只要有可能,尽量使用无状态组件

下面是一个无状态组件示例

import React, { useState, useEffect } from "react";
import { fetchUser } from "./api";

// 这是一个无状态组件
const UserInfo = (props) => {
  const { user } = props;

  const { name, age } = user;
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

const UserInfoPage = (props) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function getUser() {
      const user = await fetchUser("/userAppi");

      if (user) {
        setUser(user);
      }
    }
    getUser();
  }, []);

  return <UserInfo user={user} />;
};
 

有状态函数组件/Hooks 组件

既然有了class组件,为什么会出现Hooks组件呢?

函数组件更加契合 React 框架的设计理念,即UI=Function(data)

React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数。作为开发者,我们编写的是声明式的代码,

而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述
映射到用户可见的 UI 变化中去。

这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。

上面代码中的UserInfoPage 就是有状态的函数组件

自定义 Hooks 组件

详细内容参考官方-自定义 Hook

下面是一个数据请求的自定义 Hook

export function useFetch(request: RequestInfo, init?: RequestInit) {
  const [response, setResponse] = useState<null | Response>(null);
  const [error, setError] = useState<Error | null>();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // 用来中断 fetch请求
    const abortController = new AbortController();
    setIsLoading(true);
    (async () => {
      try {
        const response = await fetch(request, {
          ...init,
          signal: abortController.signal,
        });
        setResponse(await response?.json());
        setIsLoading(false);
      } catch (error) {
        if (error.name === "AbortError") {
          return;
        }
        setError(error);
        setIsLoading(false);
      }
    })();
    return () => {
      // 页面卸载时中断请求
      abortController.abort();
    };
  }, [init, request]);
  // 返回: 响应数据,错误信息,laoding状态
  return { response, error, isLoading };
}
 

函数式组件与类组件的区别

本质区别: 函数组件会捕获 render 内部的状态

如果你在这个在线 Demo中尝试点击基于类组件形式编写的 ProfilePage 按钮后 3s 内把用户切换为 Sophie,你就会看出效果

明明我们是在 Dan 的主页点击的关注,结果却提示了“Followed Sophie”!

这个现象必然让许多人感到困惑:user 的内容是通过 props 下发的,props 作为不可变值,为什么会从 Dan 变成 Sophie 呢?

详细分析可以参考函数式组件与类组件有何不同?

参考文档

  1. 函数式组件与类组件有何不同?--推荐 🤖
  2. React 创建组件的三种方式及其区别
  3. react-effect 官方文档
  4. Higher Order Components in a React Hooks World

最后

你还知道其它组件声明注意事项吗?欢迎在评论区留下的你的见解!文章浅陋,也请给为各位不吝赐教!

觉得有收获的朋友欢迎点赞关注一波!

往期文章

  1. 企业级前端开发规范如何搭建 🛠
  2. 前端开发者应该知道 CI 搭建流程
  3. 了解 JavaScript 模块系统的基础知识,并建立自己的库 🍜
  4. 写给初学者的 Node 包管理器教程:什么是 npm?

回复

我来回复
  • 暂无回复内容