npm パッケージにするなら、プロジェクト固有の依存(useEffectOnAny、UPLOAD_TYPE_NORMAL、PreparedUploadFileInfo など)をすべて剥がして、純粋に「URLからFileオブジェクトを得る」という関心だけに絞る必要があります。
パッケージ名のイメージ
fetch-url-to-file あたりが素直です。
エクスポートするもの
1. 純粋な非同期関数(コア)
React に依存しない関数で、Node.js やバニラJSからも使えるようにします。
// fetchUrlToFile(url, options?) => Promise<File>
export type FetchUrlToFileOptions = {
/** File オブジェクトに付けるファイル名(デフォルト: URL末尾から推定) */
fileName?: string;
/** MIME type を明示指定(デフォルト: レスポンスの Content-Type) */
mimeType?: string;
/** fetch に渡す追加オプション(headers, credentials など) */
fetchInit?: RequestInit;
/** 中断用 AbortSignal */
signal?: AbortSignal;
};
export async function fetchUrlToFile(
url: string,
options?: FetchUrlToFileOptions
): Promise<File> {
const { fileName, mimeType, fetchInit, signal } = options ?? {};
const response = await fetch(url, { ...fetchInit, signal });
if (!response.ok) {
throw new FetchUrlToFileError(
`HTTP ${response.status} ${response.statusText}`,
response.status
);
}
const blob = await response.blob();
const resolvedName =
fileName ?? extractFileNameFromUrl(url) ?? 'downloaded-file';
const resolvedType = mimeType ?? blob.type ?? 'application/octet-stream';
return new File([blob], resolvedName, { type: resolvedType });
}
2. カスタムエラークラス
呼び出し側が HTTP ステータスでハンドリングできるようにします。
export class FetchUrlToFileError extends Error {
constructor(
message: string,
public readonly status: number
) {
super(message);
this.name = 'FetchUrlToFileError';
}
}
3. React フック(サブパスエクスポート)
React を使うプロジェクト向けに /react エントリで提供します。
// fetch-url-to-file/react
import { useEffect, useReducer, useCallback } from 'react';
import { fetchUrlToFile, type FetchUrlToFileOptions } from 'fetch-url-to-file';
type Status = 'idle' | 'loading' | 'success' | 'error';
type State = {
status: Status;
file: File | undefined;
error: Error | undefined;
};
type UseFetchUrlToFileParams = FetchUrlToFileOptions & {
url: string | null | undefined;
enabled?: boolean; // デフォルト true
};
type UseFetchUrlToFileReturn = State & {
refetch: () => void;
};
export function useFetchUrlToFile(
params: UseFetchUrlToFileParams
): UseFetchUrlToFileReturn {
const { url, enabled = true, ...options } = params;
const [state, dispatch] = useReducer(reducer, initialState);
const execute = useCallback(() => {
if (!url) return;
const controller = new AbortController();
dispatch({ type: 'loading' });
fetchUrlToFile(url, { ...options, signal: controller.signal })
.then((file) => dispatch({ type: 'success', file }))
.catch((error) => {
if (!controller.signal.aborted) {
dispatch({ type: 'error', error });
}
});
return () => controller.abort();
}, [url, options.fileName, options.mimeType]);
useEffect(() => {
if (!enabled || !url) {
dispatch({ type: 'reset' });
return;
}
return execute();
}, [enabled, execute]);
return { ...state, refetch: execute };
}
package.json のエクスポート構成
利用イメージ
// React なしで使う場合
import { fetchUrlToFile } from 'fetch-url-to-file';
const file = await fetchUrlToFile('https://example.com/doc.pdf', {
fileName: 'report.pdf',
fetchInit: { credentials: 'include' },
});
// React コンポーネントから使う場合
import { useFetchUrlToFile } from 'fetch-url-to-file/react';
const { status, file, error, refetch } = useFetchUrlToFile({
url: documentFile.file_url,
fileName: documentFile.name || 'file.pdf',
mimeType: 'application/pdf',
enabled: isOpen,
});
設計判断のまとめ
コア関数をReactから完全に分離しているのは、テストが jest + jsdom だけで書けるのと、サーバーサイド(Node 18+ の fetch)やWeb Worker からも使えるようにするためです。キャンセルは元コードの isCancelled フラグではなく標準の AbortController / AbortSignal に統一しています。これによりブラウザが fetch 自体を中断できるのでリソースも無駄になりません。フック側は enabled + refetch のパターンで TanStack Query に近い使用感にしているので、既存プロジェクトに馴染みやすいはずです。
ファイル名の自動推定(extractFileNameFromUrl)やリトライ、プログレスコールバックあたりは、必要に応じてオプションとして足していける構造になっています。
npm パッケージにするなら、プロジェクト固有の依存(
useEffectOnAny、UPLOAD_TYPE_NORMAL、PreparedUploadFileInfoなど)をすべて剥がして、純粋に「URLからFileオブジェクトを得る」という関心だけに絞る必要があります。パッケージ名のイメージ
fetch-url-to-fileあたりが素直です。エクスポートするもの
1. 純粋な非同期関数(コア)
React に依存しない関数で、Node.js やバニラJSからも使えるようにします。
2. カスタムエラークラス
呼び出し側が HTTP ステータスでハンドリングできるようにします。
3. React フック(サブパスエクスポート)
React を使うプロジェクト向けに
/reactエントリで提供します。package.json のエクスポート構成
{ "name": "fetch-url-to-file", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, "./react": { "import": "./dist/react.mjs", "require": "./dist/react.cjs", "types": "./dist/react.d.ts" } }, "peerDependencies": { "react": ">=17.0.0" // ./react を使う場合のみ必要 }, "peerDependenciesMeta": { "react": { "optional": true } } }利用イメージ
設計判断のまとめ
コア関数をReactから完全に分離しているのは、テストが
jest+jsdomだけで書けるのと、サーバーサイド(Node 18+ のfetch)やWeb Worker からも使えるようにするためです。キャンセルは元コードのisCancelledフラグではなく標準のAbortController/AbortSignalに統一しています。これによりブラウザが fetch 自体を中断できるのでリソースも無駄になりません。フック側はenabled+refetchのパターンで TanStack Query に近い使用感にしているので、既存プロジェクトに馴染みやすいはずです。ファイル名の自動推定(
extractFileNameFromUrl)やリトライ、プログレスコールバックあたりは、必要に応じてオプションとして足していける構造になっています。