React+Firebase表单等待自定义Hook获取有效ID的实现问题
问题描述
- 自定义Hook
useHighestId功能正常,但在AdminPanel组件中使用时,表单的禁用输入框会先收到undefined或NaN值,希望表单能等待Hook从数据库获取到真实值后再渲染该输入。 - TypeScript阻止使用
if (highestID === NaN) { return }语句,提示类型number | undefined不存在于类型number中。
相关代码
useHighestId.ts Hook
import { db } from "../config/firebaseConfig"; import { collection, query, doc, getDoc, getDocs, DocumentSnapshot } from "firebase/firestore"; import { useEffect, useState } from "react"; import { ArticleParameters } from "../AdminPanel/AdminPanel"; export const useHighestId = (category?: string): number => { const [allCategories, setAllCategories] = useState<any[]>([]); const [articlesList, setArticlesList] = useState<any>({}); const [highestId, setHighestId] = useState<number>(0); useEffect(() => { const getCategory = async () => { const collectDataFromDB = query(collection(db, "content")); const dataFromDB: any[] = []; const documents = await getDocs(collectDataFromDB); documents.docs.map((document) => { dataFromDB.push(document.id); }); setAllCategories(dataFromDB); }; getCategory(); }, []); useEffect(() => { allCategories.map((currentCategory) => { const getArticlesFromDB = async () => { const docRef = doc(db, "content", currentCategory); const docSnap = await getDoc(docRef); setArticlesList(docSnap.data()); }; getArticlesFromDB(); }); }, [allCategories]); const articlesListAsArray: ArticleParameters[] = Object.values(articlesList); const articlesIds: number[] = []; articlesListAsArray.forEach((article) => { articlesIds.push(article?.id); }); useEffect(() => { if (articlesIds.sort().reverse()[0] !== undefined) { setHighestId(articlesIds.sort().reverse()[0]); } }, [articlesIds]); return highestId; };
AdminPanel组件实现
import { useUser } from "../hooks/useUser"; import { db, storage } from "../config/firebaseConfig"; import { doc, setDoc, collection, getDocs, addDoc, query, Timestamp, updateDoc } from "firebase/firestore"; import { ChangeEvent, FormEvent, FormEventHandler, useEffect, useState } from "react"; import { Button, Col, Form, FormCheck, Row } from "react-bootstrap"; import { TextInput } from "../utils/TextInput"; import { useHighestId } from "../hooks/useHighestId"; import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage"; export interface ArticleParameters { category: string; id: number; title: string; content: string; author: string; date: Timestamp; tags: string[]; isOnline: boolean; isAdult: boolean; databaseTitle: string; } const currentDate = new Date(); export const AdminPanel = () => { const user = useUser(); let highestIdFromHook = useHighestId(); const parameters: ArticleParameters = { category: "news", id: highestIdFromHook + 1, title: "", content: "", author: "", date: Timestamp.fromDate(currentDate), tags: [""], isOnline: false, isAdult: false, databaseTitle: "", }; const [data, setData] = useState<ArticleParameters>(parameters); const [articleDatabaseName, setArticleDatabaseName] = useState<string>(`${data.category}_id_${data.id}`); const [file, setFile] = useState<any>(""); const [percent, setPercent] = useState<number>(0); // Handle file upload event and update state function handleChange(event: any) { setFile(event.target.files[0]); } const handleUpload = () => { const storageRef = ref(storage, `/files/${file.name}`); // progress can be paused and resumed. It also exposes progress updates. // Receives the storage reference and the file to upload. const uploadTask = uploadBytesResumable(storageRef, file); uploadTask.on( "state_changed", (snapshot) => { const percent = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100); // update progress setPercent(percent); }, (err) => console.log(err), () => { // download url getDownloadURL(uploadTask.snapshot.ref).then((url) => { console.log(url); }); } ); }; const addArticle = async (newArticleData: ArticleParameters) => { await updateDoc(doc(db, "content", newArticleData.category), { [newArticleData.databaseTitle]: { id: newArticleData.id, title: newArticleData.title, content: newArticleData.content, author: newArticleData.author, date: newArticleData.date, tags: newArticleData.tags, is_online: newArticleData.isOnline, is_adult: newArticleData.isAdult, }, }); }; const handleSwitch = (e: ChangeEvent<HTMLInputElement>) => { setData({ ...data, [e.target.name]: e.target.checked }); }; const { category, id, title, content, author, date, tags, isOnline, isAdult } = data; const resetForm = () => { setArticleDatabaseName(`${data.category}_id_${data.id}`); setData({ category: "news", id: highestIdFromHook + 1, title: "", content: "", author: "", date: Timestamp.fromDate(currentDate), tags: [""], isOnline: false, isAdult: false, databaseTitle: articleDatabaseName, }); }; const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!file) { alert("Please upload an image first!"); } if (data.databaseTitle !== "") { addArticle(data); handleUpload(); resetForm(); } else { console.log("Shit"); } }; return ( <div className="articles__content--wrapper"> <h1 className="text-center mb-4">Dodaj newsa</h1> <Form style={{ width: "80%", alignSelf: "center", alignItems: "center", }} onSubmit={handleSubmit}> <TextInput input="Tytuł" isRequired="true" type="text" name="title" data={data} setData={setData} /> <TextInput input="Treść" isRequired="true" type="text" name="content" data={data} setData={setData} textarea /> <Form.Group controlId="formFile" className="mb-3"> <Form.Label>Dodaj obrazek</Form.Label> <Form.Control className="form__control--input" type="file" accept="image/*" onChange={handleChange} /> </Form.Group> <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}> <TextInput input="Data dodania" isRequired="true" disabled type="text" name="date" data={data} setData={setData} /> <TextInput input="ID Newsa" isRequired="true" disabled type="number" name="id" data={data} setData={setData} /> <div style={{ display: "flex", flexDirection: "column", justifyContent: "space-between", alignItems: "flex-start" }}> <Form.Check type="switch" label="Opublikować?" id="isOnline" name="isOnline" onChange={handleSwitch} className="me-3"></Form.Check> <Form.Check type="switch" label="Tylko dla dorosłych?" id="isAdult" name="isAdult" onChange={handleSwitch} className="mb-3 me-3"></Form.Check> </div> </div> <TextInput input="Autor" isRequired="true" disabled type="text" name="author" data={data} setData={setData} /> <TextInput input="Nazwa w bazie" isRequired="true" disabled type="text" name="databaseTitle" data={data} setData={setData} /> <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-around", alignItems: "center" }}> <Button variant="info" type="submit"> Zapisz dane </Button> <Button variant="warning" type="reset" onClick={resetForm}> Wyczyść formularz </Button> </div> </Form> </div> ); };
TextInput组件实现
import FloatingLabel from "react-bootstrap/FloatingLabel"; import Form from "react-bootstrap/Form"; import { Dispatch, SetStateAction } from "react"; import { ArticleParameters } from "../AdminPanel/AdminPanel"; import { useUser } from "../hooks/useUser"; export interface DispatchTypes { nickname?: string; email: string; password: string; error?: string; } interface Properties { input: string; isRequired: string; type: string; name: string; data: any; setData: Dispatch<SetStateAction<DispatchTypes>> | Dispatch<SetStateAction<ArticleParameters>> | any; textarea?: boolean; disabled?: boolean; } export const TextInput = ({ input, isRequired, type, name, data, setData, textarea, disabled }: Properties) => { const user = useUser(); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setData({ ...data, [name]: e.target.value }); }; if (name === "author" && user?.name !== undefined) { data.author = user?.name; } if (name === "databaseTitle") { data.databaseTitle = `${data.category}_id_${data.id}` } return ( <FloatingLabel label={`${input}${isRequired === "true" ? "*" : " (opcjonalnie)"}`} className="mb-3" style={{fontSize: "0.75rem"}}> <Form.Control as={textarea ? "textarea" : 'input'} style={textarea ? {height: "200px", fontSize: "1rem"} : { height: "45px", fontSize: "1rem"}} className="form__control--input" type={type} placeholder={`${name}${isRequired === "true" ? "*" : " (opcjonalnie)"}`} required={isRequired === "true" ? true : false} disabled={disabled ? true : false} name={name} onChange={handleChange} value={name === "date" ? data.date.toDate() : data[name]} /> </FloatingLabel> ); };
解决方案
一、解决表单提前渲染undefined/NaN的问题
步骤1:修改useHighestId Hook,添加加载状态
让Hook返回加载状态和最高ID,组件可通过状态判断数据是否加载完成:
import { db } from "../config/firebaseConfig"; import { collection, query, doc, getDoc, getDocs } from "firebase/firestore"; import { useEffect, useState } from "react"; import { ArticleParameters } from "../AdminPanel/AdminPanel"; export const useHighestId = (category?: string) => { const [allCategories, setAllCategories] = useState<any[]>([]); const [articlesList, setArticlesList] = useState<any>({}); const [highestId, setHighestId] = useState<number | undefined>(undefined); const [isLoading, setIsLoading] = useState<boolean>(true); useEffect(() => { const getCategory = async () => { try { const collectDataFromDB = query(collection(db, "content")); const documents = await getDocs(collectDataFromDB); const dataFromDB = documents.docs.map(doc => doc.id); setAllCategories(dataFromDB); } catch (err) { console.error("获取分类失败:", err); } }; getCategory(); }, []); useEffect(() => { if (allCategories.length === 0) return; const fetchAllArticles = async () => { try { const articlesPromises = allCategories.map(async (currentCategory) => { const docRef = doc(db, "content", currentCategory); const docSnap = await getDoc(docRef); return docSnap.data(); }); const allArticles = await Promise.all(articlesPromises); const mergedArticles = allArticles.reduce((acc, articles) => ({ ...acc, ...articles }), {}); setArticlesList(mergedArticles); } catch (err) { console.error("获取文章数据失败:", err); } }; fetchAllArticles(); }, [allCategories]); useEffect(() => { if (Object.keys(articlesList).length === 0) return; const articlesListAsArray: ArticleParameters[] = Object.values(articlesList); const validIds = articlesListAsArray .map(article => article?.id) .filter(id => typeof id === "number"); if (validIds.length > 0) { setHighestId(Math.max(...validIds)); } else { setHighestId(0); } setIsLoading(false); }, [articlesList]); return { highestId, isLoading }; };
步骤2:在AdminPanel中根据加载状态条件渲染表单
等待数据加载完成后再初始化表单,避免提前渲染无效值:
import { useUser } from "../hooks/useUser"; import { db, storage } from "../config/firebaseConfig"; import { doc, updateDoc, Timestamp } from "firebase/firestore"; import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import { Button, Form, FormCheck } from "react-bootstrap"; import { TextInput } from "../utils/TextInput"; import { useHighestId } from "../hooks/useHighestId"; import { getDownloadURL, ref, uploadBytesResumable } from "firebase/storage"; export interface ArticleParameters { category: string; id: number; title: string; content: string; author: string; date: Timestamp; tags: string[]; isOnline: boolean; isAdult: boolean; databaseTitle: string; } export const AdminPanel = () => { const user = useUser(); const { highestId, isLoading } = useHighestId(); const currentDate = new Date(); const [data, setData] = useState<ArticleParameters | null>(null); const [file, setFile] = useState<any>(""); const [percent, setPercent] = useState<number>(0); useEffect(() => { if (!isLoading && highestId !== undefined) { setData({ category: "news", id: highestId + 1, title: "", content: "", author: user?.name || "", date: Timestamp.fromDate(currentDate), tags: [""], isOnline: false, isAdult: false, databaseTitle: `news_id_${highestId + 1}`, }); } }, [isLoading, highestId, user, currentDate]); if (isLoading || !data) { return <div className="articles__content--wrapper text-center">加载中...</div>; } const handleChange = (event: any) => { setFile(event.target.files[0]); }; const handleUpload = () => { const storageRef = ref(storage, `/files/${file.name}`); const uploadTask = uploadBytesResumable(storageRef, file); uploadTask.on( "state_changed", (snapshot) => { const percent = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100); setPercent(percent); }, (err) => console.log(err), () => { getDownloadURL(uploadTask.snapshot.ref).then((url) => { console.log(url); }); } ); }; const addArticle = async (newArticleData: ArticleParameters) => { await updateDoc(doc(db, "content
相关产品推荐
相关产品推荐

