ユースケース開発React アプリケーションのためのOSDKチュートリアル:高度なToDoアプリケーションUseTasksフック

注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。

useTasks フック

useTasks フックは、高度な ToDo アプリケーションで特定のプロジェクトに関連するタスクを管理するカスタム React フックです。オントロジー SDK を活用してタスクデータを取得し、ユーザー情報と関連付け、購読を通じてリアルタイムの更新を提供します。このフックは、効率的なデータ取得、キャッシュ、状態管理のために stale-while-revalidate(SWR)と連携するように設計されています。

このフックは、リアルタイムのデータ購読、バッチでのデータ取得、ユーザー情報による効率的なデータ拡充のパターンを実装しています。複雑なデータ管理を内部で処理することで、タスクデータを扱うためのシンプルで使いやすいインターフェースをコンポーネントに提供します。

useTasks のリファレンスコードを表示する。

主な機能

  • ユーザーデータ取得の最適化: ユーザーIDのリクエストを重複排除してバッチ化し、API 呼び出しを最小限に抑えます。
  • リアルタイムの更新: オントロジー SDK の購読を実装し、タスクデータをバックエンドと同期した状態に保ちます。
  • SWR 統合: 購読イベントと手動取得の両方について、ローカルキャッシュを効率的に更新します。
  • エラー処理: 取得操作と購読操作の両方に対して堅牢なエラー処理を提供します。
  • データ拡充: 生のオントロジー SDK オブジェクトを、ユーザー情報で拡充されたデータ構造に変換します。
  • 型安全性: TypeScript インターフェースを使用して、全体を通じて厳密な型付けを維持します。
  • 並べ替えと絞り込み: サーバー側の並べ替えを適用し、データの表示を最適化します。
  • 依存関係管理: useCallback フックと useEffect フックの依存関係を適切に処理します。

useTasks の構造

インターフェース定義

Copied!
1 2 3 4 5 export interface ITask { osdkTask: OsdkITask.OsdkInstance; createdBy: User; assignedTo: User; }

このインターフェースは、次の処理を行います。

  • 生の OsdkITask.OsdkInstance データを追加のコンテキストでラップします
  • 作成者と担当者の両方に完全な User オブジェクトを関連付けます
  • インターフェースでの表示にすぐに使える統一されたデータ構造を作成します

データ取得

useTasks フックは、複数のステップからなるデータ取得戦略を使用します。

  1. プロジェクトIDで絞り込んだタスクデータを取得します。

    Copied!
    1 2 3 4 5 6 const tasksPage = await client(OsdkITask).where({ projectId: { $eq: project.$primaryKey }, }).fetchPage({ $includeAllBaseObjectProperties: true, $orderBy: { "dueDate": "desc", "status": "asc" }, });
  2. 一意のユーザーIDを抽出し、ユーザーの詳細を取得します。

    Copied!
    1 2 const createdByIds = _.compact(_.uniq(tasksPage.data.map((task) => task.createdBy))); const createdByUserList = await getBatchUserDetails(createdByIds);
  3. データを変換して結合します。

    Copied!
    1 2 3 4 5 const tasksList: ITask[] = tasksPage.data.map((task) => ({ osdkTask: task, assignedTo: assignedToUserList[task.assignedTo as string], createdBy: createdByUserList[task.createdBy as string], }));
  4. SWR を通じて結果をキャッシュし、返します。

    Copied!
    1 2 3 4 5 const { data, isLoading, isValidating, error, mutate } = useSWR<ITask[]>( ["tasks", project.$primaryKey], fetcher, { revalidateOnFocus: false } );

メタデータ処理

useTasks フックは、タスクオブジェクトタイプのメタデータも取得して提供します。

Copied!
1 2 3 4 const getObjectTypeMetadata = useCallback(async () => { const objectTypeMetadata = await client.fetchMetadata(OsdkITask); setMetadata(objectTypeMetadata); }, [client]);

インターフェースのコンポーネントは、このメタデータを使用して、タスクタイプの表示名、説明、その他のオントロジー情報にアクセスできます。

リアルタイムの更新管理

