fix: dublicate api calls fixed
This commit is contained in:
+56
-20
@@ -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>در حال بارگذاری پکیجها...</option>
|
||||||
|
) : (
|
||||||
|
packages.map((pkg) => (
|
||||||
<option key={pkg.id} value={pkg.name || ''}>
|
<option key={pkg.id} value={pkg.name || ''}>
|
||||||
{pkg.title} ({pkg.name})
|
{pkg.title} ({pkg.name})
|
||||||
</option>
|
</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" />
|
||||||
محصول جدید
|
محصول جدید
|
||||||
|
|||||||
+34
-17
@@ -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
|
||||||
|
const requestKey = `${fetchOptions.method || 'GET'}-${endpoint}-${JSON.stringify(options.body)}`;
|
||||||
|
|
||||||
|
// Check if there's already a pending request with the same key
|
||||||
|
if (pendingRequests.has(requestKey)) {
|
||||||
|
console.log('Deduplicating request:', endpoint);
|
||||||
|
return pendingRequests.get(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the request
|
||||||
|
const promise = fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
...fetchOptions,
|
...fetchOptions,
|
||||||
headers,
|
headers,
|
||||||
});
|
}).then(async (response) => {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({}));
|
const error = await response.json().catch(() => ({}));
|
||||||
throw new Error(error.message || 'API request failed');
|
throw new Error(error.message || 'API request failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
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();
|
||||||
Reference in New Issue
Block a user