Skip to main content

Modifying Existing Items

To update existing resources in the application, use the EditItem component. This component provides a dialog-based form that integrates with the backend API to persist changes and automatically refreshes the local data cache.

Updating an Item​

The EditItem component is typically triggered from an actions menu. It requires the current item data and a callback to execute upon a successful update.

import { EditItem } from "@/components/Items/EditItem"
import { type ItemPublic } from "@/client"

// Example usage within a menu
export const ItemActionsMenu = ({ item }: { item: ItemPublic }) => {
const [open, setOpen] = useState(false)

return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<EditItem
item={item}
onSuccess={() => setOpen(false)}
/>
</DropdownMenuContent>
</DropdownMenu>
)
}

Component Properties​

The EditItem component accepts props defined by the EditItemProps interface in frontend/src/components/Items/EditItem.tsx:

  • item: An ItemPublic object containing the current id, title, and description. This is used to initialize the form fields.
  • onSuccess: A function called after the update mutation completes successfully. In the standard implementation, this is used to close the parent dropdown menu.

Form Management and Validation​

The component uses react-hook-form with a zod schema to manage state and validation.

Validation Schema​

The schema ensures the title is present while the description remains optional:

const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})

Form Initialization​

The form is initialized with the item's existing values. Note the handling of the description to ensure it is compatible with controlled inputs:

const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: "onBlur",
defaultValues: {
title: item.title,
description: item.description ?? undefined, // Converts null to undefined for the input
},
})

Data Persistence​

Persistence is handled by a TanStack Query mutation that calls ItemsService.updateItem.

The Mutation Hook​

The mutation sends a PUT request to the /api/v1/items/{id} endpoint and manages the UI state (toasts and loading indicators).

const mutation = useMutation({
mutationFn: (data: FormData) =>
ItemsService.updateItem({ id: item.id, requestBody: data }),
onSuccess: () => {
showSuccessToast("Item updated successfully")
setIsOpen(false)
onSuccess()
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
// Refreshes the items list after the request finishes
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})

API Service​

The ItemsService.updateItem method (found in frontend/src/client/sdk.gen.ts) abstracts the HTTP request:

public static updateItem(data: ItemsUpdateItemData): CancelablePromise<ItemsUpdateItemResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/api/v1/items/{id}',
path: { id: data.id },
body: data.requestBody,
mediaType: 'application/json',
});
}

Troubleshooting and Gotchas​

Validation Timing​

The form is configured with mode: "onBlur". This means validation messages for the "Title" field will only appear once the user clicks out of the input field, rather than immediately as they type.

Dialog within Dropdowns​

The EditItem component renders a DropdownMenuItem as its trigger. To prevent the dropdown menu from closing prematurely when the dialog is clicked, the component calls e.preventDefault() on the selection event:

<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
onClick={() => setIsOpen(true)}
>
<Pencil />
Edit Item
</DropdownMenuItem>

Cache Invalidation​

The onSettled callback ensures that queryClient.invalidateQueries({ queryKey: ["items"] }) is called regardless of whether the update succeeded or failed. This guarantees that the UI stays in sync with the server state, but it may cause a brief loading flicker in the parent list component as data is refetched.