1. 17:24 09/10/2026
When building file upload features in React and React Native, developers often face the challenge of keeping validation, progress tracking, and server integration logic DRY across multiple screens and platforms. A clean way to achieve this consistency is to centralize upload behavior in custom hooks and a shared context. This approach not only reduces code duplication but also makes testing and UI updates simpler.
### Why Custom Hooks?
**1. Encapsulate Upload State** – A hook such as `useFileUpload` can manage the file list, upload progress, error messages, and success flags. By pulling these states into a single place, any component can subscribe to the latest upload status without managing local state itself.
**2. Reusable Validation Logic** – File type, size, and naming rules are common across the app. A hook can expose a `validateFile` function that returns a clear error object, allowing UI components to display inline warnings instantly.
**3. Progress and Cancellation** – Using AbortController, the hook can track upload progress via the `onProgress` event and provide a `cancelUpload` method. Components can render a progress bar or a cancel button automatically when an upload is in flight.
### Building the Hook
```js
function useFileUpload(initialFiles = []) {
const [files, setFiles] = useState(initialFiles);
const [uploading, setUploading] = useState(false);
const [errors, setErrors] = useState({});
const validateFile = (file) => {
const err = {};
if (!ALLOWED_TYPES.includes(file.type)) err.type = 'Invalid file type';
if (file.size > MAX_SIZE) err.size = 'File too large';
return err;
};
const uploadFile = async (file) => {
const err = validateFile(file);
if (Object.keys(err).length) {
setErrors(prev => ({ ...prev, [file.name]: err }));
return;
}
setErrors(prev => ({ ...prev, [file.name]: {} }));
const controller = new AbortController();
setUploading(true);
try {
const res = await fetch(`${API_URL}/upload`, {
method: 'POST',
body: file,
signal: controller.signal,
// include auth headers, etc.
});
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
setFiles(prev => prev.map(f => f.name === file.name ? { ...f, uploaded: true, url: data.url } : f));
} catch (e) {
if (!isAbortError(e)) setErrors(prev => ({ ...prev, [file.name]: { network: 'Network error' } }));
} finally {
setUploading(false);
}
return controller;
};
return { files, uploading, errors, uploadFile, setFiles };
}
```
### Sharing State with Context
Wrap the app in a `FileUploadProvider` that calls `useFileUpload` and expose its methods via React Context. Any component can use `useContext(FileUploadContext)` to trigger uploads, update the file list, or display global upload progress.
### UI Integration
- **Drag‑and‑Drop Zone** – A component can listen to `onDrop`, convert the FileList to an array, and call `uploadFile` for each entry.
- **Progress Bar** – Render a conditional `<Progress>` element when `uploading` is true, using the hook’s progress state.
- **Error Display** – Map over `errors` and show inline validation messages next to each file.
### Best Practices
- Keep the hook platform‑agnostic; React Native can use the same logic by using a generic `fetch` polyfill or a library like `react-native-fs`.
- Cache uploaded files locally (e.g., AsyncStorage) to allow offline retries.
- Use environment variables for API endpoints to switch between dev and production servers without code changes.
- Write unit tests that mock `fetch` and verify validation and error handling.
By centralizing upload logic in a custom hook and context, teams can maintain a single source of truth for file handling, improve UI consistency, and speed up future feature additions across both web and mobile implementations.