In a React Native freelancer-marketplace app, how do you upload a profile photo from a local file URI and show upload progress?
answer
- a FormData part that is only a URI
- uri, name and type
- native code reads the file
- fetch has no upload progress event
- xhr.upload.onprogress or axios onUploadProgress
basics
~20 sAppend { uri, name, type } to a FormData so native code streams the file from disk, and send it with XMLHttpRequest or axios, since fetch has no upload progress event. In Expo SDK 56+, the global expo/fetch rejects uri parts.
solid answer
~50 sReact Native's `FormData` accepts a proprietary file part: an object with `uri`, `name` and `type`. The native networking module opens that URI (a `file://` path, or a `content://` URI on Android) and streams it into the multipart body, so the image never passes through JavaScript as base64, and the boundary header is generated for you. `fetch` has no upload-progress event, so for a progress bar I use React Native's `XMLHttpRequest`: native code emits send progress and `xhr.upload.onprogress` receives `loaded` and `total`. axios wraps the same thing as `onUploadProgress`. One modern trap: since Expo SDK 56 the global `fetch` is `expo/fetch`, which rejects `{ uri }` parts; there you either use XHR or axios, append an `expo-file-system` `File` instead, or opt back into React Native's fetch. I also resize the photo before upload and throttle progress updates into state.
code
typescript · 31 linestype Photo = { uri: string; fileName: string; mimeType: string };
export function uploadProfilePhoto(
freelancerId: string,
photo: Photo,
onProgress: (fraction: number) => void,
): Promise<void> {
return new Promise((resolve, reject) => {
const body = new FormData();
body.append('freelancerId', freelancerId);
body.append('photo', {
uri: photo.uri,
name: photo.fileName,
type: photo.mimeType,
});
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/me/photo');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
onProgress(e.loaded / e.total);
}
};
xhr.onload = () =>
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject(new Error(`HTTP ${xhr.status}`));
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(body);
});
}go deeper
Recall the { uri, name, type } FormData part and that a progress bar needs XMLHttpRequest's upload.onprogress or axios's onUploadProgress, not fetch.
Explain that native code streams the file and sets the multipart boundary, why fetch lacks progress, and how the expo/fetch default in Expo SDK 56+ rejects uri parts.
Harden the flow: resize before upload, throttle progress into state, support cancel and retry, handle content:// URIs and check status codes explicitly.
Decide between direct multipart uploads to your API and pre-signed uploads straight to object storage, weighing server load, resumability on flaky networks and client complexity.
## The scenario A freelancer edits their marketplace profile, picks a photo from the library or takes one with the camera, and expects a progress bar while it uploads. The picker hands you a **local URI**, not bytes. The question is how to get that file to a multipart endpoint efficiently and how to observe progress. ## React Native's file part in FormData React Native's `FormData` accepts, besides strings, an object describing a file: - `uri` – where the file is: a `file://` path, or on Android a `content://` URI that the native layer opens through the content resolver. - `name` – becomes `filename="..."` in the part's `Content-Disposition`. - `type` – becomes the part's `Content-Type`, for example `image/jpeg`. When the request is sent, React Native's native networking module reads the file **from disk** and builds the multipart body itself. Three consequences: 1. The photo never crosses into JavaScript as a huge base64 string, which would cost memory and time on the JS thread. 2. The native layer generates the **multipart boundary** and sets `Content-Type: multipart/form-data; boundary=...`, so you do not set that header by hand. 3. Other fields (`freelancerId`, a caption) go in the same `FormData` as ordinary strings. ## Progress: why not fetch The Fetch API has **no upload-progress event**. React Native's `XMLHttpRequest` does: the native module reports bytes sent as the body is written, and the JavaScript class dispatches a `progress` event on `xhr.upload` with `loaded`, `total` and `lengthComputable`. On Android, React Native throttles these events natively so the JS thread is not flooded. | Client | Upload progress | Uses the `{ uri }` file part | |---|---|---| | React Native `XMLHttpRequest` | `xhr.upload.onprogress` | Yes | | axios | `onUploadProgress` in the request config | Yes (runs on React Native's XHR) | | React Native's built-in `fetch` | None | Yes | | `expo/fetch` (global `fetch` in Expo SDK 56+) | None | No: pass an `expo-file-system` `File` | | `expo-file-system` `file.createUploadTask()` (SDK 56+) | `onProgress` with `bytesSent` and `totalBytes` | Takes a `File`, not FormData | ## The Expo SDK 56+ trap Since **Expo SDK 56**, Expo installs `expo/fetch` as the global `fetch` on Android and iOS. Its FormData conversion supports strings, `Blob`s and file objects such as `expo-file-system`'s `File`, but it **throws for React Native's `{ uri }` parts** (its source rejects them as an unsupported part). Code copied from older tutorials, `body.append('photo', { uri, name, type })` plus `fetch`, therefore fails in a new Expo app. The fixes: - Use `XMLHttpRequest` or axios, which still run on React Native's own networking and give you progress anyway. - Append `new File(uri)` from `expo-file-system` instead of the `{ uri }` object when you want `fetch` without progress. - Set `EXPO_PUBLIC_USE_RN_FETCH=1` to keep React Native's built-in `fetch` as the global. - In Expo, `expo-file-system`'s `file.createUploadTask(url, options)` is a native upload with its own `onProgress` callback, an alternative when you do not need a hand-built form. ## What the server expects The backend sees an ordinary `multipart/form-data` request, so agree on the contract up front: - The **field name** of the file part (`photo` in the example) must match what the server's multipart parser reads. - The part's `name` and `type` become the filename and content type the server sees; send a real MIME type such as `image/jpeg` rather than a guess. - Enforce a **size limit** on the server and return a clear status, such as `413`, so the app can show a useful message instead of a generic failure. ## Making it production-grade - **Shrink first.** A camera photo can be several megabytes; resize and compress to the size the profile actually displays before uploading. - **Throttle UI updates.** Store progress in a ref and push it to state at a sensible rate, or round to whole percentages, so the screen does not re-render on every event. - **Cancel.** Keep the `XMLHttpRequest` (or an axios `signal`) so leaving the screen can call `abort()`. - **Handle failure explicitly.** Check `xhr.status` in `onload`, handle `onerror` and `ontimeout`, and let the user retry. - **Check the URI scheme.** Android pickers often return `content://` URIs; React Native's networking layer can read them, but a library that copies files may give you a cache `file://` path instead. In an interview, the strong answer names the `{ uri, name, type }` part, says **native code streams the file**, explains that **progress needs XHR (or axios)**, and mentions the **`expo/fetch`** change as the version trap.
- The upload works in an older app but throws 'Unsupported FormDataPart implementation' in a new Expo SDK 57 app. Why?Since Expo SDK 56 the global `fetch` is `expo/fetch`, whose FormData conversion does not accept React Native's `{ uri }` file parts. Send the form with `XMLHttpRequest` or axios, append an `expo-file-system` `File` instead, or set `EXPO_PUBLIC_USE_RN_FETCH=1` to restore the built-in `fetch`.
- Why not read the image as base64 and send it in a JSON body?Base64 inflates the payload by about a third and materialises the whole file as a JavaScript string on the JS thread, costing memory and blocking work. The `{ uri }` part lets native code stream the file from disk into the multipart body instead.
- How do you keep progress events from causing jank?Treat progress as a high-frequency signal: keep the latest value in a ref and update state only when the rounded percentage changes, or on a short interval. Android already throttles send-progress events natively, but each state update still re-renders the screen.
saying these in an interview costs you the question
- fetch in React Native reports upload progress through response.body.
- The photo must be converted to base64 before it can be uploaded.
- You must set the multipart Content-Type header, including the boundary, yourself.
- Any fetch in an Expo app accepts React Native's { uri } FormData parts.
- axios has its own native networking layer separate from XMLHttpRequest.