注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。
useTasks フックは、高度な ToDo アプリケーションで特定のプロジェクトに関連するタスクを管理するカスタム React フックです。オントロジー SDK を活用してタスクデータを取得し、ユーザー情報と関連付け、購読を通じてリアルタイムの更新を提供します。このフックは、効率的なデータ取得、キャッシュ、状態管理のために stale-while-revalidate(SWR)と連携するように設計されています。
このフックは、リアルタイムのデータ購読、バッチでのデータ取得、ユーザー情報による効率的なデータ拡充のパターンを実装しています。複雑なデータ管理を内部で処理することで、タスクデータを扱うためのシンプルで使いやすいインターフェースをコンポーネントに提供します。
useTasks の構造Copied!1 2 3 4 5export interface ITask { osdkTask: OsdkITask.OsdkInstance; createdBy: User; assignedTo: User; }
このインターフェースは、次の処理を行います。
OsdkITask.OsdkInstance データを追加のコンテキストでラップしますUser オブジェクトを関連付けますuseTasks フックは、複数のステップからなるデータ取得戦略を使用します。
プロジェクトIDで絞り込んだタスクデータを取得します。
Copied!1 2 3 4 5 6const tasksPage = await client(OsdkITask).where({ projectId: { $eq: project.$primaryKey }, }).fetchPage({ $includeAllBaseObjectProperties: true, $orderBy: { "dueDate": "desc", "status": "asc" }, });
一意のユーザーIDを抽出し、ユーザーの詳細を取得します。
Copied!1 2const createdByIds = _.compact(_.uniq(tasksPage.data.map((task) => task.createdBy))); const createdByUserList = await getBatchUserDetails(createdByIds);
データを変換して結合します。
Copied!1 2 3 4 5const tasksList: ITask[] = tasksPage.data.map((task) => ({ osdkTask: task, assignedTo: assignedToUserList[task.assignedTo as string], createdBy: createdByUserList[task.createdBy as string], }));
SWR を通じて結果をキャッシュし、返します。
Copied!1 2 3 4 5const { data, isLoading, isValidating, error, mutate } = useSWR<ITask[]>( ["tasks", project.$primaryKey], fetcher, { revalidateOnFocus: false } );
useTasks フックは、タスクオブジェクトタイプのメタデータも取得して提供します。
Copied!1 2 3 4const getObjectTypeMetadata = useCallback(async () => { const objectTypeMetadata = await client.fetchMetadata(OsdkITask); setMetadata(objectTypeMetadata); }, [client]);
インターフェースのコンポーネントは、このメタデータを使用して、タスクタイプの表示名、説明、その他のオントロジー情報にアクセスできます。
購読の実装は、3つの主要な更新シナリオを処理します。
追加または更新されたタスク: ユーザーの詳細を取得し、キャッシュにすでに保持されている一致するタスクを更新します。
Copied!1 2 3if (update.state === "ADDED_OR_UPDATED") { // ユーザーの詳細を取得し、キャッシュ内のタスクを更新します }
削除されたタスク: 削除されたタスクをキャッシュから除外します。
Copied!1 2 3else if (update.state === "REMOVED") { // キャッシュからタスクを削除します }
最新でないことを示す通知: 購読ですべての変更を追跡できない場合を処理します。
Copied!1 2 3onOutOfDate() { // 一部の変更を追跡できませんでした。セット内のオブジェクトを再読み込みしてください。 }
useTasks フックは、コンポーネントがアンマウントされたときに購読をクリーンアップします。
Copied!1 2 3return () => { subscription.unsubscribe(); }
useTasks フックは、次の構造を持つオブジェクトを返します。
Copied!1 2 3 4 5 6 7return { tasks: data ?? [], isLoading, isValidating, isError: error, metadata, };
このフックは、次の値を返します。
tasks:**関連付けられたユーザー情報を持つタスクオブジェクトの配列。isLoading:**初回のデータ取得が進行中かどうかを示すブール値。isValidating:**バックグラウンドで再検証が行われているかどうかを示すブール値。isError:**データ取得中に発生したエラー。metadata:**インターフェースのカスタマイズ用のオブジェクトタイプメタデータ。useTasks フックは、特定のプロジェクトに関連するタスクを取得するために、オントロジー SDK のクエリ構築パターンを実装しています。
Copied!1 2 3 4 5 6const 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 5const 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);
このパターンは、次の処理を行います。
map() を使用して、すべてのタスクからユーザーIDを抽出します_.uniq() で重複を排除します_.compact() で undefined 値を除去しますこの最適化により、ネットワークリクエストの数は O(n) から O(1) に削減されます。ここで、n はタスクの数です。
このフックは、タスクデータのリアルタイムの更新を提供するために、オントロジー SDK の購読メカニズムを実装しています。
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18const subscription = client(OsdkITask) .where({ projectId: { $eq: project.$primaryKey }, }) .subscribe({ onChange(update) { // タスクセットの変更を処理します }, onSuccessfulSubscription() { // 購読が正常に確立されました }, onError(err) { // 購読エラーを処理します }, onOutOfDate() { // データが古くなったことを知らせる通知を処理します }, });
このパターンは、次の処理を行います。
mutate 関数を通じてローカルデータキャッシュを更新しますこの実装では、SWR の mutate 関数を使用して、ネットワークリクエストを発生させずにキャッシュを更新します。
Copied!1 2 3 4 5 6mutate((currentData: ITask[] | undefined) => { if (!currentData) return []; return currentData.map((task) => task.osdkTask.$primaryKey === update.object.$primaryKey ? updatedObject : task ); }, { revalidate: false });
次の外部パッケージを useTasks フックと併用できます。
目的: データ取得、キャッシュ、状態管理のライブラリ 利点:
目的: オントロジー SDK 用の React バインディング 利点:
useOsdkClient フックを提供します目的: 事前定義されたオントロジー SDK のデータ型を持つ、アプリケーション固有の SDK
利点:
OsdkITask インターフェースを提供します目的: オントロジー SDK の中核となる型定義
利点:
目的: ユーザーデータ型用の Foundry Admin SDK
利点:
createdBy フィールドと assignedTo フィールドで使用される User データ型を提供します目的: ヘルパー関数を備えたユーティリティライブラリ 利点:
_.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 179import 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() で取り消す必要があります。