useProjects hook(useProjects 钩子 (hook))¶
The useProjects hook provides a comprehensive interface for fetching and managing project data in the advanced to-do application, with an emphasis on calculating real-time task statistics. It leverages the Ontology SDK (OSDK) to interact with backend services in a type-safe manner, offering a clean interface for components to access project data along with aggregated task metrics.
This hook implements runtime-derived properties to efficiently calculate task statistics for each project, such as the total number of tasks and their distribution across different status categories (completed, in progress, not started). By using SWR (stale-while-revalidate) for data fetching, it ensures optimized network requests while maintaining up-to-date data with configurable caching strategies.
View the useProjects reference code.
Key functions¶
- Server-side aggregation: Uses the runtime-derived properties pattern to perform server side aggregations, minimizing data transfer.
- Type safety: Ensures type safety throughout with TypeScript interfaces and OSDK type utilities.
- Task status constants: Uses constant objects for task statuses to prevent magic strings and improve code maintenance.
- Error handling: Implements robust error handling with try/catch and configurable retry mechanisms.
- Performance optimization: Configures SWR with custom options to optimize network requests and caching.
- Clean return interface: Provides a well-structured return object that components can easily consume.
- Component-friendly data: Transforms data into a format that is ready for component consumption without additional processing.
useProjects structure¶
Interface definition¶
export type IProject = Osdk.Instance<AdvanceTodoProject, never, PropertyKeys<AdvanceTodoProject>> & {
numberOfTasks: number,
numberOfCompletedTasks: number,
numberOfInProgressTasks: number,
numberOfNotStartedTasks: number,
}
This interface combines the base project type from the SDK with additional properties for task statistics:
- Uses
Osdk.Instanceto get the base type for project objects - Adds strongly-typed number properties for task statistics
- Creates a unified type that components can use without worrying about the underlying implementation
Data fetching¶
The useProjects hook uses SWR's data fetching capabilities with a custom fetcher function that does the following:
- Uses runtime-derived properties to fetch projects with task statistics in a single request
- Executes complex filtering and aggregation operations server-side
- Maps the results to the expected interface format
- Handles errors gracefully with try/catch
const fetcher = useCallback(async () => {
try {
const projectsPage = await client(AdvanceTodoProject)
.withProperties({
// Runtime-derived property definitions for task counts
})
.fetchPage();
const projects: IProject[] = projectsPage.data.map((project: ProjectWithRDP) => {
return {
...project,
numberOfTasks: project.numberOfTasks,
numberOfCompletedTasks: project.numberOfCompletedTasks,
numberOfInProgressTasks: project.numberOfInProgressTasks,
numberOfNotStartedTasks: project.numberOfNotStartedTasks,
};
});
return projects;
} catch (error) {
console.error("Error fetching projects:", error);
return [];
}
}, [client]);
SWR configuration¶
The hook configures SWR with custom options to optimize performance:
const { data, isLoading, isValidating, error } = useSWR<IProject[]>(
"projects",
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 10_000, // Avoid refetching within 10 seconds
errorRetryCount: 3 // Retry failed requests up to 3 times
}
);
As configured in the example above, these settings do the following:
revalidateOnFocus: Disables automatic revalidation when the window regains focus.dedupingInterval: Implements a 10-second deduping interval to prevent redundant requests.errorRetryCount: Configures error retry behavior for better resilience.
Return value¶
{
projects: data ?? [], // Array of projects with task statistics
isLoading: boolean, // True during initial data loading
isValidating: boolean, // True during background revalidation
isError: Error | undefined, // Error object if the request failed
}
This clean interface gives components all the information they need to handle various states and render the appropriate UI.
Implementation¶
Runtime-derived properties¶
Runtime-derived properties are a powerful pattern that allows for computing and fetching only the data needed at runtime, rather than retrieving entire objects and computing values client-side.
In this hook, runtime-derived properties are used to calculate task statistics directly on the server:
.withProperties({
"numberOfTasks": (baseObjectSet) =>
baseObjectSet.pivotTo("codingTasks").aggregate("$count").add(
baseObjectSet.pivotTo("learningTasks").aggregate("$count")),
"numberOfCompletedTasks": (baseObjectSet) =>
baseObjectSet.pivotTo("codingTasks").where({
"status": { $eq: TASK_STATUS.COMPLETED },
}).aggregate("$count").add(baseObjectSet.pivotTo("learningTasks").where({
"status": { $eq: TASK_STATUS.COMPLETED },
}).aggregate("$count")),
// Additional properties...
})
This implementation uses several key OSDK patterns:
pivotTo: Navigates from projects to their associated tasks- Purpose: Traverses relationships between projects and their tasks
-
Benefits: Enables joining related data without complex relationship management
-
where: Applies filters to select tasks with specific statuses - Purpose: Filters collections based on property conditions
-
Benefits: Reduces data transfer by filtering server-side
-
aggregate: Computes counts on the filtered sets - Purpose: Performs server-side data aggregation
-
Benefits: Minimizes data transfer by returning only aggregated results
-
Mathematical operations: Uses
addto combine counts from different task types - Purpose: Performs math operations between runtime-derived property expressions
- Benefits: Enables complex calculations without multiple queries
Benefits¶
- Performance optimization: Counting happens server-side, reducing data transfer
- Reduced client-side computation: Client receives only final counts
- Single request pattern: All statistics are fetched in one request
- Declarative data requirements: Code is self-documenting
Constants for status enumerations¶
The useProjects hook defines constants for task statuses to avoid magic strings:
const TASK_STATUS = {
COMPLETED: "COMPLETED",
IN_PROGRESS: "IN PROGRESS",
NOT_STARTED: "NOT STARTED"
} as const;
This pattern does the following:
- Creates a strongly-typed constant object with status values
- Improves code readability by providing meaningful variable names
- Centralizes status definitions for consistent usage across the codebase
- Prevents typos and inconsistencies in status string values
External packages¶
The following external packages can be used with the useProjects hook.
@advanced-to-do-application/sdk¶
Purpose: Application-specific SDK with predefined data types Benefits:
- Contains the data model for projects (
AdvanceTodoProject) - Provides typed access to project properties and relationships
- Enables consistent type enforcement across the application
- Simplifies interaction with the application's data model
@osdk/client & @osdk/react¶
Purpose: Ontology SDK client for interacting with a backend data service Benefits:
- Provides type-safe interfaces for data models
- Enables structured data queries with complex filtering
- Supports the runtime-derived property pattern for server-side computations
- Offers utilities for type manipulation through
PropertyKeysandOsdk.Instance - Exposes hooks like
useOsdkClientfor accessing the client instance
useSWR¶
Purpose: Data fetching, caching, and state management Benefits:
- Provides an elegant way to fetch and cache project data
- Handles loading and error states automatically
- Offers built-in revalidation strategies with configurable options
- Reduces network requests through intelligent caching
- Simplifies data fetching with automatic error handling
Usage example¶
import React from 'react';
import useProjects from '../dataServices/useProjects';
import { ProjectCard } from '../components/ProjectCard';
const ProjectsPage: React.FC = () => {
const { projects, isLoading, isError } = useProjects();
if (isLoading) return <div>Loading projects...</div>;
if (isError) return <div>Error loading projects: {isError.message}</div>;
return (
<div className="projects-container">
<h1>Projects ({projects.length})</h1>
{projects.length === 0 ? (
<div className="empty-state">No projects found</div>
) : (
<div className="projects-grid">
{projects.map(project => (
<ProjectCard
key={project.$primaryKey}
project={project}
/>
))}
</div>
)}
</div>
);
};
// ProjectCard component using the data
const ProjectCard: React.FC<{ project: IProject }> = ({ project }) => {
return (
<div className="project-card">
<h3>{project.name}</h3>
<div className="progress-bar">
<div
className="progress-fill"
/>
</div>
<div className="task-stats">
<div>Total Tasks: {project.numberOfTasks}</div>
<div>Completed: {project.numberOfCompletedTasks}</div>
<div>In Progress: {project.numberOfInProgressTasks}</div>
<div>Not Started: {project.numberOfNotStartedTasks}</div>
</div>
</div>
);
};
export default ProjectsPage;
Edge cases and limitations¶
Consider the following scenarios and limitations when using the useProjects hook:
- Empty results handling: The implementation handles empty result sets gracefully, returning in an empty array rather than throwing an error.
- Error recovery: The hook implements basic error retry through SWR's
errorRetryCount, but complex failure scenarios might require additional handling. - Pagination limitations: The current implementation fetches all projects at once, which could be problematic with a large number of projects. Consider implementing pagination with
fetchPageoptions for large datasets. - Task type dependency: The hook specifically looks for "codingTasks" and "learningTasks" relationships. If new task types are added to the system, the runtime-derived property queries will need to be updated.
- Polling for updates: The hook does not implement polling for updates. For applications needing real-time updates, consider adding a polling mechanism or WebSocket integration.
- Task status enumeration coupling: The hook is tightly coupled to specific task status values. If the backend changes these values, the frontend will need to be updated accordingly.
- Mathematically derived properties: While
numberOfTasksis calculated by adding counts from different task types, it could alternatively be pre-computed on the backend. Consider evaluating performance trade-offs for different approaches.
flowchart TD
A[Component renders] --> B[useProjects hook called]
B --> C[SWR checks cache]
C -->|Cache hit| D[Return cached projects]
C -->|Cache miss| E[Call fetcher function]
E --> F[Fetch projects with runtime-derived properties]
F --> G[Transform data]
G --> H[Return projects]
H --> I[Component renders projects]
subgraph "Runtime-Derived Properties Processing"
F1[Fetch base projects] --> F2[Compute task counts]
F2 --> F3[Filter by status]
F3 --> F4[Aggregate counts]
end
中文翻译¶
useProjects 钩子 (hook)¶
useProjects 钩子 (hook) 为高级待办事项应用提供了一个全面的接口,用于获取和管理项目数据,重点在于计算实时任务统计信息。它利用本体 SDK(OSDK)以类型安全的方式与后端服务交互,为组件提供干净的数据访问接口以及聚合的任务指标。
该钩子实现了运行时派生属性,以高效计算每个项目的任务统计信息,例如任务总数及其在不同状态类别(已完成、进行中、未开始)中的分布。通过使用 SWR(stale-while-revalidate)进行数据获取,它确保了优化的网络请求,同时通过可配置的缓存策略维护最新数据。
关键功能¶
- 服务端聚合: 使用运行时派生属性模式执行服务端聚合,最小化数据传输。
- 类型安全: 通过 TypeScript 接口和 OSDK 类型工具确保全程类型安全。
- 任务状态常量: 使用常量对象表示任务状态,避免魔法字符串并提高代码可维护性。
- 错误处理: 通过 try/catch 和可配置的重试机制实现健壮的错误处理。
- 性能优化: 使用自定义选项配置 SWR,优化网络请求和缓存。
- 清晰的返回接口: 提供结构良好的返回对象,便于组件使用。
- 组件友好数据: 将数据转换为可直接供组件使用的格式,无需额外处理。
useProjects 结构¶
接口定义¶
export type IProject = Osdk.Instance<AdvanceTodoProject, never, PropertyKeys<AdvanceTodoProject>> & {
numberOfTasks: number,
numberOfCompletedTasks: number,
numberOfInProgressTasks: number,
numberOfNotStartedTasks: number,
}
该接口将 SDK 中的基础项目类型与任务统计信息的附加属性相结合:
- 使用
Osdk.Instance获取项目对象的基础类型 - 为任务统计信息添加强类型的数字属性
- 创建一个统一的类型,组件可以直接使用,无需关心底层实现
数据获取¶
useProjects 钩子使用 SWR 的数据获取功能,配合自定义的 fetcher 函数,该函数执行以下操作:
- 使用运行时派生属性在单个请求中获取项目及其任务统计信息
- 在服务端执行复杂的过滤和聚合操作
- 将结果映射到预期的接口格式
- 通过 try/catch 优雅地处理错误
const fetcher = useCallback(async () => {
try {
const projectsPage = await client(AdvanceTodoProject)
.withProperties({
// 任务计数的运行时派生属性定义
})
.fetchPage();
const projects: IProject[] = projectsPage.data.map((project: ProjectWithRDP) => {
return {
...project,
numberOfTasks: project.numberOfTasks,
numberOfCompletedTasks: project.numberOfCompletedTasks,
numberOfInProgressTasks: project.numberOfInProgressTasks,
numberOfNotStartedTasks: project.numberOfNotStartedTasks,
};
});
return projects;
} catch (error) {
console.error("获取项目时出错:", error);
return [];
}
}, [client]);
SWR 配置¶
该钩子使用自定义选项配置 SWR 以优化性能:
const { data, isLoading, isValidating, error } = useSWR<IProject[]>(
"projects",
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 10_000, // 10秒内避免重新获取
errorRetryCount: 3 // 失败请求最多重试3次
}
);
如上例所示配置,这些设置执行以下操作:
revalidateOnFocus: 禁用窗口重新获得焦点时的自动重新验证。dedupingInterval: 实现10秒的去重间隔,防止冗余请求。errorRetryCount: 配置错误重试行为,提高弹性。
返回值¶
{
projects: data ?? [], // 包含任务统计信息的项目数组
isLoading: boolean, // 初始数据加载时为 true
isValidating: boolean, // 后台重新验证时为 true
isError: Error | undefined, // 请求失败时的错误对象
}
这个清晰的接口为组件提供了处理各种状态和渲染适当 UI 所需的所有信息。
实现¶
运行时派生属性¶
运行时派生属性是一种强大的模式,允许在运行时仅计算和获取所需的数据,而不是检索整个对象并在客户端计算值。
在该钩子中,运行时派生属性用于直接在服务端计算任务统计信息:
.withProperties({
"numberOfTasks": (baseObjectSet) =>
baseObjectSet.pivotTo("codingTasks").aggregate("$count").add(
baseObjectSet.pivotTo("learningTasks").aggregate("$count")),
"numberOfCompletedTasks": (baseObjectSet) =>
baseObjectSet.pivotTo("codingTasks").where({
"status": { $eq: TASK_STATUS.COMPLETED },
}).aggregate("$count").add(baseObjectSet.pivotTo("learningTasks").where({
"status": { $eq: TASK_STATUS.COMPLETED },
}).aggregate("$count")),
// 其他属性...
})
该实现使用了几个关键的 OSDK 模式:
pivotTo: 从项目导航到其关联的任务- 目的: 遍历项目与其任务之间的关系
-
优势: 无需复杂的关系管理即可连接相关数据
-
where: 应用过滤器以选择具有特定状态的任务 - 目的: 基于属性条件过滤集合
-
优势: 通过服务端过滤减少数据传输
-
aggregate: 对过滤后的集合计算计数 - 目的: 执行服务端数据聚合
-
优势: 仅返回聚合结果,最小化数据传输
-
数学运算: 使用
add组合不同任务类型的计数 - 目的: 在运行时派生属性表达式之间执行数学运算
- 优势: 无需多个查询即可实现复杂计算
优势¶
- 性能优化: 计数在服务端完成,减少数据传输
- 减少客户端计算: 客户端仅接收最终计数
- 单请求模式: 所有统计信息在一个请求中获取
- 声明式数据需求: 代码自文档化
状态枚举常量¶
useProjects 钩子定义了任务状态常量,以避免魔法字符串:
const TASK_STATUS = {
COMPLETED: "COMPLETED",
IN_PROGRESS: "IN PROGRESS",
NOT_STARTED: "NOT STARTED"
} as const;
该模式执行以下操作:
- 创建一个强类型的常量对象,包含状态值
- 通过提供有意义的变量名提高代码可读性
- 集中状态定义,确保整个代码库的一致使用
- 防止状态字符串值中的拼写错误和不一致
外部包¶
以下外部包可与 useProjects 钩子一起使用。
@advanced-to-do-application/sdk¶
目的: 具有预定义数据类型的应用特定 SDK 优势:
- 包含项目的数据模型(
AdvanceTodoProject) - 提供对项目属性和关系的类型化访问
- 确保整个应用中的一致类型强制
- 简化与应用数据模型的交互
@osdk/client & @osdk/react¶
目的: 用于与后端数据服务交互的本体 SDK 客户端 优势:
- 为数据模型提供类型安全接口
- 支持具有复杂过滤的结构化数据查询
- 支持运行时派生属性模式进行服务端计算
- 通过
PropertyKeys和Osdk.Instance提供类型操作工具 - 暴露
useOsdkClient等钩子用于访问客户端实例
useSWR¶
目的: 数据获取、缓存和状态管理 优势:
- 提供优雅的方式来获取和缓存项目数据
- 自动处理加载和错误状态
- 提供内置的重新验证策略,具有可配置选项
- 通过智能缓存减少网络请求
- 通过自动错误处理简化数据获取
使用示例¶
import React from 'react';
import useProjects from '../dataServices/useProjects';
import { ProjectCard } from '../components/ProjectCard';
const ProjectsPage: React.FC = () => {
const { projects, isLoading, isError } = useProjects();
if (isLoading) return <div>正在加载项目...</div>;
if (isError) return <div>加载项目时出错:{isError.message}</div>;
return (
<div className="projects-container">
<h1>项目 ({projects.length})</h1>
{projects.length === 0 ? (
<div className="empty-state">未找到项目</div>
) : (
<div className="projects-grid">
{projects.map(project => (
<ProjectCard
key={project.$primaryKey}
project={project}
/>
))}
</div>
)}
</div>
);
};
// 使用数据的 ProjectCard 组件
const ProjectCard: React.FC<{ project: IProject }> = ({ project }) => {
return (
<div className="project-card">
<h3>{project.name}</h3>
<div className="progress-bar">
<div
className="progress-fill"
/>
</div>
<div className="task-stats">
<div>总任务数:{project.numberOfTasks}</div>
<div>已完成:{project.numberOfCompletedTasks}</div>
<div>进行中:{project.numberOfInProgressTasks}</div>
<div>未开始:{project.numberOfNotStartedTasks}</div>
</div>
</div>
);
};
export default ProjectsPage;
边界情况和限制¶
使用 useProjects 钩子时,请考虑以下场景和限制:
- 空结果处理: 实现优雅地处理空结果集,返回空数组而不是抛出错误。
- 错误恢复: 该钩子通过 SWR 的
errorRetryCount实现基本错误重试,但复杂的失败场景可能需要额外处理。 - 分页限制: 当前实现一次性获取所有项目,对于大量项目可能会有问题。考虑对大型数据集使用
fetchPage选项实现分页。 - 任务类型依赖: 该钩子专门查找 "codingTasks" 和 "learningTasks" 关系。如果系统中添加了新的任务类型,需要更新运行时派生属性查询。
- 轮询更新: 该钩子未实现轮询更新。对于需要实时更新的应用,考虑添加轮询机制或 WebSocket 集成。
- 任务状态枚举耦合: 该钩子与特定的任务状态值紧密耦合。如果后端更改了这些值,前端需要相应更新。
- 数学派生属性: 虽然
numberOfTasks是通过添加不同任务类型的计数计算的,但也可以在后端预计算。考虑评估不同方法的性能权衡。
flowchart TD
A[组件渲染] --> B[调用 useProjects 钩子]
B --> C[SWR 检查缓存]
C -->|缓存命中| D[返回缓存的项目]
C -->|缓存未命中| E[调用 fetcher 函数]
E --> F[使用运行时派生属性获取项目]
F --> G[转换数据]
G --> H[返回项目]
H --> I[组件渲染项目]
subgraph "运行时派生属性处理"
F1[获取基础项目] --> F2[计算任务计数]
F2 --> F3[按状态过滤]
F3 --> F4[聚合计数]
end