在 React 引入 Hooks 之前,我们要实现组件功能复用,常用技巧无非 HOC 和 Render Props 。下面就这两个技巧简单说一下,毕竟我们的重点是 Hooks
高阶组件(HOC)
高阶组件:参数为组件,返回值为新组件的函数(很多文章说返回的新组件是高阶组件,其实是不准确的)
高阶组件主要用于共享组件代码,强化组件功能等场景,也就是达到DRY模式。如
//原组件(参数组件)
class Test extends Component{
render() {
return (
<div>
<p>{this.props.title} - {this.props.name}</p>
{this.props.children}
</div>
);
}
}
//返回函数组件的高阶组件
const withTest = Comp => {
const name = '高阶组件';
return props => <Comp {...props} name={name} />
}
//返回class组件的高阶组件
const withTest = Comp => {
return class extends Component{
render() {
return (
<Comp {...this.props}>
<span>this is Hoc</span>
</Comp>
);
}
}
}
复制代码
特点:
- 高阶组件是纯函数,不产生任何副作用;
- 返回的新组件和参数传入的组件是两个完全独立的组件;
- 不要在render方法中使用HOC,会发生组件不必要的卸载和重新渲染,影响性能问题;
- 组件的ref不会被高阶组件获取,需要采用Refs转发;
给高阶组件传参
//hoc.tsx
export default (wrapProps) => (WrappedComponent) => (props) => (
<WrappedComponent {...props} {...wrapProps} />
);
//index.tsx
import HOC from './hoc'
const Demo = HOC(props)(demo)
复制代码
一般用于高阶组件条件渲染或传参,如 react-redux 的 connect
高阶组件反向继承
高阶组件继承入参组件,对入参组件进行操作
export default (WrappedComponent) => {
return class Inheritance extends WrappedComponent {
componentDidMount() {
// 可以方便地得到state,做一些更深入的修改。
console.log(this.state);
}
render() {
return super.render();
}
}
}
复制代码
ES7装饰器使用高阶组件
@withHeader
@withLoading
class Demo extends Component{
}
//执行顺序 withLoading => withHeader,即从内到外执行
const enhance = recompact.compose(withHeader,withLoading);
@enhance
class Demo extends Component{
}
//也可以采用recompact组合高阶组件,从后往前执行
复制代码
优缺点
- 优点:组件逻辑复用性强,并且可以传参增强组件功能
- 缺点:加深组件嵌套层级,且多层高阶组件嵌套时容易出现props同名,无法区分的问题
PS:可以通过react-devtools等chrome插件查看组件层级
Render Props 模式
传递一个返回渲染组件的函数,由父组件决定子组件children的渲染内容
class Mouse extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div style={{ height: '100%' }}>
{this.props.render(this.state)}
//<Cat /> 不采用这种硬编程的方法写死组件
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={name => (
<Cat name={name} />
)}/>
</div>
);
}
}
复制代码
特点:
- Render Props 模式中的“render”可以任何名字,但如果命名成children,需要在propType中指定children为function;
- 与PureComponent一起使用时,切记不要传匿名函数。因为PureComponent是基于props的浅比较,每次发生更新比较时,都是一个新的匿名函数,浅比较结果都返回false,发生子组件重新渲染,从而失去了PureComponent组件原有的性能优化功能
优点
Render Props 模式很好地规避了HOC的缺点,不会产生无用的组件加深层级。但是使用时,应充分了解React组件渲染原理,使用不当可能会造成应用性能的不必要开销
Hooks出现
好了,终于轮到今天的主角。在聊 Hooks 之前,我们说一下 Class 组件的弊端:
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。往往需要借助上面提到的hoc、render prop等技巧实现功能复用,从而加深了组件嵌套层级;
- Class编写的组件代码较“重”,不管是一个多小的组件都必须是一个class实例,且存在难以理解的this指向问题(虽然可以通过一些babel polyfill解决)。
自React@16.8推出了Hooks概念以来,我们完全可以编写更加灵活的函数组件,并且可以实现自定义hooks来满足不同的业务需求
常用Hooks
useState
用于函数组件添加state变量。普通函数退出后变量就会”消失”,而 state 中的变量会被 React 保留
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
//左边采用数组解析构,第一个变量是自定义state,第二个是对应的操作函数
//useState参数是state变量的初始值,可以是基本数据类型,数组,对象等
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
## 相当于class组件的this.state
this.state = { count: 0 };
复制代码
用定义state变量时返回的操作函数修改state的值
setCount(1);
##相当于class组件的setState
this.setState({ count: this.state.count + 1 })};
复制代码
⚠️注意:与setState不同的是,更新state变量总是替换它而不是合并它,所以在update复杂结构state值的时候需要采用函数式更新
const [person, setPerson] = useState({name, age})
setPerson(pre => ({...person, age}))
复制代码
useEffect
为函数组件提供执行副作用操作的能力,例如数据获取,设置订阅以及手动更改 React 组件中的 DOM 等。useEffect Hook 相当于componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
function Example() {
const [times, setTimes] = useState(0);
useEffect(() => {
document.title = `You clicked ${times} times`;
}, [times]);
}
复制代码
- 执行机制:在函数组件内部调用useEffect Hook,第一个参数是回调函数(称之为effect)。在函数组件发生渲染时,都会执行该回调函数(包括首次渲染和更新渲染)
- effect清除:effect中可能会有一些占用内存的操作,例如数据订阅等,需要在组件卸载的时候进行清除,释放内存。effect约定在执行下一次effect执行前或者组件卸载前执行effect中返回的函数进行清除操作,如下
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [times, setTimes] = useState(0);
useEffect(() => {
console.log('effect running', times);
return () => {
console.log('effect clear');
}
}, [times]);
const onClickHandle = () => {
setTimes(++times)
}
return (<Button type="default" onClick={onClickHandle}></Button>);
}
//每次点击button(组建重新渲染)都会依次打印:
//effect running
//effect clear
复制代码
- 性能优化:有时候并不希望每次发生组件重新渲染时,都执行effect,可以给useEffect传入第二个参数。第二个参数是一个依赖项数组,表示effect执行依赖哪些数据修改,默认值是 [] 。如果依赖的数据重新渲染前后没有发生改变,effect将不会被执行
useRef
生成一个ref对象,ref.current 属性被初始化为传入的参数,该值在组件的整个生命周期内保持不变,每次渲染时返回同一个 ref 对象
const inputEl = useRef(Date.now());
复制代码
与createRef()不同的是:
- createRef生成的ref.current未被使用在DOM元素或者组件上时,始终是null,使用之后指向实际DOM或者组件实例;
- useRef生成的ref.current被初始化为传入的参数,且在组件整个生命周期内不变,绑定到DOM后,.current指向实际DOM
useImperativeHandle对应与ref转发,与 forwardRef 一起使用
useImperativeHandle(ref, createHandle, [deps])
复制代码
useMemo
利用“创建函数”和依赖项数组,生成一个memoized值,且仅当某个依赖项渲染前后的值发生改变时,“创建函数”才会执行重新计算memoized值。如:
const memoizedValue = useMemo(() => return Math.sum(a, b), [a, b]);
复制代码
- 创建函数在首次渲染时无条件执行;
- 依赖项数组默认是 [],没有依赖项时,每次发生组件重新渲染都会执行;
- 不要在“创建函数”中执行与渲染无关的操作,记住与useEffect的区别
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback,返回该回调函数的 memoized 版本,回调函数仅在某个依赖项改变时才会更新。相当于useMemo(() => fn, deps)返回传入的回调函数,而不是计算值
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
可以在某些特定的场景下调用memoizedCallback,而不需要触发组件重新渲染
更多参考 React文档
自定义Hooks
React提供的Hooks毕竟有限,实际开发中我们往往需要自定义一些hook。这里写一个useFetch自定义hook的例子
export function useFetch(url, options) {
const [data, setData] = useState({});
const fetchApi = useCallback(async () => {
const res = await fetch(url, options);
if (res.ok && res.status === 200) {
setData(res.json());
}
}, [url]);
useEffect(() => {
fetchApi();
}, [fetchApi]);
return data;
}
复制代码