React Hooks 除了可以给函数式组件赋予状态和生命周期管理外,最重要的一点是可以把共用的逻辑处理代码抽离出来给不同的视图组件进行使用,它的目的和 HOC 和 Render Props 基本相同。这篇文章将以请求远程API数据为例,最后抽离出公共的数据请求的自定义 hook 以便不同的组件需要获取数据时,可以共用一套逻辑。
本文的适用读者为:
本文的完整代码和运行示例请点击下方按钮查看:
更完善的 useRequest Hook 可以参考我的开源项目:
Github: react-request-hook-client
假设我们有一个组件,需要同时加载 Posts (博客文章) 和 Todos (待办事项) 列表,每种展示5条标题,并根据加载状态显示loading
组件。首先我们看使用一般hooks的方式。
首先我们定义需要的 states:
// PostsAndTodos.js
const [posts, setPosts] = useState([]);
const [isPostsLoading, setIsPostsLoading] = useState();
const [todos, setTodos] = useState([]);
const [isTodosLoading, setIsTodosLoading] = useState();
posts
用来保存远程加载的文章数据,isPostsLoading
保存文章数据的加载状态todos
保存待办事项,isTodosLoading
保存待办事项数据的加载状态我们使用 useEffect
来请求 posts
数据:
// PostsAndTodos.js
useEffect(() => {
const loadPosts = async () => {
setIsPostsLoading(true);
try {
let response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=5"
);
let data = await response.json();
setPosts(data);
} catch (e) {
console.log(e);
}
setIsPostsLoading(false);
};
loadPosts();
}, []);
useEffect
里定义了 loadPosts()
函数,用来请求远程数据。loadPosts()
函数里,我们在请求开始,设置加载状态为true
,在结束时,无论是否出错,都把加载状态设置为false
。state
中。加载 todos
的代码和加载 posts
的代码基本相同,只是请求的 url
和更新状态的函数名不同:
// PostsAndTodos.js
useEffect(() => {
const loadTodos = async () => {
setIsTodosLoading(true);
try {
let response = await fetch(
"https://jsonplaceholder.typicode.com/todos?_limit=5"
);
let data = await response.json();
setTodos(data);
} catch (e) {
console.log(e);
} finally {
setIsTodosLoading(false);
}
};
loadTodos();
}, []);
我们这里简单的使用
和 展示
posts
和 todos
的列表:
<div>
<h1>Postsh1>
<ul>
{isPostsLoading ? (
<div>loading...div>
) : (
posts.map(post => <li key={post.id}>{post.title}li>)
)}
ul>
<h1>Todosh1>
<ul>
{isTodosLoading ? (
<div>loading...div>
) : (
todos.map(todo => <li key={todo.id}>{todo.title}li>)
)}
ul>
div>
这样做问题显而易见,我们把请求数据的逻辑基本上重复了两遍,唯一不同的就是请求的 url
,这样不符合可复用的代码设计规范,而且这个组件充斥着大量的业务逻辑和展示逻辑代码,十分臃肿,也违背了组件分离的设计原则,即负责展示的组件应当和业务处理的代码分离。那么如何进行优化呢?
使用 React 自定义 Hooks,我们可以把请求数据、更新加载状态、更新错误状态的代码全部写到自定义的 Hooks 中,然后在需要它的组件中调用即可,一般只需要一行代码。接下来我们就对上边的例子进行改造。
我们新建一个useRequest.js
的文件,然后定义一个同名的函数:
function useRequest(url) {
// 代码
// return ...
}
React 官方推荐自定义的 Hooks 使用 use
开头,因为这样的命名规范使得阅读代码的人知道使用它可以做什么事情。这个函数有如下特点:
接下来我们编写它的业务逻辑,其实在使用自定义 Hooks 多次之后就会发现,函数式组件里的第一行代码到 return
之前的代码都可以直接放到自定义 Hooks 里边,因为它们都属于业务代码,而 return
中的代码负责展示逻辑。
我们复制加载 posts
或者 todos
的代码到 useRequest
的函数中,然后稍加改动:
// useRequest.js
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState();
const [error, setError] = useState();
useEffect(() => {
const loadData = async () => {
setIsLoading(true);
try {
let response = await fetch(url);
let data = await response.json();
setData(data);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
return [data, isLoading, error];
这里我们把状态和数据的命名改为了更通用的 data
, isLoading
, 和 error
,因为我们不限制特定 domain,任何需要加载数据的组件都可以使用此 hook。在函数的最后我们返回了 data
, isLoading
, error
这三个状态供调用此 hook 的组件使用,这里返回值的类型没有要求,如果返回值比较多,也可以返回一个对象,如:
return {data, isLoading, error}
现在我们再看看加载数据的代码,在新的 PostsAndTodosWithHooks.js
文件中,我们只需要在组件里调用 useRequest
hook 即可进行数据抓取:
// PostsAndTodosWithHooks.js
const [posts, isPostsLoading] = useRequest(
"https://jsonplaceholder.typicode.com/posts?_limit=5"
);
const [todos, isTodosLoading] = useRequest(
"https://jsonplaceholder.typicode.com/todos?_limit=5"
);
useRequest
hook 需要一个 url
参数,即请求的数据的地址。data
和 isLoading
起了别名。useRequest
hook中的 data
和 isLoading
更新时,PostsAndTodos
组件中用到这两个状态的值也会同步更新。运行代码,效果和之前一致,但是代码简洁很多,hook 和展示组件各司其职,使得业务逻辑和视图的代码都变得清晰易读。而且复用业务逻辑,也使得代码量大大减少,此例则直接减少了50%。
可能会有人好奇这两次调用同一个 Hook 状态不会冲突吗?如果 posts
先加载完,那么 todos
会不会在加载完成之后覆盖 posts
的数据?答案是不会。不同的 hook 的调用,所有的状态都是隔离的,即使是同一 hook 调用了多次,它们也都各自维护自己的状态,互不影响。当 hook 中的状态更新时,会同步到调用它的组件中。
大家可以在本示例的基础上进行扩展,比如加上请求方式、请求头、请求参数之类的变量,还可以加上缓存数据,停止请求或重新触发请求的代码,另外像组件的事件处理(如分页)都可定义到自定义的 hooks 中,然后所有需要(如分页)的组件都可以使用它完成相应的功能。
React 自定义 Hooks 提供了极其重要的功能,可以让业务逻辑从展示组件中抽离出来,并可以多次复用,极大的减少了代码量,提高了效率。掌握它会对前端开发有巨大的帮助,在这里回顾一下它的特点和注意事项:
use
开头你学会了吗?有问题欢迎留言。