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)); + } +}