Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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

src/types/task.ts
export interface Task {
  '@id': string;
  id: number;
  title: string;
  done: boolean;
}

export interface TaskCollection {
  'hydra:member': Task[];
  'hydra:totalItems': number;
}
src/hooks/useProjectTasks.ts
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

src/pages/ProjectDetailPage.tsx
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).