A Project's Task List
A Project's Task List
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The nested endpoint from chapter 39 (/api/projects/{id}/tasks) is NOW used on the detail page (chapter 81) to show ONLY the tasks of ONE project.
The Task type and hook
export interface Task {
'@id': string;
id: number;
title: string;
done: boolean;
}
export interface TaskCollection {
'hydra:member': Task[];
'hydra:totalItems': number;
}import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import type { TaskCollection } from '../types/task';
export function useProjectTasks(projectId: number) {
return useQuery({
queryKey: ['projects', projectId, 'tasks'],
queryFn: async () => {
const response = await apiClient.get<TaskCollection>(
`/projects/${projectId}/tasks`,
);
return response.data;
},
});
}queryKey: ['projects', projectId, 'tasks'] instead of simply ['tasks'] – COMPOSITE query keys are COMMON practice for PARAMETERIZED queries: EACH project gets ITS OWN cache entry, instead of sharing ONE common one.
Extending the detail page
import { useParams } from 'react-router-dom';
import { useProjectTasks } from '../hooks/useProjectTasks';
function ProjectDetailPage() {
const { id } = useParams<{ id: string }>();
const projectId = Number(id);
const { data, isPending } = useProjectTasks(projectId);
if (isPending) {
return <p>Loading tasks...</p>;
}
return (
<ul>
{data['hydra:member'].map((task) => (
<li key={task.id}>
{task.done ? '☑' : '☐'} {task.title}
</li>
))}
</ul>
);
}
export default ProjectDetailPage;Number(id) is NEEDED since useParams ALWAYS returns string (URL parameters are TEXT) – but the projectId prop OF useProjectTasks expects number, MATCHING the backend id.
Achtung: WITHOUT a guard for Number.isNaN(projectId), an INVALID URL like /projects/abc would trigger a request with NaN in the path – for a PRODUCTION-READY UI, an EARLY check with a redirect to a 404 page would be the CLEANER solution.
Tipp: invalidateQueries({ queryKey: ['projects', projectId, 'tasks'] }) in a FUTURE useCreateTask hook (chapter 87) must match the SAME composite key – A typo HERE would NOT update the list, WITHOUT TypeScript being able to CATCH it (query keys are simple arrays AT runtime).