fix: prevent page jump when adding/removing tags with optimistic updates
- Update UI immediately when adding/removing tags without full page reload - Fetch updated recipe data in background to get proper tag IDs - Revert optimistic update on error and reload - Maintains scroll position and focus in tag input field Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -141,24 +141,35 @@ function RecipeDetail() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
const optimisticTag = { tag: { id: 'temp', name: trimmedTag } };
|
||||
setRecipe({
|
||||
...recipe,
|
||||
tags: [...(recipe.tags || []), optimisticTag]
|
||||
});
|
||||
setTagInput('');
|
||||
// Keep focus in input field
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
|
||||
try {
|
||||
setSavingTags(true);
|
||||
// Send array of tag names (strings) to API
|
||||
const updatedTags = [...existingTagNames, trimmedTag];
|
||||
await recipesApi.update(id, { tags: updatedTags });
|
||||
|
||||
// Reload the recipe to get the updated tag structure from API
|
||||
await loadRecipe(id);
|
||||
setTagInput('');
|
||||
|
||||
// Reload available tags to include newly created ones
|
||||
loadTags();
|
||||
|
||||
// Keep focus in input field
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
// Fetch the updated recipe to get the proper tag IDs, but don't reload the whole page
|
||||
const response = await recipesApi.getById(id);
|
||||
if (response.data) {
|
||||
setRecipe(response.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add tag:', err);
|
||||
alert('Failed to add tag');
|
||||
// Revert optimistic update on error
|
||||
await loadRecipe(id);
|
||||
} finally {
|
||||
setSavingTags(false);
|
||||
}
|
||||
@@ -167,10 +178,21 @@ function RecipeDetail() {
|
||||
const handleRemoveTag = async (tagToRemove: string) => {
|
||||
if (!id || !recipe) return;
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
const previousTags = recipe.tags;
|
||||
const updatedTagsOptimistic = (recipe.tags || []).filter(tagItem => {
|
||||
const tagName = typeof tagItem === 'string' ? tagItem : tagItem.tag?.name || tagItem.name;
|
||||
return tagName !== tagToRemove;
|
||||
});
|
||||
setRecipe({
|
||||
...recipe,
|
||||
tags: updatedTagsOptimistic
|
||||
});
|
||||
|
||||
try {
|
||||
setSavingTags(true);
|
||||
// Convert existing tags to string array and filter out the removed tag
|
||||
const existingTagNames = (recipe.tags || [])
|
||||
const existingTagNames = (previousTags || [])
|
||||
.map(tagItem =>
|
||||
typeof tagItem === 'string' ? tagItem : tagItem.tag?.name || tagItem.name
|
||||
)
|
||||
@@ -178,11 +200,16 @@ function RecipeDetail() {
|
||||
const updatedTags = existingTagNames.filter(tag => tag !== tagToRemove);
|
||||
await recipesApi.update(id, { tags: updatedTags });
|
||||
|
||||
// Reload the recipe to get the updated tag structure from API
|
||||
await loadRecipe(id);
|
||||
// Fetch the updated recipe to get the proper tag structure
|
||||
const response = await recipesApi.getById(id);
|
||||
if (response.data) {
|
||||
setRecipe(response.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to remove tag:', err);
|
||||
alert('Failed to remove tag');
|
||||
// Revert optimistic update on error
|
||||
await loadRecipe(id);
|
||||
} finally {
|
||||
setSavingTags(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user