はじめに
ページネーション、ローディング状態管理、キャッシュ、トークンの自動更新——API通信まわりのコードは、機能が増えるたびに気づけば同じようなロジックがあちこちに散らばっていきます。axiosやfetchを薄くラップしただけの自前フックが、プロジェクトごとに少しずつ違う形で量産されていく経験がある方も多いのではないでしょうか。
そんな「リクエスト周りの車輪の再発明」に終止符を打つのが、今回紹介するAlovaです。単なるHTTPクライアントではなく、リクエストにまつわる典型的な処理をあらかじめ戦略として用意してくれる「リクエスト戦略レイヤー」というのが最大の特徴です。
説明を読むより、実際にcreateAlovaでインスタンスを作りuseRequestでAPIを呼んでみる方が早いと思います。先に挙動を確認したい方はこちらからどうぞ。
Alovaとは
Alovaは「The request strategy layer for JavaScript」を掲げるライブラリで、20以上の組み込み戦略によってリクエスト関連のコード量を最大70%削減できるとされています。React・Vue・Svelte・Angular・Solidなど15以上のフレームワークに対応し、Next.jsやNuxt 3のようなSSR環境、さらにNode.js・Bun・Denoといったサーバーサイドやミニプログラムまでカバーする、フレームワーク非依存の設計になっています。
主な特徴
- 豊富な組み込み戦略 - ページネーション、データ監視リクエスト、プリロード、フォーム送信、トークン自動更新など、よくあるUIパターンをフックとして提供
- マルチフレームワーク対応 - React・Vue・Svelte・Angular・Solid・Preact・Qwik・Litなど、使っているUIライブラリを問わず同じ書き方で使える
- サーバーサイド戦略も内蔵 - リトライ、レート制限、分散ロックによるアトミック処理など、バックエンドで使える戦略も用意されている
- 柔軟なキャッシュ機構 - メモリ・永続化キャッシュを設定でき、同一リクエストの重複実行を抑制できる
インストール
npmを使う場合は以下のコマンドでインストールできます。
npm i alova
Yarnやpnpmを使っている場合も同様です。
yarn add alova
# または
pnpm add alova
利用するUIフレームワーク用のアダプター(例: alova/react、alova/vue)も併せて必要になります。
Alovaのサンプルを動かす
以下は、createAlovaでインスタンスを作成し、useRequestフックでダミーAPI(jsonplaceholder)からTodoを1件取得するサンプルです。immediate: falseを指定しているため、コンポーネントがマウントされた直後は何も起きず、「取得」ボタンを押した瞬間にsend(id)が呼ばれて初めて通信が走ります。IDを書き換えてからボタンを押すと、その値がuseRequestのハンドラー関数(id => getTodo(id))へそのまま渡り、別のTodoが取得できます。
const alovaInstance = createAlova({
baseURL: 'https://jsonplaceholder.typicode.com',
requestAdapter: adapterFetch(),
statesHook: ReactHook,
responded: response => response.json()
})
const getTodo = id => alovaInstance.Get(`/todos/${id}`)
// immediate: false にすると send() を呼ぶまでリクエストは発生しない
const { loading, data, error, send } = useRequest(id => getTodo(id), {
immediate: false
})
// ボタン押下時に send(id) で引数付きリクエストを発火
<button onClick={() => send(id)}>取得</button>
実際に動かせるサンプルが以下です。IDを1〜200の範囲で書き換えて「取得」を押してみてください。
loadingがtrueの間はボタンのラベルが「取得中...」に切り替わり、リクエストの状態がひと目で分かります。send()にはこのように任意の引数を渡せるので、フォームの入力値やクリックしたアイテムのIDをそのままハンドラー関数に届けるのに使えます。immediate: falseを外せば、コンポーネントの初回描画時に自動でリクエストが送られる通常のuseRequestの挙動に戻ります。
基本的な使い方
まずはAlovaのインスタンスを作成し、APIエンドポイントを定義します。
import { createAlova } from 'alova';
import adapterFetch from 'alova/fetch';
import ReactHook from 'alova/react';
export const alovaInstance = createAlova({
baseURL: '/api',
requestAdapter: adapterFetch(),
statesHook: ReactHook,
responded: response => response.json()
});
// リクエストメソッドの定義
export const getTodos = () => alovaInstance.Get('/todos');
Reactコンポーネント内では、useRequestフックでリクエストを実行し、ローディングやエラーの状態をまとめて受け取れます。
import { useRequest } from 'alova/client';
import { getTodos } from './api';
function TodoList() {
const { loading, data, error } = useRequest(getTodos);
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラーが発生しました</p>;
return (
<ul>
{data?.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
useRequestを使うだけで、キャッシュの有無に応じたリクエストの重複排除や再実行のハンドリングが自動的に行われます。
実践的なユースケース
ページネーション付き一覧の実装
一覧画面でありがちなページネーション処理も、usePaginationを使えばわずかな行数で実装できます。
import { usePagination } from 'alova/client';
import { alovaInstance } from './alova';
const getTodoList = (page, pageSize) =>
alovaInstance.Get('/todos', {
params: { page, pageSize }
});
function TodoPage() {
const { loading, data, page, pageCount, total, onNext, onPrev } =
usePagination(getTodoList, {
initialPage: 1,
initialPageSize: 10
});
return (
<div>
{loading ? '読み込み中...' : data.map(item => <div key={item.id}>{item.title}</div>)}
<button onClick={onPrev} disabled={page <= 1}>前へ</button>
<span>{page} / {pageCount}(全{total}件)</span>
<button onClick={onNext}>次へ</button>
</div>
);
}
ページ数の計算やローディング状態の管理を自前で書く必要がなくなり、UIロジックに専念できます。
AlovaのusePaginationサンプルを動かす
jsonplaceholderの/todosエンドポイントは_page・_limitのクエリパラメータでページ分割に対応しており、全件数はレスポンスヘッダーのX-Total-Countに入っています。このようにレスポンスの形がAlovaの既定(data・totalというフィールド名)と違う場合は、usePaginationのtotal・dataオプションでレスポンスのどこを見ればよいかを教えてあげます。
const getTodoList = (page, pageSize) =>
alovaInstance.Get('/todos', { params: { _page: page, _limit: pageSize } })
const { loading, data, page, pageCount, total, onNext, onPrev } = usePagination(
getTodoList,
{
initialPage: 1,
initialPageSize: 5,
total: res => res.total, // レスポンスの total フィールドを件数として使う
data: res => res.list // レスポンスの list フィールドを一覧として使う
}
)
「次へ」「前へ」を押すたびにpageが変わり、そのつどusePaginationが新しいページ分のリクエストを自動で送ります。
initialPageSizeを5から10に変えると1ページあたりの件数が増え、pageCount(総ページ数)も自動的に再計算されます。onNext・onPrevはページ番号の増減とリクエストの再送を1つにまとめたショートカットなので、ページ送りボタンを押すたびに自前でページ番号を計算し直す必要がありません。
検索条件変更時の自動再取得
検索キーワードやフィルター条件が変わるたびにリクエストを送りたいケースでは、useWatcherが便利です。
import { useWatcher } from 'alova/client';
import { useState } from 'react';
function SearchResults() {
const [keyword, setKeyword] = useState('');
const { loading, data } = useWatcher(
() => alovaInstance.Get('/search', { params: { keyword } }),
[keyword],
{ debounce: [500] } // 500ms のデバウンスで無駄なリクエストを抑制
);
return (
<div>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="検索キーワードを入力"
/>
{loading ? '検索中...' : data?.map(item => <p key={item.id}>{item.title}</p>)}
</div>
);
}
依存する値が変化するたびにデバウンス付きでリクエストが実行されるため、検索ボックスの実装にありがちな「入力のたびにAPIが飛んでしまう」問題をシンプルに解決できます。
AlovaのuseWatcherサンプルを動かす
ここではjsonplaceholderの/postsエンドポイントが持つ全文検索パラメータqを使い、キーワードに一致する投稿を検索します。searchPosts関数でAlovaのGetメソッドを組み立て、useWatcherの第2引数(監視対象の配列)にkeywordを渡すことで、値が変わるたびに自動で再リクエストされるようにしています。
const searchPosts = keyword =>
alovaInstance.Get('/posts', { params: { q: keyword } })
const { loading, data = [] } = useWatcher(
() => searchPosts(keyword),
[keyword],
{ debounce: [500], immediate: true }
)
入力欄にキーワードを打ち込んでみてください。500ミリ秒入力が止まったタイミングでuseWatcherがリクエストを送り直します。
キーワードをofficiaのような単語に変えると、titleにその単語を含む投稿だけへ即座に絞り込まれるのが分かります。空欄にするとqパラメータが空文字になり全件が返るので、debounceの秒数やqをjson-server互換のtitle_likeなどに差し替えて挙動を比較してみるのもよいでしょう。
まとめ
Alovaは、リクエストにまつわる「よくある処理」を戦略として切り出すことで、API通信まわりのコードを大幅にシンプルにしてくれるライブラリです。ページネーションや検索の自動再取得だけでなく、フォーム送信管理やトークン自動更新といった戦略も用意されているため、実務でありがちなユースケースの多くをカバーできます。
React QueryやSWRのようなデータフェッチライブラリに近い立ち位置ですが、フレームワーク非依存かつサーバーサイド戦略まで内蔵している点がAlovaならではの強みです。次にAPI通信のコードが肥大化してきたと感じたら、一度試してみてはいかがでしょうか。
