useTasks hook(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¶
export interface ITask {
osdkTask: OsdkITask.OsdkInstance;
createdBy: User;
assignedTo: User;
}
This interface does the following:
- Wraps the raw
OsdkITask.OsdkInstancedata with additional context - Associates full
Userobjects 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:
- Fetch task data filtered by project ID:
const tasksPage = await client(OsdkITask).where({
projectId: { $eq: project.$primaryKey },
}).fetchPage({
$orderBy: { "dueDate": "desc", "status": "asc" },
});
- Extract unique user IDs and fetch user details:
const createdByIds = _.uniq(tasksPage.data.map((task) => task.createdBy as string));
const createdByUserList = await getBatchUserDetails(createdByIds);
- Transform and combine the data:
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:
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:
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.
Real-time update management¶
The subscription implementation handles three key update scenarios:
- Added or updated tasks: Fetches user details and updates the cache.
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.
else if (update.state === "REMOVED") {
// Remove the task from the cache
}
- Out-of-date notification: Handles cases where the subscription cannot track all changes.
onOutOfDate() {
// We could not keep track of all changes. Please reload the objects.
}
The useTasks hook cleans up the subscription when the component unmounts:
return () => {
subscription.unsubscribe();
}
Return value¶
The useTasks hook returns an object with the following structure:
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:
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
OsdkITaskinterface - 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:
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:
- Extracts user IDs from all tasks using
map() - Removes null/undefined values with
_.compact() - Eliminates duplicates with
_.uniq() - Fetches all user details in a single batch operation
- 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:
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
mutatefunction - 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:
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
useOsdkClienthook 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
OsdkITaskinterface 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
lodash¶
Purpose: Utility library with helper functions Benefits:
- Used for
_.compact()to remove null/undefined values from arrays - Used for
_.uniq()to deduplicate user IDs before batch fetching - Improves performance by reducing redundant user detail requests
- Simplifies data transformation operations with functional utilities
Usage example¶
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;
Edge cases and limitations¶
Consider the following scenarios and limitations when using the useTasks hook:
- Subscription edge cases: The subscription system handles additions, updates, and removals, but the "out-of-date" scenario only logs a message without taking corrective action.
- User detail fallback: When user details cannot be found for a task's creator or assignee, the hook defaults to using the current user. This might not always be appropriate and could lead to incorrect user attribution.
- Error recovery: While the hook logs fetch errors, it does not provide a mechanism for retrying failed fetches beyond SWR's built-in retry functionality.
- 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
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.
中文翻译¶
useTasks 钩子 (hook)¶
useTasks 钩子是一个自定义 React 钩子,用于管理高级待办事项应用中与特定项目关联的任务。它利用本体 SDK(OSDK)获取任务数据,将其与用户信息关联,并通过订阅(subscription)提供实时更新。该钩子设计为与 stale-while-revalidate(SWR)配合使用,以实现高效的数据获取、缓存和状态管理。
该钩子实现了实时数据订阅、批量数据检索以及通过用户信息高效丰富数据的模式。通过在内部处理数据管理的复杂性,它为组件提供了一个简洁易用的接口来处理任务数据。
主要功能¶
- 优化的用户数据获取: 对用户 ID 请求进行去重和批处理,以最小化 API 调用。
- 实时更新: 实现 OSDK 订阅,使任务数据与后端保持同步。
- SWR 集成: 高效更新本地缓存,同时支持订阅事件和手动获取。
- 错误处理: 为获取和订阅操作提供健壮的错误处理。
- 数据丰富: 将原始 OSDK 对象转换为包含用户信息的丰富数据结构。
- 类型安全: 通过 TypeScript 接口保持强类型。
- 排序与过滤: 应用服务端排序以优化数据展示。
- 依赖管理: 在
useCallback和useEffect钩子中正确处理依赖关系。
useTasks 结构¶
接口定义¶
export interface ITask {
osdkTask: OsdkITask.OsdkInstance;
createdBy: User;
assignedTo: User;
}
该接口实现以下功能:
- 将原始
OsdkITask.OsdkInstance数据包装并附加上下文 - 为创建者和分配者关联完整的
User对象 - 创建一个统一的数据结构,可直接用于界面展示
数据获取¶
useTasks 钩子采用多步骤数据检索策略:
- 按项目 ID 过滤获取任务数据:
const tasksPage = await client(OsdkITask).where({
projectId: { $eq: project.$primaryKey },
}).fetchPage({
$orderBy: { "dueDate": "desc", "status": "asc" },
});
- 提取唯一的用户 ID 并获取用户详情:
const createdByIds = _.uniq(tasksPage.data.map((task) => task.createdBy as string));
const createdByUserList = await getBatchUserDetails(createdByIds);
- 转换并合并数据:
const tasksList: ITask[] = tasksPage.data.map((task) => ({
osdkTask: task,
assignedTo: assignedToUserList[task.assignedTo as string],
createdBy: createdByUserList[task.createdBy as string],
}));
- 通过 SWR 缓存并返回结果:
const { data, isLoading, isValidating, error, mutate } = useSWR<ITask[]>(
["tasks", project.$primaryKey],
fetcher,
{ revalidateOnFocus: false }
);
元数据处理¶
useTasks 钩子还会获取并提供关于任务对象类型的元数据:
const getObjectTypeMetadata = useCallback(async () => {
const objectTypeMetadata = await client.fetchMetadata(OsdkITask);
setMetadata(objectTypeMetadata);
}, []);
界面组件可以使用这些元数据来获取任务类型的显示名称、描述以及其他本体信息。
实时更新管理¶
订阅实现处理三种关键的更新场景:
- 新增或更新的任务: 获取用户详情并更新缓存。
if (update.state === "ADDED_OR_UPDATED") {
// 获取用户详情并更新缓存中的任务
}
- 已删除的任务: 从缓存中过滤掉已删除的任务。
else if (update.state === "REMOVED") {
// 从缓存中移除任务
}
- 过期通知: 处理订阅无法跟踪所有变更的情况。
onOutOfDate() {
// 无法跟踪所有变更,请重新加载对象。
}
当组件卸载时,useTasks 钩子会清理订阅:
return () => {
subscription.unsubscribe();
}
返回值¶
useTasks 钩子返回一个包含以下结构的对象:
return {
tasks: data ?? [],
isLoading,
isValidating,
isError: error,
metadata,
};
该钩子返回以下内容:
tasks: 包含关联用户信息的任务对象数组。isLoading: 布尔值,表示初始数据获取是否正在进行。isValidating: 布尔值,表示后台重新验证是否正在进行。isError: 数据获取过程中发生的任何错误。metadata: 用于界面自定义的对象类型元数据。
实现¶
OSDK 查询构建模式¶
useTasks 钩子实现了 OSDK 查询构建模式,用于获取与特定项目关联的任务:
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 钩子通过批量获取用户数据来优化网络请求:
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);
该模式实现以下功能:
- 使用
map()从所有任务中提取用户 ID - 使用
_.compact()移除空值/未定义值 - 使用
_.uniq()消除重复项 - 在单个批量操作中获取所有用户详情
- 创建一个按 ID 查找用户信息的映射表
这种优化将网络请求次数从 O(n) 减少到 O(1),其中 n 是任务数量。
实时数据订阅¶
该钩子实现了 OSDK 订阅机制,为任务数据提供实时更新:
const subscription = client(OsdkITask)
.where({
projectId: { $eq: project.$primaryKey },
})
.subscribe({
onChange(update) {
// 处理任务集的变更
},
onSuccessfulSubscription() {
// 订阅成功建立
},
onError(err) {
// 处理订阅错误
},
onOutOfDate() {
// 处理过期通知
},
});
该模式实现以下功能:
- 创建按项目过滤的任务变更实时订阅
- 处理不同的更新类型(新增、更新、删除)
- 通过 SWR 的
mutate函数更新本地数据缓存 - 提供全面的错误处理和生命周期管理
- 在组件卸载时清理订阅
实现使用 SWR 的 mutate 函数更新缓存,而无需触发网络请求:
mutate((currentData: ITask[] | undefined) => {
if (!currentData) return [];
return currentData.map((task) =>
task.osdkTask.$primaryKey === update.object.$primaryKey ? updatedObject : task
);
}, { revalidate: false });
外部包¶
以下外部包可与 useTasks 钩子一起使用。
useSWR¶
用途: 数据获取、缓存和状态管理库 优势:
- 提供获取任务数据的自动缓存
- 处理加载和错误状态,提升用户体验
- 提供内置的变更能力,支持实时更新
- 通过智能重新验证策略减少不必要的网络请求
- 通过声明式 API 简化复杂的数据获取工作流
@osdk/react¶
用途: 本体 SDK 的 React 绑定 优势:
- 提供
useOsdkClient钩子,用于访问 OSDK 客户端实例 - 确保整个应用中的客户端配置一致性
- 自动处理身份验证和会话管理
- 实现对后端服务的类型安全访问
@advanced-to-do-application/sdk¶
-
用途: 包含预定义 OSDK 类型的应用特定 SDK
-
优势:
-
提供表示任务数据模型的
OsdkITask接口 -
确保处理任务对象时的类型安全
-
通过客户端启用 OSDK 查询能力
-
使用预定义类型支持应用的本体模型
lodash¶
用途: 包含辅助函数的工具库 优势:
- 使用
_.compact()移除数组中的空值/未定义值 - 使用
_.uniq()在批量获取前对用户 ID 进行去重 - 通过减少冗余的用户详情请求提升性能
- 使用函数式工具简化数据转换操作
使用示例¶
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>正在加载任务...</div>;
if (isError) return <div>加载任务时出错:{isError.message}</div>;
// 根据所选过滤器过滤任务
const filteredTasks = filter === 'ALL'
? tasks
: tasks.filter(task => task.osdkTask.status === filter);
return (
<div className="task-list">
<h2>{project.name} 的任务</h2>
<div className="filter-controls">
<button
className={filter === 'ALL' ? 'active' : ''}
onClick={() => setFilter('ALL')}
>
全部 ({tasks.length})
</button>
<button
className={filter === 'COMPLETED' ? 'active' : ''}
onClick={() => setFilter('COMPLETED')}
>
已完成 ({tasks.filter(t => t.osdkTask.status === 'COMPLETED').length})
</button>
<button
className={filter === 'IN PROGRESS' ? 'active' : ''}
onClick={() => setFilter('IN PROGRESS')}
>
进行中 ({tasks.filter(t => t.osdkTask.status === 'IN PROGRESS').length})
</button>
</div>
<table className="task-table">
<thead>
<tr>
<th>{metadata?.propertyMetadata?.title?.displayName || '标题'}</th>
<th>状态</th>
<th>截止日期</th>
<th>分配对象</th>
<th>创建者</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()
: '未设置'}
</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 || '未分配'}</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 || '未知'}</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
{filteredTasks.length === 0 && (
<div className="empty-state">
未找到{filter !== 'ALL' ? ` ${filter.toLowerCase()}` : ''}任务。
</div>
)}
</div>
);
}
export default TaskList;
边界情况与限制¶
使用 useTasks 钩子时,请考虑以下场景和限制:
- 订阅边界情况: 订阅系统处理新增、更新和删除操作,但"过期"场景仅记录消息,未采取纠正措施。
- 用户详情回退: 当无法找到任务创建者或分配者的用户详情时,钩子默认使用当前用户。这可能并不总是合适的,并可能导致用户归属错误。
- 错误恢复: 虽然钩子会记录获取错误,但除了 SWR 内置的重试功能外,它不提供重试失败获取的机制。
- 大数据集处理: 当前实现一次性获取所有任务,未使用分页,这可能导致大型项目出现性能问题。
- 复杂过滤: 所有过滤操作在获取所有任务后在客户端进行。对于非常大的任务集,服务端过滤会更高效。
- 订阅竞态条件: 如果多个订阅事件在短时间内连续发生,更新缓存时可能出现竞态条件。
- 对
currentUser的依赖: 该钩子依赖于当前用户的可用性,但如果管理模块无法加载用户信息,则没有健壮的回退方案。