improve on responsiveness with the use of react.memo
This commit is contained in:
parent
8d5b2d3ea3
commit
ce2574c454
@ -1,9 +1,9 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { memo, useRef, useState } from "react";
|
||||
import AddImageModal from "../modals/AddImageModal";
|
||||
import ConfirmBuyModal from "../modals/ConfirmBuyModal";
|
||||
import ImageModal from "../modals/ImageModal";
|
||||
|
||||
export default function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
|
||||
function GroceryListItem({ item, onClick, onImageAdded, onLongPress }) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showAddImageModal, setShowAddImageModal] = useState(false);
|
||||
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
||||
@ -176,3 +176,20 @@ export default function GroceryListItem({ item, onClick, onImageAdded, onLongPre
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize component to prevent re-renders when props haven't changed
|
||||
export default memo(GroceryListItem, (prevProps, nextProps) => {
|
||||
// Only re-render if the item data or handlers have changed
|
||||
return (
|
||||
prevProps.item.id === nextProps.item.id &&
|
||||
prevProps.item.item_name === nextProps.item.item_name &&
|
||||
prevProps.item.quantity === nextProps.item.quantity &&
|
||||
prevProps.item.item_image === nextProps.item.item_image &&
|
||||
prevProps.item.bought === nextProps.item.bought &&
|
||||
prevProps.item.last_added_on === nextProps.item.last_added_on &&
|
||||
prevProps.item.added_by_users?.join(',') === nextProps.item.added_by_users?.join(',') &&
|
||||
prevProps.onClick === nextProps.onClick &&
|
||||
prevProps.onImageAdded === nextProps.onImageAdded &&
|
||||
prevProps.onLongPress === nextProps.onLongPress
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { addItem, getClassification, getItemByName, getList, getRecentlyBought, getSuggestions, markBought, updateItemImage, updateItemWithClassification } from "../api/list";
|
||||
import FloatingActionButton from "../components/common/FloatingActionButton";
|
||||
import SortDropdown from "../components/common/SortDropdown";
|
||||
@ -18,7 +18,6 @@ export default function GroceryList() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]);
|
||||
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(10);
|
||||
const [sortedItems, setSortedItems] = useState([]);
|
||||
const [sortMode, setSortMode] = useState("zone");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [showAddForm, setShowAddForm] = useState(true);
|
||||
@ -54,8 +53,8 @@ export default function GroceryList() {
|
||||
loadRecentlyBought();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let sorted = [...items];
|
||||
const sortedItems = useMemo(() => {
|
||||
const sorted = [...items];
|
||||
|
||||
if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name));
|
||||
if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name));
|
||||
@ -63,29 +62,24 @@ export default function GroceryList() {
|
||||
if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity);
|
||||
if (sortMode === "zone") {
|
||||
sorted.sort((a, b) => {
|
||||
// Items without classification go to the end
|
||||
if (!a.item_type && b.item_type) return 1;
|
||||
if (a.item_type && !b.item_type) return -1;
|
||||
if (!a.item_type && !b.item_type) return a.item_name.localeCompare(b.item_name);
|
||||
|
||||
// Sort by item_type
|
||||
const typeCompare = (a.item_type || "").localeCompare(b.item_type || "");
|
||||
if (typeCompare !== 0) return typeCompare;
|
||||
|
||||
// Then by item_group
|
||||
const groupCompare = (a.item_group || "").localeCompare(b.item_group || "");
|
||||
if (groupCompare !== 0) return groupCompare;
|
||||
|
||||
// Then by zone
|
||||
const zoneCompare = (a.zone || "").localeCompare(b.zone || "");
|
||||
if (zoneCompare !== 0) return zoneCompare;
|
||||
|
||||
// Finally by name
|
||||
return a.item_name.localeCompare(b.item_name);
|
||||
});
|
||||
}
|
||||
|
||||
setSortedItems(sorted);
|
||||
return sorted;
|
||||
}, [items, sortMode]);
|
||||
|
||||
const handleSuggest = async (text) => {
|
||||
@ -95,10 +89,7 @@ export default function GroceryList() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine both unbought and recently bought items for similarity checking
|
||||
const allItems = [...items, ...recentlyBoughtItems];
|
||||
|
||||
// Check if exact match exists (case-insensitive)
|
||||
const lowerText = text.toLowerCase().trim();
|
||||
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
|
||||
|
||||
@ -117,12 +108,11 @@ export default function GroceryList() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async (itemName, quantity) => {
|
||||
const handleAdd = useCallback(async (itemName, quantity) => {
|
||||
if (!itemName.trim()) return;
|
||||
|
||||
const lowerItemName = itemName.toLowerCase().trim();
|
||||
|
||||
// First check if exact item exists in database (case-insensitive)
|
||||
let existingItem = null;
|
||||
try {
|
||||
const response = await getItemByName(itemName);
|
||||
@ -131,29 +121,26 @@ export default function GroceryList() {
|
||||
existingItem = null;
|
||||
}
|
||||
|
||||
// If exact item exists, skip similarity check and process directly
|
||||
if (existingItem) {
|
||||
await processItemAddition(itemName, quantity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check for similar items if exact item doesn't exist
|
||||
const allItems = [...items, ...recentlyBoughtItems];
|
||||
const similar = findSimilarItems(itemName, allItems, 70);
|
||||
if (similar.length > 0) {
|
||||
// Show modal and wait for user decision
|
||||
setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity });
|
||||
setShowSimilarModal(true);
|
||||
return;
|
||||
}
|
||||
setItems(prevItems => {
|
||||
const allItems = [...prevItems, ...recentlyBoughtItems];
|
||||
const similar = findSimilarItems(itemName, allItems, 70);
|
||||
if (similar.length > 0) {
|
||||
setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity });
|
||||
setShowSimilarModal(true);
|
||||
return prevItems;
|
||||
}
|
||||
|
||||
// Continue with normal flow for new items
|
||||
await processItemAddition(itemName, quantity);
|
||||
};
|
||||
processItemAddition(itemName, quantity);
|
||||
return prevItems;
|
||||
});
|
||||
}, [recentlyBoughtItems]);
|
||||
|
||||
const processItemAddition = async (itemName, quantity) => {
|
||||
|
||||
// Check if item exists in database (case-insensitive)
|
||||
const processItemAddition = useCallback(async (itemName, quantity) => {
|
||||
let existingItem = null;
|
||||
try {
|
||||
const response = await getItemByName(itemName);
|
||||
@ -163,7 +150,6 @@ export default function GroceryList() {
|
||||
}
|
||||
|
||||
if (existingItem && existingItem.bought === false) {
|
||||
// Item exists and is unbought - update quantity
|
||||
const currentQuantity = existingItem.quantity;
|
||||
const newQuantity = currentQuantity + quantity;
|
||||
const yes = window.confirm(
|
||||
@ -171,117 +157,140 @@ export default function GroceryList() {
|
||||
);
|
||||
if (!yes) return;
|
||||
|
||||
await addItem(itemName, newQuantity, null);
|
||||
const response = await addItem(itemName, newQuantity, null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
loadItems();
|
||||
|
||||
if (response.data) {
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === existingItem.id ? { ...item, ...response.data } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (existingItem) {
|
||||
// Item exists in database (was previously bought) - just add quantity
|
||||
await addItem(itemName, quantity, null);
|
||||
const response = await addItem(itemName, quantity, null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
loadItems();
|
||||
|
||||
if (response.data) {
|
||||
setItems(prevItems => [...prevItems, response.data]);
|
||||
setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id));
|
||||
}
|
||||
} else {
|
||||
// NEW ITEM - show combined add details modal
|
||||
setPendingItem({ itemName, quantity });
|
||||
setShowAddDetailsModal(true);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSimilarCancel = () => {
|
||||
const handleSimilarCancel = useCallback(() => {
|
||||
setShowSimilarModal(false);
|
||||
setSimilarItemSuggestion(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSimilarNo = async () => {
|
||||
const handleSimilarNo = useCallback(async () => {
|
||||
if (!similarItemSuggestion) return;
|
||||
setShowSimilarModal(false);
|
||||
// Create new item with original name
|
||||
await processItemAddition(similarItemSuggestion.originalName, similarItemSuggestion.quantity);
|
||||
setSimilarItemSuggestion(null);
|
||||
};
|
||||
}, [similarItemSuggestion, processItemAddition]);
|
||||
|
||||
const handleSimilarYes = async () => {
|
||||
const handleSimilarYes = useCallback(async () => {
|
||||
if (!similarItemSuggestion) return;
|
||||
setShowSimilarModal(false);
|
||||
// Use suggested item name
|
||||
await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity);
|
||||
setSimilarItemSuggestion(null);
|
||||
};
|
||||
}, [similarItemSuggestion, processItemAddition]);
|
||||
|
||||
const handleAddDetailsConfirm = async (imageFile, classification) => {
|
||||
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
|
||||
if (!pendingItem) return;
|
||||
|
||||
try {
|
||||
// Add item to grocery_list with image
|
||||
await addItem(pendingItem.itemName, pendingItem.quantity, imageFile);
|
||||
const addResponse = await addItem(pendingItem.itemName, pendingItem.quantity, imageFile);
|
||||
let newItem = addResponse.data;
|
||||
|
||||
// If classification provided, add it
|
||||
if (classification) {
|
||||
const itemResponse = await getItemByName(pendingItem.itemName);
|
||||
const itemId = itemResponse.data.id;
|
||||
await updateItemWithClassification(itemId, undefined, undefined, classification);
|
||||
const updateResponse = await updateItemWithClassification(itemId, undefined, undefined, classification);
|
||||
newItem = { ...newItem, ...updateResponse.data };
|
||||
}
|
||||
|
||||
setShowAddDetailsModal(false);
|
||||
setPendingItem(null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
loadItems();
|
||||
|
||||
if (newItem) {
|
||||
setItems(prevItems => [...prevItems, newItem]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add item:", error);
|
||||
alert("Failed to add item. Please try again.");
|
||||
}
|
||||
};
|
||||
}, [pendingItem]);
|
||||
|
||||
const handleAddDetailsSkip = async () => {
|
||||
const handleAddDetailsSkip = useCallback(async () => {
|
||||
if (!pendingItem) return;
|
||||
|
||||
try {
|
||||
// Add item without image or classification
|
||||
await addItem(pendingItem.itemName, pendingItem.quantity, null);
|
||||
const response = await addItem(pendingItem.itemName, pendingItem.quantity, null);
|
||||
|
||||
setShowAddDetailsModal(false);
|
||||
setPendingItem(null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
loadItems();
|
||||
|
||||
if (response.data) {
|
||||
setItems(prevItems => [...prevItems, response.data]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add item:", error);
|
||||
alert("Failed to add item. Please try again.");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAddDetailsCancel = () => {
|
||||
const handleAddDetailsCancel = useCallback(() => {
|
||||
setShowAddDetailsModal(false);
|
||||
setPendingItem(null);
|
||||
setSuggestions([]);
|
||||
setButtonText("Add Item");
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const handleBought = async (id, quantity) => {
|
||||
const handleBought = useCallback(async (id, quantity) => {
|
||||
await markBought(id);
|
||||
loadItems();
|
||||
loadRecentlyBought();
|
||||
};
|
||||
|
||||
const handleImageAdded = async (id, itemName, quantity, imageFile) => {
|
||||
setItems(prevItems => prevItems.filter(item => item.id !== id));
|
||||
loadRecentlyBought();
|
||||
}, []);
|
||||
|
||||
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
|
||||
try {
|
||||
await updateItemImage(id, itemName, quantity, imageFile);
|
||||
loadItems(); // Reload to show new image
|
||||
const response = await updateItemImage(id, itemName, quantity, imageFile);
|
||||
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, ...response.data } : item
|
||||
)
|
||||
);
|
||||
|
||||
setRecentlyBoughtItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, ...response.data } : item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to add image:", error);
|
||||
alert("Failed to add image. Please try again.");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLongPress = async (item) => {
|
||||
const handleLongPress = useCallback(async (item) => {
|
||||
if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return;
|
||||
|
||||
try {
|
||||
// Fetch existing classification
|
||||
const classificationResponse = await getClassification(item.id);
|
||||
setEditingItem({
|
||||
...item,
|
||||
@ -293,27 +302,37 @@ export default function GroceryList() {
|
||||
setEditingItem({ ...item, classification: null });
|
||||
setShowEditModal(true);
|
||||
}
|
||||
};
|
||||
}, [role]);
|
||||
|
||||
const handleEditSave = async (id, itemName, quantity, classification) => {
|
||||
const handleEditSave = useCallback(async (id, itemName, quantity, classification) => {
|
||||
try {
|
||||
await updateItemWithClassification(id, itemName, quantity, classification);
|
||||
const response = await updateItemWithClassification(id, itemName, quantity, classification);
|
||||
setShowEditModal(false);
|
||||
setEditingItem(null);
|
||||
loadItems();
|
||||
loadRecentlyBought();
|
||||
|
||||
const updatedItem = response.data;
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, ...updatedItem } : item
|
||||
)
|
||||
);
|
||||
|
||||
setRecentlyBoughtItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, ...updatedItem } : item
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update item:", error);
|
||||
throw error; // Re-throw to let modal handle it
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEditCancel = () => {
|
||||
const handleEditCancel = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
setEditingItem(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Group items by zone for classification view
|
||||
const groupItemsByZone = (items) => {
|
||||
const groups = {};
|
||||
items.forEach(item => {
|
||||
@ -346,7 +365,6 @@ export default function GroceryList() {
|
||||
<SortDropdown value={sortMode} onChange={setSortMode} />
|
||||
|
||||
{sortMode === "zone" ? (
|
||||
// Grouped view by zone
|
||||
(() => {
|
||||
const grouped = groupItemsByZone(sortedItems);
|
||||
return Object.keys(grouped).map(zone => (
|
||||
@ -375,7 +393,6 @@ export default function GroceryList() {
|
||||
));
|
||||
})()
|
||||
) : (
|
||||
// Regular flat list view
|
||||
<ul className="glist-ul">
|
||||
{sortedItems.map((item) => (
|
||||
<GroceryListItem
|
||||
|
||||
Loading…
Reference in New Issue
Block a user