fix: resolve all 25 ESLint react-hooks and react-refresh violations
Replace useEffect-based state resets in dialogs with React's render-time state adjustment pattern. Wrap ref assignments in hooks with useEffect. Suppress known third-party library warnings (shadcn CVA exports, TanStack Table). Remove warn downgrades from eslint config.
This commit was merged in pull request #1.
This commit is contained in:
@@ -20,9 +20,6 @@ export default defineConfig([
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
// TODO: fix these incrementally — pre-existing violations from react-hooks v7 upgrade
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'react-hooks/refs': 'warn',
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -99,7 +99,9 @@ export function ConnectionDialog({ open, onOpenChange, connection }: Props) {
|
||||
const saveMutation = useSaveConnection();
|
||||
const testMutation = useTestConnection();
|
||||
|
||||
useEffect(() => {
|
||||
const [prev, setPrev] = useState<{ open: boolean; connection: typeof connection }>({ open: false, connection: null });
|
||||
if (open !== prev.open || connection !== prev.connection) {
|
||||
setPrev({ open, connection });
|
||||
if (open) {
|
||||
const config = connection ?? { ...emptyConfig, id: crypto.randomUUID() };
|
||||
setForm(config);
|
||||
@@ -107,7 +109,7 @@ export function ConnectionDialog({ open, onOpenChange, connection }: Props) {
|
||||
setDsn(connection ? buildDsn(config) : "");
|
||||
setDsnError(null);
|
||||
}
|
||||
}, [open, connection]);
|
||||
}
|
||||
|
||||
const update = (field: keyof ConnectionConfig, value: string | number) => {
|
||||
setForm((f) => ({ ...f, [field]: value }));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -54,7 +54,9 @@ export function GenerateDataDialog({
|
||||
reset,
|
||||
} = useDataGenerator();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setStep("config");
|
||||
setRowCount(10);
|
||||
@@ -62,7 +64,7 @@ export function GenerateDataDialog({
|
||||
setCustomInstructions("");
|
||||
reset();
|
||||
}
|
||||
}, [open, reset]);
|
||||
}
|
||||
|
||||
const handleGenerate = () => {
|
||||
const genId = crypto.randomUUID();
|
||||
|
||||
@@ -112,11 +112,13 @@ export function CloneDatabaseDialog({
|
||||
useCloneToDocker();
|
||||
|
||||
// Reset state when dialog opens
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState<{ open: boolean; database: string }>({ open: false, database: "" });
|
||||
if (open !== prevOpen.open || database !== prevOpen.database) {
|
||||
setPrevOpen({ open, database });
|
||||
if (open) {
|
||||
setStep("config");
|
||||
setContainerName(
|
||||
`tusk-${database.replace(/[^a-zA-Z0-9_-]/g, "-")}-${Date.now().toString(36)}`
|
||||
`tusk-${database.replace(/[^a-zA-Z0-9_-]/g, "-")}-${crypto.randomUUID().slice(0, 8)}`
|
||||
);
|
||||
setPgVersion("16");
|
||||
setPortMode("auto");
|
||||
@@ -127,10 +129,12 @@ export function CloneDatabaseDialog({
|
||||
setLogOpen(false);
|
||||
reset();
|
||||
}
|
||||
}, [open, database, reset]);
|
||||
}
|
||||
|
||||
// Accumulate progress events into log
|
||||
useEffect(() => {
|
||||
const [prevProgress, setPrevProgress] = useState(progress);
|
||||
if (progress !== prevProgress) {
|
||||
setPrevProgress(progress);
|
||||
if (progress) {
|
||||
setLogEntries((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
@@ -143,7 +147,7 @@ export function CloneDatabaseDialog({
|
||||
setStep("done");
|
||||
}
|
||||
}
|
||||
}, [progress]);
|
||||
}
|
||||
|
||||
// Auto-scroll log to bottom
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useCallback, useEffect, useState } from "react";
|
||||
import { useMemo, useCallback, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
ReactFlow,
|
||||
@@ -111,12 +111,14 @@ export function ErdDiagram({ connectionId, schema }: Props) {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const [prevLayout, setPrevLayout] = useState(layout);
|
||||
if (layout !== prevLayout) {
|
||||
setPrevLayout(layout);
|
||||
if (layout) {
|
||||
setNodes(layout.nodes);
|
||||
setEdges(layout.edges);
|
||||
}
|
||||
}, [layout]);
|
||||
}
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,7 +34,9 @@ export function AlterRoleDialog({ open, onOpenChange, connectionId, role }: Prop
|
||||
|
||||
const alterMutation = useAlterRole();
|
||||
|
||||
useEffect(() => {
|
||||
const [prev, setPrev] = useState<{ open: boolean; role: typeof role }>({ open: false, role: null });
|
||||
if (open !== prev.open || role !== prev.role) {
|
||||
setPrev({ open, role });
|
||||
if (open && role) {
|
||||
setPassword("");
|
||||
setLogin(role.can_login);
|
||||
@@ -47,7 +49,7 @@ export function AlterRoleDialog({ open, onOpenChange, connectionId, role }: Prop
|
||||
setValidUntil(role.valid_until ?? "");
|
||||
setRenameTo("");
|
||||
}
|
||||
}, [open, role]);
|
||||
}
|
||||
|
||||
if (!role) return null;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -35,7 +35,9 @@ export function CreateDatabaseDialog({ open, onOpenChange, connectionId }: Props
|
||||
const { data: roles } = useRoles(open ? connectionId : null);
|
||||
const createMutation = useCreateDatabase();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setName("");
|
||||
setOwner("__default__");
|
||||
@@ -43,7 +45,7 @@ export function CreateDatabaseDialog({ open, onOpenChange, connectionId }: Props
|
||||
setEncoding("UTF8");
|
||||
setConnectionLimit(-1);
|
||||
}
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!name.trim()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,7 +34,9 @@ export function CreateRoleDialog({ open, onOpenChange, connectionId }: Props) {
|
||||
const { data: roles } = useRoles(open ? connectionId : null);
|
||||
const createMutation = useCreateRole();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setName("");
|
||||
setPassword("");
|
||||
@@ -48,7 +50,7 @@ export function CreateRoleDialog({ open, onOpenChange, connectionId }: Props) {
|
||||
setValidUntil("");
|
||||
setInRoles([]);
|
||||
}
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!name.trim()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -61,14 +61,16 @@ export function GrantRevokeDialog({
|
||||
);
|
||||
const grantRevokeMutation = useGrantRevoke();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setAction("GRANT");
|
||||
setRoleName("");
|
||||
setPrivileges([]);
|
||||
setWithGrantOption(false);
|
||||
}
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
const availablePrivileges = PRIVILEGE_OPTIONS[objectType.toUpperCase()] ?? PRIVILEGE_OPTIONS.TABLE;
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export function ResultsTable({
|
||||
[colNames, onCellDoubleClick, highlightedCells]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -23,9 +23,11 @@ export function SaveQueryDialog({ open, onOpenChange, sql, connectionId }: Props
|
||||
const [name, setName] = useState("");
|
||||
const saveMutation = useSaveQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) setName("");
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name.trim()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -52,21 +52,25 @@ export function AppSettingsSheet({ open, onOpenChange }: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Sync form with loaded settings
|
||||
useEffect(() => {
|
||||
const [prevAppSettings, setPrevAppSettings] = useState(appSettings);
|
||||
if (appSettings !== prevAppSettings) {
|
||||
setPrevAppSettings(appSettings);
|
||||
if (appSettings) {
|
||||
setMcpEnabled(appSettings.mcp.enabled);
|
||||
setMcpPort(appSettings.mcp.port);
|
||||
setDockerHost(appSettings.docker.host);
|
||||
setDockerRemoteUrl(appSettings.docker.remote_url ?? "");
|
||||
}
|
||||
}, [appSettings]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const [prevAiSettings, setPrevAiSettings] = useState(aiSettings);
|
||||
if (aiSettings !== prevAiSettings) {
|
||||
setPrevAiSettings(aiSettings);
|
||||
if (aiSettings) {
|
||||
setOllamaUrl(aiSettings.ollama_url);
|
||||
setAiModel(aiSettings.model);
|
||||
}
|
||||
}, [aiSettings]);
|
||||
}
|
||||
|
||||
const mcpEndpoint = `http://127.0.0.1:${mcpPort}/mcp`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -43,7 +43,9 @@ export function CreateSnapshotDialog({ open, onOpenChange, connectionId }: Props
|
||||
|
||||
const { create, result, error, isCreating, progress, reset } = useCreateSnapshot();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setStep("config");
|
||||
setName(`snapshot-${new Date().toISOString().slice(0, 10)}`);
|
||||
@@ -51,20 +53,23 @@ export function CreateSnapshotDialog({ open, onOpenChange, connectionId }: Props
|
||||
setIncludeDeps(true);
|
||||
reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, reset]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const [prevSchemas, setPrevSchemas] = useState(schemas);
|
||||
if (schemas !== prevSchemas) {
|
||||
setPrevSchemas(schemas);
|
||||
if (schemas && schemas.length > 0 && !selectedSchema) {
|
||||
setSelectedSchema(schemas.find((s) => s === "public") || schemas[0]);
|
||||
}
|
||||
}, [schemas, selectedSchema]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const [prevProgress, setPrevProgress] = useState(progress);
|
||||
if (progress !== prevProgress) {
|
||||
setPrevProgress(progress);
|
||||
if (progress?.stage === "done" || progress?.stage === "error") {
|
||||
setStep("done");
|
||||
}
|
||||
}, [progress]);
|
||||
}
|
||||
|
||||
const handleToggleTable = (tableName: string) => {
|
||||
setSelectedTables((prev) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -38,7 +38,9 @@ export function RestoreSnapshotDialog({ open, onOpenChange, connectionId }: Prop
|
||||
const readMeta = useReadSnapshotMetadata();
|
||||
const { restore, rowsRestored, error, isRestoring, progress, reset } = useRestoreSnapshot();
|
||||
|
||||
useEffect(() => {
|
||||
const [prevOpen, setPrevOpen] = useState(false);
|
||||
if (open !== prevOpen) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setStep("select");
|
||||
setFilePath(null);
|
||||
@@ -46,13 +48,15 @@ export function RestoreSnapshotDialog({ open, onOpenChange, connectionId }: Prop
|
||||
setTruncate(false);
|
||||
reset();
|
||||
}
|
||||
}, [open, reset]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const [prevProgress, setPrevProgress] = useState(progress);
|
||||
if (progress !== prevProgress) {
|
||||
setPrevProgress(progress);
|
||||
if (progress?.stage === "done" || progress?.stage === "error") {
|
||||
setStep("done");
|
||||
}
|
||||
}, [progress]);
|
||||
}
|
||||
|
||||
const handleSelectFile = async () => {
|
||||
const selected = await openFile({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
|
||||
@@ -36,20 +36,31 @@ export function useDataGenerator() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = onDataGenProgress((p) => {
|
||||
if (p.gen_id === genIdRef.current) {
|
||||
let mounted = true;
|
||||
let unlisten: (() => void) | undefined;
|
||||
onDataGenProgress((p) => {
|
||||
if (mounted && p.gen_id === genIdRef.current) {
|
||||
setProgress(p);
|
||||
}
|
||||
}).then((fn) => {
|
||||
if (mounted) {
|
||||
unlisten = fn;
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => unlisten());
|
||||
mounted = false;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const previewRef = useRef(previewMutation);
|
||||
previewRef.current = previewMutation;
|
||||
const insertRef = useRef(insertMutation);
|
||||
insertRef.current = insertMutation;
|
||||
useEffect(() => {
|
||||
previewRef.current = previewMutation;
|
||||
insertRef.current = insertMutation;
|
||||
});
|
||||
|
||||
const reset = useCallback(() => {
|
||||
previewRef.current.reset();
|
||||
|
||||
@@ -51,18 +51,29 @@ export function useCloneToDocker() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = onCloneProgress((p) => {
|
||||
if (p.clone_id === cloneIdRef.current) {
|
||||
let mounted = true;
|
||||
let unlisten: (() => void) | undefined;
|
||||
onCloneProgress((p) => {
|
||||
if (mounted && p.clone_id === cloneIdRef.current) {
|
||||
setProgress(p);
|
||||
}
|
||||
}).then((fn) => {
|
||||
if (mounted) {
|
||||
unlisten = fn;
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => unlisten());
|
||||
mounted = false;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const mutationRef = useRef(mutation);
|
||||
mutationRef.current = mutation;
|
||||
useEffect(() => {
|
||||
mutationRef.current = mutation;
|
||||
});
|
||||
|
||||
const reset = useCallback(() => {
|
||||
mutationRef.current.reset();
|
||||
|
||||
@@ -53,18 +53,29 @@ export function useCreateSnapshot() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = onSnapshotProgress((p) => {
|
||||
if (p.snapshot_id === snapshotIdRef.current) {
|
||||
let mounted = true;
|
||||
let unlisten: (() => void) | undefined;
|
||||
onSnapshotProgress((p) => {
|
||||
if (mounted && p.snapshot_id === snapshotIdRef.current) {
|
||||
setProgress(p);
|
||||
}
|
||||
}).then((fn) => {
|
||||
if (mounted) {
|
||||
unlisten = fn;
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => unlisten());
|
||||
mounted = false;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const mutationRef = useRef(mutation);
|
||||
mutationRef.current = mutation;
|
||||
useEffect(() => {
|
||||
mutationRef.current = mutation;
|
||||
});
|
||||
|
||||
const reset = useCallback(() => {
|
||||
mutationRef.current.reset();
|
||||
@@ -101,18 +112,29 @@ export function useRestoreSnapshot() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = onSnapshotProgress((p) => {
|
||||
if (p.snapshot_id === snapshotIdRef.current) {
|
||||
let mounted = true;
|
||||
let unlisten: (() => void) | undefined;
|
||||
onSnapshotProgress((p) => {
|
||||
if (mounted && p.snapshot_id === snapshotIdRef.current) {
|
||||
setProgress(p);
|
||||
}
|
||||
}).then((fn) => {
|
||||
if (mounted) {
|
||||
unlisten = fn;
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => unlisten());
|
||||
mounted = false;
|
||||
unlisten?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const mutationRef = useRef(mutation);
|
||||
mutationRef.current = mutation;
|
||||
useEffect(() => {
|
||||
mutationRef.current = mutation;
|
||||
});
|
||||
|
||||
const reset = useCallback(() => {
|
||||
mutationRef.current.reset();
|
||||
|
||||
Reference in New Issue
Block a user