useTasks hook

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.

Key functions

  • Optimized user data fetching: Deduplicates and batches user ID requests to minimize API calls.
  • Real-time updates: Implements OSDK subscriptions to keep task data in sync with the backend.
  • SWR integration: Efficiently updates the local cache for both subscription events and manual fetches.
  • Error handling: Provides robust error handling for both fetching and subscription operations.
  • Data enrichment: Transforms raw OSDK objects into enriched data structures with user information.
  • Type safety: Maintains strong typing throughout with TypeScript interfaces.
  • Sorting and filtering: Applies server-side sorting to optimize data presentation.
  • Dependency management: Properly handles dependencies in useCallback and useEffect hooks.

useTasks structure

Interface definition

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

This interface does the following:

  • Wraps the raw OsdkITask.OsdkInstance data with additional context
  • Associates full User objects for both the creator and assignee
  • Creates a unified data structure that is ready for display in the interface

Data fetching

The useTasks hook employs a multi-step data retrieval strategy:

  1. Fetch task data filtered by project 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. Extract unique user IDs and fetch user details:

    Copied!
    1 2 const createdByIds = _.compact(_.uniq(tasksPage.data.map((task) => task.createdBy))); const createdByUserList = await getBatchUserDetails(createdByIds);
  3. Transform and combine the data:

    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. Cache and return the result through SWR:

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

Metadata handling

The useTasks hook also fetches and provides metadata about the task object type:

Copied!
1 2 3 4 const 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.

Real-time update management

The subscription implementation handles three key update scenarios:

  1. Added or updated tasks: Fetches user details and updates the matching task already held in the cache.

    Copied!
    1 2 3 if (update.state === "ADDED_OR_UPDATED") { // Fetch user details and update the task in the cache }
  2. Removed tasks: Filters the removed task out of the cache.

    Copied!
    1 2 3 else if (update.state === "REMOVED") { // Remove the task from the cache }
  3. Out-of-date notification: Handles cases where the subscription cannot track all changes.

    Copied!
    1 2 3 onOutOfDate() { // 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 3 return () => { subscription.unsubscribe(); }

Return value

The useTasks hook returns an object with the following structure:

Copied!
1 2 3 4 5 6 7 return { 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.

Implementation

OSDK query building pattern

The useTasks hook implements the OSDK query building pattern for fetching tasks associated with a specific project:

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" }, });

This pattern does the following:

  • Creates a query targeting the OsdkITask interface
  • Filters tasks to only include those associated with the specified project
  • Includes all base object properties with $includeAllBaseObjectProperties: true
  • Orders results by due date (descending) and status (ascending)
  • Returns a paginated result with the matching tasks

The $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.

Batch user data fetching

The useTasks hook optimizes network requests by fetching user data in batches:

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);

This pattern does the following:

  1. Extracts user IDs from all tasks using map()
  2. Eliminates duplicates with _.uniq()
  3. Removes undefined values with _.compact()
  4. Fetches all user details in a single batch operation
  5. Creates a lookup map of user information by ID

This optimization reduces the number of network requests from O(n) to O(1), where n is the number of tasks.

Real-time data subscription

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 18 const 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:

  • Creates a live subscription to task changes filtered by project
  • Handles different update types (additions, updates, removals)
  • Updates the local data cache through SWR's mutate function
  • Provides comprehensive error handling and lifecycle management
  • Cleans up the subscription when the component unmounts

The implementation uses SWR's mutate function to update the cache without triggering a network request:

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 });

External packages

The following external packages can be used with the useTasks hook.

useSWR

Purpose: Data fetching, caching, and state management library Benefits:

  • Provides automatic caching of fetched task data
  • Handles loading and error states for better UX
  • Offers built-in mutation capabilities for real-time updates
  • Reduces unnecessary network requests through smart revalidation strategies
  • Simplifies complex data fetching workflows with a declarative API

@osdk/react

Purpose: React bindings for the Ontology SDK Benefits:

  • Provides the useOsdkClient hook for accessing the OSDK client instance
  • Ensures consistent client configuration across the application
  • Handles authentication and session management automatically
  • Enables type-safe access to backend services

@advanced-to-do-application/sdk

Purpose: Application-specific SDK with predefined OSDK types

Benefits:

  • Provides the OsdkITask interface representing the task data model
  • Ensures type safety when working with task objects
  • Enables OSDK query capabilities through the client
  • Supports the application's ontology model with predefined types

@osdk/api

Purpose: Core Ontology SDK type definitions

Benefits:

  • Keeps metadata handling type-safe when rendering ontology-defined display names

@osdk/foundry.admin

Purpose: Foundry Admin SDK for user data types

Benefits:

  • Provides the User type used for the createdBy and assignedTo fields
  • Ensures user data is typed consistently with the Foundry Admin API

lodash

Purpose: Utility library with helper functions Benefits:

  • Used for _.uniq() to deduplicate user IDs before batch fetching
  • Used for _.compact() to drop tasks with no createdBy or assignedTo before batch fetching
  • Used for _.compact() to drop tasks with no createdBy or assignedTo before batch fetching
  • Improves performance by reducing redundant user detail requests
  • Simplifies data transformation operations with functional utilities

Usage example

The 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 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>>({}); // 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;

Edge cases and limitations

Consider the following scenarios and limitations when using the useTasks hook:

  • Subscription edge cases: The subscription system handles removals and updates to tasks already in the cache, but the 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.
  • User detail fallback: When user details cannot be found for a task's creator or assignee, the lookup returns undefined and the corresponding ITask field is unset. Components must handle missing user data.
  • Error recovery: The hook logs subscription errors through 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.
  • Large dataset handling: The current implementation fetches all tasks at once without pagination, which could cause performance issues with large projects.
  • Complex filtering: All filtering happens client-side after fetching all tasks. For very large task sets, server-side filtering would be more efficient.
  • Subscription race conditions: If multiple subscription events happen in quick succession, there is potential for race conditions when updating the cache.
  • Dependency on useAdmin: The hook depends on getBatchUserDetails from useAdmin to resolve user information, and does not have a robust fallback if that request fails.
  • Profile pictures are not batched: The 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().