Skip to content

perf(web): memoize RegistryClient in useProfile to stop per-call client churn (#660) - #733

Open
Oyinkans0la12 wants to merge 1 commit into
TrusTrove:mainfrom
Oyinkans0la12:perf/memoize-registry-client-in-use-profile
Open

perf(web): memoize RegistryClient in useProfile to stop per-call client churn (#660)#733
Oyinkans0la12 wants to merge 1 commit into
TrusTrove:mainfrom
Oyinkans0la12:perf/memoize-registry-client-in-use-profile

Conversation

@Oyinkans0la12

Copy link
Copy Markdown
Contributor

Summary

Closes #660

Memoizes a single RegistryClient instance in useProfile so the SDK client is constructed once per hook mount and reused across all three call sites (profile fetch, verified-state fetch, and register mutation), instead of being re-constructed on every queryFn/mutationFn invocation.

Problem

apps/web/hooks/useProfile.ts constructed new RegistryClient(registryContractID) independently in three separate places:

  1. profileQuery's queryFn (~line 73)
  2. isVerifiedQuery's queryFn (~line 93)
  3. registerMutation's mutationFn (~line 114)

Because those factories run on every query/mutation execution, each refetch — including interval/polling-based refetches — and each register call spun up a brand-new SDK client object (which internally builds a Stellar Contract instance and validates the contract ID). This is avoidable object churn on a hook that refetches regularly.

Root Cause

The client object is stateless for the purpose of these calls — the contract ID is a module-level constant (registryContractID), so nothing about the client depends on the wallet address, query args, or render state. There is no reason to rebuild it per call.

Solution

Following the existing pattern already established in usePool.ts (const poolClient = useMemo(() => new PoolClient(poolContractID), [])):

  • Added a single registryClient created inside useMemo(..., []) at the top of useProfile, so it is constructed exactly once per mount.
  • Replaced all three new RegistryClient(registryContractID) call sites with the memoized registryClient.
  • Mirrored usePool's defensive handling: the useMemo wraps construction in a try/catch and falls back to null, and each call site guards with throw new Error("Registry client not available"). This keeps the graceful error behavior for misconfigured environments (empty NEXT_PUBLIC_REGISTRY_CONTRACT_ID) intact — previously the constructor error surfaced when a query/mutation ran, and it still surfaces at the point of use instead of crashing the render (the hook is used by Navbar, so a render-time throw would break every page).

Files changed

File Change
apps/web/hooks/useProfile.ts Add useMemo-memoized registryClient; reuse it in all three call sites with null guards
apps/web/hooks/useProfile.test.ts New acceptance test asserting the RegistryClient constructor is called exactly once per mount

Acceptance Criteria Verification

The issue's acceptance criteria:

useProfile constructs exactly one RegistryClient instance per mount, verified by a test that spies on the RegistryClient constructor.

Covered by the new test "constructs exactly one RegistryClient instance per mount", which:

  • Mounts the hook with a connected wallet,
  • Triggers a re-render, a register mutation, and a refetchProfile() (both queries refetched),
  • Asserts RegistryClient was constructed exactly once and with the expected contract ID,
  • Unmounts and remounts to assert a fresh mount constructs a fresh single instance (total of two constructions across two mounts).

Local Verification (all CI pre-checks pass)

Ran the same checks CI runs in .github/workflows/ci.yml on this branch:

  • pnpm typecheck — TypeScript passes across SDK + web workspaces (after @trusttrove/sdk build, matching CI's build-first order)
  • pnpm test — SDK suite + all 425 web tests pass (11/11 in useProfile.test.ts)
  • pnpm --filter web lint — clean (only pre-existing warnings, no errors)
  • pnpm build (with the CI contract-ID env vars) — SDK + Next.js production build succeed
  • npx prettier --check . — all files formatted
  • ✅ Go backend checks (unchanged by this PR, verified green): go build ./..., go vet ./..., go test ./...

Testing Notes

All existing component/page tests (Navbar, InvoiceCard, marketplace, dashboard, profile) mock @/hooks/useProfile entirely, so no other tests are affected by this change. The SDK and contract layers are untouched.

Checklist

  • Code follows existing repo conventions (mirrors usePool.ts)
  • Tests added for the acceptance criteria
  • All CI checks (frontend + backend) pass locally
  • Prettier formatting enforced

useProfile re-constructed a new RegistryClient on every queryFn/mutationFn
call, including each interval refetch. Construct the client once per mount
with useMemo (mirroring usePool) and reuse it across the profile,
isVerified, and register call sites, guarding against a null client when
the contract ID is not configured.

Add a test that spies on the RegistryClient constructor to verify exactly
one instance is created per mount and reused across re-renders, refetches,
and mutations.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@drips-wave

drips-wave Bot commented Sep 7, 2026

Copy link
Copy Markdown

@Oyinkans0la12 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

@Oyinkans0la12 is attempting to deploy a commit to the K1NGD4VID Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useProfile constructs a new RegistryClient on every call instead of memoizing it

1 participant