[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
Dan Abramov committed
Apr 30, 2020 at 01:24 UTC
dd7f0deb947f836170cd1019dbbc2e44d30a21ce
3 files changed
+94
-43
fixtures/blocks/src/Comments.js
+1
-3
@@ -10,9 +10,7 @@ import {fetch} from 'react-data/fetch';
10
11
function load(postId) {
12
return {
13
- comments: JSON.parse(
14
- fetch('http://localhost:3001/comments?postId=' + postId)
15
- ),
13
+ comments: fetch('http://localhost:3001/comments?postId=' + postId).json(),
14
};
15
}
16
fixtures/blocks/src/Post.js
+4
-1
@@ -11,8 +11,10 @@ import {fetch} from 'react-data/fetch';
11
import loadComments from './Comments';
12
13
function load(params) {
14
+ const postResponse = fetch('http://localhost:3001/posts/' + params.id);
15
return {
15
- post: JSON.parse(fetch('http://localhost:3001/posts/' + params.id)),
16
+ post: postResponse.json(),
17
+ meta: postResponse.status + ' ' + postResponse.statusText,
18
Comments: loadComments(params.id),
19
};
20
}
@@ -23,6 +25,7 @@ function Post(props, data) {
25
<h1>Post {data.post.id}</h1>
26
<h4>{data.post.title}</h4>
27
<p>{data.post.body}</p>
28
+ <pre>{data.meta}</pre>
29
<hr />
30
<Suspense fallback={<p>Loading comments...</p>}>
31
<data.Comments />
packages/react-data/src/fetch/ReactDataFetch.js
+89
-39
@@ -32,6 +32,8 @@ type RejectedResult = {|
32
33
type Result = PendingResult | ResolvedResult | RejectedResult;
34
35
+// TODO: this is a browser-only version. Add a separate Node entry point.
36
+const nativeFetch = window.fetch;
37
const fetchKey = {};
38
39
function readResultMap(): Map<string, Result> {
@@ -44,50 +46,98 @@ function readResultMap(): Map<string, Result> {
46
return map;
47
}
48
47
-// TODO: options, auth, etc.
48
-export function fetch(url: string): Object {
49
- const map = readResultMap();
50
- const entry = map.get(url);
51
- if (entry === undefined) {
52
- let resolve = () => {};
53
- const wakeable: Wakeable = new Promise(r => {
54
- // TODO: should this be a plain thenable instead?
55
- resolve = r;
56
- });
57
- const result: Result = {
58
- status: Pending,
59
- value: wakeable,
60
- };
61
- map.set(url, result);
62
- const xhr = new XMLHttpRequest();
63
- xhr.onload = function() {
64
- // TODO: should we handle status codes?
65
- if (result.status !== Pending) {
66
- return;
49
+function toResult(thenable): Result {
50
+ const result: Result = {
51
+ status: Pending,
52
+ value: thenable,
53
+ };
54
+ thenable.then(
55
+ value => {
56
+ if (result.status === Pending) {
57
+ const resolvedResult = ((result: any): ResolvedResult);
58
+ resolvedResult.status = Resolved;
59
+ resolvedResult.value = value;
60
}
68
- const resolvedResult = ((result: any): ResolvedResult);
69
- resolvedResult.status = Resolved;
70
- resolvedResult.value = xhr.response;
71
- resolve();
72
- };
73
- xhr.onerror = function() {
74
- if (result.status !== Pending) {
75
- return;
61
+ },
62
+ err => {
63
+ if (result.status === Pending) {
64
+ const rejectedResult = ((result: any): RejectedResult);
65
+ rejectedResult.status = Rejected;
66
+ rejectedResult.value = err;
67
}
77
- const rejectedResult = ((result: any): RejectedResult);
78
- rejectedResult.status = Rejected;
79
- // TODO: use something else as the error value?
80
- rejectedResult.value = xhr;
81
- resolve();
82
- };
83
- xhr.open('GET', url);
84
- xhr.send();
85
- throw wakeable;
86
- }
87
- const result: Result = entry;
68
+ },
69
+ );
70
+ return result;
71
+}
72
+
73
+function readResult(result: Result) {
74
if (result.status === Resolved) {
75
return result.value;
76
} else {
77
throw result.value;
78
}
79
}
80
+
81
+function Response(nativeResponse) {
82
+ this.headers = nativeResponse.headers;
83
+ this.ok = nativeResponse.ok;
84
+ this.redirected = nativeResponse.redirected;
85
+ this.status = nativeResponse.status;
86
+ this.statusText = nativeResponse.statusText;
87
+ this.type = nativeResponse.type;
88
+ this.url = nativeResponse.url;
89
+
90
+ this._response = nativeResponse;
91
+ this._arrayBuffer = null;
92
+ this._blob = null;
93
+ this._json = null;
94
+ this._text = null;
95
+}
96
+
97
+Response.prototype = {
98
+ constructor: Response,
99
+ arrayBuffer() {
100
+ return readResult(
101
+ this._arrayBuffer ||
102
+ (this._arrayBuffer = toResult(this._response.arrayBuffer())),
103
+ );
104
+ },
105
+ blob() {
106
+ return readResult(
107
+ this._blob || (this._blob = toResult(this._response.blob())),
108
+ );
109
+ },
110
+ json() {
111
+ return readResult(
112
+ this._json || (this._json = toResult(this._response.json())),
113
+ );
114
+ },
115
+ text() {
116
+ return readResult(
117
+ this._text || (this._text = toResult(this._response.text())),
118
+ );
119
+ },
120
+};
121
+
122
+export function fetch(url: string, options: mixed): Object {
123
+ const map = readResultMap();
124
+ let entry = map.get(url);
125
+ if (!entry) {
126
+ if (options) {
127
+ if (options.method || options.body || options.signal) {
128
+ // TODO: wire up our own cancellation mechanism.
129
+ // TODO: figure out what to do with POST.
130
+ throw Error('Unsupported option');
131
+ }
132
+ }
133
+ const thenable = nativeFetch(url, options);
134
+ entry = toResult(thenable);
135
+ map.set(url, entry);
136
+ }
137
+ const nativeResponse = (readResult(entry): any);
138
+ if (nativeResponse._reactResponse) {
139
+ return nativeResponse._reactResponse;
140
+ } else {
141
+ return (nativeResponse._reactResponse = new Response(nativeResponse));
142
+ }
143
+}