A fetch API polyfill for React Native with text streaming support
This is a fork of GitHub's fetch polyfill, the fetch implementation React Native currently provides. This project features an alternative fetch implementation directy built on top of React Native's Networking API instead of XMLHttpRequest
for performance gains. At the same time, it aims to fill in some gaps of the WHATWG specification for fetch, namely the support for text streaming.
In practice, this implementation is a drop-in replacement to GitHub's polyfill as it closely follows its implementation. Do not use this implementation if your application does not require to stream text.
GitHub's fetch polyfill, originally designed with the intention to be used in web browsers without support for the fetch standard, most notably does not support the consumption of a response body as a stream.
However, as React Native does not yet provide direct access to the underlying byte stream for responses, we either have to fallback to XMLHttpRequest or React Native's networking API for iOS and Android. Currently, only strings can be transfered through the bridge, thus binary data has to be base64-encoded (source) and while React Native's XHR provides progress events to receive incremental data, it concatenates the response string as data comes in. Although very inefficient, the response can be sliced up, each chunk encoded into its UTF-8 representation with TextEncoder and finally enqueued to the stream.
Instead of relying on XMLHttpRequest
, which degrades performance, we remove it out of the equation and have fetch interact with React Native's Networking API directly instead. To make Response.body
work, ReadableStream
's controller was integrated with native progress events. It's important to stress that progress events are only fired when the native response type is set to text
(https://github.com/facebook/react-native/blob/v0.63.4/Libraries/Network/RCTNetworking.mm#L544-L547), therefore limiting streaming to text-only transfers. If you wish to consume binary data, either blob
or base64
response types have to be used. In this case, the downside is that the final response body is read as a whole and enqueued to the stream's controller as a single chunk. There is no way to read a partial response of a binary transfer.
For more context, read the following:
- JakeChampion#746
- facebook/react-native#27741
- https://hpbn.co/xmlhttprequest/#streaming-data-with-xhr
Related:
React Native v0.62.0+ is the minimum version supported where the Networking API has been made public.
This implementation depends on the following web APIs which are not currently available in React Native:
It should be possible remove the dependency on TextEncoder
and TextDecoder
, but not on ReadableStream
. Either way, beware the bundle size of your application will inevitable increase.
To polyfill the above APIs, use react-native-polyfill-globals.
$ npm install react-native-fetch-api --save
The APIs provided by GitHub's implementation in React Native have to be replaced by those provided by this implementation. To do so, check and install react-native-polyfill-globals and follow the instructions therein.
No need to import anything after the setup is done. All APIs will be available globally.
Example:
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
Check fetch's official documentation to learn more about the concepts and extended usage.
A non-standard option was added to fetch
to enable incremental events in React Native's networking layer.
fetch('https://jsonplaceholder.typicode.com/todos/1', { reactNative: { textStreaming: true } })
.then(response => response.body)
.then(stream => ...)
It's possible to abort an on-going request and React Native already supports AbortController
, so there is no need for a polyfill.
const controller = new AbortController();
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal: controller.signal })
.then(response => response.json())
.then(json => console.log(json))
Learn more about aborting fetch at https://developers.google.com/web/updates/2017/09/abortable-fetch.
There is no concept of Cross-Origin Resource Sharing (CORS) in native apps. React Native only accepts a boolean value for the credentials
option. As such, to send cookies you can either use same-origin
and include
.
The Set-Cookie
response header returned from the server is a forbidden header name and therefore can't be programmatically read with response.headers.get()
. Instead, the platform's native networking stack automatically manages cookies for you.
If you run into issues with cookie-based authentication, read the following:
- https://reactnative.dev/docs/network#known-issues-with-fetch-and-cookie-based-authentication
- https://build.affinity.co/persisting-sessions-with-react-native-4c46af3bfd83
- https://medium.com/locastic/react-native-cookie-based-authentication-80ee18f4c71b
Alternatively, you may consider using the react-native-cookies.
The only values supported for the cache
option are no-cache
and no-store
and Both achieve exactly the same result. All other values are ignored. Following GitHub's implementation, a cache-busting mechanism is provided by using the query parameter _
which holds the number of milliseconds elapsed since the Epoch when either no-cache
or no-store
are specified.
The fetch specification defines these values for the redirect
option: follow
(the default), error
, and manual
. React Native does not accept such option but it does transparently follow a redirect response given the Location
header for 30x status codes.
To run the test suite, you must use react-native-test-runner
CLI. Run the run-tests.js
wrapper script to spin up a local HTTP server to execute the networking tests against.
$ ./run-tests.js --platform ios --simulator '<simulator>' test/index.js
Where <simulator>
can be a combination of a device type and iOS version, e.g. iPhone 11 (14.1)
, or a device UUID.
Check which simulators are available in your system by running the following command:
$ xcrun xctrace list devices
$ ./run-tests.js --platform android --emulator '<emulator>' test/index.js
Where <emulator>
is the name of the Android Virtual Device (AVD), e.g. Pixel_API_28_AOSP
.
Check which emulators are available in your system by running the following command:
$ emulator -list-avds