俺作为 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;
复制代码
每次 render
时 handleClick
函数在都会重新生成,所以有性能影响。 这听起来是个大问题,但是在大多数应用程序中,这种方法的性能影响微乎其微。
总之:
如果你遇到性能问题,请避免在 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.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,控制则会出现警告
在此示例中,我们使用的是 class
组件,但是同样的功能也可用于函数组件
,或者是由 React.memo/React.forwardRef
创建的组件。
不过嘛! 我们现在都用 Typescript, 以上知识点仅作了解哈
高阶组件(HOC)
一般组件是将 props/state 转换为 UI,而高阶组件是将组件转换为另一个组件。
简单来说,高阶组件(HOC)是参数为组件,返回值为新组件的函数
HOC 本质其实是设计模式中的装饰器模式
HOC 在 React 的第三方库中很常见,例如 Redux 的 connect
HOC 的优点:降低原始组件代码逻辑复杂度,将通用逻辑抽离到高阶组件中去,最终实现组件逻辑复用
- 获取原始组件的实例
ref
- 抽离原始组件的
state
/props
适用场景:
比如两个页面 UI 几乎一样,功能几乎相同,仅仅几个操作不太一样,却写了两个耦合很多的页面级组件。由于它的耦合性过多,经常会添加一个功能(这两个组件都要添加),去改完第一个的时候,还要改第二个。
所以加新功能的时候,写一个高阶组件,往 HOC 里添加方法,把那组件包装一下,这样新代码就不会再出现耦合,旧的逻辑并不会改变
下面是一个复用 Table 数据请求的高阶组件的示例
详细代码请查看在线 Demo
高阶组件的问题
无法获取原始组件的 ref
refs
将不会透传给被包裹的组件。这是因为ref
不是prop
属性。就像key
一样,其被 React 进行了特殊处理。如果你对 HOC 添加ref
,该ref
将引用最外层的容器组件,而不是被包裹的组件。详细请参考在高阶组件中转发 refs
如何处理呢?
使用 React.forwardRef
API 明确地将 refs
转发到内部的组件。React.forwardRef
接受一个渲染函数,其接收 props
和 ref
参数并返回一个 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 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上
render props
“render prop” 是指一种在 React 组件之间使用一个值为函数的 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.children
为undefined
; - 如果当前组件只有一个子节点,
props.children
为object
; - 如果当前组件有多个子节点,
props.children
就为array
。
我们要怎么样将父组件的 doSomething
方法传递给{props.children}
呢?也就是怎么样在父组件中对不确定的子组件进行 props 传递呢?
React 提供的React.Children给了我们很方便的操作。其中:
React.cloneElement
的作用是克隆并返回一个新的 ReactElement (内部子元素也会跟着克隆),新返回的元素会保留有旧元素的 props
、ref
、key
,也会集成新的 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
往控制台打印数据也是副作用;
函数组件还有以下几个显著的特点:
-
组件不会被实例化,整体渲染性能得到提升
因为组件被精简成一个 render 方法的函数来实现的,不会有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。
-
组件不能访问
this
对象函数组件由于没有实例化过程,所以无法访问组件 this 中的对象,例如:
this.ref
、this.state
等均不能访问。若想访问就不能使用这种形式来创建组件 -
无状态组件只能访问输入的 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 呢?
详细分析可以参考函数式组件与类组件有何不同?
参考文档
- 函数式组件与类组件有何不同?–推荐 ?
- React 创建组件的三种方式及其区别
- react-effect 官方文档
- Higher Order Components in a React Hooks World
最后
你还知道其它组件声明注意事项吗?欢迎在评论区留下的你的见解!文章浅陋,也请给为各位不吝赐教!
觉得有收获的朋友欢迎点赞,关注一波!