如何构建自己的 react hooks
我们组的前端妹子在组内分享时谈到了 react 的钩子,趁此机会我也对我所理解的内容进行下总结,方便更多的同学了解。在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API,我们非常有必要了解下他的使用方法,并能够结合我们的业务编写几个自定义的 hooks。
1. 常用的一个 hooks
官方中提供了几个内置的钩子,我们简单了解下他们的用法。
1.1 useState: 状态钩子
需要更新页面状态的数据,我们可以把他放到 useState
的钩子里。例如点击按钮一下,数据加 1 的操作:
const [count, setCount] = useState(0);
return (<>
<p>{ count}</p>
<button onClick = {
() => setCount(count + 1)
}> add 1 </button>
</>
);
在 typescript 的体系中,count 的类型,默认就是当前初始值的类型,例如上面例子中的变量就是 number
类型。如果我们想自定义这个变量的类型,可以在 useState 后面进行定义:
const [count, setCount] = useState<number | null>(null); // 变量count为number类型或者null类型
同时,使用 useState 改变状态时,是整个把 state 替换掉的,因此,若状态变量是个 object 类型的数据,我只想修改其中的某个字段,在之前 class 组件内调用 setState 时,他内部会自动合并数据。
class Home extends React.Component {
state = {
name: 'wenzi',
age: 20,
score: 89
};
update() {
this.setState({
score: 98
}); // 内部自动合并
}
}
但在 function 组件内使用 useState
时,需要自己先合并数据,然后再调用方法,否则会造成字段的丢失。
const [person, setPerson] = useState({
name: 'wenzi',
age: 20,
score: 89
});
setPerson({
...person,
{
score: 98
}
}); // 先合并数据 { name: 'wenzi', age: 20, score: 98 }
setPerson({
score: 98
}); // 仅传入要修改的字段,后name和age字段丢失
1.2 useEffect: 副作用钩子
useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
useEffect 钩子在组件初始化完毕时,一定会执行一次,在组件重新渲染的过程中,是否还要 update,还要看传入的第 2 个参数。
- 当只有回调函数这一个参数时,组件的每次更新,回调都会执行;
- 当有 2 个参数时,只有第 2 参数里的数据发生变化时,回调才执行;
- 只想在组件初始化完毕时只执行一次,第 2 个参数可以传入一个空的数组;
我们可以看下这个例子,无论点击 add按钮
还是 settime按钮
,useEffect 的回调都会执行:
const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0);
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
});
return ( <>
<p>count: {count} </p>
<p>nowtime: {nowtime} </p>
<button onClick = {() => setCount(count + 1)}> add 1 </button>
<button onClick = {() => setNowtime(Date.now())} > set now time </button>
</>);
};
若改成下面的这样,回调仅会在 count 发生变化时才会在控制台输出,仅修改 nowtime 的值时没有输出:
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
}, [count]);
useEffect 的回调函数还可以返回一个函数,这个函数在 effect 生命周期结束之前调用。为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。
基于上面的代码,我们稍微修改一下:
useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
return () => console.log('effect callback will be cleared');
}, [count]);
基于这个机制,在一些存在添加绑定和取消绑定的案例上特别合适,例如监听页面的窗口大小变化、设置定时器、与后端的 websocket 接口建立连接和断开连接等,都可以预计 useEffect 进行二次的封装,形成自定义的 hook。关于自定义 hook,下面我们会讲到。
1.3 useMemo 和 useCallback
function 组件中定义的变量和方法,在组件重新渲染时,都会重新重新进行计算,例如下面的这个例子:
const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0);
const getSum = () => {
const sum = ((1 + count) * count) / 2;
return sum + ' , ' + Math.random(); // 这个random是为了看到区别
};
return ( <>
<p> count: {count}< /p>
<p> sum: {getSum()}</p>
<p> nowtime: {nowtime}</p>
<button onClick = {() => setCount(count + 1)} > add 1 </button>
<button onClick = {() => setNowtime(Date.now())}> set now time </button>
</>);
};
这里有 2 个按钮,一个是 count+1,一个设置当前的时间戳, getSun()
方法是计算从 1 到 count 的和,我们每次点击 add 按钮后,sum 方法都会重新计算和。可是当我们点击 settime 按钮时,getSum 方法也会重新计算,这是没有必要的。
这里我们可以使用 useMemo
来修改下:
const sum = useMemo(() => ((1 + count) * count) / 2 + ' , ' + Math.random(), [count]);
<p> {sum} </p>;
修改后就可以看到,sum 的值只有在 count 发生变化的时候才重新计算,当点击 settime 按钮的时候,sum 并没有重新计算。这要得益于 useMemo
钩子的特性:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 返回回调里 return 的值,而且 memoizedValue 它仅会在某个依赖项改变时才重新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
在上面的例子里,只有 count 变量发生变化时,才重新计算 sum,否则 sum 的值保持不变。
useCallback 与 useMemo 类型,只不过 useCallback 返回的是一个函数,例如:
const fn = useCallback(() => {
return ((1 + count) * count) / 2 + ' , ' + nowtime;
}, [count]);
2. 实现几个自定义的 hook
在官方文档里,实现了好友的在线与离线功能。这里我们自己也学着实现几个 hook。
2.1 获取窗口变化的宽高
我们通过监听resize
事件来获取实时获取window窗口的宽高,对这个方法进行封装后可以在生命周期结束前能自动解绑resize事件:
const useWinResize = () => {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const resize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', resize);
return () => window.removeEventListener('resize', resize);
}, []);
return size;
}
使用起来也非常方便:
const Home = () => {
const {width, height} = useWinResize();
return <div>
<p>width: {width}</p>
<p>height: {height}</p>
</div>;
};
点击链接useWinResize的使用可以查看demo演示。
2.2 定时器 useInterval
在前端中使用定时器时,通常要在组件生命周期结束前清除定时器,如果定时器的周期发生变化了,还要先清除定时器再重新按照新的周期来启动。这种最常用的场景就是九宫格抽奖,用户点击开始抽奖后,先缓慢启动,然后逐渐变快,接口返回中奖结果后,再开始减速,最后停止。
我们很容易想到用 useEffect
来实现这样的一个 hook:
const useInterval = (callback, delay) => {
useEffect(() => {
if (delay !== null) {
let id = setInterval(callback, delay);
return () => clearInterval(id);
}
}, [delay]);
};
我们把这段代码用到项目中试试:
const Home = () => {
const [count, setCount] = useState(0);
useInterval(() => {
console.log(count);
setCount(count + 1);
}, 500);
return <div > {
count
} < /div>;
};
可是这段运行后很奇怪,页面从 0 到 1 后,就再也不变了, console.log(count)
的输出表明代码并没有卡死,那么问题出在哪儿了?
React 组件中的 props 和 state 是可以改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果,它们之间不再有相关性。
useEffect() Hook 也「丢弃」上一次渲染结果,它会清除上一次 effect 再建立下一个 effect,下一个 effect 锁住新的 props 和 state,这也是我们第一次尝试简单示例可以正确工作的原因。
但 setInterval 不会「丢弃」。 它会一直引用老的 props 和 state 直到你把它换掉 —— 不重置时间你是无法做到的。
这里就要用到useRef这个 hook 了,我们把 callback 存储到 ref 中,当 callback 更新时去更新 ref.current
的值:
const useInterval = (callback, delay) => {
const saveCallback = useRef();
useEffect(() => {
// 每次渲染后,保存新的回调到我们的 ref 里
saveCallback.current = callback;
});
useEffect(() => {
function tick() {
saveCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
当我们使用新的 useInterval
时,发现就可以自增了,点击查看样例useInterval 的简单使用。
这里我们使用一个变量来控制增加的速度:
const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500);
useInterval(() => {
setCount(count + 1);
}, diff);
return ( <div>
<p> count: {count} </p>
<p> diff: {diff}ms </p>
<p>
<button onClick = {() => setDiff(diff - 50)}> 加快50ms </button>
<button onClick = {() => setDiff(diff + 50)} > 减慢50ms </button>
</p>
</div>);
分别点击两个按钮,可以调整count增加的速度。
3. 总结
使用react hook可以做很多有意思的事情,这里我们也仅仅是举几个简单的例子,后续我们也会更加深入了解hook的原理。
▼我是来腾讯的小小前端开发工程师,
长按识别二维码关注,与大家共同学习、讨论▼
如何构建自己的 react hooks的更多相关文章
- React Hooks 深入系列 —— 设计模式
本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...
- React Hooks 深入系列
本文基于近段时间对 hooks 碎片化的理解作一次简单梳理, 个人博客.同时欢迎关注基于 hooks 构建的 UI 组件库 -- snake-design. 在 class 已经融入 React 生态 ...
- 使用 React hooks 转化 class 的一些思考
Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 使用 React hooks 转化 class 的一些思考 ...
- 蒲公英 · JELLY技术周刊 Vol.17: 90 行代码实现 React Hooks
蒲公英 · JELLY技术周刊 Vol.17 React Hooks 相信大家都不陌生,自被设计出以来就备受好评,在很多场景中都有极高的使用率,其中原理更是很多大厂面试中的必考题,很多朋友都能够如数家 ...
- 蒲公英 · JELLY技术周刊 Vol.21 -- 技术周刊 · React Hooks vs Vue 3 + Composition API
蒲公英 · JELLY技术周刊 Vol.21 选 React 还是 Vue,每个人心中都会有自己的答案,有很多理由去 pick 心水的框架,但是当我们扪心自问,我们真的可以公正的来评价这两者之间的差异 ...
- 通过 React Hooks 声明式地使用 setInterval
本文由云+社区发表 作者:Dan Abramov 接触 React Hooks 一定时间的你,也许会碰到一个神奇的问题: setInterval 用起来没你想的简单. Ryan Florence 在他 ...
- 初探React Hooks & SSR改造
Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...
- React hooks实践
前言 最近要对旧的项目进行重构,统一使用全新的react技术栈.同时,我们也决定尝试使用React hooks来进行开发,但是,由于React hooks崇尚的是使用(也只能使用)function c ...
- 如何使用TDD和React Testing Library构建健壮的React应用程序
如何使用TDD和React Testing Library构建健壮的React应用程序 当我开始学习React时,我努力的一件事就是以一种既有用又直观的方式来测试我的web应用程序. 每次我想测试它时 ...
随机推荐
- JavaScript专题之事件循环
准备知识 1. 进程(process) 进程是系统资源分配一个独立单位,一个程序至少有一个进程.比方说:一个工厂代表一个 CPU, 一个车间就是一个进程,任一时刻,只能有一个进程在运行,其他进程处于非 ...
- Netty - 粘包和半包(上)
在网络传输中,粘包和半包应该是最常出现的问题,作为 Java 中最常使用的 NIO 网络框架 Netty,它又是如何解决的呢?今天就让我们来看看. 定义 TCP 传输中,客户端发送数据,实际是把数据写 ...
- Cocos Creator实现1024游戏,免费提供代码。
效果预览 获取代码 私信或留言. 游戏介绍 ● Github上的代码,不能进行合并操作,修改以后,功能类似2048,空白块赏随机位置,生成2,可以往左.右.上.下滑动,数字会朝着指定方向运动,相 ...
- Dropzone.js
2015-11-25 发布 DropzoneJS 官网没有中文版的,很多东西只能跟着自己的感觉去理解,有些地方把握不了是否准确,在网上搜了一下中文版,看到一位大神简易的中文版 个人觉得和原官网对比着看 ...
- JavaScript 实用技巧
1数组中删除重复 let arr = [1,2,4,3,6,4] Array.from(new Set(arr)) // es6中 .from()[1,2,4,3,6] [...new Set(arr ...
- Textbox输入状态提示
前: <DockPanel Margin="> <TextBox SelectionChanged="TextBox_SelectionChanged" ...
- 用最简单的方式学Python单链表
Python 实现单链表 在本博客中,我们介绍单链表这种数据结构,链表结构为基于数组的序列提供了另一种选择(例如Python列表). 基于数组的序列和链表都能够对其中的元素保持一定得顺序,但采用的方式 ...
- Java网络编程(二)IP、URL和HTTP
一.IP InetAddress类有一些静态工厂方法,可以连接到DNS服务器来解析主机名. 示例1:InetAddress address = InetAddress.getByName(" ...
- 数据结构(四十五)选择排序(1.直接选择排序(O(n²))2.堆排序(O(nlogn)))
一.选择排序的定义 选择排序的基本思想是:每次从待排序的数据元素集合中选取最小(或最大)的数据元素放到数据元素集合的最前(或最后),数据元素集合不断缩小,当数据元素集合为空时排序过程结束.常用的选择排 ...
- 数据结构(四十二)散列表查找(Hash Table)
一.散列表查找的基础知识 1.散列表查找的定义 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时,根据这个确定的对应关系找到 ...