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.
useProjects
structureCopied!1 2 3 4 5 6
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:
Osdk.Instance
to get the base type for project objectsThe useProjects
hook uses SWR's data fetching capabilities with a custom fetcher function that does the following:
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
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]);
The hook configures SWR with custom options to optimize performance:
Copied!1 2 3 4 5 6 7 8 9
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.Copied!1 2 3 4 5 6
{ 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.
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:
Copied!1 2 3 4 5 6 7 8 9 10 11 12
.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
where
: Applies filters to select tasks with specific statuses
aggregate
: Computes counts on the filtered sets
Mathematical operations: Uses add
to combine counts from different task types
The useProjects
hook defines constants for task statuses to avoid magic strings:
Copied!1 2 3 4 5
const TASK_STATUS = { COMPLETED: "COMPLETED", IN_PROGRESS: "IN PROGRESS", NOT_STARTED: "NOT STARTED" } as const;
This pattern does the following:
The following external packages can be used with the useProjects
hook.
Purpose: Application-specific SDK with predefined data types Benefits:
AdvanceTodoProject
)Purpose: Ontology SDK client for interacting with a backend data service Benefits:
PropertyKeys
and Osdk.Instance
useOsdkClient
for accessing the client instancePurpose: Data fetching, caching, and state management Benefits:
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
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;
Consider the following scenarios and limitations when using the useProjects
hook:
errorRetryCount
, but complex failure scenarios might require additional handling.fetchPage
options for large datasets.numberOfTasks
is 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.Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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