Skip to content

fromFetch()

📘 RxJS公式ドキュメント - fromFetch

fromFetch() は、モダンな Fetch API をベースにした HTTP通信を Observable として扱うための Creation Function です。ajax() と比較して軽量で、最新のWeb標準に準拠しています。

基本的な使い方

シンプルなGETリクエスト

fromFetch() を使った最もシンプルな例は、URLを渡してレスポンスを手動でパースする方法です。

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const data$ = fromFetch('https://jsonplaceholder.typicode.com/todos/1').pipe(
  switchMap(response => {
    if (response.ok) {
      // レスポンスが成功の場合、JSONをパース
      return response.json();
    } else {
      // HTTPエラーの場合、エラーをスロー
      return throwError(() => new Error(`HTTP Error: ${response.status}`));
    }
  }),
  catchError(error => {
    console.error('エラー:', error);
    return of({ error: true, message: error.message });
  })
);

data$.subscribe({
  next: data => console.log('データ:', data),
  error: error => console.error('購読エラー:', error),
  complete: () => console.log('完了')
});

// 出力:
// データ: { userId: 1, id: 1, title: "delectus aut autem", completed: false }
// 完了

IMPORTANT

ajax() との重要な違い

  • fromFetch() は、HTTPエラー(4xx, 5xx)でも error コールバックを呼び出しません
  • レスポンスの ok プロパティを手動でチェックする必要があります
  • .json() などのパース処理も手動で行います

HTTPメソッド別の使い方

GET リクエスト

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

interface User {
  id: number;
  name: string;
  email: string;
}

const users$ = fromFetch('https://jsonplaceholder.typicode.com/users').pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json() as Promise<User[]>;
  })
);

users$.subscribe({
  next: users => console.log('ユーザー一覧:', users),
  error: error => console.error('エラー:', error)
});

POST リクエスト

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

interface CreateUserRequest {
  name: string;
  email: string;
}

interface CreateUserResponse {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

const newUser: CreateUserRequest = {
  name: 'Taro Yamada',
  email: 'taro@example.com'
};

const createUser$ = fromFetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify(newUser)
}).pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json() as Promise<CreateUserResponse>;
  })
);

createUser$.subscribe({
  next: user => console.log('作成成功:', user),
  error: error => console.error('作成失敗:', error)
});

PUT リクエスト

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

interface UpdateUserRequest {
  name: string;
  email: string;
}

const updatedUser: UpdateUserRequest = {
  name: 'Jiro Tanaka',
  email: 'jiro@example.com'
};

const updateUser$ = fromFetch('https://api.example.com/users/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(updatedUser)
}).pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  })
);

updateUser$.subscribe({
  next: user => console.log('更新成功:', user),
  error: error => console.error('更新失敗:', error)
});

DELETE リクエスト

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

const deleteUser$ = fromFetch('https://api.example.com/users/1', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer token123'
  }
}).pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    // DELETEは通常、空のレスポンスまたはステータスのみを返す
    return response.status === 204 ? of(null) : response.json();
  })
);

deleteUser$.subscribe({
  next: result => console.log('削除成功:', result),
  error: error => console.error('削除失敗:', error)
});

実践的なパターン

汎用的なHTTPエラーハンドリング関数

fromFetch() では手動でエラーチェックが必要なため、汎用関数を作成すると便利です。

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';

function fetchJSON<T>(url: string, options?: RequestInit): Observable<T> {
  return fromFetch(url, options).pipe(
    switchMap(response => {
      if (!response.ok) {
        throw new Error(`HTTP Error ${response.status}: ${response.statusText}`);
      }
      return response.json() as Promise<T>;
    })
  );
}

// 使用例
interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const todo$ = fetchJSON<Todo>('https://jsonplaceholder.typicode.com/todos/1');

todo$.subscribe({
  next: todo => console.log('Todo:', todo),
  error: error => console.error('エラー:', error)
});

HTTPステータスコードによる詳細な処理

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';
import { throwError } from 'rxjs';

