1. 21:00 10/10/2026
**Giới thiệu**
Khi phát triển ứng dụng đa nền tảng với React, việc tái sử dụng logic upload file giúp tiết kiệm thời gian và giảm lỗi. Bài viết này hướng dẫn bạn cách xây dựng một quy trình upload file duy nhất, có thể sử dụng được cả trên React Web và React Native, đồng thời duy trì tính nhất quán và dễ kiểm thử.
**Tại sao cần một giải pháp thống nhất**
- **Mã nguồn một lần, hiệu suất cao:** Viết logic upload chỉ một lần và sử dụng nó trên cả web lẫn di động.
- **Hành vi người dùng đồng nhất:** Người dùng có trải nghiệm giống nhau bất kể nền tảng.
- **Dễ bảo trì:** Sửa lỗi hoặc thêm tính năng chỉ cần thực hiện ở một nơi.
**Các thành phần chính**
1. **Custom Hooks** – Bao bọc các API cụ thể của nền tảng (ví dụ: `react-native-image-picker` so với `HTML input`).
2. **Context API** – Chia sẻ trạng thái upload (danh sách tệp, tiến trình, lỗi) cho toàn bộ cây component.
3. **React Hook Form** – Quản lý trạng thái biểu mẫu một cách hiệu quả mà không cần state cục bộ.
4. **React Query (hoặc similar data‑fetching library)** – Đồng bộ hóa danh sách tệp đã upload lên backend.
**Ví dụ thực tế**
*Bước 1: Tạo custom hook*
```javascript
// useUpload.ts
import { useState, useCallback } from 'react';
import { Platform } from 'react-native';
import * as ImagePicker from 'react-native-image-picker';
import * as imagepickerWeb from 'image-picker-web'; // thư viện giả định cho web
export const useUpload = () => {
const [files, setFiles] = useState([]);
const [progress, setProgress] = useState(0);
const [error, setError] = useState(null);
const pickImage = useCallback(async () => {
try {
let result;
if (Platform.OS === 'web') {
result = await imagepickerWeb.launchImageLibrary();
} else {
result = await ImagePicker.launchImageLibrary();
}
if (!result.canceled && result.assets?.length) {
setFiles(prev => [...prev, ...result.assets]);
}
} catch (e) {
setError(e.message);
}
}, []);
const uploadAll = useCallback(async () => {
// giả lập upload từng tệp
const total = files.length;
for (let i = 0; i < total; i++) {
// gọi API upload thực tế tại đây
await new Promise(r => setTimeout(r, 500));
setProgress(Math.round(((i + 1) / total) * 100));
}
}, [files]);
return { files, progress, error, pickImage, uploadAll };
};
```
*Bước 2: Bao bọc trong React Context*
```javascript
// UploadContext.tsx
import React, { createContext, useContext } from 'react';
import { useUpload } from './useUpload';
const UploadContext = createContext(null);
export const UploadProvider = ({ children }) => {
const value = useUpload();
return <UploadContext.Provider value={value}>{children}</UploadContext.Provider>;
};
export const useUploadContext = () => {
const ctx = useContext(UploadContext);
if (!ctx) throw new Error('useUploadContext phải được sử dụng trong UploadProvider');
return ctx;
};
```
*Bước 3: Tích hợp với React Hook Form*
```javascript
// UploadForm.tsx
import { useForm } from 'react-hook-form';
import { useUploadContext } from './UploadContext';
const UploadForm = () => {
const { register, handleSubmit } = useForm();
const { pickImage, uploadAll, files, progress } = useUploadContext();
const onSubmit = () => uploadAll();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<button type="button" onClick={pickImage}>Chọn ảnh</button>
<input {...register('files')} type="hidden" />
<button type="submit">Upload tất cả</button>
{progress > 0 && <progress value={progress} max="100" />}<br/>
{files.map(f => <div key={f.uri}>{f.fileName}</div>)}
</form>
);
};
```
**Lợi ích đạt được**
- **Tái sử dụng:** Cùng một hook và context hoạt động trên cả web và di động.
- **Kiểm thử dễ dàng:** Logic upload có thể được mock và kiểm tra trong môi trường Node.
- **Mở rộng:** Dễ dàng thêm validation, retry, hoặc tích hợp cloud storage.
**Kết luận**
Bằng cách kết hợp custom hooks, Context API, React Hook Form và React Query, bạn có thể tạo ra một quy trình upload file mạnh mẽ, có thể tái sử dụng cho cả React Web và React Native. Cách tiếp cận này giúp mã nguồn sạch sẽ, hành vi người dùng đồng nhất và quy trình phát triển nhanh hơn.