Просмотр исходного кода

chore(deps): update eslint-plugin-react-hooks and fix linting issues (#233)

* build(dep): bump eslint-plugin-react-hooks

* refactor: move SimpleBarList component outside of Analytics component

* refactor: defer state update in DataTableBulkActions to avoid cascading renders

* refactor: update mock user authentication to compute expiry at success time

* refactor: optimize LongText and NewChat components by removing unnecessary useEffect hooks
Sat Naing 9 месяцев назад
Родитель
Сommit
630a05e21f

+ 1 - 1
package.json

@@ -70,7 +70,7 @@
     "@types/react-dom": "^19.2.1",
     "@vitejs/plugin-react-swc": "^4.1.0",
     "eslint": "^9.37.0",
-    "eslint-plugin-react-hooks": "^5.2.0",
+    "eslint-plugin-react-hooks": "^7.0.0",
     "eslint-plugin-react-refresh": "^0.4.23",
     "globals": "^16.4.0",
     "knip": "^5.64.2",

+ 35 - 6
pnpm-lock.yaml

@@ -175,8 +175,8 @@ importers:
         specifier: ^9.37.0
         version: 9.37.0(jiti@2.6.1)
       eslint-plugin-react-hooks:
-        specifier: ^5.2.0
-        version: 5.2.0(eslint@9.37.0(jiti@2.6.1))
+        specifier: ^7.0.0
+        version: 7.0.0(eslint@9.37.0(jiti@2.6.1))
       eslint-plugin-react-refresh:
         specifier: ^0.4.23
         version: 0.4.23(eslint@9.37.0(jiti@2.6.1))
@@ -2044,9 +2044,9 @@ packages:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
     engines: {node: '>=10'}
 
-  eslint-plugin-react-hooks@5.2.0:
-    resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
-    engines: {node: '>=10'}
+  eslint-plugin-react-hooks@7.0.0:
+    resolution: {integrity: sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==}
+    engines: {node: '>=18'}
     peerDependencies:
       eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
 
@@ -2251,6 +2251,12 @@ packages:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
 
+  hermes-estree@0.25.1:
+    resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+  hermes-parser@0.25.1:
+    resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
   ignore@5.3.2:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
@@ -2986,6 +2992,12 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
+  zod-validation-error@4.0.2:
+    resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+    engines: {node: '>=18.0.0'}
+    peerDependencies:
+      zod: ^3.25.0 || ^4.0.0
+
   zod@3.25.76:
     resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
 
@@ -4854,9 +4866,16 @@ snapshots:
 
   escape-string-regexp@4.0.0: {}
 
-  eslint-plugin-react-hooks@5.2.0(eslint@9.37.0(jiti@2.6.1)):
+  eslint-plugin-react-hooks@7.0.0(eslint@9.37.0(jiti@2.6.1)):
     dependencies:
+      '@babel/core': 7.28.0
+      '@babel/parser': 7.28.0
       eslint: 9.37.0(jiti@2.6.1)
+      hermes-parser: 0.25.1
+      zod: 4.1.12
+      zod-validation-error: 4.0.2(zod@4.1.12)
+    transitivePeerDependencies:
+      - supports-color
 
   eslint-plugin-react-refresh@0.4.23(eslint@9.37.0(jiti@2.6.1)):
     dependencies:
@@ -5064,6 +5083,12 @@ snapshots:
     dependencies:
       function-bind: 1.1.2
 
+  hermes-estree@0.25.1: {}
+
+  hermes-parser@0.25.1:
+    dependencies:
+      hermes-estree: 0.25.1
+
   ignore@5.3.2: {}
 
   ignore@7.0.5: {}
@@ -5664,6 +5689,10 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
+  zod-validation-error@4.0.2(zod@4.1.12):
+    dependencies:
+      zod: 4.1.12
+
   zod@3.25.76: {}
 
   zod@4.1.12: {}

+ 5 - 1
src/components/data-table/bulk-actions.tsx

@@ -41,7 +41,11 @@ export function DataTableBulkActions<TData>({
   useEffect(() => {
     if (selectedCount > 0) {
       const message = `${selectedCount} ${entityName}${selectedCount > 1 ? 's' : ''} selected. Bulk actions toolbar is available.`
-      setAnnouncement(message)
+
+      // Use queueMicrotask to defer state update and avoid cascading renders
+      queueMicrotask(() => {
+        setAnnouncement(message)
+      })
 
       // Clear announcement after a delay
       const timer = setTimeout(() => setAnnouncement(''), 3000)

+ 10 - 11
src/components/long-text.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react'
+import { useRef, useState } from 'react'
 import { cn } from '@/lib/utils'
 import {
   Popover,
@@ -26,18 +26,17 @@ export function LongText({
   const ref = useRef<HTMLDivElement>(null)
   const [isOverflown, setIsOverflown] = useState(false)
 
-  useEffect(() => {
-    if (checkOverflow(ref.current)) {
-      setIsOverflown(true)
-      return
+  // Use ref callback to check overflow when element is mounted
+  const refCallback = (node: HTMLDivElement | null) => {
+    ref.current = node
+    if (node && checkOverflow(node)) {
+      queueMicrotask(() => setIsOverflown(true))
     }
-
-    setIsOverflown(false)
-  }, [])
+  }
 
   if (!isOverflown)
     return (
-      <div ref={ref} className={cn('truncate', className)}>
+      <div ref={refCallback} className={cn('truncate', className)}>
         {children}
       </div>
     )
@@ -48,7 +47,7 @@ export function LongText({
         <TooltipProvider delayDuration={0}>
           <Tooltip>
             <TooltipTrigger asChild>
-              <div ref={ref} className={cn('truncate', className)}>
+              <div ref={refCallback} className={cn('truncate', className)}>
                 {children}
               </div>
             </TooltipTrigger>
@@ -61,7 +60,7 @@ export function LongText({
       <div className='sm:hidden'>
         <Popover>
           <PopoverTrigger asChild>
-            <div ref={ref} className={cn('truncate', className)}>
+            <div ref={refCallback} className={cn('truncate', className)}>
               {children}
             </div>
           </PopoverTrigger>

+ 8 - 8
src/features/auth/sign-in/components/user-auth-form.tsx

@@ -54,19 +54,19 @@ export function UserAuthForm({
   function onSubmit(data: z.infer<typeof formSchema>) {
     setIsLoading(true)
 
-    // Mock successful authentication
-    const mockUser = {
-      accountNo: 'ACC001',
-      email: data.email,
-      role: ['user'],
-      exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours from now
-    }
-
     toast.promise(sleep(2000), {
       loading: 'Signing in...',
       success: () => {
         setIsLoading(false)
 
+        // Mock successful authentication with expiry computed at success time
+        const mockUser = {
+          accountNo: 'ACC001',
+          email: data.email,
+          role: ['user'],
+          exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours from now
+        }
+
         // Set user and access token
         auth.setUser(mockUser)
         auth.setAccessToken('mock-access-token')

+ 7 - 5
src/features/chats/components/new-chat.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
 import { Check, X } from 'lucide-react'
 import { showSubmittedData } from '@/lib/show-submitted-data'
 import { Badge } from '@/components/ui/badge'
@@ -41,14 +41,16 @@ export function NewChat({ users, onOpenChange, open }: NewChatProps) {
     setSelectedUsers(selectedUsers.filter((user) => user.id !== userId))
   }
 
-  useEffect(() => {
-    if (!open) {
+  const handleOpenChange = (newOpen: boolean) => {
+    onOpenChange(newOpen)
+    // Reset selected users when dialog closes
+    if (!newOpen) {
       setSelectedUsers([])
     }
-  }, [open])
+  }
 
   return (
-    <Dialog open={open} onOpenChange={onOpenChange}>
+    <Dialog open={open} onOpenChange={handleOpenChange}>
       <DialogContent className='sm:max-w-[600px]'>
         <DialogHeader>
           <DialogTitle>New message</DialogTitle>

+ 37 - 41
src/features/dashboard/components/analytics.tsx

@@ -8,47 +8,6 @@ import {
 import { AnalyticsChart } from './analytics-chart'
 
 export function Analytics() {
-  type Item = { name: string; value: number }
-
-  const SimpleBarList = ({
-    items,
-    valueFormatter,
-    barClass,
-  }: {
-    items: Item[]
-    valueFormatter: (n: number) => string
-    barClass: string
-  }) => {
-    const max = Math.max(...items.map((i) => i.value), 1)
-    return (
-      <ul className='space-y-3'>
-        {items.map((i) => {
-          const width = `${Math.round((i.value / max) * 100)}%`
-          return (
-            <li
-              key={i.name}
-              className='flex items-center justify-between gap-3'
-            >
-              <div className='min-w-0 flex-1'>
-                <div className='text-muted-foreground mb-1 truncate text-xs'>
-                  {i.name}
-                </div>
-                <div className='bg-muted h-2.5 w-full rounded-full'>
-                  <div
-                    className={`h-2.5 rounded-full ${barClass}`}
-                    style={{ width }}
-                  />
-                </div>
-              </div>
-              <div className='ps-2 text-xs font-medium tabular-nums'>
-                {valueFormatter(i.value)}
-              </div>
-            </li>
-          )
-        })}
-      </ul>
-    )
-  }
   return (
     <div className='space-y-4'>
       <Card>
@@ -191,3 +150,40 @@ export function Analytics() {
     </div>
   )
 }
+
+function SimpleBarList({
+  items,
+  valueFormatter,
+  barClass,
+}: {
+  items: { name: string; value: number }[]
+  valueFormatter: (n: number) => string
+  barClass: string
+}) {
+  const max = Math.max(...items.map((i) => i.value), 1)
+  return (
+    <ul className='space-y-3'>
+      {items.map((i) => {
+        const width = `${Math.round((i.value / max) * 100)}%`
+        return (
+          <li key={i.name} className='flex items-center justify-between gap-3'>
+            <div className='min-w-0 flex-1'>
+              <div className='text-muted-foreground mb-1 truncate text-xs'>
+                {i.name}
+              </div>
+              <div className='bg-muted h-2.5 w-full rounded-full'>
+                <div
+                  className={`h-2.5 rounded-full ${barClass}`}
+                  style={{ width }}
+                />
+              </div>
+            </div>
+            <div className='ps-2 text-xs font-medium tabular-nums'>
+              {valueFormatter(i.value)}
+            </div>
+          </li>
+        )
+      })}
+    </ul>
+  )
+}