React为我们提供了灵活性,使我们可以选择如何解决应用程序中的问题(如状态、网络和风格管理)。一个伟大的代码库有问题点,并以标准和一致的可重复模式来解决。
而且,作为前端工程师,正确地将网络状态的变化信息转达给用户是至关重要的,因为我们建立的大多数应用程序需要与一个或多个服务器进行交互。我们可以通过使用自定义React Hooks来实现这些目标。
在这篇文章中,我将介绍网络请求存在的各种状态,并告诉你如何在自定义Hooks中保持请求管理。我还会指导你建立一个采用这些Hooks的小程序。
什么是网络请求?
一个网络请求通常存在于这些状态中。
idleloading/processing/in-flightsuccesserror
idle 网络请求状态是网络请求的默认(和结束)阶段。在loading 阶段,客户端等待服务器的确认和数据包,然后过渡到success 或error 状态。
网络状态的转换。
使用自定义React Hooks对网络请求进行本地化
为了保持网络请求的可测试性并与业务逻辑解耦,最好用自定义Hooks来管理请求。这可以保持你的代码精简,并使你容易执行特殊的一次性操作,如对网络响应进行数据转换。
例如,一个获取博客文章列表的请求可以保存在一个usePostsQuery 自定义Hook中,就像下面这个。
import { useState, useEffect } from 'react'
const api = {
GET: async (url) => {
const response = await fetch(url);
const data = await response.json();
return data;
}
}
export default function usePostsQuery() {
const [error, setError] = useState()
const [status, setStatus] = useState('idle')
const [data, setData] =useState()
const startFetch = async () => {
try {
let data = await api.GET('/posts')
setError()
setStatus('success')
setData(data)
} catch (error) {
setError(error)
setStatus('error')
}
}
useEffect(() => {
startFetch()
}, []);
return {
data,
error,
isSuccess: status === 'success',
isError: status === 'error'
refetch: startFetch
}
}
通过利用React Query(我的首选工具),这个Hook可以变得更加简洁。
import { useQuery } from "react-query";
export default function usePostsQuery() {
return useQuery("posts", () =>
api.GET("/posts")
);
}
创建项目
让我们建立一个名为Betflix 的小应用,你既可以访问也可以克隆它。这个应用将允许朋友们从一组赛程中选择体育队并进行预测。
注意:为了简洁起见,我将跳过解释这个概念验证中使用的比较平凡的组件。欢迎你来探索这个的全部代码。
首先,我们将创建一个新的React项目并启动开发服务器。
npx create-react-app betflix
cd betflix
npm start
我们需要安装HTTP请求的依赖项、无服务器函数代理和管理数据库(用于保存固定装置和其他记录)。
npm install react-query react-toast-notifications http-proxy-middleware @supabase/supabase-js --save
我还会把Netlify Lambda和CLI作为开发依赖项。
npm install netlify-lambda netlify -D
当我们完成的时候,你应该有一个像下面这样的目录结构。
用自定义的React Hooks显示固定装置列表
我们将更新<App /> 组件,以显示从无服务器的后端获取的固定装置列表。我们将按顺序创建和处理对赌注列表和固定设备列表的请求。
import "./App.css";
import Fixture from "./components/Fixture";
import Loader from "./components/Loader";
import useBetsQuery from "./hooks/queries/useBetsQuery";
import useFixturesQuery from "./hooks/queries/useFixturesQuery";
function App() {
const { data, isLoading, isError } = useFixturesQuery();
const {
data: bets,
isLoading: betsLoading,
isError: betsErrored,
} = useBetsQuery();
if (isLoading || betsLoading) return <Loader />;
if (isError || betsErrored)
return <p>We encountered an error fetching data</p>;
const sortFixtures = (fixtureA, fixtureB) => {
return (
bets.hasOwnProperty(fixtureB.fixture.id) -
bets.hasOwnProperty(fixtureA.fixture.id) ||
fixtureB.fixture.status.elapsed - fixtureA.fixture.status.elapsed
);
};
return (
<div className="App">
<header className="App-header">
<h1 className="App-header__title">Upcoming fixtures</h1>
</header>
<section className="Fixtures">
{data.results.response.length ? (
<>
{data.results.response
.sort(sortFixtures)
.map(({ fixture, teams: { away, home } }) => (
<Fixture
key={fixture.id}
fixture={fixture}
away={away}
home={home}
isBetPlaced={bets.hasOwnProperty(fixture.id)}
defaultSelectedTeam={bets[fixture.id].choice}
defaultAmount={bets[fixture.id].amount}
/>
))}
</>
) : (
<div>No fixtures at the moment</div>
)}
</section>
</div>
);
}
export default App;
从上面的代码中,我们已经声明了对useBetsQuery 和useFixturesQuery 的依赖,所以我们现在要定义它们。
useBetsQuery 是一个自定义的Hook,用于获取投注列表并将其转换为一个键入对象的映射,我们可以用它来跟踪一个固定装置的投注状态。
让我们在/src/hooks/queries 中创建useBetsQuery.js ,并更新它。
import { useQuery } from "react-query";
const ENDPOINT = "/.netlify/functions/fetchBets";
// Normalize the bets payload into a keyed map with `fixture_id` as the key
function normalizeBets(betsList) {
return betsList.reduce(
(acc, curr) => ({
...acc,
[curr.fixture_id]: curr,
}),
{}
);
}
// Because we'll use the fetch API (instead of Axios), we need to explicitly return a
// Promise when an error occurs so React Query can change the status.
const getBets = async (url) => {
const response = await fetch(url);
const data = await response.json();
if (response.ok) {
return normalizeBets(data.results);
}
return Promise.reject(new Error(data.message));
};
export default function useBetsQuery() {
return useQuery("bets", () => getBets(ENDPOINT));
}
完成这些后,我们还需要创建我们要获取的自定义Hook。在src/hooks/queries 中创建useFixturesQuery.js 钩子,并添加下面的代码。
import { useQuery } from "react-query";
const getFixtures = async (url) => {
const response = await fetch(url);
const data = await response.json();
return data;
};
export default function useFixturesQuery() {
return useQuery("fixtures", () =>
getFixtures("/.netlify/functions/fetchFixtures")
);
}
我们现在准备定义显示单个灯具信息的组件。
创建<Fixture /> 组件
我们将在src/components/Fixture.js 中创建<Fixture/> 组件,显示主队和客队的信息。我们还引入了两个新的React Hooks,即useMutationNotification 和usePlaceBetMutation Hooks。
useMutationNotification 是一个有趣的自定义Hook,它允许我们以可预测的符合人体工程学的方式处理网络状态变化,因此我们可以直接对用户发起的行动提供反馈。
import { useEffect, useState } from "react";
import { useToasts } from "react-toast-notifications";
import { ReactComponent as ArrowLeft } from "../assets/svg/arrowLeft.svg";
import { ReactComponent as ChevronRight } from "../assets/svg/chevronRight.svg";
import TeamCard from "./TeamCard";
import FormInput from "./FormInput";
import useMutationNotification from "../hooks/useMutationNotification";
import usePlaceBetMutation from "../hooks/queries/usePlaceBetMutation";
import Loader from "./Loader";
function Fixture({
fixture,
away,
home,
isBetPlaced,
defaultAmount,
defaultSelectedTeam,
}) {
const [amount, setAmount] = useState(defaultAmount || 0);
const [selectedTeam, setSelectedTeam] = useState(defaultSelectedTeam);
const [betPlaced, setBetPlaced] = useState(isBetPlaced);
const { addToast } = useToasts();
const [doPlaceBetRequest, placeBetState] = usePlaceBetMutation();
useMutationNotification({
...placeBetState,
useServerMessage: false,
entity: "bet",
actionType: "place",
});
useEffect(() => {
if (placeBetState.isSuccess) setBetPlaced(true);
}, [placeBetState.isSuccess]);
const teams = {
away,
home,
};
const status = !fixture.status.elapsed ? "Up next" : "In progress";
const doAmountUpdate = (e) => setAmount(e.target.value);
const doTeamUpdate = (team) => {
if (betPlaced) return;
setSelectedTeam(team);
};
const doPlaceBet = () => {
if (!selectedTeam || amount <= 0) {
addToast("Please select a team and add an amount", {
appearance: "info",
autoDismiss: true,
});
return;
}
doPlaceBetRequest({
amount,
choice: selectedTeam,
fixture_id: fixture.id,
});
};
return (
<div className="Fixture">
<section className="Fixture__teams">
<TeamCard
name={home.name}
logo={home.logo}
id={home.id}
type={"home"}
selected={selectedTeam === "home"}
onTeamChange={doTeamUpdate}
/>
<div className="Fixture__separator">vs</div>
<TeamCard
name={away.name}
logo={away.logo}
id={away.id}
type={"away"}
selected={selectedTeam === "away"}
onTeamChange={doTeamUpdate}
/>
</section>
{!betPlaced ? (
<>
<section className="Fixture__controls">
<div className="Fixture__control">
<FormInput
label={"Amount"}
name={`amount-${fixture.id}`}
type="number"
value={amount}
onChange={doAmountUpdate}
/>
</div>
<div className="Fixture__controls__separator">
<ArrowLeft />
</div>
<div className="Fixture__control">
<FormInput
label={"Potential Winnings"}
name={`potential-winnings-${fixture.id}`}
value={amount * 2 || 0}
disabled
/>
</div>
</section>
<section className="Fixture__footer">
<div className="Fixture__status">
<span className="Fixture__status__dot"></span>
{status}
</div>
{!placeBetState.isLoading ? (
<button className="Button" onClick={doPlaceBet}>
Place bet <ChevronRight />
</button>
) : (
<Loader />
)}
</section>
</>
) : (
<section className="Fixture__controls">
<p>
You placed a <b>${amount}</b> bet on{" "}
<b className="u-text-primary">{teams[selectedTeam]?.name}</b> to
potentially win <b className="u-text-primary">${amount * 2}</b>
</p>
</section>
)}
</div>
);
}
Fixture.defaultProps = {
isBetPlaced: false,
};
export default Fixture;
在上面的代码中,我们声明了对一些Hooks的依赖性。
useMutationNotification 将接受网络请求状态选项(isError 和isSuccess),并允许我们显示来自服务器的错误信息(如果我们将useServerMessage 设置为true ),或者将entity 和actionType 字符串传入,以向用户提供一个通用信息。
让我们在src/hooks 中创建useMutationNotification.js ,并用下面的代码来更新它。
import { useEffect, useState } from "react";
import useShowToast from "./useShowToast";
function capFirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function useMutationNotification({
isError,
isSuccess,
actionType = "create",
entity,
data,
error,
useServerMessage = true,
}) {
const [notificationConfig, setNotificationConfig] = useState(null);
const showToast = useShowToast();
useEffect(() => {
if (isError) {
setNotificationConfig({
type: "error",
message: useServerMessage
? error.message
: `${entity} could not be ${actionType}d`,
});
}
}, [
useServerMessage,
isError,
setNotificationConfig,
entity,
actionType,
error,
]);
useEffect(() => {
if (isSuccess) {
setNotificationConfig({
type: "success",
message: useServerMessage
? data.message
: `${entity} successfully ${actionType}d`,
});
}
}, [
useServerMessage,
isSuccess,
setNotificationConfig,
entity,
actionType,
data,
]);
useEffect(() => {
if (notificationConfig) {
const { type, message } = notificationConfig;
showToast({ type, message: capFirst(message) });
}
}, [notificationConfig, showToast]);
}
export default useMutationNotification;
然后我们将定义我们打算在下注时使用的usePlaceBet 变异。我们将返回突变动作和它的状态。在src/hooks/queries 中创建usePlaceBetMutation ,并将其更新为下面的代码。
import { useMutation } from "react-query";
const ENDPOINT = "/.netlify/functions/placeBet";
export default function usePlaceBetMutation() {
const request = async (payload) => {
const res = await fetch(ENDPOINT, {
method: "POST",
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) return Promise.reject(new Error(data.message));
return data;
};
const { mutate, ...mutationState } = useMutation(request);
return [mutate, mutationState];
}
有了这些更新,我们现在可以用简单、易读的方式处理突变的网络状态变化。
结论
对网络状态变化做出反应可能是一种挑战,但它也是为用户提供更有意义的体验的一个巨大机会。
你可以查看React Query文档,了解更多关于在构建React应用时为用户增强网络状态体验的信息。你可以在GitHub上找到这个演示概念验证的完整源代码。
The postImprove async network state handling with custom React Hooksappeared first onLogRocket Blog.