const api$ = fromFetch('https://api.example.com/data').pipe(
  switchMap(response => {
    switch (response.status) {
      case 200:
        return response.json();
      case 204:
        // No Content - 空のレスポンス
        return of(null);
      case 401:
        throw new Error('認証が必要です');
      case 403:
        throw new Error('アクセスが拒否されました');
      case 404:
        throw new Error('リソースが見つかりません');
      case 500:
        throw new Error('サーバーエラーが発生しました');
      default:
        throw new Error(`予期しないHTTPステータス: ${response.status}`);
    }
  })
);

api$.subscribe({
  next: data => console.log('データ:', data),
  error: error => console.error('エラー:', error)
});

タイムアウトとリトライ

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap, timeout, retry } from 'rxjs/operators';

const api$ = fromFetch('https://api.example.com/slow-endpoint').pipe(
  timeout(5000), // 5秒でタイムアウト
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  }),
  retry(2) // 失敗時に2回リトライ
);

api$.subscribe({
  next: data => console.log('データ:', data),
  error: error => console.error('エラー:', error)
});

リクエストのキャンセル(AbortController)

fromFetch() は、Fetch APIの AbortController を使ったリクエストのキャンセルに対応しています。

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

const controller = new AbortController();
const signal = controller.signal;

const api$ = fromFetch('https://api.example.com/data', {
  signal // AbortController の signal を渡す
}).pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  })
);

const subscription = api$.subscribe({
  next: data => console.log('データ:', data),
  error: error => console.error('エラー:', error)
});

// 3秒後にリクエストをキャンセル
setTimeout(() => {
  controller.abort();
  // または subscription.unsubscribe();
}, 3000);

TIP

RxJSによる自動キャンセル

unsubscribe() を呼ぶだけで、RxJSが内部的に AbortController を使ってリクエストをキャンセルします。手動で AbortController を設定する必要はありません。

ユーザー入力に応じた検索(switchMap)

typescript
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { fromFetch } from 'rxjs/fetch';

interface SearchResult {
  id: number;
  title: string;
}

const searchInput = document.querySelector('#search') as HTMLInputElement;

const search$ = fromEvent(searchInput, 'input').pipe(
  map(event => (event.target as HTMLInputElement).value),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => {
    if (query.length === 0) {
      return of([]);
    }
    return fromFetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`).pipe(
      switchMap(response => {
        if (!response.ok) {
          throw new Error(`HTTP Error: ${response.status}`);
        }
        return response.json() as Promise<SearchResult[]>;
      })
    );
  })
);

search$.subscribe({
  next: results => console.log('検索結果:', results),
  error: error => console.error('検索エラー:', error)
});

複数のリクエストを並列実行

typescript
import { fromFetch } from 'rxjs/fetch';
import { forkJoin } from 'rxjs';
import { switchMap } from 'rxjs/operators';

interface User {
  id: number;
  name: string;
}

interface Post {
  id: number;
  title: string;
}

const users$ = fromFetch('https://jsonplaceholder.typicode.com/users').pipe(
  switchMap(response => response.json() as Promise<User[]>)
);

const posts$ = fromFetch('https://jsonplaceholder.typicode.com/posts').pipe(
  switchMap(response => response.json() as Promise<Post[]>)
);

forkJoin({
  users: users$,
  posts: posts$
}).subscribe({
  next: ({ users, posts }) => {
    console.log('Users:', users);
    console.log('Posts:', posts);
  },
  error: error => console.error('いずれかのリクエストが失敗:', error)
});

よくある使用例

1. 認証トークン付きリクエスト

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

function getAuthToken(): string {
  return localStorage.getItem('authToken') || '';
}

function fetchWithAuth<T>(url: string, options: RequestInit = {}): Observable<T> {
  return fromFetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${getAuthToken()}`,
      'Content-Type': 'application/json'
    }
  }).pipe(
    switchMap(response => {
      if (response.status === 401) {
        throw new Error('認証が必要です。再ログインしてください。');
      }
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }
      return response.json() as Promise<T>;
    })
  );
}

// 使用例
interface UserProfile {
  id: number;
  name: string;
  email: string;
}

const profile$ = fetchWithAuth<UserProfile>('https://api.example.com/profile');

