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.