最近,我已经写了关于使用Hooks和Render Props获取数据的文章,那篇文章的目的是获取和展示数据。这篇文章中,我将重看并用实例拓展那边文章,展示怎么使用相同的概念来执行其他类型的Action(比如,POST数据)。
我们怎么提炼出数据Action?
无论你正在获取、更新、新增、删除 或 执行自定义Action,这些操作都有一个共同点。它们都经历三个阶段 —— 加载、成功和失败。这三个阶段组成了UI开发的部分。我们做的任何操作(有时都不是一个API Action),都有这三个状态。
现在,让我们思考这种接口。我们的接口接受一个异步的Action(在我们这,它是一个函数)并且返回三个状态的方式来执行这个Action。
我要说的是,只是使用Hook
或一个组件是不够的。这个Action必须遵守提供的接口,这样我们的逻辑才能正常工作。要记住何时提炼各种逻辑,是很重要的一点。我们应该在已经提炼的想法的所有部分上,建立接口/协议。
使用Hooks来构建异步Action功能
Hooks
提供了一种方式,让我们将自定义的,复用的功能整合到我们的组件中。我们曾为视觉逻辑
复用组件,现在我们也可以使用hooks
来复用业务逻辑
。在写整个实现之前,我们先看一下我们怎么使用它:
import React from 'react';
import { useAction } from './hooks';
import { saveProfile } from './api';
function App() {
// 第二个元素只是使用数组解构的一个变量
const [state, perform] = useAction(saveProfile);
// 我们将数据当做参数发送到执行函数中
// 在我们的例子中,=== "updateProfile"
async function handleSuccess(e) {
e.preventDefault();
await perform({ age: "12456" });
}
async function handleError(e) {
e.preventDefault();
await perform({ age: "test" });
}
// 从hook获取的功能结果的辅助函数
// 这个函数中的测试值是从hook函数中返回出来的
function renderStatus() {
if (state.loading) return "Loading...";
if (state.error) return `Error: ${state.error.message}`;
return `Data: ${JSON.stringify(state.data)}`;
}
return (
<div className="App">
<button onClick={handleSuccess}>Success!</button>
<button onClick={handleError}>Error!</button>
<div className="status">{renderStatus()}</div>
</div>
);
}
现在再看功能的实现:
// hooks.js
import { useState } from 'react';
export const useAction = (action) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
// 传进hook来的“action”参数没有被执行,它只是被存在这个函数的作用于内
// 这样,当使用下面这个函数执行这个“action”时,我们就可以使用它了
// 这个函数会被当做返回值数组的第二个元素
const performAction = async (body = null) => {
try {
setLoading(true);
setData(null);
setError(null);
const data = await action(body);
setData(data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
}
return [{ loading, data, error }, performAction];
}
这个简单的hook给我们让当前state的Action执行,且用一个函数来执行这个Action。body
参数在执行Action
函数中,因为非GET
请求通常有一个请求body
。在上面的例子中,如果我们调用执行函数
,loading
、data
和error
的state
将由hook
设置。然后,我们就可以很简单地使用这些state
来完成视觉逻辑(显示加载条,显示错误信息,或显示成功的操作)的功能。
就像你看到的,我们正使用try/catch/finally
块来决定我们是成功还是失败。这里使用的try catch
并不是我们知道的编程中的“真”的try catch
。它们是promise的resolve/reject
被封装到了一个异步的块中。前面我已经提到了,Action必须遵守hook
提供的接口来统一化和简单化开发体验。在我们的场景下,这个接口是成功的数据必须被return
出去,error
必须被thrown
。下面是一个API调用代码的例子:
// api.js
export const updateProfile = async (body) => {
const response = await fetch(SOME_URL_HERE, {
method: 'PUT',
body,
headers: {
'Content-Type': 'application/json'
}
}
const data = await response.json();
if (!response.ok) {
throw new Error(data);
}
return data;
}
通常,fetch
会为非API相关的异常抛出异常(网络掉线,JSON验证错误等)。然而,我们还为API逻辑抛出异常(bad request,接口404等)。这允许我们为API上的异常用相同的方式(存储在state上)显示处理异常,并使得处理Action逻辑更简单。如果你想从API异常中区分出fetch
异常,你可以创建一个单独的异常class(比如ApiError),并将其抛出,而非一般的JS错误。
就是这样。现在,得益于Hooks
,我们可以使用一个简单且强大的接口来执行异步数据相关的Action。
Demo
我已经写了一个简单的demo来表达它是怎么工作的。这个demo和代码示例之间唯一的区别是,没有真正调用一个真实的fetch
请求,我模拟了一个API请求。
示例地址:https://xopqqqxx3o.codesandbox.io/
结论
这篇文章的要点,是推广数据获取的方式来执行任何类型的异步Action。自定义hooks
使得构建自定义和复用的功能贴上任何组件。这种非冲突的行为给我们一个安全的方式来将业务逻辑从视觉逻辑中区分开来,而不需要再创建一个组件。