The useTasks hook is a custom React hook that manages tasks associated with a specific project in the advanced to-do application. It leverages the Ontology SDK (OSDK) to fetch task data, associate it with user information, and provide real-time updates through subscriptions. This hook is designed to work with stale-while-revalidate (SWR) for efficient data fetching, caching, and state management.
This hook implements patterns for real-time data subscription, batch data retrieval, and efficient data enrichment with user information. By handling the complexity of data management internally, it provides components with a clean, easy-to-use interface for working with task data.
View the useTasks reference code.
useTasks structureCopied!1 2 3 4 5export interface ITask { osdkTask: OsdkITask.OsdkInstance; createdBy: User; assignedTo: User; }
This interface does the following:
OsdkITask.OsdkInstance data with additional contextUser objects for both the creator and assigneeThe useTasks hook employs a multi-step data retrieval strategy:
Fetch task data filtered by project 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" }, });
Extract unique user IDs and fetch user details:
Copied!1 2const createdByIds = _.compact(_.uniq(tasksPage.data.map((task) => task.createdBy))); const createdByUserList = await getBatchUserDetails(createdByIds);
Transform and combine the data:
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], }));
Cache and return the result through SWR:
Copied!1 2 3 4 5const { data, isLoading, isValidating, error, mutate } = useSWR<ITask[]>( ["tasks", project.$primaryKey], fetcher, { revalidateOnFocus: false } );
The useTasks hook also fetches and provides metadata about the task object type:
Copied!1 2 3 4const getObjectTypeMetadata = useCallback(async () => { const objectTypeMetadata = await client.fetchMetadata(OsdkITask); setMetadata(objectTypeMetadata); }, [client]);
This metadata can be used by interface components to access display names, descriptions, and other ontology information about the task type.
The subscription implementation handles three key update scenarios:
Added or updated tasks: Fetches user details and updates the matching task already held in the cache.
Copied!1 2 3if (update.state === "ADDED_OR_UPDATED") { // Fetch user details and update the task in the cache }
Removed tasks: Filters the removed task out of the cache.
Copied!1 2 3else if (update.state === "REMOVED") { // Remove the task from the cache }
Out-of-date notification: Handles cases where the subscription cannot track all changes.
Copied!1 2 3onOutOfDate() { // We could not keep track of all changes. Reload the objects in your set. }
The useTasks hook cleans up the subscription when the component unmounts:
Copied!1 2 3return () => { subscription.unsubscribe(); }
The useTasks hook returns an object with the following structure:
Copied!1 2 3 4 5 6 7return { tasks: data ?? [], isLoading, isValidating, isError: error, metadata, };
The hook returns the following:
tasks: An array of task objects with associated user information.isLoading: A Boolean value indicating if the initial data fetch is in progress.isValidating: A Boolean value indicating if a background revalidation is happening.isError: Any error that occurred during data fetching.metadata: Object type metadata for interface customization.The useTasks hook implements the OSDK query building pattern for fetching tasks associated with a specific project:
Copied!1 2 3 4 5 6const tasksPage = await client(OsdkITask).where({ projectId: { $eq: project.$primaryKey }, }).fetchPage({ $includeAllBaseObjectProperties: true, $orderBy: { "dueDate": "desc", "status": "asc" }, });
This pattern does the following:
OsdkITask interface$includeAllBaseObjectProperties: trueThe $includeAllBaseObjectProperties: true option is particularly important as it ensures that when we later use $as to pivot to concrete implementations, all necessary data is already available.
The useTasks hook optimizes network requests by fetching user data in batches:
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);
This pattern does the following:
map()_.uniq()undefined values with _.compact()This optimization reduces the number of network requests from O(n) to O(1), where n is the number of tasks.
The hook implements the OSDK subscription mechanism to provide real-time updates to task data:
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) { // Handle changes to the task set }, onSuccessfulSubscription() { // Subscription successfully established }, onError(err) { // Handle subscription errors }, onOutOfDate() { // Handle out-of-date notifications }, });
This pattern does the following:
mutate functionThe implementation uses SWR's mutate function to update the cache without triggering a network request:
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 });
The following external packages can be used with the useTasks hook.
Purpose: Data fetching, caching, and state management library Benefits:
Purpose: React bindings for the Ontology SDK Benefits:
useOsdkClient hook for accessing the OSDK client instancePurpose: Application-specific SDK with predefined OSDK types
Benefits:
OsdkITask interface representing the task data modelPurpose: Core Ontology SDK type definitions
Benefits:
Purpose: Foundry Admin SDK for user data types
Benefits:
User type used for the createdBy and assignedTo fieldsPurpose: Utility library with helper functions Benefits:
_.uniq() to deduplicate user IDs before batch fetching_.compact() to drop tasks with no createdBy or assignedTo before batch fetching_.compact() to drop tasks with no createdBy or assignedTo before batch fetchingThe useTasks hook returns User objects from the Foundry Admin API, which describe a user but do not include an image. To display avatars alongside task assignees, request each picture separately with getCurrentProfilePictureUrl from the useAdmin hook, as shown in the following example.
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>>({}); // Kept in a ref so the effect can check which pictures are already loaded // without depending on the state that it sets. 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 { // Users without a profile picture are rendered without an avatar. } }), ); if (isActive) { setProfilePictureUrls({ ...loadedUrls.current }); } } loadProfilePictures(); return () => { isActive = false; }; }, [users, getCurrentProfilePictureUrl]); // `getCurrentProfilePictureUrl` returns blob URLs from `URL.createObjectURL`, which // the browser retains until they are revoked. 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'); // Deduplicate creators and assignees so each picture is requested only once, // in the same spirit as the batched user lookup inside the hook. 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>; // Filter tasks based on the selected filter 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;
Consider the following scenarios and limitations when using the useTasks hook:
onOutOfDate callback takes no corrective action. Tasks that are newly added to the object set are also ignored until the next revalidation, because the ADDED_OR_UPDATED branch only replaces tasks that are already present in the cache.undefined and the corresponding ITask field is unset. Components must handle missing user data.onError, but the SWR fetcher has no try/catch. Fetch failures surface only through the returned isError value, and retries are limited to SWR's built-in behavior.useAdmin: The hook depends on getBatchUserDetails from useAdmin to resolve user information, and does not have a robust fallback if that request fails.User objects returned by the hook contain no image data. Profile pictures require a separate getCurrentProfilePictureUrl request for each user, as shown in the usage example, and each request returns a blob URL that the consuming component must revoke with URL.revokeObjectURL().