From fd00681b0cd237d3d8ac15fae9aa052022b0f2bc Mon Sep 17 00:00:00 2001 From: Ruud Andriessen Date: Sun, 8 Mar 2026 18:52:04 +0100 Subject: [PATCH] refactor: replace useEffect with React Query and render-time state updates Remove 3 useEffect hooks and replace with modern React patterns: - AddProjectDialog: useQuery for data fetching, useMutation for form submission - SessionState: render-time state reset instead of useEffect dependency tracking Eliminates 4 manual state variables (installations, repos, loading, creating, error) and removes useCallback/useEffect chain. Co-Authored-By: Claude Haiku 4.5 --- src/components/add-project-dialog.tsx | 140 +++++++++++--------------- src/lib/session-state.ts | 8 +- 2 files changed, 65 insertions(+), 83 deletions(-) diff --git a/src/components/add-project-dialog.tsx b/src/components/add-project-dialog.tsx index 22df1f5..306c1df 100644 --- a/src/components/add-project-dialog.tsx +++ b/src/components/add-project-dialog.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { ArrowUpRight, Check, Loader2, Lock, Search, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -11,15 +12,6 @@ import { fetchInstallations, } from "@/server/functions/installations"; -type Installation = { - installationId: number; - accountLogin: string; - accountType: string; - createdAt: number; - deletedAt: number | null; - updatedAt: number | null; -}; - type GitHubRepo = { id: number; fullName: string; @@ -45,33 +37,24 @@ export function AddProjectDialog({ existingProjects: Project[]; autoInstall?: boolean; }) { - const [installations, setInstallations] = useState([]); - const [repos, setRepos] = useState([]); - const [installAppUrl, setInstallAppUrl] = useState(null); const [selected, setSelected] = useState>(new Set()); const [filter, setFilter] = useState(""); - const [loading, setLoading] = useState(false); - const [creating, setCreating] = useState(false); - const [error, setError] = useState(null); - const existingRepoUrls = new Set(existingProjects.map((p) => p.repo_url).filter(Boolean)); - - const loadData = useCallback(async () => { - setLoading(true); - setError(null); - setSelected(new Set()); - setFilter(""); - try { + const { + data, + isLoading: loading, + error: queryError, + } = useQuery({ + queryKey: ["add-project-repos"], + queryFn: async () => { const [installs, installAppResponse] = await Promise.all([ fetchInstallations(), fetchInstallAppUrl(), ]); - setInstallations(installs); - setInstallAppUrl(installAppResponse.url); if (autoInstall && installs.length === 0 && installAppResponse.url) { window.location.assign(installAppResponse.url); - return; + return { installations: installs, repos: [], installAppUrl: installAppResponse.url }; } const allRepos: RepoWithInstallation[] = []; @@ -85,19 +68,18 @@ export function AddProjectDialog({ } }), ); - setRepos(allRepos); - } catch { - setError("Failed to load repositories from GitHub."); - } finally { - setLoading(false); - } - }, [autoInstall]); - useEffect(() => { - if (open) { - void loadData(); - } - }, [open, loadData]); + return { installations: installs, repos: allRepos, installAppUrl: installAppResponse.url }; + }, + enabled: open, + refetchOnWindowFocus: false, + }); + + const installations = data?.installations ?? []; + const repos = data?.repos ?? []; + const installAppUrl = data?.installAppUrl ?? null; + + const existingRepoUrls = new Set(existingProjects.map((p) => p.repo_url).filter(Boolean)); function toggleRepo(htmlUrl: string) { setSelected((prev) => { @@ -108,48 +90,34 @@ export function AddProjectDialog({ }); } - async function handleAdd() { - if (!organizationId) { - setError("No active organization selected."); - return; - } + const addMutation = useMutation({ + mutationFn: async (reposToAdd: RepoWithInstallation[]) => { + if (!organizationId) { + throw new Error("No active organization selected."); + } - setCreating(true); - setError(null); - try { const now = Date.now(); - const selectedRepos = repos - .filter((r) => selected.has(r.htmlUrl)) - .map((r, index) => { - const createdAt = now + index; - return { - id: crypto.randomUUID(), - organization_id: organizationId, - created_at: BigInt(createdAt), - updated_at: BigInt(createdAt), - name: r.fullName, - repo_url: r.htmlUrl, - installation_id: r.installationId, - setup_command: null, - run_command: null, - run_port: null, - }; - }); + const projects = reposToAdd.map((r, index) => { + const createdAt = now + index; + return { + id: crypto.randomUUID(), + organization_id: organizationId, + created_at: BigInt(createdAt), + updated_at: BigInt(createdAt), + name: r.fullName, + repo_url: r.htmlUrl, + installation_id: r.installationId, + setup_command: null, + run_command: null, + run_port: null, + }; + }); - if (selectedRepos.length === 0) { - setError("Select at least one repository."); - return; - } - - const tx = projectsCollection.insert(selectedRepos); + const tx = projectsCollection.insert(projects); onClose(); await tx.isPersisted.promise; - } catch { - setError("Failed to create projects."); - } finally { - setCreating(false); - } - } + }, + }); const lowerFilter = filter.toLowerCase(); const filteredRepos = repos.filter((r) => r.fullName.toLowerCase().includes(lowerFilter)); @@ -160,7 +128,11 @@ export function AddProjectDialog({ { - if (!nextOpen) { + if (nextOpen) { + setSelected(new Set()); + setFilter(""); + addMutation.reset(); + } else { onClose(); } }} @@ -281,13 +253,21 @@ export function AddProjectDialog({ )} - {error ?
{error}
: null} + {(addMutation.error ?? queryError) ? ( +
+ {addMutation.error?.message ?? "Failed to load repositories from GitHub."} +
+ ) : null}
-