我正在为网站上的管理面板编写一个页面。我有一个组件,可以从后端接收数据(例如产品)并将其传递到目录编辑器组件。例如,我添加了一个新产品,使请求无效,然后使用 console.log 我看到请求被重新执行,但新数据没有传输到 CatalogEditor。需要刷新页面,然后组件中就会显示新的数据
为什么会这样?
管理面板组件
import HomeEditor from "@/components/home-editor";
import AboutEditor from "@/components/about-editor";
import ContactEditor from "@/components/contact-editor";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import CatalogEditor from "@/components/catalog-editor";
import { Product } from "@/types/product";
import { useEffect, useState } from "react";
interface AdminRightPanelProps {
selectedMenu: string;
}
const CUSTOM_URL = process.env.NEXT_PUBLIC_CUSTOM_URL || "";
export default function AdminRightPanel({
selectedMenu,
}: AdminRightPanelProps) {
const { data: homePageContent, isLoading: isHomePageLoading } =
useQuery<HomePageContent>({
queryKey: ["homePageContent"],
queryFn: async () => {
const response = await axios.get(`${CUSTOM_URL}api/content/home`);
return response.data;
},
});
const { data: aboutPageContent, isLoading: isAboutPageLoading } =
useQuery<AboutPageContent>({
queryKey: ["aboutPageContent"],
queryFn: async () => {
const response = await axios.get(`${CUSTOM_URL}api/content/about`);
return response.data;
},
});
const { data: contactPageContent, isLoading: isContactPageLoading } =
useQuery<ContactPageContent>({
queryKey: ["contactPageContent"],
queryFn: async () => {
const response = await axios.get(`${CUSTOM_URL}api/content/contact`);
return response.data;
},
});
const { data: products, isLoading: isProductsLoading } = useQuery<Product[]>({
queryKey: ["products"],
queryFn: async () => {
const response = await axios.get(`${CUSTOM_URL}api/content/products`);
return response.data;
},
});
const [productItems, setProductItems] = useState<Product[]>([]);
useEffect(() => {
if (products) {
setProductItems(products);
}
}, [products]);
const isLoading =
isHomePageLoading ||
isAboutPageLoading ||
isContactPageLoading ||
isProductsLoading;
if (isLoading) {
return <div className="m-auto">Loading...</div>;
}
switch (selectedMenu) {
case "home":
return <HomeEditor content={homePageContent} />;
case "catalog":
return <CatalogEditor products={productItems} />;
case "about":
return <AboutEditor content={aboutPageContent} />;
case "contact":
return <ContactEditor content={contactPageContent} />;
}
}
目录编辑器组件
import { useToast } from "@/hooks/use-toast";
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { Product } from "@/types/product";
import { ImageDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useQueryClient } from "@tanstack/react-query";
import EditProductItem from "@/components/edit-product-item";
interface CatalogEditorProps {
products: Product[];
}
const CUSTOM_URL = process.env.NEXT_PUBLIC_CUSTOM_URL || "";
export default function CatalogEditor({ products }: CatalogEditorProps) {
const { toast } = useToast();
const queryClient = useQueryClient();
const inputFileRef = useRef<HTMLInputElement | null>(null);
const [category, setCategory] = useState<string | null>(null);
const [base64Image, setBase64Image] = useState<string | null>(null);
const [isButtonsDisabled, setIsButtonsDisabled] = useState<boolean>(false);
const [isSubmitDisabled, setIsSubmitDisabled] = useState<boolean>(false);
useEffect(() => {
setIsButtonsDisabled(base64Image === null || base64Image === "");
}, [base64Image]);
useEffect(() => {
setIsSubmitDisabled(
base64Image === null ||
base64Image === "" ||
category === null ||
category === "",
);
}, [base64Image, category]);
function handleFileUpload() {
inputFileRef.current?.click();
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setBase64Image(reader.result as string);
};
reader.readAsDataURL(file);
}
}
async function onSubmit() {
if (base64Image && category) {
const product: Product = {
id: 0,
base64image: base64Image,
category: category,
};
fetch(`${CUSTOM_URL}api/content/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(product),
})
.then((response) => {
if (response.ok) {
toast({
title: "Дані оновлено!",
description: "Зміни скоро будуть застосовані!",
});
} else {
toast({
title: "Помилка!",
description: "Щось пішло не так :( Спробуйте пізніше!",
});
}
})
.catch((error) => {
toast({
title: "Помилка!",
description: "Щось пішло не так :( Спробуйте пізніше!",
});
});
setBase64Image(null);
await queryClient.refetchQueries({ queryKey: ["products"] });
}
}
return (
<div className="w-full m-4">
<div>
<div className="flex w-full h-[240px] border border-[#18181B] p-4 rounded-lg my-2">
<div
className="flex flex-row min-w-[300px] cursor-pointer"
onClick={handleFileUpload}
>
{base64Image ? (
<div className="flex items-center justify-center h-[210px] w-full">
<Image
className="col-span-1 max-h-[210px] object-contain"
width={300}
height={300}
src={base64Image}
alt="product"
/>
</div>
) : (
<div className="flex items-center justify-center h-[210px] w-[300px] border border-[#18181B]">
<ImageDown /> <p className="ml-1">Завантажте товар</p>
</div>
)}
<input
ref={inputFileRef}
id="picture"
type="file"
style={{ display: "none" }}
onChange={handleFileChange}
/>
</div>
<div className="flex flex-col mx-4 w-full">
<p className="text-lg mb-2 ">Категорія</p>
<Select onValueChange={(value) => setCategory(value)}>
<SelectTrigger className="w-full" disabled={isButtonsDisabled}>
<SelectValue placeholder="Виберіть категорію" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Категорії</SelectLabel>
<SelectItem value="sofa">Диван</SelectItem>
<SelectItem value="corner">Кут</SelectItem>
<SelectItem value="kit">Комплект</SelectItem>
<SelectItem value="ottoman">Тахта</SelectItem>
<SelectItem value="bed">Ліжко</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button
className="mt-2 rounded-md font-[600] py-[20px] px-6 text-white bg-red-500 hover:bg-red-800 duration-300"
onClick={() => setBase64Image("")}
disabled={isButtonsDisabled}
>
Видалити продукт
</Button>
</div>
</div>
<Button
className="mt-1 ml-1 rounded-[50px] font-[600] py-[20px] px-6 text-white bg-gray hover:bg-black duration-300"
onClick={onSubmit}
disabled={isSubmitDisabled}
>
Зберегти
</Button>
</div>
<div className="flex justify-between my-4 text-lg mx-2">
<p>Наявні продукти</p>
<p>Кількість - {products.length}</p>
</div>
{products?.map((item, index) => (
<EditProductItem key={index} item={item} />
))}
</div>
);
}
您遇到的问题是由于 React Query 处理缓存的方式造成的。添加新产品时,查询不会自动重新获取数据,除非明确指示这样做。以下是发生这种情况的原因以及解决方法。
问题
您正在通过 CatalogEditor 中的 API 请求添加产品,但是虽然更改成功,但 CatalogEditor 在页面刷新之前不会反映新数据。这是因为 React Query 缓存了产品查询的结果,并且需要被告知在突变后重新获取此数据。
解决方案
为了确保 CatalogEditor 在更新后拥有最新的产品数据,您应该在突变后使产品查询无效或重新获取。您已经在 onSubmit 函数中使用了 queryClient.refetchQueries(),但问题可能是由于 AdminRightPanel 中的 useEffect 缺少依赖项所致。
在突变回调中重新获取查询:确保您在添加产品后使用 queryClient.invalidateQueries 或 queryClient.refetchQueries 重新获取产品数据。
更新AdminRightPanel的状态:不使用useEffect将产品同步到productItems,而是直接使用产品状态。如果需要本地修改,请确保产品具有 queryKey 依赖项以触发更新。
调整
// CatalogEditor.js
async function onSubmit() {
if (base64Image && category) {
const product = { id: 0, base64image: base64Image, category: category };
await fetch(`${CUSTOM_URL}api/content/products`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product),
});
// Invalidate or refetch the products query after the mutation
queryClient.invalidateQueries("products");
}
}
// 管理右侧面板:
export default function AdminRightPanel({ selectedMenu }) {
const { data: products } = useQuery(["products"], fetchProducts);
switch (selectedMenu) {
case "catalog":
return <CatalogEditor products={products} />;
// other cases...
}
}
解释
使用invalidateQueries可确保缓存的数据在突变后立即刷新,因此CatalogEditor将收到更新的产品列表,而无需重新加载页面。此外,直接将产品作为 prop 传递可以使 AdminRightPanel 更好地响应后端数据的变化。
此方法应该可以解决问题并确保 CatalogEditor 在每次更新后显示最新数据。希望这有帮助!