React・外のデータとつなぐ

カスタムフックにまとめる

お気に入りをlocalStorageに保存してリロードしても残るようにし、その処理をカスタムフックへ切り出します。useで始める名前の意味、状態は共有されないこと、切り出す判断のしかたまで扱います。

いまのお気に入りには、大きな弱点があります。リロードすると全部消えます。

stateはメモリの上の値なので、ページを読み直せば初期値に戻ります。ブラウザに残すにはlocalStorageを使います。

JavaScriptブラウザに覚えさせる:localStoragelocalStorageそのものの説明はこちら。文字列しか保存できないところまで含めて、考え方はReactでも同じです

まず素直に書いてみる

App.jsxに直接書くと、こうなります。

// 初期値をlocalStorageから読む
const [favoriteIds, setFavoriteIds] = useState(() => {
  const saved = localStorage.getItem('zukan-favorites')
  return saved ? JSON.parse(saved) : []
})

// 変わるたびに保存する
useEffect(() => {
  localStorage.setItem('zukan-favorites', JSON.stringify(favoriteIds))
}, [favoriteIds])

2つ、初めて出てくる書き方があります。

useStateに関数を渡す

useState(() => { … })

useStateの初期値に関数を渡すと、Reactは最初の1回だけそれを実行します。

useState(localStorage.getItem(…))と書いてしまうと、再描画のたびにlocalStorageを読みにいきます(使われるのは最初の値だけなのに)。読みこみのように少し重い処理は、関数の形で渡します。

依存配列に値を書く

useEffect(() => { … }, [favoriteIds])

前回は[](最初の1回だけ)でしたが、今回は[favoriteIds]お気に入りが変わるたびに保存したいからです。

App.jsxが太ってきた

上のコードを足すと、Appはこうなります。

  • 鳥のデータのstate
  • 読みこみ中のstate
  • 検索語のstate
  • お気に入りのstate
  • 「お気に入りだけ」のstate
  • fetchのEffect
  • 保存のEffect
  • お気に入りの追加削除の関数

画面の組み立てを読みにきたのに、保存の話が混ざって読みにくい——このときが切り出しどきです。

カスタムフック=Hooksを使う関数

「お気に入りに関すること」だけを別のファイルへ移します。

import { useEffect, useState } from 'react'

const STORAGE_KEY = 'zukan-favorites'

export function useFavorites() {
  const [favoriteIds, setFavoriteIds] = useState(() => {
    const saved = localStorage.getItem(STORAGE_KEY)
    return saved ? JSON.parse(saved) : []
  })

  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(favoriteIds))
  }, [favoriteIds])

  function toggleFavorite(id) {
    setFavoriteIds((current) =>
      current.includes(id)
        ? current.filter((favoriteId) => favoriteId !== id)
        : [...current, id],
    )
  }

  return { favoriteIds, toggleFavorite }
}

使う側は2行になります。

import { useFavorites } from './hooks/useFavorites.js'

const { favoriteIds, toggleFavorite } = useFavorites()

中身は、さっきまでApp.jsxにあったコードそのままです。移しただけで、新しい概念は何も足していません。

名前はuseで始める

export function useFavorites() { … }    // ○
export function getFavorites() { … }    // ✕ 中でHooksは呼べない

useで始まる名前は、Reactと開発ツールにとって「これはHooksを含む」という合図です。この名前でないと、

  • Hooksを条件の中で呼んでいても警告してもらえない
  • 読む人が「ふつうの関数」だと思って、ifの中で呼んでしまう

という事故が起きます。中でHooksを呼ぶなら、必ずuseから始める。これは決まりだと思ってください。

状態は共有されない

よくある勘違いです。

// A.jsx
const { favoriteIds } = useFavorites()

// B.jsx
const { favoriteIds } = useFavorites()   // ← Aとは別の状態

カスタムフックが共有するのは処理(ロジック)であって、状態そのものではありません。呼んだ場所ごとに独立したstateが作られます。

前回の「stateの持ち上げ」と組み合わせて、共通の親で1回だけ呼び、propsで配る——今回もその形にします。

公式ドキュメントも見てみる

ja.react.dev外部サイトカスタムフックでロジックを再利用する – React公式の「カスタムフックでロジックを再利用する」。「カスタムフックはstateそのものではなくstateを扱うロジックを共有する」の節が、この回のいちばん大事なところです

このレッスンのまとめ

  1. stateはリロードで消える。残すならlocalStorage(保存できるのは文字列だけ)
  2. useState(() => …)で初期値の計算を最初の1回だけにする
  3. 依存配列は「いつやり直すか」——[favoriteIds]なら値が変わるたびに保存
  4. カスタムフックはuseで始まる、Hooksを使うただの関数
  5. 共有されるのは処理であって状態ではない——呼んだ場所ごとに独立する
  6. 切り出すのは役目に名前が付くとき。無理に細かくしない

次からは章が変わって、ページを増やします。一覧と詳細をURLで分けるところから。

やってみよう:お気に入りを保存する

お気に入りの処理をカスタムフックに切り出し、リロードしても残るようにします。

  1. src/hooks/フォルダを作り、useFavorites.jsを上のコードで作る
  2. App.jsxからfavoriteIdsのstateとtoggleFavorite関数を消す
  3. 代わりにconst { favoriteIds, toggleFavorite } = useFavorites()を書く
  4. ブラウザで星をいくつか付け、リロードして残っているかを確認する

この時点のフォルダはこうなります。

shima-zukan/
├── node_modules/
├── public/
│   └── birds.json
├── src/
│   ├── components/
│   │   └── BirdCard.jsx
│   ├── hooks/
│   │   └── useFavorites.js   ← 今回作る
│   ├── App.jsx
│   ├── index.css
│   └── main.jsx
├── index.html
├── package.json
└── vite.config.js
8羽のカードが並び、シマエナガ・フクロウ・ハクチョウの3羽に★が付いている。上部に「全8羽のうち、お気に入りは3羽」と表示されている
これはリロードしたあとの画面です。★が3つ残っています

開発者ツールのApplication(アプリケーション)タブでLocal Storageを開くと、zukan-favoritesというキーに[1,4,8]のような配列が入っているのが見えます。

よくある質問

カスタムフックとは何ですか?
Hooksを使う処理を、useで始まる名前の関数にまとめたものです。コンポーネントから状態まわりのロジックだけを切り出して、他の部品でも使い回せるようにします。中身はふつうのJavaScriptの関数です。
カスタムフックを2つのコンポーネントで使うと、状態は共有されますか?
されません。呼んだ場所ごとに独立した状態が作られます。共有したいときは、共通の親で1回だけ呼んでpropsで配るか、Contextを使います。
なぜ名前をuseで始めるのですか?
Reactと開発ツールが「これはHooksを含む関数だ」と判断するための約束だからです。名前がuseで始まっていないと、Hooksのルール違反を警告してもらえなくなります。
useStateの初期値に関数を渡すのはなぜですか?
初期値の計算を最初の1回だけにするためです。localStorageの読みこみのように毎回やると無駄になる処理は、関数の形で渡すと再描画のたびに実行されなくなります。