使用 Fetch API 实现 React Suspense
别废话,把包裹给我!💰
悬念是如何产生的?🔮
这如何应用于取钩?🎣
哎呀,这难题该怎么解决呢?🤔
听起来像是内存泄漏💧
结论🍬
Dan Abramov 回复了一位 React 开发者关于 Suspense 为什么没有响应 fetch API 的问题:
从传奇人物 Dan Abramov 本人那里,我们听到了诸如“目前还没有与 React Suspense 兼容的数据获取解决方案”、“React Cache 将是第一个”以及“Suspense 仅限于代码拆分”之类的金句。
如果我除了对丹尼尔·“阿布拉·卡达布拉”·阿布拉莫夫的作品印象深刻之外,还有一件事要对他说,那就是:
让我们揭开 React Suspense 背后的奥秘。为了便于理解,我将介绍我是如何创建这个包的。
别废话,把包裹给我!💰
如果你只是来寻找解决方案,我不会责怪你。你可以fetch-suspense在 NPM 上找到所需的一切,而最全面的文档则可以在GitHub 代码库中找到。
import useFetch from 'fetch-suspense';
const MyComponent = () => {
// "Look! In the example! It's a fetch() request! It's a hook!"
// "No! It's kind of like both at the same time."
const serverResponse = useFetch('/path/to/api', { method: 'POST' });
// The return value is the body of the server's response.
return <div>{serverResponse}</div>;
};
悬念是如何产生的?🔮
许多新的 React 功能都内置到 React 库中,而不是作为外部包,这是因为与驱动 React 的引擎(称为React Fiber)紧密耦合可以带来性能优势。
由于 React Fiber 与 Suspense 和 hooks 等功能直接集成,因此在 React 16.5 中无法完全复制 Suspense 的功能。不过,您可以创建一个性能稍逊的 polyfill。我将使用一些 polyfill 示例,以便您更好地理解 Suspense 的工作原理。
class Suspense extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null
};
}
componentDidCatch(e) {
this.setState({ error: e });
}
render() {
if (this.state.error) {
return this.props.fallback;
}
return this.props.children;
}
}
/*
<Suspense fallback={<Loading />}>
<ErrorThrower />
</Suspense>
*/
这就是古老的类组件:React 开发早期时代的遗迹。该componentDidCatch方法会在子组件抛出错误时触发。这允许你用友好的用户界面替换未捕获的 JavaScript 错误,或者在应用程序出错时实现其他重要的逻辑。
上述操作会挂载 Suspense 组件。由于本地状态没有错误,Suspense 的子组件也会被挂载。在这种情况下,组件<ErrorThrower />被挂载后会抛出错误。
该错误会向上冒泡到 Suspense 实例,并由该componentDidCatch方法接收。该方法通过将错误保存到其状态来处理该错误,从而导致重新渲染。
由于本地状态出现错误,它现在渲染失败,因此不再渲染其 children 属性,也不再渲染其<ErrorThrower />子元素。取而代之的是,它渲染了fallback我们已设置为模态<Loading />框的 prop。
这就是 Suspense 现在的工作原理,只不过它不再抛出错误,而是抛出JavaScript Promise 。当 Suspense 捕获到 Promise 时,它会重新渲染页面,显示回退属性,而不是之前抛出 Promise 的子元素。当 Promise 解析后,它会再次重新渲染页面;这次不再显示回fallback退属性,而是尝试重新渲染原来的子元素,因为此时子元素已经准备好渲染,不会再像以前那样随意抛出 Promise 了。
一种实现方式可能如下所示:
class Suspense extends React.Component {
constructor(props) {
super(props);
this.state = {
promise: null
};
}
componentDidCatch(e) {
// Drake meme where he says no to errors here.
if (e instanceof Error) {
throw e;
}
// Drake meme where he says yes to promises here.
if (e instanceof Promise) {
this.setState({
promise: e
}, () => {
// When the promise finishes, go back to rendering the original children.
e.then(() => {
this.setState({ promise: null });
});
});
}
// This line isn't compatible with the Drake meme format.
else {
throw e;
}
}
render() {
if (this.state.promise) {
return this.props.fallback;
}
return this.props.children;
}
}
/*
<Suspense fallback={<Loading />}>
<PromiseThrower />
</Suspense>
*/
需要注意的是,最初的子线程在回退机制发生之前就尝试渲染,但从未成功。
这如何应用于取钩?🎣
你应该已经明白,fetch hook 需要抛出 Promise。它确实会这样做。这个 Promise 恰好就是 fetch 请求。当 Suspense 收到这个抛出的 fetch 请求时,它会回退到渲染其fallbackprop。当这个 fetch 请求完成后,它会再次尝试渲染组件。
这里有个棘手的小问题——发起 fetch 请求的组件只是尝试渲染,但失败了。实际上,它根本不属于任何组件fallback!它没有实例,从未挂载,也没有任何状态(甚至连 React Hook 状态都没有);它没有组件生命周期,也没有任何副作用。那么,当它再次尝试渲染时,它如何知道这次 fetch 请求的响应呢?Suspense 没有传递这个响应,而且由于它没有被实例化,所以无法附加任何数据。
哎呀,这难题该怎么解决呢?🤔
我们用记忆化解决了这个问题!
“喜欢那个炫酷的新React.memo功能吗?”
“是的!”(理论上)
“不!”(更直白地说)
它没有使用React.memo基于 props 对 React 组件进行记忆化的函数。相反,我使用一个无限深度的数组来记忆传递给 fetch 的参数。
如果收到一个请求,试图获取之前已经请求过的数据(第一次尝试实例化失败后返回 Promise),则直接返回第一次请求的 Promise 中最终解析出的数据。如果这是一个全新的请求,则获取数据,将其缓存到记忆化数组中,并抛出获取数据的 Promise。通过将当前请求与记忆化数组中的所有条目进行比较,我们可以判断该请求是否已被分发过。
const deepEqual = require('deep-equal');
interface FetchCache {
fetch?: Promise<void>;
error?: any;
init: RequestInit | undefined;
input: RequestInfo;
response?: any;
}
const fetchCaches: FetchCache[] = [];
const useFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
for (const fetchCache of fetchCaches) {
// The request hasn't changed since the last call.
if (
deepEqual(input, fetchCache.input) &&
deepEqual(init, fetchCache.init)
) {
// If we logged an error during this fetch request, THROW the error.
if (Object.prototype.hasOwnProperty.call(fetchCache, 'error')) {
throw fetchCache.error;
}
// If we received a response to this fetch request, RETURN it.
if (Object.prototype.hasOwnProperty.call(fetchCache, 'response')) {
return fetchCache.response;
}
// If we do not have a response or error, THROW the promise.
throw fetchCache.fetch;
}
}
// The request is new or has changed.
const fetchCache: FetchCache = {
fetch:
// Make the fetch request.
fetch(input, init)
// Parse the response.
.then(response => {
// Support JSON.
if (Object.prototype.hasOwnProperty.call(response.headers, 'Content-Type')) {
return response.json();
}
// Not JSON.
return response.text();
})
// Cache the response for when this component
// attempts to render again later.
.then(response => {
fetchCache.response = response;
})
// Cache the error for when this component
// attempts to render again later.
.catch(e => {
fetchCache.error = e;
}),
init,
input
};
// Add this metadata to the memoization array.
fetchCaches.push(fetchCache);
// Throw the Promise! Suspense to the rescue!
throw fetchCache.fetch;
};
听起来像是内存泄漏💧
这可能是功能特性,也可能是漏洞!
但如果您认为这是项目中的一个 bug,可以通过为 fetch 请求设置一个以毫秒为单位的生命周期来使缓存失效。向钩子传递第三个参数(一个数字)useFetch将指示它在指定毫秒数后从 memoization 数组中删除元数据。我们实现起来非常简单,如下所示:
// NEW: lifespan parameter
const useFetch = (
input: RequestInfo,
init?: RequestInit | undefined,
lifespan: number = 0
) => {
// ...
const fetchCache: FetchCache = {
fetch:
// Make the fetch request.
fetch(input, init)
.then( /* ... */ )
.then( /* ... */ )
.catch( /* ... */ )
// Invalidate the cache.
.then(() => {
// If the user defined a lifespan,
if (lifespan > 0) {
// Wait for the duration of the lifespan,
setTimeout(
() => {
// Find this fetch request and kill it
// from the memoization array.
const index = fetchCaches.indexOf(fetchCache);
if(index !== -1) {
fetchCaches.splice(index, 1);
}
},
lifespan
);
}
}),
// ...
};
// ...
};
// ...
当数据获取完成且元数据更新完毕后,计时器会开始计时。重要的是,生命周期计时器必须在catchPromise 的结束之后执行,因为即使发生错误,我们也希望它能够被设置。
结论🍬
当丹·阿布拉莫夫告诉你你不能做某件事时,你也要去做。
如果你喜欢这篇文章,请随意点赞或送个独角兽。操作快捷、简单,而且完全免费!如果你有任何问题或相关的好建议,请在下方评论区留言。
要阅读更多我的专栏文章,您可以在LinkedIn、Medium和Twitter上关注我,或者访问 CharlesStover.com 查看我的作品集。
文章来源:https://dev.to/charlesstover/react-suspense-with-the-fetch-api-374j

