Editing a Project
Editing a Project
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
EXACTLY the same pattern as useCreateProject (chapter 78), this time for PATCH – the REST semantics from chapter 10 carry over 1:1 to the hook structure.
Writing the update hook
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import type { Project } from '../types/project';
interface UpdateProjectInput {
id: number;
name?: string;
description?: string;
}
export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, ...input }: UpdateProjectInput) => {
const response = await apiClient.patch<Project>(
`/projects/${id}`,
input,
{ headers: { 'Content-Type': 'application/merge-patch+json' } },
);
return response.data;
},
onSuccess: (updatedProject) => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
queryClient.setQueryData(['project', updatedProject.id], updatedProject);
},
});
}Achtung: Content-Type: application/merge-patch+json MUST be set EXPLICITLY – the default header from chapter 74 is application/ld+json, which does NOT give the semantics expected by chapter 10 for PATCH.
setQueryData IN ADDITION to invalidateQueries: THIS updates the cache IMMEDIATELY with the server response (NO extra request needed, since the CURRENT data is ALREADY available), WHILE invalidateQueries reloads the LIST in the background.
An editable form
import { useState, type FormEvent } from 'react';
import { useUpdateProject } from '../hooks/useUpdateProject';
import type { Project } from '../types/project';
function EditProjectForm({ project }: { project: Project }) {
const [name, setName] = useState(project.name);
const updateProject = useUpdateProject();
function handleSubmit(event: FormEvent) {
event.preventDefault();
updateProject.mutate({ id: project.id, name });
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit" disabled={updateProject.isPending}>
Save
</button>
{updateProject.isSuccess && <p>Saved!</p>}
</form>
);
}
export default EditProjectForm;useState(project.name) initializes the form WITH the CURRENT values – EXACTLY like a classic Symfony FormType (createForm(ProjectType::class, $project)), just MANUALLY replicated on the React side.
Tipp: isSuccess IN ADDITION to isPending/isError – TanStack Query MODELS the ENTIRE lifecycle of a mutation (idle → pending → success/error), USEFUL for brief feedback like "Saved!".