跳转至

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.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:
const tasksPage = await client(OsdkITask).where({
    projectId: { $eq: project.$primaryKey },
}).fetchPage({
    $orderBy: { "dueDate": "desc", "status": "asc" },
});
  1. Extract unique user IDs and fetch user details:
const createdByIds = _.uniq(tasksPage.data.map((task) => task.createdBy as string));
const createdByUserList = await getBatchUserDetails(createdByIds);
  1. 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],
}));
  1. 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:

  1. 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
}
  1. Removed tasks: Filters the removed task out of the cache.
else if (update.state === "REMOVED") {
    // Remove the task from the cache
}
  1. 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 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:

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. Removes null/undefined values with _.compact()
  3. Eliminates duplicates with _.uniq()
  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:

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:

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

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)配合使用,以实现高效的数据获取、缓存和状态管理。

该钩子实现了实时数据订阅、批量数据检索以及通过用户信息高效丰富数据的模式。通过在内部处理数据管理的复杂性,它为组件提供了一个简洁易用的接口来处理任务数据。

查看 useTasks 参考代码。

主要功能

  • 优化的用户数据获取: 对用户 ID 请求进行去重和批处理,以最小化 API 调用。
  • 实时更新: 实现 OSDK 订阅,使任务数据与后端保持同步。
  • SWR 集成: 高效更新本地缓存,同时支持订阅事件和手动获取。
  • 错误处理: 为获取和订阅操作提供健壮的错误处理。
  • 数据丰富: 将原始 OSDK 对象转换为包含用户信息的丰富数据结构。
  • 类型安全: 通过 TypeScript 接口保持强类型。
  • 排序与过滤: 应用服务端排序以优化数据展示。
  • 依赖管理:useCallbackuseEffect 钩子中正确处理依赖关系。

useTasks 结构

接口定义

export interface ITask {
    osdkTask: OsdkITask.OsdkInstance;
    createdBy: User;
    assignedTo: User;
}

该接口实现以下功能:

  • 将原始 OsdkITask.OsdkInstance 数据包装并附加上下文
  • 为创建者和分配者关联完整的 User 对象
  • 创建一个统一的数据结构,可直接用于界面展示

数据获取

useTasks 钩子采用多步骤数据检索策略:

  1. 按项目 ID 过滤获取任务数据:
const tasksPage = await client(OsdkITask).where({
    projectId: { $eq: project.$primaryKey },
}).fetchPage({
    $orderBy: { "dueDate": "desc", "status": "asc" },
});
  1. 提取唯一的用户 ID 并获取用户详情:
const createdByIds = _.uniq(tasksPage.data.map((task) => task.createdBy as string));
const createdByUserList = await getBatchUserDetails(createdByIds);
  1. 转换并合并数据:
const tasksList: ITask[] = tasksPage.data.map((task) => ({
    osdkTask: task,
    assignedTo: assignedToUserList[task.assignedTo as string],
    createdBy: createdByUserList[task.createdBy as string],
}));
  1. 通过 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);
}, []);

界面组件可以使用这些元数据来获取任务类型的显示名称、描述以及其他本体信息。

实时更新管理

订阅实现处理三种关键的更新场景:

  1. 新增或更新的任务: 获取用户详情并更新缓存。
if (update.state === "ADDED_OR_UPDATED") {
    // 获取用户详情并更新缓存中的任务
}
  1. 已删除的任务: 从缓存中过滤掉已删除的任务。
else if (update.state === "REMOVED") {
    // 从缓存中移除任务
}
  1. 过期通知: 处理订阅无法跟踪所有变更的情况。
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);

该模式实现以下功能:

  1. 使用 map() 从所有任务中提取用户 ID
  2. 使用 _.compact() 移除空值/未定义值
  3. 使用 _.uniq() 消除重复项
  4. 在单个批量操作中获取所有用户详情
  5. 创建一个按 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 的依赖: 该钩子依赖于当前用户的可用性,但如果管理模块无法加载用户信息,则没有健壮的回退方案。