我们在之前的文章 如何构建自己的 react hooks 中,也介绍过如何构建自定义的 hook。这篇文章也是我在公司内的一次分享,从定义一个简单的 hook,然后一步步引导大家,让大家了解各种 hooks 的封装。方便在后续的开发过程中,能够找到适合自己的 hooks,或者自己也可以封装几个来使用。 从 React16.8 开始,可以「函数组件+hooks」来进行开发。如我们常用的 useState(), useEffect(), useRef()等,这里我们就不展开说了。 但这些内置的 hooks,都是一些原子化的操作,稍微复杂点的需求,就写通过各种 hooks 的组合才能完成。 这里有几个注意点: 使用原生方法绑定事件时,在卸载组件时也要解绑事件,否则在组件产生刷新时,会造成绑定多次事件(使用 React 的合成事件不用解绑)。 如下面给 window 添加 resize 事件,在组件卸载时再解除绑定。 若项目中经常有需要绑定原生事件的场景,每次都得手动绑定事件,然后再解绑事件。我可以自定义一个 hook,专门用来绑定和解绑事件。 一个自定义 hook,就定义好了。我们把上面的 window resize 事件用这个自定义的 hook 来实现下: 在 React 中写定时器,像上面绑定事件一样,一定要注意清除定时器,否则在组件刷新时会产生多个定时器。 一个简单的场景:验证码按钮倒计时 10s,倒计时期间禁用。 一个错误的使用方式: 尽管由于定时器的存在,组件始终会一直重新渲染,但定时器的回调函数是挂载期间定义的,所以它的闭包永远是对挂载时 Counter 作用域的引用,故 count 永远不会超过 10。 参考:如何实现一个定时器的 hook。 成功的实现方式有多种,我们来写一个相对比较好理解的一种: 我们是利用了 useState() 的传入 callback 的特点,可以把 count 的数据在 React 内部进行维护,规避掉闭包的问题。 但这是只有一个 useState() 时,若有多个 useState() 时,总不能用多层嵌套来实现吧? 再考虑一个比较复杂的定时器场景:九宫格的抽奖,点击中间的按钮后,选中边框绕着外层的 8 个图标开始顺时针旋转,慢慢提速直到最高速度,等接口返回结果后,再慢慢减速,最后停到中奖的位置。 这里面涉及到了多个 useState() 的操作: 可以看到,这个定时器是比较复杂的,而且涉及到多个 useState() 的操作。 然后我们来实现一个 useInterval 的自定义 hook,来实现定时器的操作,让调用者更加专注于业务。 上面的倒计时,我们用新定义的 useInterval() 来实现: 这个 useInterval() 的 hook,可以在 callback 中编写任意的逻辑;而且定时器的延迟时间也可以随时调整。 我们平时在 React 中请求数据时,很多场景都会这么写: 多个页面中都有类似的场景时,每次都要写多个 useState(),设置 loading 等。这些复用的功能可以抽离出一个数据请求的 hook。 我们先自己来实现一个简单的 hook,然后再稍微了解下开源组件的功能。 使用封装好的 useRequest() 这个 hook: 这里我们只是简单的封装了一下,把 loading, result 和 error 的情形封装了下,并没有考虑更多的实现。 使用: 该示例中,useSWR hook 接受一个字符串 key 和一个函数 fetcher。key 是数据的唯一标识符(通常是 API URL),并传递给 fetcher。fetcher 可以是任何返回数据的异步函数,你可以使用原生的 fetch 或 Axios 之类的工具。 跟我们上面实现的很像,但他的功能更多,包括但不限于: 比如 这里不做介绍了,只是告诉大家还有一个使用量比较高的库。各位可自行查阅相关文档。 上面都是单独介绍了一些自定义 hook,或者这个 npm 包仅是参与一种功能。这部分我介绍两个多个 hooks 的合集。 ahooks 是阿里出的一整套 hooks 的合集,这里面也有数据请求的 hook。 基本用法: 它也有很多的用法,只是跟 swr 的用法不一样而已: 除此之外,还有很多其他 hook,各位按照他的规范使用即可。 这是国外开发者维护的一个 hooks 仓库,地址:beautiful-react-hooks,目前 GitHub 上有 6.6k 的 stars。 我之前也给这个仓库贡献过代码: 我们这里以不同的视角讲解了如何进行自定义的 hook,各位在后续的开发过程中,也可以根据需要,引入这些 hook 包,或者自行实现。 出个小题,请实现一个 使用:1. React 自带的 hooks #
函数组件
和其他hooks
中使用;普通的 js 或 ts 文件无法调用的;2. 一个简单的自定义 hook #
const App = () => {
useEffect(() => {
// 为什么把回调单独提取出来?
const listener = () => {
const width =
document.documentElement.clientWidth || document.body.clientWidth;
const height =
document.documentElement.clientHeight || document.body.clientHeight;
console.log(width, height);
};
window.addEventListener("resize", listener);
return () => window.removeEventListener("resize", listener);
}, []);
};
const useEventListener = (eventName: string,
handler: (ev: Event) => void,
options?: any // options是配置,可以配置绑定的元素,是否只触发一次等) => {
const dom: Element = options?.target || window;
const handlerRef = useRef < any > null;
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
if (typeof handlerRef.current === "function") {
dom.addEventListener(eventName, handler);
return () => dom.removeEventListener(eventName, handler);
}
}, [dom, eventName, handler]);
};
const App = () => {
useEventListener("resize", () => {
const width =
document.documentElement.clientWidth || document.body.clientWidth;
const height =
document.documentElement.clientHeight || document.body.clientHeight;
console.log(width, height);
}); // 本身就是要绑定到window上的,这里可以不传要绑定的元素
};
3. 倒计时的 hook #
const App = () => {
useEffect(() => {
const timer = setInterval(() => {
console.log(Date.now());
}, 1000);
return () => clearInterval(timer);
}, []);
};
function App() {
const [count, setCount] = useState(10);
useEffect(() => {
const timer = setInterval(() => {
console.log("in setInterval", Date.now());
if (count <= 0) {
clearInterval(timer);
} else {
setCount(count - 1);
}
}, 1000);
return () => clearInterval(timer);
}, []);
return <div className="App">{count}div>;
}
function App() {
const [count, setCount] = useState(10);
useEffect(() => {
const timer = setInterval(() => {
setCount((n) => {
if (n <= 0) {
clearInterval(timer);
return 0;
} else {
return n - 1;
}
});
}, 1000);
return () => clearInterval(timer);
}, []);
return <div className="App">{count}div>;
}
/**
* 自定义的定时器hook
* @param callback 回调函数
* @param delay 延迟时间,若为null则表示停止定时器
* @see https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks
*/
const useInterval = (callback: () => void, delay: number | null): void => {
// 将 callback 放在 useRef() 中,方便随时获取到最新的回调函数
const savedCallback = useRef(callback);
// 没有依赖项,每次组件刷新时,都获取到最新的callback
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
/**
* 只有在 delay 不为 null 时才启动定时器,
* 而且这里添加了 delay 作为依赖项,每次 delay 发生变动时,
* 都会清除之前的定时器,然后启动新的定时器,方便延迟时间的调整
*/
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
const App = () => {
const [count, setCount] = useState(10);
useInterval(
() => {
setCount(count - 1);
},
count > 0 ? 1000 : null // 当count>0时正常倒计时,否则停止倒计时
);
};
4. 数据请求的 hook #
const App = () => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch("https://www.api.com")
.then((response) => response.json())
.then((res) => {
setLoading(false);
setResult(res);
})
.catch((err) => {
setLoading(false);
setError(err);
});
}, []);
};
4.1 自己来实现一个请求的 hook #
const useRequest = (request: () => Promise<any>) => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState<any>(null);
const aa = useCallback(async () => {
setLoading(true);
try {
const result = await request();
setLoading(false);
setResult(result);
} catch (err) {
setLoading(false);
setError(err);
}
}, [request]);
useEffect(() => {
aa();
}, [aa]);
return { loading, result, error };
};
const App = () => {
const { loading, result, error } = useRequest(() =>
fetch("https://www.api.com").then((response) => response.json())
);
console.log(loading, result, error);
return <div>{JSON.stringify(result)}div>;
};
4.2 开源 hook:swr #
import useSWR from "swr";
function Profile() {
const { data, error, isLoading } = useSWR("/api/user", fetcher);
if (error) return <div>failed to loaddiv>;
if (isLoading) return <div>loading...div>;
return <div>hello {data.name}!div>;
}
mutate(key)
,可以重新触发指定 key 的请求;mutate(key)
,可以在任意组件内来触发其他组件的数据更新。之前我们遇到过一个场景,简历的流转有多个阶段,每个阶段都有对应的简历数量;当我在某个组件内流转 1 个或者多个简历后,每个阶段对应的简历数量就需要更新。这里我们可以不用关心简历数量所在的组件和更新简历状态的组件,他们之间关系。只需要mutate
就可以触发。4.3 开源 hook:react-query #
5. 各种开源 hooks 合集 #
5.1 ahooks #
const { data, error, loading } = useRequest(getUsername);
5.2 beautiful-react-hooks #
5. 总结 #
useSwitch(defaultValue)
的 hook,可以传入初始值,然后返回两个参数[state, toggle]:
const [state, toggle] = useSwitch(true);
const handleClick = () => {
toggle();
};
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论