profile$.subscribe({
  next: profile => console.log('プロフィール:', profile),
  error: error => console.error('エラー:', error)
});

2. ファイルのダウンロード(Blob)

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

const downloadFile$ = fromFetch('https://api.example.com/files/report.pdf').pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    // Blobとして取得
    return response.blob();
  })
);

downloadFile$.subscribe({
  next: blob => {
    // Blobからダウンロードリンクを生成
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'report.pdf';
    a.click();
    window.URL.revokeObjectURL(url);
    console.log('ダウンロード完了');
  },
  error: error => console.error('ダウンロードエラー:', error)
});

3. GraphQL クエリ

typescript
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';

interface GraphQLResponse<T> {
  data?: T;
  errors?: Array<{ message: string }>;
}

interface User {
  id: string;
  name: string;
  email: string;
}

function graphqlQuery<T>(query: string, variables?: any): Observable<T> {
  return fromFetch('https://api.example.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query, variables })
  }).pipe(
    switchMap(response => {
      if (!response.ok) {
        throw new Error(`HTTP Error: ${response.status}`);
      }
      return response.json() as Promise<GraphQLResponse<T>>;
    }),
    map(result => {
      if (result.errors) {
        throw new Error(result.errors.map(e => e.message).join(', '));
      }
      if (!result.data) {
        throw new Error('データが返されませんでした');
      }
      return result.data;
    })
  );
}

// 使用例
const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

const user$ = graphqlQuery<{ user: User }>(query, { id: '1' });

user$.subscribe({
  next: ({ user }) => console.log('ユーザー:', user),
  error: error => console.error('エラー:', error)
});

4. ページネーション付きAPI

typescript
import { fromFetch } from 'rxjs/fetch';
import { expand, takeWhile, reduce, switchMap } from 'rxjs/operators';

interface PaginatedResponse<T> {
  data: T[];
  page: number;
  totalPages: number;
}

function fetchAllPages<T>(baseUrl: string): Observable<T[]> {
  return fromFetch(`${baseUrl}?page=1`).pipe(
    switchMap(response => response.json() as Promise<PaginatedResponse<T>>),
    expand(response =>
      response.page < response.totalPages
        ? fromFetch(`${baseUrl}?page=${response.page + 1}`).pipe(
            switchMap(res => res.json() as Promise<PaginatedResponse<T>>)
          )
        : []
    ),
    takeWhile(response => response.page <= response.totalPages, true),
    reduce((acc, response) => [...acc, ...response.data], [] as T[])
  );
}

// 使用例
interface Item {
  id: number;
  name: string;
}

const allItems$ = fetchAllPages<Item>('https://api.example.com/items');

allItems$.subscribe({
  next: items => console.log('全アイテム:', items),
  error: error => console.error('エラー:', error)
});

fromFetch() のオプション

fromFetch() は、Fetch API の RequestInit オプションをそのまま使用できます。

typescript
interface RequestInit {
  method?: string;              // HTTPメソッド (GET, POST, PUT, DELETE等)
  headers?: HeadersInit;        // リクエストヘッダー
  body?: BodyInit | null;       // リクエストボディ
  mode?: RequestMode;           // cors, no-cors, same-origin
  credentials?: RequestCredentials; // omit, same-origin, include
  cache?: RequestCache;         // キャッシュモード
  redirect?: RequestRedirect;   // リダイレクト処理
  referrer?: string;            // リファラー
  integrity?: string;           // サブリソース整合性
  signal?: AbortSignal;         // AbortController のシグナル
}

ajax() vs fromFetch() の比較

機能ajax()fromFetch()
ベース技術XMLHttpRequestFetch API
自動JSONパースgetJSON()❌ 手動で .json()
自動HTTPエラー検出✅ 4xx/5xxで自動エラー❌ 手動で response.ok チェック
進捗監視
タイムアウト✅ ビルトイン❌ RxJSの timeout() で実装
リクエストキャンセル✅ unsubscribe()✅ unsubscribe() または AbortController
IE11対応❌ polyfill必要
バンドルサイズやや大きい小さい
Service Worker対応