購読の実装は、3つの主要な更新シナリオを処理します。

  1. 追加または更新されたタスク: ユーザーの詳細を取得し、キャッシュにすでに保持されている一致するタスクを更新します。

    Copied!
    1 2 3 if (update.state === "ADDED_OR_UPDATED") { // ユーザーの詳細を取得し、キャッシュ内のタスクを更新します }
  2. 削除されたタスク: 削除されたタスクをキャッシュから除外します。

    Copied!
    1 2 3 else if (update.state === "REMOVED") { // キャッシュからタスクを削除します }
  3. 最新でないことを示す通知: 購読ですべての変更を追跡できない場合を処理します。

    Copied!
    1 2 3 onOutOfDate() { // 一部の変更を追跡できませんでした。セット内のオブジェクトを再読み込みしてください。 }

useTasks フックは、コンポーネントがアンマウントされたときに購読をクリーンアップします。

Copied!
1 2 3 return () => { subscription.unsubscribe(); }

戻り値

useTasks フックは、次の構造を持つオブジェクトを返します。

Copied!
1 2 3 4 5 6 7 return { tasks: data ?? [], isLoading, isValidating, isError: error, metadata, };

このフックは、次の値を返します。

  • **tasks:**関連付けられたユーザー情報を持つタスクオブジェクトの配列。
  • **isLoading:**初回のデータ取得が進行中かどうかを示すブール値。
  • **isValidating:**バックグラウンドで再検証が行われているかどうかを示すブール値。
  • **isError:**データ取得中に発生したエラー。
  • **metadata:**インターフェースのカスタマイズ用のオブジェクトタイプメタデータ。

実装

オントロジー SDK のクエリ構築パターン

useTasks フックは、特定のプロジェクトに関連するタスクを取得するために、オントロジー SDK のクエリ構築パターンを実装しています。

Copied!
1 2 3 4 5 6 const tasksPage = await client(OsdkITask).where({ projectId: { $eq: project.$primaryKey }, }).fetchPage({ $includeAllBaseObjectProperties: true, $orderBy: { "dueDate": "desc", "status": "asc" }, });

このパターンは、次の処理を行います。

  • OsdkITask インターフェースを対象とするクエリを作成します
  • 指定されたプロジェクトに関連するタスクのみを含むように絞り込みます
  • $includeAllBaseObjectProperties: true により、すべてのベースオブジェクトプロパティを含めます
  • 結果を解決期日(降順)とステータス(昇順)で並べ替えます
  • 一致するタスクを含むページ分割された結果を返します

$includeAllBaseObjectProperties: true オプションは特に重要です。後で $as を使用して具体的な実装にピボットするときに、必要なすべてのデータがすでに利用可能であることを保証するためです。

ユーザーデータのバッチ取得

useTasks フックは、ユーザーデータをバッチで取得することでネットワークリクエストを最適化します。

Copied!
1 2 3 4 5 const createdByIds = _.compact(_.uniq(tasksPage.data.map((task) => task.createdBy))); const createdByUserList = await getBatchUserDetails(createdByIds); const assignedToIds = _.compact(_.uniq(tasksPage.data.map((task) => task.assignedTo))); const assignedToUserList = await getBatchUserDetails(assignedToIds);

このパターンは、次の処理を行います。

  1. map() を使用して、すべてのタスクからユーザーIDを抽出します
  2. _.uniq() で重複を排除します
  3. _.compact() で undefined 値を除去します
  4. 1回のバッチ操作ですべてのユーザーの詳細を取得します
  5. ID をキーとするユーザー情報の参照用マップを作成します

この最適化により、ネットワークリクエストの数は O(n) から O(1) に削減されます。ここで、n はタスクの数です。

リアルタイムのデータ購読

このフックは、タスクデータのリアルタイムの更新を提供するために、オントロジー SDK の購読メカニズムを実装しています。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const subscription = client(OsdkITask) .where({ projectId: { $eq: project.$primaryKey }, }) .subscribe({ onChange(update) { // タスクセットの変更を処理します }, onSuccessfulSubscription() { // 購読が正常に確立されました }, onError(err) { // 購読エラーを処理します }, onOutOfDate() { // データが古くなったことを知らせる通知を処理します }, });

このパターンは、次の処理を行います。

  • プロジェクトで絞り込んだタスクの変更に対するライブ購読を作成します
  • さまざまな種類の更新(追加、更新、削除)を処理します
  • SWR の mutate 関数を通じてローカルデータキャッシュを更新します
  • 包括的なエラー処理とライフサイクル管理を提供します
  • コンポーネントがアンマウントされたときに購読をクリーンアップします

この実装では、SWR の mutate 関数を使用して、ネットワークリクエストを発生させずにキャッシュを更新します。

Copied!
1 2 3 4 5 6 mutate((currentData: ITask[] | undefined) => { if (!currentData) return []; return currentData.map((task) => task.osdkTask.$primaryKey === update.object.$primaryKey ? updatedObject : task ); }, { revalidate: false });

外部パッケージ

次の外部パッケージを useTasks フックと併用できます。

useSWR

目的: データ取得、キャッシュ、状態管理のライブラリ 利点:

  • 取得したタスクデータを自動的にキャッシュします
  • 読み込み状態とエラー状態を処理し、UX を向上させます
  • リアルタイムの更新に対応する組み込みのミューテーション機能を提供します
  • スマートな再検証戦略により、不要なネットワークリクエストを削減します
  • 宣言的な API により、複雑なデータ取得ワークフローを簡素化します

@osdk/react

目的: オントロジー SDK 用の React バインディング 利点:

  • オントロジー SDK のクライアントインスタンスにアクセスするための useOsdkClient フックを提供します
  • アプリケーション全体で一貫したクライアント設定を保証します
  • 認証とセッション管理を自動的に処理します
  • バックエンドサービスへの型安全なアクセスを可能にします

@advanced-to-do-application/sdk

目的: 事前定義されたオントロジー SDK のデータ型を持つ、アプリケーション固有の SDK

利点:

  • タスクデータモデルを表す OsdkITask インターフェースを提供します
  • タスクオブジェクトを扱う際の型安全性を保証します
  • クライアントを通じてオントロジー SDK のクエリ機能を利用できるようにします
  • 事前定義されたデータ型により、アプリケーションのオントロジーモデルをサポートします

@osdk/api

目的: オントロジー SDK の中核となる型定義

利点:

  • オントロジーで定義された表示名を表示する際、メタデータ処理の型安全性を維持します

@osdk/foundry.admin

目的: ユーザーデータ型用の Foundry Admin SDK

利点:

  • createdBy フィールドと assignedTo フィールドで使用される User データ型を提供します
  • ユーザーデータが Foundry Admin API と一貫したデータ型で扱われることを保証します

lodash

目的: ヘルパー関数を備えたユーティリティライブラリ 利点:

  • バッチ取得の前にユーザーIDの重複を排除するため、_.uniq() を使用します
  • バッチ取得の前に createdBy または assignedTo がないタスクを除外するため、_.compact() を使用します
  • バッチ取得の前に createdBy または assignedTo がないタスクを除外するため、_.compact() を使用します
  • ユーザーの詳細に対する冗長なリクエストを削減し、パフォーマンスを向上させます
  • 関数型のユーティリティにより、データ変換操作を簡素化します

使用例

useTasks フックは Foundry Admin API から User オブジェクトを返します。このオブジェクトはユーザーを記述しますが、画像は含みません。タスクの担当者の横にアバターを表示するには、次の例に示すように、useAdmin フックの getCurrentProfilePictureUrl でそれぞれの画像を個別にリクエストします。

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 import React, { useEffect, useMemo, useRef, useState } from 'react'; import type { User } from '@osdk/foundry.admin'; import useTasks from '../dataServices/useTasks'; import useAdmin from '../dataServices/useAdmin'; import { IProject } from '../dataServices/useProjects'; function useProfilePictureUrls(users: User[]) { const { getCurrentProfilePictureUrl } = useAdmin(); const [profilePictureUrls, setProfilePictureUrls] = useState<Record<string, string>>({}); // effect がすでに読み込まれた画像を確認できるように、ref に保持します。 // これにより、effect 自身が設定する状態に依存せずに済みます。 const loadedUrls = useRef<Record<string, string>>({}); useEffect(() => { let isActive = true; async function loadProfilePictures() { const usersToLoad = users.filter((user) => loadedUrls.current[user.id] === undefined); if (usersToLoad.length === 0) { return; } await Promise.all( usersToLoad.map(async (user) => { try { loadedUrls.current[user.id] = await getCurrentProfilePictureUrl(user); } catch { // プロフィール画像のないユーザーは、アバターなしで表示されます。 } }), ); if (isActive) { setProfilePictureUrls({ ...loadedUrls.current }); } } loadProfilePictures(); return () => { isActive = false; }; }, [users, getCurrentProfilePictureUrl]); // `getCurrentProfilePictureUrl` は `URL.createObjectURL` で生成された blob URL を返します。 // ブラウザーは、これらの URL が取り消されるまで保持します。 useEffect(() => () => { Object.values(loadedUrls.current).forEach((url) => URL.revokeObjectURL(url)); }, []); return profilePictureUrls; } function UserCell({ user, fallback, profilePictureUrls }: { user: User | undefined; fallback: string; profilePictureUrls: Record<string, string>; }) { if (user == null) { return <div className="user-info"><span>{fallback}</span></div>; } const profilePictureUrl = profilePictureUrls[user.id]; return ( <div className="user-info"> {profilePictureUrl != null && ( <img src={profilePictureUrl} alt={`Profile picture for ${user.username}.`} className="user-avatar" /> )} <span>{user.username}</span> </div> ); } function TaskList({ project }: { project: IProject }) { const { tasks, isLoading, isError, metadata } = useTasks(project); const [filter, setFilter] = useState('ALL'); // 各画像が1回だけリクエストされるように、作成者と担当者の重複を除去します。 // フック内でユーザーを一括参照するのと同じ考え方です。 const taskUsers = useMemo(() => { const usersById = new Map<string, User>(); tasks.forEach((task) => { [task.createdBy, task.assignedTo].forEach((user) => { if (user != null) { usersById.set(user.id, user); } }); }); return Array.from(usersById.values()); }, [tasks]); const profilePictureUrls = useProfilePictureUrls(taskUsers); if (isLoading) return <div>Loading tasks...</div>; if (isError) return <div>Error loading tasks: {isError.message}</div>; // 選択したフィルターに基づいてタスクを絞り込みます const filteredTasks = filter === 'ALL' ? tasks : tasks.filter(task => task.osdkTask.status === filter); return ( <div className="task-list"> <h2>Tasks for {project.name}</h2> <div className="filter-controls"> <button className={filter === 'ALL' ? 'active' : ''} onClick={() => setFilter('ALL')} > All ({tasks.length}) </button> <button className={filter === 'COMPLETED' ? 'active' : ''} onClick={() => setFilter('COMPLETED')} > Completed ({tasks.filter(t => t.osdkTask.status === 'COMPLETED').length}) </button> <button className={filter === 'IN PROGRESS' ? 'active' : ''} onClick={() => setFilter('IN PROGRESS')} > In Progress ({tasks.filter(t => t.osdkTask.status === 'IN PROGRESS').length}) </button> </div> <table className="task-table"> <thead> <tr> <th>{metadata?.properties?.title?.displayName || 'Title'}</th> <th>Status</th> <th>Due Date</th> <th>Assigned To</th> <th>Created By</th> </tr> </thead> <tbody> {filteredTasks.map((task) => ( <tr key={task.osdkTask.$primaryKey}> <td>{task.osdkTask.title}</td> <td> <span className={`status-badge ${task.osdkTask.status?.toLowerCase().replace(' ', '-') ?? 'unknown'}`}> {task.osdkTask.status} </span> </td> <td> {task.osdkTask.dueDate ? new Date(task.osdkTask.dueDate).toLocaleDateString() : 'Not set'} </td> <td> <UserCell user={task.assignedTo} fallback="Unassigned" profilePictureUrls={profilePictureUrls} /> </td> <td> <UserCell user={task.createdBy} fallback="Unknown" profilePictureUrls={profilePictureUrls} /> </td> </tr> ))} </tbody> </table> {filteredTasks.length === 0 && ( <div className="empty-state"> No {filter !== 'ALL' ? filter.toLowerCase() : ''} tasks found. </div> )} </div> ); } export default TaskList;

エッジケースと制限事項

useTasks フックを使用する際は、次のシナリオと制限事項を考慮してください。

  • 購読のエッジケース: 購読システムは、すでにキャッシュにあるタスクの削除と更新を処理しますが、onOutOfDate コールバックは修正処理を行いません。また、ADDED_OR_UPDATED 分岐はすでにキャッシュにあるタスクのみを置き換えるため、オブジェクトセットに新しく追加されたタスクは、次の再検証まで無視されます。
  • ユーザー詳細のフォールバック: タスクの作成者または担当者のユーザー詳細が見つからない場合、参照処理は undefined を返し、対応する ITask フィールドは未設定になります。コンポーネントは、ユーザーデータがない場合に対応する必要があります。
  • エラーからの復旧: フックは onError を通じて購読エラーをログに記録しますが、SWR のフェッチャーには try/catch がありません。取得の失敗は、戻り値の isError を通じてのみ表面化し、再試行は SWR の組み込み動作に限定されます。
  • 大規模なデータセットの処理: 現在の実装では、ページネーションを使用せずにすべてのタスクを一度に取得するため、大規模なプロジェクトではパフォーマンスの問題が発生する可能性があります。
  • 複雑な絞り込み: 絞り込みはすべて、すべてのタスクを取得した後にクライアント側で行われます。タスクの集合が非常に大きい場合は、サーバー側で絞り込む方が効率的です。
  • 購読の競合状態: 複数の購読イベントが短い間隔で連続して発生すると、キャッシュの更新時に競合状態が発生する可能性があります。
  • useAdmin への依存関係: フックはユーザー情報を解決するために useAdmin の getBatchUserDetails に依存しており、そのリクエストに失敗した場合の堅牢なフォールバックはありません。
  • プロフィール画像はバッチ処理されない: フックが返す User オブジェクトには画像データが含まれていません。使用例に示すように、プロフィール画像にはユーザーごとに個別の getCurrentProfilePictureUrl リクエストが必要です。各リクエストは blob URL を返し、その URL を使用するコンポーネントは URL.revokeObjectURL() で取り消す必要があります。