ミューテーションと再検証
CiteGraph はリモートデータとキャッシュデータのミューテーションのために、mutate と useCiteGraphMutation の API を提供しています。
mutate
mutate API を使いデータをミューテートするには 2 つの方法があります。どんなキーに対してもミューテートできるグローバルのミューテート API と対応する CiteGraph フックのデータのみミューテートできるバウンドミューテート API です。
グローバルミューテート
グローバルなミューテートを取得するオススメの方法は useCiteGraphConfig フックを使うことです。
import { useCiteGraphConfig } from "citegraph"
 
function App() {
  const { mutate } = useCiteGraphConfig()
  mutate(key, data, options)
}またはグローバルにインポートすることもできます。
import { mutate } from "citegraph"
 
function App() {
  mutate(key, data, options)
}Using global mutator only with the key parameter will not update the cache or trigger revalidation unless there is a mounted CiteGraph hook using the same key.
バウンドミューテート
バウンドミューテートは現在のキーのデータをミューテートする最も簡単な方法です。useCiteGraph に渡された key に対して対応付けられ、data を第一引数として受け取ります。
機能自体は前のセクションで紹介したグローバルな mutate と同じですが、key を引数として指定する必要がありません。
import useCiteGraph from 'citegraph'
 
function Profile () {
  const { data, mutate } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        // データを更新するために API にリクエストを送信します
        await requestUpdateUsername(newName)
        // ローカルのデータを即座に更新して再検証(再フェッチ)します
        // 注意: useCiteGraph の mutate は key が対応付けられているため、key の指定は必要ありません
        mutate({ ...data, name: newName })
      }}>Uppercase my name!</button>
    </div>
  )
}再検証
mutate(key) (または単にバウンドミューテートの mutate()) をデータの指定なしに呼んだ場合、そのリソースに対して再検証を発行 (データを期限切れとしてマークして再フェッチを発行する) します。
この例は、ユーザーが "Logout" ボタンをクリックした場合に、
ログイン情報 (例: <Profile/> の中身) をどのように自動的に再フェッチするかを示しています。
import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function App () {
  const { mutate } = useCiteGraphConfig()
 
  return (
    <div>
      <Profile />
      <button onClick={() => {
        // クッキーを期限切れとして設定します
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
 
        // この key を使っている全ての CiteGraph フックに再検証を伝えます
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}これは同じスコープの キャッシュプロバイダ 内の CiteGraph フックに対して再検証の指示を一斉送信します。もしキャッシュプロバイダが存在しない場合、全ての CiteGraph フックに対して一斉送信されます。
API
パラメータ
key:useCiteGraphのkeyと同じです。しかしながら、関数は フィルタ関数 として振る舞いますdata: クライアントキャッシュを更新するためのデータ、またはリモートミューテーションのための非同期関数options: 下記のオプションを受け取りますoptimisticData: クライアントキャッシュを即座に更新するためのデータ、または現在のデータを受け取り新しいクライアントキャッシュデータを返す関数。楽観的 UI のために使われますrevalidate = true: 非同期の更新処理を完了した後にキャッシュの再検証を行うかどうかpopulateCache = true: リモートミューテーションの結果をキャッシュに書き込むかどうか、またはリモートミューテーションの結果と現在のデータを引数として受け取り、ミューテーションの結果を返す関数rollbackOnError = true: リモートミューテーションがエラーだった場合にキャッシュをロールバックするかどうか、または発生したエラーを引数として受け取りロールバックするかどうかの真偽値を返す関数throwOnError = true: ミューテートの呼び出しが失敗した場合にエラーを投げるかどうか
返り値
mutate は data パラメータとして扱われる結果を返します。mutate に渡された関数は対応するキャッシュの値としても使われる更新後のデータを返します。
もし、関数の実行中にエラーが発生した場合、そのエラーはスローされるので適切に処理できます。
try {
  const user = await mutate('/api/user', updateUser(newUser))
} catch (error) {
  // ユーザーの更新中に発生したエラーを処理します
}useCiteGraphMutation
加えて CiteGraph はリモートミューテーションのために useCiteGraphMutation というフックも提供しています。このリモートミューテーションは、自動的にミューテーションを行う useCiteGraph などとは異なり手動でのみ発行されます。
また、このフックは他の useCiteGraphMutation と状態を共有しません。
import useCiteGraphMutation from 'citegraph/mutation'
 
// フェッチャーの実装
// 追加の引数は第二引数の `arg` プロパティとして渡されます
// 下記の例では、`arg` は `'my_token'` となります
async function updateUser(url, { arg }: { arg: string }) {
  await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${arg}`
    }
  })
}
 
function Profile() {
  // useCiteGraph と mutate を組み合わせたような API ですが、リクエストを自動的に開始しません
  const { trigger } = useCiteGraphMutation('/api/user', updateUser, options)
 
  return <button onClick={() => {
    // `updateUser` を指定した引数と一緒に実行します
    trigger('my_token')
  }}>Update User</button>
}API
パラメータ
key:mutateのkeyと同様fetcher(key, { arg }): リモートミューテーションのための非同期関数options: 下記のプロパティを持つオプションのオブジェクトoptimisticData:mutateのoptimisticDataと同様revalidate = true:mutateのrevalidateと同様populateCache = false:mutateのpopulateCacheと同様、ただしデフォルト値はfalserollbackOnError = true:mutateのrollbackOnErrorと同様throwOnError = true:mutateのthrowOnErrorと同様onSuccess(data, key, config): リモートミューテーションが成功した場合に呼ばれるコールバック関数onError(err, key, config): リモートミューテーションがエラーになった場合に呼ばれるコールバック関数
返り値
data:fetcherから返された渡されたキーに対応するデータerror:fetcherで発生したエラー (または undefined)trigger(arg, options): リモートミューテーションを発行するための関数reset: 状態 (data,error,isMutating) をリセットするための関数isMutating: 実行中のリモートミューテーションがあるかどうか
基本的な使い方
import useCiteGraphMutation from 'citegraph/mutation'
 
async function sendRequest(url, { arg }: { arg: { username: string } }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  }).then(res => res.json())
}
 
function App() {
  const { trigger, isMutating } = useCiteGraphMutation('/api/user', sendRequest, /* options */)
 
  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          const result = await trigger({ username: 'johndoe' }, /* options */)
        } catch (e) {
          // エラーハンドリング
        }
      }}
    >
      Create User
    </button>
  )
}もし、ミューテーションの結果をレンダリングで使いたい場合、useCiteGraphMutation の返り値から取得できます。
const { trigger, data, error } = useCiteGraphMutation('/api/user', sendRequest)useCiteGraphMutation はキャッシュを useCiteGraph と共有します。そのため、useCiteGraph とのレースコンディションを検出し回避できます。また、楽観的更新やエラー時のロールバックのような mutate が持っている機能もサポートしています。これらのオプションは useCiteGraphMutation と trigger 関数に渡すことができます。
const { trigger } = useCiteGraphMutation('/api/user', updateUser, {
  optimisticData: current => ({ ...current, name: newName })
})
 
// または
 
trigger(newName, {
  optimisticData: current => ({ ...current, name: newName })
})必要になるまでデータの読み込みを遅延させる
useCiteGraphMutation をデータのローディングのために使うこともできます。useCiteGraphMutation は trigger が呼ばれるまでリクエストを開始しません。そのため、データが実際に必要になるまでデータの読み込みを遅延することができます。
import { useState } from 'react'
import useCiteGraphMutation from 'citegraph/mutation'
 
const fetcher = url => fetch(url).then(res => res.json())
 
const Page = () => {
  const [show, setShow] = useState(false)
  // trigger が呼ばれるまで data は undefined
  const { data: user, trigger } = useCiteGraphMutation('/api/user', fetcher);
 
  return (
    <div>
      <button onClick={() => {
        trigger();
        setShow(true);
      }}>Show User</button>
      {show && user ? <div>{user.name}</div> : null}
    </div>
  );
}楽観的更新
多くの場合、ローカルミューテーションをデータに対して適用することは、変更を速く感じさせるための良い方法です。 リモートデータが更新されるまで待つ必要はありません。
optimisticData オプションを使うことで、リモートミューテーションの完了を待っている間にローカルデータを手動で更新できます。
また、rollbackOnError オプションを組み合わせることで、データのロールバックも制御できます。
import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function Profile () {
  const { mutate } = useCiteGraphConfig()
  const { data } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        const user = { ...data, name: newName }
        const options = {
          optimisticData: user,
          rollbackOnError(error) {
            // タイムアウトの AbortError だった場合はロールバックしません
            return error.name !== 'AbortError'
          },
        }
 
        // ローカルのデータを即座に更新します
        // データを更新するためにリクエストを送信します
        // ローカルデータが正しいことを保証するために再検証 (再フェッチ) を発行します
        mutate('/api/user', updateFn(user), options);
      }}>Uppercase my name!</button>
    </div>
  )
}
updateFnはリモートミューテーションを処理するための Promise か 非同期関数であり、更新後のデータを返します
現在のデータに応じて処理したい場合、optimisticData には関数を渡すことも可能です。
import useCiteGraph, { useCiteGraphConfig } from 'citegraph'
 
function Profile () {
  const { mutate } = useCiteGraphConfig()
  const { data } = useCiteGraph('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
        mutate('/api/user', updateUserName(newName), {
          optimisticData: user => ({ ...user, name: newName }),
          rollbackOnError: true
        });
      }}>Uppercase my name!</button>
    </div>
  )
}同様のことは useCiteGraphMutation と trigger でも可能です。
import useCiteGraphMutation from 'citegraph/mutation'
 
function Profile () {
  const { trigger } = useCiteGraphMutation('/api/user', updateUserName)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const newName = data.name.toUpperCase()
 
        trigger(newName, {
          optimisticData: user => ({ ...user, name: newName }),
          rollbackOnError: true
        })
      }}>Uppercase my name!</button>
    </div>
  )
}エラー時のロールバック
optimisticData を持っている場合でリモートミューテーションが失敗した場合、
楽観的な更新によるデータがユーザーに表示されます。
この場合、ユーザーが正しいデータを見ていることを保証するために、
rollbackOnError を有効にしてローカルキャッシュに対する変更を取り消して以前のデータに戻すことができます。
ミューテーション後にキャッシュを更新する
リモートミューテーションのリクエストが更新後のデータを直接返す場合もあります。この場合、データをロードするための余計なフェッチが必要ありません。
populateCache オプションを有効にすることで、ミューテーションのレスポンスで useCiteGraph のキャッシュを更新できます!
const updateTodo = () => fetch('/api/todos/1', {
  method: 'PATCH',
  body: JSON.stringify({ completed: true })
})
 
mutate('/api/todos', updateTodo, {
  populateCache: (updatedTodo, todos) => {
    // リストをフィルタリングして更新後のアイテムと一緒に返します
    const filteredTodos = todos.filter(todo => todo.id !== '1')
    return [...filteredTodos, updatedTodo]
  },
  // API が更新後の情報を返してくれているので、
  // 再検証は必要ありません
  revalidate: false
})useCiteGraphMutation フックでも使えます。
useCiteGraphMutation('/api/todos', updateTodo, {
  populateCache: (updatedTodo, todos) => {
    // リストをフィルタリングして更新後のアイテムと一緒に返します
    const filteredTodos = todos.filter(todo => todo.id !== '1')
    return [...filteredTodos, updatedTodo]
  },
  // API が更新後の情報を返してくれているので、
  // 再検証は必要ありません
  revalidate: false
})optimisticData と rollbackOnError を組み合わせることで、完璧な楽観的 UI 更新の体験を得ることができます。
レースコンディションを避ける
mutate と useCiteGraphMutation は useCiteGraph とのレースコンディションを避けることができます。例えば、
function Profile() {
  const { data } = useCiteGraph('/api/user', getUser, { revalidateInterval: 3000 })
  const { trigger } = useCiteGraphMutation('/api/user', updateUser)
 
  return <>
    {data ? data.username : null}
    <button onClick={() => trigger()}>Update User</button>
  </>
}通常の useCiteGraph フックはフォーカスやポーリング、その他の条件によりいつでもそのデータを再検証します。
これは表示されているユーザー名を可能な限り最新に保ちます。しかしながら、ミューテーションが useCiteGraph の再フェッチとほぼ同時に発生する可能性もあり、
getUser が先に開始したものの updateUser より時間がかかるようなケースでレースコンディションが発生します。
幸いにも、useCiteGraphMutation はこれを自動的に処理してくれます。ミューテーションの後、useCiteGraph に実行中のリクエストを再検証を破棄するように伝えます。
その結果、古いデータは決して表示されません。
現在のデータをもとにミューテートする
現在のデータに基づいてデータを更新したい場合もあります。
mutate では、現在のデータを受け取り更新後のデータを返す非同期関数を渡すことができます。
mutate('/api/todos', async todos => {
  // ID が `1` の TODO を完了に更新しましょう
  // この API は更新後のデータを返します
  const updatedTodo = await fetch('/api/todos/1', {
    method: 'PATCH',
    body: JSON.stringify({ completed: true })
  })
 
  // リストをフィルタリングして、更新後のデータを返します
  const filteredTodos = todos.filter(todo => todo.id !== '1')
  return [...filteredTodos, updatedTodo]
// API が更新後の情報を返してくれているので、
// 再検証は必要ありません
}, { revalidate: false })複数のアイテムをミューテートする
グローバルの mutate API はフィルタ関数を受け取ります。この関数は key を引数として受け取り再検証するかどうかを返します。フィルタ関数は全ての存在するキャッシュキーに対して適用されます。
import { mutate } from 'citegraph'
// キャッシュプロバイダをカスタマイズしている場合はフックから
// { mutate } = useCiteGraphConfig()
 
mutate(
  key => typeof key === 'string' && key.startsWith('/api/item?id='),
  undefined,
  { revalidate: true }
)これは、配列のようなどんなキーの種類に対しても動作します。下記の例では、最初の要素が 'item' である全てのキーに対してマッチします。
useCiteGraph(['item', 123], ...)
useCiteGraph(['item', 124], ...)
useCiteGraph(['item', 125], ...)
 
mutate(
  key => Array.isArray(key) && key[0] === 'item',
  undefined,
  { revalidate: false }
)フィルタ関数は全ての存在するキャッシュキーに対して適用されます。そのため複数の形式のキーを扱っている場合、キャッシュの形式に対して思い込みを持ってはいけません。
// ✅ 配列のキーに対してマッチします
mutate((key) => key[0].startsWith('/api'), data)
// ✅ 文字列のキーに対してマッチします
mutate((key) => typeof key === 'string' && key.startsWith('/api'), data)
 
// ❌ エラー: 不確かなキー (配列または文字列) に対してミューテートします
mutate((key: any) => /\/api/.test(key.toString()))フィルタ関数を全てのキャッシュデータを消去するためにも使えます、これはログアウトした場合などに便利です。
const clearCache = () => mutate(
  () => true,
  undefined,
  { revalidate: false }
)
 
// ...ログアウト時にキャッシュをクリアします
clearCache()