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.
This commit is contained in:
zetaloop
2026-02-22 10:03:00 +08:00
parent c9dbf5037e
commit 519fb92c34
9 changed files with 36 additions and 59 deletions
+1 -7
View File
@@ -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<HTMLInputElement>(null)
useEffect(() => {
setNickname(user?.nickname ?? "")
setBio(user?.bio ?? "")
setAvatar(user?.avatar ?? "")
}, [user])
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
return (
@@ -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<z.infer<typeof serviceSchema>>({
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<typeof serviceSchema>) => {
@@ -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)))
}
+9 -13
View File
@@ -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<string | undefined>(undefined)
const [selectedOrderId, setSelectedOrderId] = useState<string | undefined>(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() {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Select value={postType} onValueChange={setPostType}>
<Select value={effectivePostType} onValueChange={setPostType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal"></SelectItem>
{currentRole === "consumer" && <SelectItem value="show_order"></SelectItem>}
{canShowOrder && <SelectItem value="show_order"></SelectItem>}
<SelectItem value="quote"></SelectItem>
</SelectContent>
</Select>
</div>
{postType === "show_order" && (
{effectivePostType === "show_order" && (
<div className="space-y-2">
<Label></Label>
<Select value={selectedOrderId} onValueChange={setSelectedOrderId}>
@@ -143,7 +139,7 @@ export default function NewPostPage() {
</div>
)}
{postType === "quote" && (
{effectivePostType === "quote" && (
<div className="space-y-2">
<Label></Label>
<Select value={selectedQuotePostId} onValueChange={setSelectedQuotePostId}>
+4 -8
View File
@@ -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())
-2
View File
@@ -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)
+8 -8
View File
@@ -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<TabFilter | "pending">("all")
const { currentRole, user } = useAuthStore()
const userId = user?.id ?? "u1"
return <OrderListContent key={currentRole} currentRole={currentRole} userId={userId} />
}
function OrderListContent({ currentRole, userId }: { currentRole: UserRole; userId: string }) {
const [tab, setTab] = useState<TabFilter | "pending">("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