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 5
export 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
const tasksPage = await client(OsdkITask).where({ projectId: { $eq: project.$primaryKey }, }).fetchPage({ $orderBy: { "dueDate": "desc", "status": "asc" }, });
Extract unique user IDs and fetch user details:
Copied!1 2
const createdByIds = _.uniq(tasksPage.data.map((task) => task.createdBy as string)); const createdByUserList = await getBatchUserDetails(createdByIds);
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], }));
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 } );
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); }, []);
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 cache.
Copied!1 2 3
if (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 3
else 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 3
onOutOfDate() { // We could not keep track of all changes. Please reload the objects. }
The useTasks
hook cleans up the subscription when the component unmounts:
Copied!1 2 3
return () => { subscription.unsubscribe(); }
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.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:
OsdkITask
interface$includeAllBaseObjectProperties: true
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.
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:
map()
_.compact()
_.uniq()
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 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:
mutate
functionThe 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 });
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:
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
Purpose: Utility library with helper functions Benefits:
_.compact()
to remove null/undefined values from arrays_.uniq()
to deduplicate user IDs before batch fetchingCopied!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
import React, { useState } from 'react'; import useTasks from '../dataServices/useTasks'; import { IProject } from '../dataServices/useProjects'; function TaskList({ project }: { project: IProject }) { const { tasks, isLoading, isError, metadata } = useTasks(project); const [filter, setFilter] = useState('ALL'); 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?.propertyMetadata?.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(' ', '-')}`}> {task.osdkTask.status} </span> </td> <td> {task.osdkTask.dueDate ? new Date(task.osdkTask.dueDate).toLocaleDateString() : 'Not set'} </td> <td> <div className="user-info"> {task.assignedTo?.photoUrl && ( <img src={task.assignedTo.photoUrl} alt={task.assignedTo.displayName} className="user-avatar" /> )} <span>{task.assignedTo?.displayName || 'Unassigned'}</span> </div> </td> <td> <div className="user-info"> {task.createdBy?.photoUrl && ( <img src={task.createdBy.photoUrl} alt={task.createdBy.displayName} className="user-avatar" /> )} <span>{task.createdBy?.displayName || 'Unknown'}</span> </div> </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:
currentUser
: The hook depends on the current user being available but does not have a robust fallback if the admin module fails to load user information.