TIP

使い分けのポイント

  • モダンブラウザのみ対応: fromFetch() を推奨
  • レガシーブラウザ対応が必要: ajax() を使用
  • 進捗監視が必要: ajax() を使用
  • 軽量なHTTP通信: fromFetch() が最適
  • Service Worker内での使用: fromFetch() のみ対応

よくあるエラーと対処法

1. HTTPエラーが error コールバックで捕捉されない

問題:

typescript
// ❌ 404エラーでも next が呼ばれる
fromFetch('https://api.example.com/not-found').subscribe({
  next: response => console.log('成功:', response), // ← 404でもこちらが呼ばれる
  error: error => console.error('エラー:', error)
});

対処法:

typescript
// ✅ response.ok を手動でチェック
fromFetch('https://api.example.com/not-found').pipe(
  switchMap(response => {
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    return response.json();
  })
).subscribe({
  next: data => console.log('データ:', data),
  error: error => console.error('エラー:', error) // ← こちらが呼ばれる
});

2. CORS エラー

対処法:

  • サーバー側でCORSヘッダーを設定
  • mode: 'cors' を明示的に指定
  • 開発時はプロキシサーバーを使用
typescript
fromFetch('https://api.example.com/data', {
  mode: 'cors',
  credentials: 'include' // Cookieを含める場合
});

3. タイムアウトの実装

Fetch APIにはタイムアウト機能がないため、RxJSの timeout() を使用します。

typescript
import { fromFetch } from 'rxjs/fetch';
import { timeout, switchMap } from 'rxjs/operators';

const api$ = fromFetch('https://api.example.com/slow').pipe(
  timeout(5000), // 5秒でタイムアウト
  switchMap(response => response.json())
);

ベストプラクティス

1. 汎用的なfetchJSON関数を作成

typescript
function fetchJSON<T>(url: string, options?: RequestInit): Observable<T> {
  return fromFetch(url, options).pipe(
    switchMap(response => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      return response.json() as Promise<T>;
    })
  );
}

2. TypeScriptの型を活用

typescript
// ✅ 良い例: 型を明示的に指定
interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const todo$ = fetchJSON<Todo>('https://jsonplaceholder.typicode.com/todos/1');

// ❌ 悪い例: 型指定なし
const todo$ = fromFetch('https://jsonplaceholder.typicode.com/todos/1')
  .pipe(switchMap(res => res.json()));

3. エラーハンドリングを必ず実装

typescript
// ✅ 良い例: response.ok とcatchError
const api$ = fromFetch('/api/data').pipe(
  switchMap(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }),
  catchError(error => {
    console.error('エラー:', error);
    return of(defaultValue);
  })
);

4. 購読解除を忘れない

typescript
// ✅ 良い例: takeUntil で自動解除
class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    fromFetch('/api/data')
      .pipe(
        switchMap(res => res.json()),
        takeUntil(this.destroy$)
      )
      .subscribe(...);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

まとめ

fromFetch() は、モダンな Fetch API をベースにした軽量なHTTP通信の Creation Function です。

主な特徴:

  • Fetch APIベースで、最新のWeb標準に準拠
  • 軽量でバンドルサイズが小さい
  • Service Worker内でも使用可能
  • 手動でのエラーチェックとレスポンスパースが必要

使用場面:

  • モダンブラウザのみをサポートする場合
  • バンドルサイズを小さくしたい場合
  • Service Worker内でHTTP通信を行う場合
  • Fetch APIの機能(Request/Responseオブジェクトなど)を直接使いたい場合

注意点:

  • HTTPエラーでも error コールバックは呼ばれない(手動で response.ok をチェック)
  • JSONパースは手動で行う(response.json()
  • 進捗監視は非対応
  • IE11などレガシーブラウザでは polyfill が必要

推奨される使い方:

  • 汎用的な fetchJSON() 関数を作成して再利用
  • TypeScriptの型を活用して型安全性を確保
  • 必ずエラーハンドリングを実装
  • 不要になったら必ず購読解除

関連ページ

参考リソース

Released under the CC-BY-4.0 license.