如何使用 React 钩子处理/链接依赖于另一个的同步副作用
Posted
技术标签:
【中文标题】如何使用 React 钩子处理/链接依赖于另一个的同步副作用【英文标题】:How to handle/chain synchronous side effects that depend on another with React hooks 【发布时间】:2019-08-28 05:48:30 【问题描述】:我正在尝试将我的应用程序从 redux 重写为新的上下文 + 挂钩,但不幸的是,我很难找到一种好方法来处理依赖于前一个响应的一系列同步副作用。
在我当前的 redux 应用程序中,我大量使用了通常通过 redux-saga 或 thunk 处理的同步/链式操作和 API 请求。因此,当返回第一个 API 请求的响应时,该数据将用于下一个 API 请求等。
我已经制作了一个自定义钩子“useFetch”(在这个例子中它没有做太多,因为它是一个简化版本,我还必须对其进行一些小调整才能在代码和框上工作 - 请参见下面的代码)。问题在于,由于“钩子规则”,我不能在 useEffect 钩子中使用自定义钩子。那么,如果您有自己的获取数据的钩子,如何在执行下一个请求之前等待第一个请求的响应?即使我最终放弃了 useFetch 抽象并创建了一个普通的 fetch 请求,如何避免最终导致许多 useEffects 钩子的臃肿混乱?这可以做得更优雅一点,还是 context + hooks 与 redux saga/thunk 竞争处理副作用还为时过早?
下面的示例代码非常简单。它应该尝试模拟的是:
-
查询人员 api 端点以获取人员
一旦我们有了这个人
响应,查询工作端点(使用现实世界中的人员 ID
情景)
一旦我们有了人员和工作,根据响应
从人员和工作端点,查询同事端点
找到从事特定工作的同事。
这是代码。添加延迟到 useFetch 挂钩以模拟现实世界中的延迟:
import React, useEffect, useState from "react";
import render from "react-dom";
import "./styles.css";
const useFetch = (url, delay = 0) =>
const [data, setData] = useState(null);
useEffect(() =>
const fetchData = async () =>
// const result = await fetch(url,
// method: "GET",
// headers: "Content-Type": "application/json"
// );
//const response = await result.json();
const response = await import(url);
setTimeout(function()
setData(response);
, delay);
;
fetchData();
, [url]);
return data;
;
function App()
const [person, setPerson] = useState();
const [job, setJob] = useState();
const [collegues, setCollegues] = useState();
// first we fetch the person /api/person based on the jwt most likely
const personData = useFetch("./person.json", 5000);
// now that we have the person data, we use the id to query for the
// persons job /api/person/1/jobs
const jobData = useFetch("./job.json", 3000);
// now we can query for a persons collegues at job x /api/person/1/job/1/collegues
const colleguesData = useFetch("./collegues.json", 1000);
console.log(personData);
console.log(jobData);
console.log(colleguesData);
// useEffect(() =>
// setPerson(useFetch("./person.json", 5000));
// , []);
// useEffect(() =>
// setJob(useFetch("./job.json", 3000));
// , [person]);
// useEffect(() =>
// setCollegues(useFetch("./collegues.json",1000));
// , [job]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
const rootElement = document.getElementById("root");
render(<App />, rootElement);
运行示例:https://codesandbox.io/s/2v44lron3n?fontsize=14(您可能需要进行更改 - 空格或删除分号 - 使其工作)
希望这样的事情(或更好的解决方案)是可能的,否则我将无法从很棒的 redux-saga/thunks 迁移到 context + hooks。
最佳答案: https://www.youtube.com/watch?v=y55rLsSNUiM
【问题讨论】:
【参考方案1】:Hooks 不会取代您处理异步操作的方式,它们只是对您习惯做的一些事情的抽象,例如调用 componentDidMount
,或处理 state
等。
在您给出的示例中,您实际上并不需要自定义钩子:
function App()
const [data, setData] = useState(null);
useEffect(() =>
const fetchData = async () =>
const job = await import("./job.json");
const collegues = await import("./collegues.json");
const person = await import("./person.json");
setData(
job,
collegues,
person
)
;
fetchData()
, []);
return <div className="App">JSON.stringify(data)</div>;
话虽如此,也许如果您提供一个实际的 redux-saga 或 thunk 代码示例,并且您想要重构,我们可以看到完成该操作的步骤。
编辑:
话虽如此,如果你还想做这样的事情,你可以看看这个:
https://github.com/dai-shi/react-hooks-async
import React from 'react';
import useFetch from 'react-hooks-async/dist/use-async-task-fetch';
const UserInfo = ( id ) =>
const url = `https://reqres.in/api/users/$id?delay=1`;
const pending, error, result, abort = useFetch(url);
if (pending) return <div>Loading...<button onClick=abort>Abort</button></div>;
if (error) return <div>Error:error.name' 'error.message</div>;
if (!result) return <div>No result</div>;
return <div>First Name:result.data.first_name</div>;
;
const App = () => (
<div>
<UserInfo id='1' />
<UserInfo id='2' />
</div>
);
编辑
这是一个有趣的方法https://swr.now.sh/#dependent-fetching
【讨论】:
嗨卢西亚诺。谢谢。然而,我确实声明我的 .json 文件只是这个示例的替代品,以便在代码和框中编译它 - 所以你不应该依赖导入而不是 fetch。 Fetch 确实是必要的,因为在运行的代码框示例之外,它将是一个 api 端点/url,而不是一个 json 文件。此外,我试图用 useFetch 说明的是抽象一些可重用的代码来告诉一些关于请求状态的信息,例如 isLoading 和 isError。在 useEffect 中使用许多原始提取并将它们全部放入一个大意大利面条 useEffect 不是一个好的选择 我想说的是fetchData
函数可以调用您需要的不同api,您可以处理您的数据并最终将其设置为您想要的状态。这与做dispatch('ASYNC_ACTION',...)
并从传奇中获取它是一样的。使用钩子+上下文时这不会改变。您只需要创建一个异步函数并调用它。链式异步操作就是这样,无论是从 json 导入还是调用真正的 api。
如果您对这种方法不满意,您可以尝试 redux + hooks 的早期实现,并且您可以调度您的异步操作并使用 thunk 或 saga 以与您相同的方式处理它们前。这是一个早期实现github.com/facebookincubator/redux-react-hook 的示例,但正如您所说,您需要钩子+上下文,真正唯一的区别是,您将调用一个函数并等待它解决,而不是调度一个动作,然后相应地设置状态.链接它们就像链接任何承诺
最后,很快你就会看到 react-redux 添加了一个userRedux
钩子,他们必须重构很多东西才能解决这个问题。所以也许,你甚至根本不需要替换 redux:github.com/reduxjs/react-redux/releases/tag/v7.0.1
谢谢,但实际上我认为我找到了最好的答案:youtube.com/watch?v=y55rLsSNUiM【参考方案2】:
这是现实生活中的常见场景,您希望等待第一次提取完成,然后再进行下一次提取。
请查看新的代码框: https://codesandbox.io/s/p92ylrymkj
当您获取请求时,我使用了生成器。以正确的顺序检索数据。点击获取数据按钮后,进入控制台查看。
希望这就是你要找的。p>
【讨论】:
非常感谢,非常好。我在接受你的回答或 Doğancan 的回答之间犹豫不决。你的伟大之处在于生成器和一个正在运行的示例。但是如果你在组件渲染中使用钩子而不是function* steps
,那就太棒了。我不确定在没有一些抽象层(例如 redux-saga(takeLatest 用于取消等)的情况下,使用原始生成器而不是 async/await 是否真的会受益,除非你是他们的铁杆,但我确实找到了github.com/neurosnap/use-cofx -作为一个显然了解生成器的人,您对 cofx/useCofx 有意见吗?【参考方案3】:
我相信您会遇到这个问题,因为 fetch 钩子的实现很差,而且这个例子太基础了,我们无法理解。如果您将在同一个组件中使用工作、同事和人员,您应该明确说明。如果是这样,将它们分开总是更明智的做法。
话虽如此,让我举一个我自己的 fetch hook 的例子:
const loading, error, value = useResource<Person>(getPerson, personId)
我有这样一个钩子,它有自己的加载状态、错误、值等。 它需要两个参数: - 获取方法 - 获取方法参数
拥有这样的结构,您可以将资源链接在一起。 useResource 实现只是创建一个 state 并在 useEffect 中,检查属性是否更改等。如果更改,它会调用 fetch 方法。
【讨论】:
我猜你可能是对的,如果它可以将第二个参数从你的钩子参数传递到其中的 useEffect 钩子 - 所以personId
是useEffect
的第二个参数在useResource
里面,比如useEffect(() => ,[personId])
。没有检查这是否有效,但如果我理解你的话,那是你的建议,对吧?
不是这样的。每当更新组件时都会执行 useEffect。这意味着,personId 可能在任何执行中都已更改。在常规反应组件中,您将在 componentDidUpdate 方法中做什么?您将检查是否已经具有 personId = 1 的状态值(示例)。如果你没有它,你会为 id 1 获取它。在下一次迭代中,假设你有 personId = 2。相同的检查将导致另一个获取。这个逻辑非常通用,可以写在你的 fetch 钩子(useResource)中。
最后会变成这样:useEffect(() => if (fetchProperty !== oldFetchProperty) fetch() )。当然,使用 JSON.stringify 或标识符键进行更好的等号检查。以上是关于如何使用 React 钩子处理/链接依赖于另一个的同步副作用的主要内容,如果未能解决你的问题,请参考以下文章
给定两个 Linux 静态库,如何判断一个是不是依赖于另一个?
如何在 useEffect 中使用 setTimeout 重置 React 钩子状态