---
title: "ブログにいいねボタンを実装した"
published: 2026-02-01
tags: [React, Astro, TypeScript, Cloudflare Workers]
author: kubosho
---
# ブログにいいねボタンを実装した
このブログにいいねボタンを実装しました。以下の構成で実装しています。
```text
LikeButton (UI)
↓
React custom hooks (SWRフック、楽観的更新、デバウンス + バッファリング)
↓
POST /api/likes/[id] (Astro APIエンドポイント)
↓
PostgreSQL
```
フロントエンドはReactコンポーネントとカスタムフックを組み合わせて、APIリクエストとUI更新を担い、バックエンドはAstroのAPI Routesを使っていいね周りのAPIを実装し、PostgreSQLを使ってデータを永続化しています。
## なぜ実装したのか
ブログ記事を読んだ人から反応が欲しかったためです。
いいね機能は、コメント機能と比較して、読者が手軽に反応を示せる手段だと考えています。読者は「参考になった」「面白かった」という気持ちをワンクリックで伝えられます。また、書き手である自分にとっても、どの記事が読者に響いているかを把握する指標になる可能性があると考えました。
### いいねボタンを連打可能にした理由
実装にあたっては、ログイン不要で読者が何度でもいいねできる仕様にしました。気軽に連打できる体験をさせたかったのと、個人ブログにわざわざログインをする人はいないと考えたためです。
### 完璧を求めない判断
また「いいね数が多少失われても問題ない」という前提で設計しました。
たとえば、いいねのリクエストのリトライキューの実装にlocalStorageではなくsessionStorageを使っています。いいねの送信や反映が失敗して無かったことになったとしても、不要なリトライキューがブラウザーに残り続け、リクエストが繰り返されてしまうことを防ぐようにしました。
## フロントエンドの実装
まずはフロントエンドの実装から見ていきましょう。
### LikeButtonコンポーネント
いいねボタン用のコンポーネントは、React組み込みのフックやカスタムフックを組み合わせて、拍手アイコンといいね数を表示している構成です。
Astroのコンポーネントでコンポーネントを作らなかった理由は、[Share state between Astro components | Docs](https://docs.astro.build/en/recipes/sharing-state/)というドキュメントを見て、Astroで状態を管理するためにNano Storesというライブラリを導入しないといけないことを知り、それが若干面倒に感じたためです。
では実装を見ていきましょう。
```tsx
export function LikeButton({ entryId, likeLabel, onClick }: Props): React.JSX.Element {
// 状態・カスタムフック定義
const [clapping, setClapping] = useState(false);
const { counts, handleLikes, isLoading } = useLikes({ entryId });
// ...
```
`clapping` はクリック時に表示するアニメーション用の状態です。`useLikes` フックではいいねの数と操作用のハンドラーを定義しています。
```tsx
// ...
const handleClick = useCallback(() => {
handleLikes();
setClapping(false);
requestAnimationFrame(() => {
setClapping(true);
});
onClick?.();
}, [handleLikes, onClick]);
// ...
```
クリック時に `requestAnimationFrame` を使い、アニメーションが毎回実行されるようにしています。
```tsx
// ...
return (
{counts}
);
}
```
いいね数を表示する要素に対して `aria-live="polite"` を指定することで、いいね数をスクリーンリーダーで読まれるようにしています。
### 楽観的UI更新
ユーザーがいいねボタンをクリックした瞬間に、サーバーからのレスポンスを待たずにUIを更新しています。
データ取得にはSWRを使っています。TanStack Queryを使うことも考えましたが、今回はいいね数の取得とUIの更新に使うだけなので、APIがシンプルなSWRで十分と判断しました。
まずSWRを使っていいね数を取得し、バッファリング用のカスタムフックを読み込みます。
```tsx
export function useLikes({ entryId }: UseLikeParams): UseLikeReturn {
const { data, isLoading, mutate } = useSWR(
`/api/likes/${entryId}`,
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
const { updateLikeCounts } = useLikesBuffer();
// ...
```
タブがフォーカスされた時やネットワーク再接続時の自動再検証は必要ないので、`useSWR` のオプションに `revalidateOnFocus: false` と `revalidateOnReconnect: false` を指定しています。
いいねボタンがクリックされた時のハンドラー定義は以下の通りです。
```tsx
// ...
const handleLikes = useCallback(() => {
// 即座にUI更新
void mutate({
id: entryId,
counts: countsRef.current + 1
}, { revalidate: false });
// バッファに追加
updateLikeCounts(entryId, 1);
}, [entryId, mutate, updateLikeCounts]);
return { counts: countsRef.current, handleLikes, isLoading };
}
```
SWRの `mutate` 関数を使ってローカルのキャッシュを即座に更新しています。`revalidate: false` でサーバーへの再検証リクエストを抑制しています。同時に `updateLikeCounts` でバッファにいいねの回数を追加し、後でまとめてリクエストをするようにしています。
### バッファリングによるリクエスト最適化
いいねボタンを連打した場合、ボタンを押した回数分そのままAPIリクエストした場合に、無駄なリクエストが増え、サーバーとユーザーどちらにも嬉しくない事態になります。そこで一定時間内はいいねボタンの押した回数をバッファリングしておき、いいねを押した回数だけいいね数を加算してリクエストを送信するようにしています。
以下のコードでは、バッファリング用の定数とrefを定義しています。
```tsx
const FLUSH_TIMER = 1_000;
export function useLikesBuffer(): UseLikeBufferReturn {
const bufferedIncrementsRef = useRef