From 519fb92c348674120966164e415bf120bf98ac46 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 22 Feb 2026 10:03:00 +0800 Subject: [PATCH] refactor(react-hooks): enable stricter effect rules Turn on react-hooks/set-state-in-effect and react-hooks/incompatible-library, then remove effect-driven local state sync patterns across affected pages. Keep behavior stable by deriving values from source state, remounting tab state by role key, and replacing useForm watch with useWatch. --- app/(account)/settings/page.tsx | 8 +------ .../dashboard/services/new/page.tsx | 8 +++---- .../dashboard/shop/templates/page.tsx | 4 ---- app/(main)/post/new/page.tsx | 22 ++++++++----------- app/(main)/search/page.tsx | 12 ++++------ app/(order)/order/[id]/page.tsx | 2 -- app/(order)/orders/page.tsx | 16 +++++++------- components/order-actions.tsx | 19 +++++++--------- eslint.config.mjs | 4 ++-- 9 files changed, 36 insertions(+), 59 deletions(-) diff --git a/app/(account)/settings/page.tsx b/app/(account)/settings/page.tsx index d144354..8aec803 100644 --- a/app/(account)/settings/page.tsx +++ b/app/(account)/settings/page.tsx @@ -2,7 +2,7 @@ import { Camera } from "lucide-react" import Link from "next/link" -import { useEffect, useRef, useState } from "react" +import { useRef, useState } from "react" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -23,12 +23,6 @@ export default function SettingsPage() { const [avatar, setAvatar] = useState(user?.avatar ?? "") const fileRef = useRef(null) - useEffect(() => { - setNickname(user?.nickname ?? "") - setBio(user?.bio ?? "") - setAvatar(user?.avatar ?? "") - }, [user]) - const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role) return ( diff --git a/app/(dashboard)/dashboard/services/new/page.tsx b/app/(dashboard)/dashboard/services/new/page.tsx index d7c0218..1cf9305 100644 --- a/app/(dashboard)/dashboard/services/new/page.tsx +++ b/app/(dashboard)/dashboard/services/new/page.tsx @@ -5,7 +5,7 @@ import { ArrowLeft } from "lucide-react" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { useEffect } from "react" -import { useForm } from "react-hook-form" +import { useForm, useWatch } from "react-hook-form" import { z } from "zod" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -48,7 +48,7 @@ export default function NewServicePage() { register, handleSubmit, setValue, - watch, + control, formState: { errors, isSubmitting }, } = useForm>({ resolver: standardSchemaResolver(serviceSchema), @@ -74,8 +74,8 @@ export default function NewServicePage() { setValue("availability", editingService.availability.join("、")) }, [editingService, setValue]) - const selectedGameId = watch("gameId") - const selectedUnit = watch("unit") + const selectedGameId = useWatch({ control, name: "gameId" }) + const selectedUnit = useWatch({ control, name: "unit" }) const games = listGames() const onSubmit = async (data: z.infer) => { diff --git a/app/(dashboard)/dashboard/shop/templates/page.tsx b/app/(dashboard)/dashboard/shop/templates/page.tsx index 35fab30..52bafa0 100644 --- a/app/(dashboard)/dashboard/shop/templates/page.tsx +++ b/app/(dashboard)/dashboard/shop/templates/page.tsx @@ -51,10 +51,6 @@ export default function ShopTemplatesPage() { } }, [showSavedToast]) - useEffect(() => { - setSections([...shop.templateConfig.sections].sort((a, b) => a.order - b.order)) - }, [shop]) - const toggleSection = (type: ShopSection["type"]) => { setSections((prev) => prev.map((s) => (s.type === type ? { ...s, enabled: !s.enabled } : s))) } diff --git a/app/(main)/post/new/page.tsx b/app/(main)/post/new/page.tsx index 7322b6a..e9f182f 100644 --- a/app/(main)/post/new/page.tsx +++ b/app/(main)/post/new/page.tsx @@ -4,7 +4,7 @@ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema" import { ArrowLeft, ImagePlus, X } from "lucide-react" import Link from "next/link" import { useRouter } from "next/navigation" -import { useEffect, useState } from "react" +import { useState } from "react" import { useForm } from "react-hook-form" import { z } from "zod" import { Badge } from "@/components/ui/badge" @@ -46,6 +46,8 @@ export default function NewPostPage() { const [imageCount, setImageCount] = useState(0) const [selectedQuotePostId, setSelectedQuotePostId] = useState(undefined) const [selectedOrderId, setSelectedOrderId] = useState(undefined) + const canShowOrder = currentRole === "consumer" + const effectivePostType = canShowOrder || postType !== "show_order" ? postType : "normal" const { register, @@ -61,12 +63,6 @@ export default function NewPostPage() { ) } - useEffect(() => { - if (currentRole !== "consumer" && postType === "show_order") { - setPostType("normal") - } - }, [currentRole, postType]) - const availableOrders = orders.filter( (order) => order.status === "completed" && order.consumerId === userId, ) @@ -87,8 +83,8 @@ export default function NewPostPage() { content: data.content, images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg"), tags: selectedTags, - linkedOrderId: postType === "show_order" ? selectedOrderId : undefined, - quotedPostId: postType === "quote" ? selectedQuotePostId : undefined, + linkedOrderId: effectivePostType === "show_order" ? selectedOrderId : undefined, + quotedPostId: effectivePostType === "quote" ? selectedQuotePostId : undefined, }) router.push("/community") @@ -113,19 +109,19 @@ export default function NewPostPage() {
- 普通帖 - {currentRole === "consumer" && 秀单帖} + {canShowOrder && 秀单帖} 引用帖
- {postType === "show_order" && ( + {effectivePostType === "show_order" && (
diff --git a/app/(main)/search/page.tsx b/app/(main)/search/page.tsx index cfbd7ff..b5b45fa 100644 --- a/app/(main)/search/page.tsx +++ b/app/(main)/search/page.tsx @@ -377,19 +377,15 @@ function SearchPageContent() { const game = searchParams.get("game") return game ? [game] : [] }) - const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({ min: "", max: "" }) + const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({ + min: "", + max: "", + }) const [onlyOnline, setOnlyOnline] = useState(false) const [minRating, setMinRating] = useState("0") const [sortBy, setSortBy] = useState("composite") const [visibleCount, setVisibleCount] = useState(12) - useEffect(() => { - const q = searchParams.get("q") - if (q !== null && q !== searchQuery) { - setSearchQuery(q) - } - }, [searchParams, searchQuery]) - useEffect(() => { const timer = setTimeout(() => { const params = new URLSearchParams(searchParams.toString()) diff --git a/app/(order)/order/[id]/page.tsx b/app/(order)/order/[id]/page.tsx index 39eb920..082b661 100644 --- a/app/(order)/order/[id]/page.tsx +++ b/app/(order)/order/[id]/page.tsx @@ -55,8 +55,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri if (!order) return if (order.status !== "pending_accept" && order.status !== "pending_close") return - setNowTs(Date.now()) - const timer = setInterval(() => { setNowTs(Date.now()) }, 1000) diff --git a/app/(order)/orders/page.tsx b/app/(order)/orders/page.tsx index afb6538..41cb0bd 100644 --- a/app/(order)/orders/page.tsx +++ b/app/(order)/orders/page.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { statusLabels } from "@/lib/constants" -import type { OrderStatus } from "@/lib/types" +import type { OrderStatus, UserRole } from "@/lib/types" import { cn } from "@/lib/utils" import { useAuthStore } from "@/store/auth" import { useChatStore } from "@/store/chat" @@ -50,19 +50,19 @@ const ownerTabs = [ ] export default function OrderListPage() { - const [tab, setTab] = useState("all") const { currentRole, user } = useAuthStore() + const userId = user?.id ?? "u1" + + return +} + +function OrderListContent({ currentRole, userId }: { currentRole: UserRole; userId: string }) { + const [tab, setTab] = useState("all") const orders = useOrderStore((state) => state.orders) const sessions = useChatStore((state) => state.sessions) const ensureOrderSession = useChatStore((state) => state.ensureOrderSession) - const userId = user?.id ?? "u1" const ownerShopId = "shop1" - useEffect(() => { - if (!currentRole) return - setTab("all") - }, [currentRole]) - const tabs = currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs diff --git a/components/order-actions.tsx b/components/order-actions.tsx index 2ecfb3e..1a58f79 100644 --- a/components/order-actions.tsx +++ b/components/order-actions.tsx @@ -10,7 +10,7 @@ import { XCircle, } from "lucide-react" import Link from "next/link" -import { useEffect, useState } from "react" +import { useEffect } from "react" import { Button } from "@/components/ui/button" import { notifySuccess } from "@/lib/toast" import type { OrderStatus } from "@/lib/types" @@ -37,26 +37,23 @@ export default function OrderActions({ }: OrderActionsProps) { const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId)) const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus) + const sessions = useChatStore((state) => state.sessions) const ensureOrderSession = useChatStore((state) => state.ensureOrderSession) const dispatchMode = useShopStore((state) => { if (!order?.shopId) return "manual" const shop = state.shops.find((item) => item.id === order.shopId) return shop?.dispatchMode ?? "manual" }) - const [resolvedChatSessionId, setResolvedChatSessionId] = useState(chatSessionId) + const resolvedChatSessionId = + chatSessionId ?? + sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id const status = order?.status ?? initialStatus useEffect(() => { - if (chatSessionId) { - setResolvedChatSessionId(chatSessionId) - return - } - - if (!order) return - const session = ensureOrderSession(order) - setResolvedChatSessionId(session.id) - }, [chatSessionId, order, ensureOrderSession]) + if (chatSessionId || !order || resolvedChatSessionId) return + ensureOrderSession(order) + }, [chatSessionId, order, ensureOrderSession, resolvedChatSessionId]) useEffect(() => { if (!order) return diff --git a/eslint.config.mjs b/eslint.config.mjs index 284ca6c..e9a6069 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,8 +16,8 @@ const eslintConfig = defineConfig([ }, rules: { "react-hooks/exhaustive-deps": "error", - "react-hooks/set-state-in-effect": "off", - "react-hooks/incompatible-library": "off", + "react-hooks/set-state-in-effect": "error", + "react-hooks/incompatible-library": "error", "@next/next/no-async-client-component": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-non-null-assertion": "error",