随着练习功能的增多,我们编写的React代码变得越来越复杂。像是上节课中我编写的React代码,仅仅是增加了一个加载数据的功能,我们就需要向App.js
中引入了三个state和一个钩子函数:
const [stuData, setStuData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null);\ useEffect(()=>{ const fetchData = async () => { try{ setLoading(true); setError(null); const res = await fetch('http://localhost:1337/api/students'); if(res.ok){ const data = await res.json(); setStuData(data.data); }else{ throw new Error('数据加载失败!'); } }catch (e){ setError(e); }finally { setLoading(false); } }; fetchData(); }, []);
随着这种代码的增多App.js
中的代码会变得越来越多,难以维护。所以我们就迫切的需要一个东西可以将这些代码存储起来,一来可以降低单个文件中的代码数量,二来也可以让这些代码方便在多个组件中复用。
但是问题就来了,这些代码无论是state还是effect其实都是钩子函数,但是钩子函数又不是说随便在哪都能写的,这要怎么处理呢?
我们在刚刚介绍钩子函数时就说过,钩子函数只能运行在函数组件或自定义钩子中,所以要提取这些代码我们只有一个选择,那就是自定义钩子。
自定义钩子是个什么玩意?它其实一点也不神秘,自定钩子就是一个普通的函数。普通函数怎么定义,它就怎么定义。但是它又不那么普通,因为钩子函数的名字必须以use开头,使用use开头后,React就能自动识别出它是一个钩子函数,这样才会以钩子函数的方式去处理它。
使用钩子
- 创建一个函数,命名为useXxx
- 在函数中正常调用React中的各种钩子
- 在组件中引用钩子
这个步骤看着是不是特别草率,但事实其实就这么一回事。当函数使用use开头后,React就允许我们在其中调用React的钩子函数,所以我们就可以通过自定义的钩子函数,将组件中的涉及到钩子的代码封装起来,方便调用。
将上述代码修改为自定义钩子:
src/hooks/useFetch.js
import {useEffect, useState} from "react"; const useFetch = (url) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function fetchData(){ try{ setLoading(true); setError(null); const res = await fetch('http://localhost:1337/api/students'); if(!res.ok){ throw new Error('数据加载失败!'); } const data = await res.json(); setData(data.data); }catch (e){ setError(e); }finally { setLoading(false); } } return {data, loading, error, fetchData}; }; export default useFetch;
App.js调用钩子
const {data:stuData, loading, error, fetchData} = useFetch(); useEffect(()=>{ fetchData(); }, [])
这样一来,将App.js中发送请求相关的钩子都编写到useFetch中,并将App中会用到的变量作为返回值返回,而作为App来说,只需要调用useFetch,即可获取到stuData、loading、error等数据以及fetchData函数,这样一来大大简化了App中的代码,同时使得其他组件也可以通过useFetch来发送请求加载数据。
等等,好像还有个问题,我们将请求地址在useFetch中写死了,难道我们只向一个地址发请求吗?这样做不合适吧?当然不合适,我们可以将钩子中那些会发生的变化的值作为参数传递,比如请求地址,如此一来我们就可以向任意地址发送请求啦!但是这里我我就不写了,自己试一试吧!
超哥,有些地方没明白。
如果我在两个不同的组件中,都调用了useFetch(),那么这两个组件中拿到的loading是同一个loading吗?我自己的试验,好像拿到的是同一个loading.
那么基于上面的结果,如果我在同一个组件中调用两次useFetch(),那么拿到的data应该是相同的变量.那么第二次api请求的结果就会覆盖第一次请求的结果.
所以这样封装不就会出问题了吗?
不是一个,每次调用都会创建一个,函数作用域嘛,不出意外的话应该是你的测试有问题,或是哪里理解的不对。
使用自定义钩子中传过来的函数造成组件重新渲染,执行到const {data:stuData, loading, error, fetchData} = useFetch();这一步时,数据会不会重新被覆盖,被覆盖那每次执行都得到一样的值,但是实际上每次数据都会变,那么const {data:stuData, loading, error, fetchData} = useFetch();是不是只有最开始渲染的时候才会执行一次,后面的更新不会执行。还有一个问题就是组件为什么可以通过传过来的函数对自定义钩子函数中的数据进行操作,然后再引起组件本身的重新渲染,这是react帮我们做的吗?
是的,就和state一样,第一次是初始值,其他时候就是获取当前值