From dd7f0deb947f836170cd1019dbbc2e44d30a21ce Mon Sep 17 00:00:00 2001
From: Dan Abramov
Date: Thu, 30 Apr 2020 01:24:04 +0100
Subject: [PATCH] [Blocks] Use native fetch (#18784)
* [Blocks] Use native fetch
* Use the prototype
* Support arrayBuffer() and blob()
* ctor
* Simplify
* Use an expando
* Keep a map of formats
* Unused
* Remove unnecessary second property read
* Keep it simple
* Store the original thenable
---
fixtures/blocks/src/Comments.js | 4 +-
fixtures/blocks/src/Post.js | 5 +-
.../react-data/src/fetch/ReactDataFetch.js | 128 ++++++++++++------
3 files changed, 94 insertions(+), 43 deletions(-)
diff --git a/fixtures/blocks/src/Comments.js b/fixtures/blocks/src/Comments.js
index c91b795fd4..abb67ca919 100644
--- a/fixtures/blocks/src/Comments.js
+++ b/fixtures/blocks/src/Comments.js
@@ -10,9 +10,7 @@ import {fetch} from 'react-data/fetch';
function load(postId) {
return {
- comments: JSON.parse(
- fetch('http://localhost:3001/comments?postId=' + postId)
- ),
+ comments: fetch('http://localhost:3001/comments?postId=' + postId).json(),
};
}
diff --git a/fixtures/blocks/src/Post.js b/fixtures/blocks/src/Post.js
index 3ca854f22b..3f62ce297b 100644
--- a/fixtures/blocks/src/Post.js
+++ b/fixtures/blocks/src/Post.js
@@ -11,8 +11,10 @@ import {fetch} from 'react-data/fetch';
import loadComments from './Comments';
function load(params) {
+ const postResponse = fetch('http://localhost:3001/posts/' + params.id);
return {
- post: JSON.parse(fetch('http://localhost:3001/posts/' + params.id)),
+ post: postResponse.json(),
+ meta: postResponse.status + ' ' + postResponse.statusText,
Comments: loadComments(params.id),
};
}
@@ -23,6 +25,7 @@ function Post(props, data) {
Post {data.post.id}
{data.post.title}
{data.post.body}
+ {data.meta}
Loading comments...
}>
diff --git a/packages/react-data/src/fetch/ReactDataFetch.js b/packages/react-data/src/fetch/ReactDataFetch.js
index ade15cdfa0..1d4a71ff04 100644
--- a/packages/react-data/src/fetch/ReactDataFetch.js
+++ b/packages/react-data/src/fetch/ReactDataFetch.js
@@ -32,6 +32,8 @@ type RejectedResult = {|
type Result = PendingResult | ResolvedResult | RejectedResult;
+// TODO: this is a browser-only version. Add a separate Node entry point.
+const nativeFetch = window.fetch;
const fetchKey = {};
function readResultMap(): Map {
@@ -44,50 +46,98 @@ function readResultMap(): Map {
return map;
}
-// TODO: options, auth, etc.
-export function fetch(url: string): Object {
- const map = readResultMap();
- const entry = map.get(url);
- if (entry === undefined) {
- let resolve = () => {};
- const wakeable: Wakeable = new Promise(r => {
- // TODO: should this be a plain thenable instead?
- resolve = r;
- });
- const result: Result = {
- status: Pending,
- value: wakeable,
- };
- map.set(url, result);
- const xhr = new XMLHttpRequest();
- xhr.onload = function() {
- // TODO: should we handle status codes?
- if (result.status !== Pending) {
- return;
+function toResult(thenable): Result {
+ const result: Result = {
+ status: Pending,
+ value: thenable,
+ };
+ thenable.then(
+ value => {
+ if (result.status === Pending) {
+ const resolvedResult = ((result: any): ResolvedResult);
+ resolvedResult.status = Resolved;
+ resolvedResult.value = value;
}
- const resolvedResult = ((result: any): ResolvedResult);
- resolvedResult.status = Resolved;
- resolvedResult.value = xhr.response;
- resolve();
- };
- xhr.onerror = function() {
- if (result.status !== Pending) {
- return;
+ },
+ err => {
+ if (result.status === Pending) {
+ const rejectedResult = ((result: any): RejectedResult);
+ rejectedResult.status = Rejected;
+ rejectedResult.value = err;
}
- const rejectedResult = ((result: any): RejectedResult);
- rejectedResult.status = Rejected;
- // TODO: use something else as the error value?
- rejectedResult.value = xhr;
- resolve();
- };
- xhr.open('GET', url);
- xhr.send();
- throw wakeable;
- }
- const result: Result = entry;
+ },
+ );
+ return result;
+}
+
+function readResult(result: Result) {
if (result.status === Resolved) {
return result.value;
} else {
throw result.value;
}
}
+
+function Response(nativeResponse) {
+ this.headers = nativeResponse.headers;
+ this.ok = nativeResponse.ok;
+ this.redirected = nativeResponse.redirected;
+ this.status = nativeResponse.status;
+ this.statusText = nativeResponse.statusText;
+ this.type = nativeResponse.type;
+ this.url = nativeResponse.url;
+
+ this._response = nativeResponse;
+ this._arrayBuffer = null;
+ this._blob = null;
+ this._json = null;
+ this._text = null;
+}
+
+Response.prototype = {
+ constructor: Response,
+ arrayBuffer() {
+ return readResult(
+ this._arrayBuffer ||
+ (this._arrayBuffer = toResult(this._response.arrayBuffer())),
+ );
+ },
+ blob() {
+ return readResult(
+ this._blob || (this._blob = toResult(this._response.blob())),
+ );
+ },
+ json() {
+ return readResult(
+ this._json || (this._json = toResult(this._response.json())),
+ );
+ },
+ text() {
+ return readResult(
+ this._text || (this._text = toResult(this._response.text())),
+ );
+ },
+};
+
+export function fetch(url: string, options: mixed): Object {
+ const map = readResultMap();
+ let entry = map.get(url);
+ if (!entry) {
+ if (options) {
+ if (options.method || options.body || options.signal) {
+ // TODO: wire up our own cancellation mechanism.
+ // TODO: figure out what to do with POST.
+ throw Error('Unsupported option');
+ }
+ }
+ const thenable = nativeFetch(url, options);
+ entry = toResult(thenable);
+ map.set(url, entry);
+ }
+ const nativeResponse = (readResult(entry): any);
+ if (nativeResponse._reactResponse) {
+ return nativeResponse._reactResponse;
+ } else {
+ return (nativeResponse._reactResponse = new Response(nativeResponse));
+ }
+}