skip to content

In a React Native freelancer-marketplace app, how do you upload a profile photo from a local file URI and show upload progress?

level: middleimportance: should knowfreq 46%

answer

  1. a FormData part that is only a URI
  2. uri, name and type
  3. native code reads the file
  4. fetch has no upload progress event
  5. xhr.upload.onprogress or axios onUploadProgress

basics

~20 s

Append { 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 s

React 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 lines
typescript
type 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

for a junior

Recall the { uri, name, type } FormData part and that a progress bar needs XMLHttpRequest's upload.onprogress or axios's onUploadProgress, not fetch.

for a middle

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.

for a senior

Harden the flow: resize before upload, throttle progress into state, support cancel and retry, handle content:// URIs and check status codes explicitly.

for a principal

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.