fix: dublicate api calls fixed

This commit is contained in:
2026-02-20 20:38:16 +03:30
parent 74f2a82a0e
commit f641409d8a
2 changed files with 97 additions and 44 deletions
+59 -23
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { packagesApi } from '@/lib/api/packages'; import { packagesApi } from '@/lib/api/packages';
import { productsApi } from '@/lib/api/products'; import { productsApi } from '@/lib/api/products';
@@ -17,27 +17,26 @@ export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null); const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLoadingPackages, setIsLoadingPackages] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false); const [isFormVisible, setIsFormVisible] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
// Load packages on mount // Use refs to track initial loads and prevent duplicate calls
const initialLoadRef = useRef(false);
const prevPackageRef = useRef<string | null>(null);
// Load packages on mount - only once
useEffect(() => { useEffect(() => {
if (token) { if (token && !initialLoadRef.current) {
initialLoadRef.current = true;
loadPackages(); loadPackages();
} }
}, [token]); }, [token]);
// Load products when package is selected
useEffect(() => {
if (selectedPackage && token) {
loadProducts(selectedPackage.name!);
} else {
setProducts([]);
}
}, [selectedPackage, token]);
const loadPackages = async () => { const loadPackages = async () => {
setIsLoadingPackages(true);
setError(null);
try { try {
const data = await packagesApi.getAllPackages(token!); const data = await packagesApi.getAllPackages(token!);
setPackages(data); setPackages(data);
@@ -46,13 +45,24 @@ export default function ProductsPage() {
} }
} catch (err) { } catch (err) {
setError('خطا در دریافت لیست پکیج‌ها'); setError('خطا در دریافت لیست پکیج‌ها');
} finally {
setIsLoadingPackages(false);
} }
}; };
const loadProducts = async (packageName: string) => { const loadProducts = useCallback(async (packageName: string, force = false) => {
// Prevent duplicate loads of the same package
if (!force && prevPackageRef.current === packageName) {
console.log('Skipping duplicate load for package:', packageName);
return;
}
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
prevPackageRef.current = packageName;
try { try {
console.log('Loading products for package:', packageName);
const data = await productsApi.getProducts(packageName, token!); const data = await productsApi.getProducts(packageName, token!);
setProducts(data); setProducts(data);
} catch (err) { } catch (err) {
@@ -60,13 +70,24 @@ export default function ProductsPage() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; }, [token]);
// Load products when package changes - with proper dependency
useEffect(() => {
if (selectedPackage?.name && token) {
loadProducts(selectedPackage.name);
} else {
setProducts([]);
}
}, [selectedPackage?.name, token, loadProducts]);
const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const pkg = packages.find(p => p.name === e.target.value); const pkg = packages.find(p => p.name === e.target.value);
setSelectedPackage(pkg || null); setSelectedPackage(pkg || null);
setIsFormVisible(false); setIsFormVisible(false);
setSelectedProduct(null); setSelectedProduct(null);
setError(null);
setSuccess(null);
}; };
const handleCreate = () => { const handleCreate = () => {
@@ -93,7 +114,11 @@ export default function ProductsPage() {
try { try {
await productsApi.deleteProduct(selectedPackage.name!, product.id, token!); await productsApi.deleteProduct(selectedPackage.name!, product.id, token!);
setSuccess('محصول با موفقیت حذف شد'); setSuccess('محصول با موفقیت حذف شد');
await loadProducts(selectedPackage.name!); // Force reload products after delete
if (selectedPackage.name) {
prevPackageRef.current = null; // Reset ref to force load
await loadProducts(selectedPackage.name, true);
}
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'خطا در حذف محصول'); setError(err instanceof Error ? err.message : 'خطا در حذف محصول');
} finally { } finally {
@@ -119,7 +144,12 @@ export default function ProductsPage() {
setSuccess('محصول با موفقیت ایجاد شد'); setSuccess('محصول با موفقیت ایجاد شد');
} }
await loadProducts(selectedPackage.name!); // Force reload products after create/update
if (selectedPackage.name) {
prevPackageRef.current = null; // Reset ref to force load
await loadProducts(selectedPackage.name, true);
}
setIsFormVisible(false); setIsFormVisible(false);
setSelectedProduct(null); setSelectedProduct(null);
} catch (err) { } catch (err) {
@@ -154,13 +184,18 @@ export default function ProductsPage() {
id="package" id="package"
value={selectedPackage?.name || ''} value={selectedPackage?.name || ''}
onChange={handlePackageChange} onChange={handlePackageChange}
className="block w-full px-3 py-2 text-slate-900 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" disabled={isLoadingPackages}
className="block w-full px-3 py-2 text-slate-900 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:opacity-50"
> >
{packages.map((pkg) => ( {isLoadingPackages ? (
<option key={pkg.id} value={pkg.name || ''}> <option>در حال بارگذاری پکیجها...</option>
{pkg.title} ({pkg.name}) ) : (
</option> packages.map((pkg) => (
))} <option key={pkg.id} value={pkg.name || ''}>
{pkg.title} ({pkg.name})
</option>
))
)}
</select> </select>
</div> </div>
@@ -184,7 +219,8 @@ export default function ProductsPage() {
<div className="mb-4 flex justify-end"> <div className="mb-4 flex justify-end">
<button <button
onClick={handleCreate} onClick={handleCreate}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" disabled={isLoading}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
> >
<PlusIcon className="h-5 w-5 ml-2" /> <PlusIcon className="h-5 w-5 ml-2" />
محصول جدید محصول جدید
+38 -21
View File
@@ -4,6 +4,9 @@ interface ApiOptions extends RequestInit {
token?: string; token?: string;
} }
// Simple in-memory cache for ongoing requests
const pendingRequests = new Map();
class ApiClient { class ApiClient {
private async request<T>( private async request<T>(
endpoint: string, endpoint: string,
@@ -24,17 +27,34 @@ class ApiClient {
headers['Authorization'] = `Bearer ${token}`; headers['Authorization'] = `Bearer ${token}`;
} }
const response = await fetch(`${API_BASE_URL}${endpoint}`, { // Create a unique key for this request
...fetchOptions, const requestKey = `${fetchOptions.method || 'GET'}-${endpoint}-${JSON.stringify(options.body)}`;
headers,
});
if (!response.ok) { // Check if there's already a pending request with the same key
const error = await response.json().catch(() => ({})); if (pendingRequests.has(requestKey)) {
throw new Error(error.message || 'API request failed'); console.log('Deduplicating request:', endpoint);
return pendingRequests.get(requestKey);
} }
return response.json(); // Make the request
const promise = fetch(`${API_BASE_URL}${endpoint}`, {
...fetchOptions,
headers,
}).then(async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || 'API request failed');
}
return response.json();
}).finally(() => {
// Clean up after request completes
pendingRequests.delete(requestKey);
});
// Store the promise
pendingRequests.set(requestKey, promise);
return promise;
} }
async post<T>(endpoint: string, data: any, token?: string): Promise<T> { async post<T>(endpoint: string, data: any, token?: string): Promise<T> {
@@ -64,17 +84,25 @@ class ApiClient {
}); });
} }
async patch<T>(endpoint: string, data: any, token?: string): Promise<T> {
const body = data instanceof FormData ? data : JSON.stringify(data);
return this.request<T>(endpoint, {
method: 'PATCH',
body,
token,
});
}
async delete<T>(endpoint: string, data?: any, token?: string): Promise<T> { async delete<T>(endpoint: string, data?: any, token?: string): Promise<T> {
const options: ApiOptions = { const options: ApiOptions = {
method: 'DELETE', method: 'DELETE',
token, token,
}; };
// Handle DELETE with body if data is provided
if (data) { if (data) {
options.body = data instanceof FormData ? data : JSON.stringify(data); options.body = data instanceof FormData ? data : JSON.stringify(data);
// Don't set Content-Type for FormData
if (!(data instanceof FormData)) { if (!(data instanceof FormData)) {
options.headers = { options.headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -85,20 +113,9 @@ class ApiClient {
return this.request<T>(endpoint, options); return this.request<T>(endpoint, options);
} }
// Convenience method for DELETE with JSON body
async deleteWithBody<T>(endpoint: string, data: any, token?: string): Promise<T> { async deleteWithBody<T>(endpoint: string, data: any, token?: string): Promise<T> {
return this.delete<T>(endpoint, data, token); return this.delete<T>(endpoint, data, token);
} }
async patch<T>(endpoint: string, data: any, token?: string): Promise<T> {
const body = data instanceof FormData ? data : JSON.stringify(data);
return this.request<T>(endpoint, {
method: 'PATCH',
body,
token,
});
}
} }
export const apiClient = new ApiClient(); export const apiClient = new ApiClient();