1. 19:37 09/10/2026
Khi xây dựng các ứng dụng đa nền tảng với React và React Native, việc xử lý upload file thường là một trong những tính năng phức tạp nhất. Việc đồng bộ hóa tiến trình, kết quả validation và việc đồng bộ dữ liệu với server trên cả hai nền tảng có thể nhanh chóng trở nên lộn xộn nếu không có một chiến lược rõ ràng.
Dưới đây là một phương pháp tiếp cận thực tế, đã được chứng minh, kết hợp React Query để quản lý dữ liệu bất đồng bộ với một Context API tùy chỉnh để chia sẻ logic giữa code web và native. Cách tiếp cận này giúp bạn có được quy trình upload file mạnh mẽ, dễ kiểm tra và dễ bảo trì.
## 1. Xác định các mối quan tâm cốt lõi
- **Validation** – quy tắc nghiệp vụ, kiểm tra kích thước, loại MIME.
- **Tiến trình upload** – tỷ lệ phần trăm hoàn thành, xử lý lỗi.
- **Tích hợp server** – các endpoint, xử lý phản hồi.
- **Bộ nhớ cache và đồng bộ dữ liệu** – lưu trữ các metadata upload trên client để đồng bộ sau.
Bằng cách xác định các mối quan tâm này ngay từ đầu, bạn có thể bao bọc từng mối quan tâm trong các phần riêng biệt, có thể tái sử dụng.
## 2. Tạo một hook validation chung
```ts
// shared/validation.ts
export interface FileError { field: string; message: string; }
export function validateFile(file: File): FileError[] {
const errors: FileError[] = [];
if (!file.type.startsWith('image/')) {
errors.push({ field: 'type', message: 'Chỉ chấp nhận file ảnh.' });
}
if (file.size > 5_000_000) {
errors.push({ field: 'size', message: 'Kích thước tối đa là 5 MB.' });
}
return errors;
}
```
Hook này có thể được import bởi cả React‑JS và React‑Native mà không cần phụ thuộc vào các thư viện platform‑specific.
## 3. Xây dựng một hook upload tùy chỉnh sử dụng React Query
```ts
// shared/useFileUpload.ts
import { useMutation, useQueryClient } from 'react‑query';
import { validateFile } from './validation';
import { FileUploadContext } from './FileUploadContext';
interface UploadResult {
id: string;
path: string;
status: 'success' | 'error';
}
export function useFileUpload() {
const qc = useQueryClient();
const { setUploads } = React.useContext(FileUploadContext);
const mutation = useMutation<
UploadResult,
Error,
File
>(async (file) => {
const errs = validateFile(file);
if (errs.length) throw new Set(errs); // custom error shape
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: form });
if (!res.ok) throw new Error('Upload thất bại');
return res.json();
}, {
onSuccess: (data) => {
qc.invalidateQueries('uploads');
setUploads((prev) => [...prev, data]);
},
});
return {
upload: (file: File) => mutation.mutateAsync(file),
isLoading: mutation.isLoading,
error: mutation.error,
};
}
```
React Query tự động xử lý việc cache, làm mới và đồng bộ dữ liệu giữa các query, trong khi Context cung cấp một danh sách các mục upload thời gian thực cho UI (ví dụ: một thanh tiến trình).
## 4. Bao bọc ứng dụng trong một Provider
```ts
// shared/FileUploadContext.tsx
import React, { createContext, useState, ReactNode } from 'react';
export interface UploadItem { id: string; progress: number; status: 'pending'|'uploading'|'done'|'error'; }
export const FileUploadContext = createContext<{
uploads: UploadItem[];
setUploads: React.Dispatch<React.SetStateAction<UploadItem[]>>;
}>({ uploads: [], setUploads: () => {} });
export const FileUploadProvider = ({ children }: { children: ReactNode }) => {
const [uploads, setUploads] = useState<UploadItem[]>([]);
return (
<FileUploadContext.Provider value={{ uploads, setUploads }}>
{children}
</FileUploadContext.Provider>
);
};
```
Cả React‑Web và React‑Native đều được bao bọc bởi `<FileUploadProvider>`, đảm bảo danh sách upload được đồng bộ.
## 5. Triển khai giao diện người dùng
**React‑Web:** sử dụng `<input type="file">` thông thường và một thành phần `<UploadProgress>` để hiển thị tỷ lệ phần trăm hoàn thành.
**React‑Native:** sử dụng `react-native-document-picker` để chọn file, sau đó truyền file đã chọn vào hook.
Cả hai nền tảng đều có thể render `<UploadList>` từ Context, cung cấp một cái nhìn thống nhất về các tác vụ đang diễn ra.
## 6. Mẹo thực tế
- **Xử lý lỗi** – ném một đối tượng lỗi tùy chỉnh từ hook validation; UI có thể hiển thị thông báo lỗi dựa trên `error.field`.
- **Hiển thị tiến trình** – đối với các file lớn, hãy sử dụng `onProgress` từ axios hoặc một polyfill tương tự; cập nhật state thông qua một hiệu ứng phụ trong Context.
- **Offline support** – lưu trữ các file đã chọn và các metadata upload vào AsyncStorage (RN) hoặc IndexedDB (web) và retry khi có kết nối.
- **Kiểm tra** – mock `fetch` trong Jest và kiểm tra flow validation; sử dụng React Query's `queryClient` để kiểm tra việc cache.
## 7. Kết luận
Bằng cách tách validation, upload và state management thành các gói riêng biệt, được chia sẻ, bạn có thể tái sử dụng code trên cả React‑JS và React‑Native, đồng thời vẫn giữ cho từng nền tảng có khả năng tùy biến cao. Cách tiếp cận này mang lại quy trình upload file mạnh mẽ, có khả năng mở rộng, đồng thời cung cấp cho người dùng phản hồi trực quan nhất quán.