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:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Camera } from "lucide-react"
|
import { Camera } from "lucide-react"
|
||||||
import Link from "next/link"
|
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -23,12 +23,6 @@ export default function SettingsPage() {
|
|||||||
const [avatar, setAvatar] = useState(user?.avatar ?? "")
|
const [avatar, setAvatar] = useState(user?.avatar ?? "")
|
||||||
const fileRef = useRef<HTMLInputElement>(null)
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNickname(user?.nickname ?? "")
|
|
||||||
setBio(user?.bio ?? "")
|
|
||||||
setAvatar(user?.avatar ?? "")
|
|
||||||
}, [user])
|
|
||||||
|
|
||||||
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
|
const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ArrowLeft } from "lucide-react"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -48,7 +48,7 @@ export default function NewServicePage() {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
control,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<z.infer<typeof serviceSchema>>({
|
} = useForm<z.infer<typeof serviceSchema>>({
|
||||||
resolver: standardSchemaResolver(serviceSchema),
|
resolver: standardSchemaResolver(serviceSchema),
|
||||||
@@ -74,8 +74,8 @@ export default function NewServicePage() {
|
|||||||
setValue("availability", editingService.availability.join("、"))
|
setValue("availability", editingService.availability.join("、"))
|
||||||
}, [editingService, setValue])
|
}, [editingService, setValue])
|
||||||
|
|
||||||
const selectedGameId = watch("gameId")
|
const selectedGameId = useWatch({ control, name: "gameId" })
|
||||||
const selectedUnit = watch("unit")
|
const selectedUnit = useWatch({ control, name: "unit" })
|
||||||
const games = listGames()
|
const games = listGames()
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof serviceSchema>) => {
|
const onSubmit = async (data: z.infer<typeof serviceSchema>) => {
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ export default function ShopTemplatesPage() {
|
|||||||
}
|
}
|
||||||
}, [showSavedToast])
|
}, [showSavedToast])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSections([...shop.templateConfig.sections].sort((a, b) => a.order - b.order))
|
|
||||||
}, [shop])
|
|
||||||
|
|
||||||
const toggleSection = (type: ShopSection["type"]) => {
|
const toggleSection = (type: ShopSection["type"]) => {
|
||||||
setSections((prev) => prev.map((s) => (s.type === type ? { ...s, enabled: !s.enabled } : s)))
|
setSections((prev) => prev.map((s) => (s.type === type ? { ...s, enabled: !s.enabled } : s)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
|
|||||||
import { ArrowLeft, ImagePlus, X } from "lucide-react"
|
import { ArrowLeft, ImagePlus, X } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -46,6 +46,8 @@ export default function NewPostPage() {
|
|||||||
const [imageCount, setImageCount] = useState(0)
|
const [imageCount, setImageCount] = useState(0)
|
||||||
const [selectedQuotePostId, setSelectedQuotePostId] = useState<string | undefined>(undefined)
|
const [selectedQuotePostId, setSelectedQuotePostId] = useState<string | undefined>(undefined)
|
||||||
const [selectedOrderId, setSelectedOrderId] = useState<string | undefined>(undefined)
|
const [selectedOrderId, setSelectedOrderId] = useState<string | undefined>(undefined)
|
||||||
|
const canShowOrder = currentRole === "consumer"
|
||||||
|
const effectivePostType = canShowOrder || postType !== "show_order" ? postType : "normal"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -61,12 +63,6 @@ export default function NewPostPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentRole !== "consumer" && postType === "show_order") {
|
|
||||||
setPostType("normal")
|
|
||||||
}
|
|
||||||
}, [currentRole, postType])
|
|
||||||
|
|
||||||
const availableOrders = orders.filter(
|
const availableOrders = orders.filter(
|
||||||
(order) => order.status === "completed" && order.consumerId === userId,
|
(order) => order.status === "completed" && order.consumerId === userId,
|
||||||
)
|
)
|
||||||
@@ -87,8 +83,8 @@ export default function NewPostPage() {
|
|||||||
content: data.content,
|
content: data.content,
|
||||||
images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg"),
|
images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg"),
|
||||||
tags: selectedTags,
|
tags: selectedTags,
|
||||||
linkedOrderId: postType === "show_order" ? selectedOrderId : undefined,
|
linkedOrderId: effectivePostType === "show_order" ? selectedOrderId : undefined,
|
||||||
quotedPostId: postType === "quote" ? selectedQuotePostId : undefined,
|
quotedPostId: effectivePostType === "quote" ? selectedQuotePostId : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push("/community")
|
router.push("/community")
|
||||||
@@ -113,19 +109,19 @@ export default function NewPostPage() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>帖子类型</Label>
|
<Label>帖子类型</Label>
|
||||||
<Select value={postType} onValueChange={setPostType}>
|
<Select value={effectivePostType} onValueChange={setPostType}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="normal">普通帖</SelectItem>
|
<SelectItem value="normal">普通帖</SelectItem>
|
||||||
{currentRole === "consumer" && <SelectItem value="show_order">秀单帖</SelectItem>}
|
{canShowOrder && <SelectItem value="show_order">秀单帖</SelectItem>}
|
||||||
<SelectItem value="quote">引用帖</SelectItem>
|
<SelectItem value="quote">引用帖</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{postType === "show_order" && (
|
{effectivePostType === "show_order" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>关联订单</Label>
|
<Label>关联订单</Label>
|
||||||
<Select value={selectedOrderId} onValueChange={setSelectedOrderId}>
|
<Select value={selectedOrderId} onValueChange={setSelectedOrderId}>
|
||||||
@@ -143,7 +139,7 @@ export default function NewPostPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{postType === "quote" && (
|
{effectivePostType === "quote" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>引用帖子</Label>
|
<Label>引用帖子</Label>
|
||||||
<Select value={selectedQuotePostId} onValueChange={setSelectedQuotePostId}>
|
<Select value={selectedQuotePostId} onValueChange={setSelectedQuotePostId}>
|
||||||
|
|||||||
@@ -377,19 +377,15 @@ function SearchPageContent() {
|
|||||||
const game = searchParams.get("game")
|
const game = searchParams.get("game")
|
||||||
return game ? [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 [onlyOnline, setOnlyOnline] = useState(false)
|
||||||
const [minRating, setMinRating] = useState("0")
|
const [minRating, setMinRating] = useState("0")
|
||||||
const [sortBy, setSortBy] = useState("composite")
|
const [sortBy, setSortBy] = useState("composite")
|
||||||
const [visibleCount, setVisibleCount] = useState(12)
|
const [visibleCount, setVisibleCount] = useState(12)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const q = searchParams.get("q")
|
|
||||||
if (q !== null && q !== searchQuery) {
|
|
||||||
setSearchQuery(q)
|
|
||||||
}
|
|
||||||
}, [searchParams, searchQuery])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const params = new URLSearchParams(searchParams.toString())
|
const params = new URLSearchParams(searchParams.toString())
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
if (!order) return
|
if (!order) return
|
||||||
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
||||||
|
|
||||||
setNowTs(Date.now())
|
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setNowTs(Date.now())
|
setNowTs(Date.now())
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus, UserRole } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
@@ -50,19 +50,19 @@ const ownerTabs = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function OrderListPage() {
|
export default function OrderListPage() {
|
||||||
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
|
||||||
const { currentRole, user } = useAuthStore()
|
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 orders = useOrderStore((state) => state.orders)
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
||||||
const userId = user?.id ?? "u1"
|
|
||||||
const ownerShopId = "shop1"
|
const ownerShopId = "shop1"
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentRole) return
|
|
||||||
setTab("all")
|
|
||||||
}, [currentRole])
|
|
||||||
|
|
||||||
const tabs =
|
const tabs =
|
||||||
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { notifySuccess } from "@/lib/toast"
|
import { notifySuccess } from "@/lib/toast"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
@@ -37,26 +37,23 @@ export default function OrderActions({
|
|||||||
}: OrderActionsProps) {
|
}: OrderActionsProps) {
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
||||||
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
|
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
|
||||||
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
||||||
const dispatchMode = useShopStore((state) => {
|
const dispatchMode = useShopStore((state) => {
|
||||||
if (!order?.shopId) return "manual"
|
if (!order?.shopId) return "manual"
|
||||||
const shop = state.shops.find((item) => item.id === order.shopId)
|
const shop = state.shops.find((item) => item.id === order.shopId)
|
||||||
return shop?.dispatchMode ?? "manual"
|
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
|
const status = order?.status ?? initialStatus
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatSessionId) {
|
if (chatSessionId || !order || resolvedChatSessionId) return
|
||||||
setResolvedChatSessionId(chatSessionId)
|
ensureOrderSession(order)
|
||||||
return
|
}, [chatSessionId, order, ensureOrderSession, resolvedChatSessionId])
|
||||||
}
|
|
||||||
|
|
||||||
if (!order) return
|
|
||||||
const session = ensureOrderSession(order)
|
|
||||||
setResolvedChatSessionId(session.id)
|
|
||||||
}, [chatSessionId, order, ensureOrderSession])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!order) return
|
if (!order) return
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ const eslintConfig = defineConfig([
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react-hooks/exhaustive-deps": "error",
|
"react-hooks/exhaustive-deps": "error",
|
||||||
"react-hooks/set-state-in-effect": "off",
|
"react-hooks/set-state-in-effect": "error",
|
||||||
"react-hooks/incompatible-library": "off",
|
"react-hooks/incompatible-library": "error",
|
||||||
"@next/next/no-async-client-component": "error",
|
"@next/next/no-async-client-component": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"@typescript-eslint/no-non-null-assertion": "error",
|
"@typescript-eslint/no-non-null-assertion": "error",
|
||||||
|
|||||||
Reference in New Issue
Block a user