mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-10-30 15:19:21 +00:00
* refactor: reduce useLoadFeature re-renders
Three targeted optimizations to cut unnecessary re-renders during
feature loading:
1. Replace 3 separate useState calls in useAsync (packages/utils) with
a single useReducer. Each state transition is now one dispatch = one
render, eliminating double-renders from setData + setLoading firing
in separate microtasks on promise resolution.
2. Cache loaded feature module in a ref inside useLoadFeature. Survives
isEnabled flicker during chain switches (true -> undefined -> true)
by returning the cached module from a resolved promise instead of
re-running import(). Feature modules are static so re-fetching is
pure waste.
3. Stabilize the stub proxy reference in useLoadFeature. The Proxy is
created once per hook instance and reads meta values ($isLoading,
$isDisabled, etc.) from a ref. The object reference stays the same
across all not-ready states, so children receiving the feature object
don't re-render on meta changes alone.
* refactor: eliminate $isLoading from useLoadFeature
Remove the intermediate loading state from the feature loading lifecycle.
Features now transition directly from not-ready to ready in a single
render, reducing unnecessary re-renders. Replace useAsync with a simpler
useEffect + useState that only triggers one state update.
The useAsync optimizations (useReducer) are kept in packages/utils as
they benefit all consumers.
* refactor: reduce useLoadFeature cyclomatic complexity to 9
Extract getFeature, getError, and toError helpers to move branch
points out of the main hook function. Simplify the final useMemo
condition since $isReady is derived from !!feature.
* refactor: add shared feature registry to useLoadFeature
Replace per-instance cachedFeatureRef with a module-level registry
(featureCache Map) shared across all hook instances. When multiple
components call useLoadFeature(SameFeature), only the first triggers
a load — subsequent components get the result synchronously via the
useState initializer (1 render instead of 2).
Also deduplicates concurrent load() calls via a pendingLoads Map.
Errors are not cached globally so retry is possible on remount.
* revert: undo useAsync useReducer optimization
The useReducer replacement caused an infinite render loop when
asyncCall returns undefined — dispatch({ type: 'reset' }) always
creates a new state object reference, unlike the original useState
calls which bail out on same primitive values. This caused OOM
crashes in useAddressActivity tests.
The useLoadFeature optimizations (shared registry, stable proxy,
eliminated $isLoading) are independent and remain in place.
---------
Co-authored-by: Hanzo Dev <[email protected]>
7.8 KiB
7.8 KiB
Data Model: Hypernative v3 Public API
Feature: 001-migrate-hypernative-v3
Date: 2026-01-28
Overview
This document defines the public API surface for the Hypernative feature after migration to v3 architecture. The API is split into:
- Feature Contract - Lazy-loaded components and services (accessed via
useLoadFeature) - Direct Exports - Hooks and store (always loaded, imported directly)
- Type Exports - TypeScript types (compile-time only)
Feature Contract (Lazy-Loaded)
The feature contract defines what is accessible via useLoadFeature(HypernativeFeature).
Components (PascalCase)
| Export | Type | Description | Stub Behavior |
|---|---|---|---|
HnBanner |
Component | Main promotional banner with signup flow | Renders null |
HnDashboardBanner |
Component | Dashboard-specific banner variant | Renders null |
HnMiniTxBanner |
Component | Mini banner for transaction pages | Renders null |
HnPendingBanner |
Component | Pending transaction banner | Renders null |
HnQueueAssessmentBanner |
Component | Queue assessment results banner | Renders null |
HnActivatedSettingsBanner |
Component | Settings activation confirmation | Renders null |
HnSecurityReportBtn |
Component | Security report button | Renders null |
HnLoginCard |
Component | OAuth login card for settings | Renders null |
HypernativeLogo |
Component | Hypernative brand logo | Renders null |
Services (camelCase)
| Export | Type | Description | Stub Behavior |
|---|---|---|---|
isHypernativeGuard |
Function | Guard bytecode detection service | undefined |
Usage Pattern:
const hn = useLoadFeature(HypernativeFeature)
// Components - always callable, render null when not ready
return <hn.HnBanner />
// Services - check $isReady before calling
if (hn.$isReady) {
const isGuard = await hn.isHypernativeGuard(chainId, address, provider)
}
Direct Exports (Always Loaded)
These are imported directly from @/features/hypernative, not via useLoadFeature.
Hooks
| Export | Return Type | Description |
|---|---|---|
useIsHypernativeEligible |
{ isHypernativeEligible: boolean, loading: boolean } |
Check if safe is eligible for Hypernative |
useHypernativeOAuth |
HypernativeAuthStatus |
OAuth flow management (isAuthenticated, initiateLogin, logout) |
useIsHypernativeGuard |
HypernativeGuardCheckResult |
Check if guard is installed on safe |
useIsHypernativeFeatureEnabled |
boolean |
Check if main feature flag is enabled |
useIsHypernativeQueueScanFeature |
boolean |
Check if queue scan feature flag is enabled |
useHnAssessmentSeverity |
HnSeverity | null |
Get assessment severity for current context |
OAuth Helper Functions
| Export | Signature | Description |
|---|---|---|
savePkce |
(data: PkceData) => void |
Save PKCE data to storage |
readPkce |
() => PkceData | null |
Read PKCE data from storage |
clearPkce |
() => void |
Clear PKCE data from storage |
Store Exports
| Export | Type | Description |
|---|---|---|
hnStateSlice |
Redux Slice | Hypernative state slice |
calendlySlice |
Redux Slice | Calendly integration slice |
selectHnState |
Selector | Select Hypernative state |
selectCalendlyState |
Selector | Select Calendly state |
Constants
| Export | Type | Description |
|---|---|---|
HYPERNATIVE_OUTREACH_ID |
string |
Outreach campaign ID |
HYPERNATIVE_ALLOWLIST_OUTREACH_ID |
string |
Allowlist campaign ID |
Type Exports (Compile-Time Only)
Public Types
| Type | Description |
|---|---|
HypernativeContract |
Feature contract interface |
HypernativeEligibility |
Return type for useIsHypernativeEligible |
HypernativeAuthStatus |
OAuth status type |
PkceData |
PKCE token data type |
HypernativeGuardCheckResult |
Guard check result type |
BannerVisibilityResult |
Banner visibility state type |
BannerType |
Banner type enum |
Meta Properties
When using useLoadFeature(HypernativeFeature), these properties are always available:
| Property | Type | Description |
|---|---|---|
$isDisabled |
boolean |
True if feature flag is disabled |
$isReady |
boolean |
True when loaded and enabled |
$error |
Error | undefined |
Error if loading failed |
Feature Flag Mapping
| Folder Name | Feature Flag | Controlled By |
|---|---|---|
hypernative |
FEATURES.HYPERNATIVE |
Feature handle (createFeatureHandle) |
Sub-feature flags are checked by hooks:
FEATURES.HYPERNATIVE_QUEUE_SCAN→useIsHypernativeQueueScanFeatureFEATURES.HYPERNATIVE_RELAX_GUARD_CHECK→useIsHypernativeGuard(internal)
Validation Rules
Component Props
Components maintain their existing prop interfaces. No changes to component APIs.
Hook Return Types
All hooks maintain their existing return types for backward compatibility:
// useIsHypernativeEligible
type HypernativeEligibility = {
isHypernativeEligible: boolean
loading: boolean
}
// useHypernativeOAuth
type HypernativeAuthStatus = {
isAuthenticated: boolean
isLoading: boolean
initiateLogin: () => Promise<void>
logout: () => void
error: Error | null
}
// useIsHypernativeGuard
type HypernativeGuardCheckResult = {
isHypernativeGuard: boolean
loading: boolean
error: Error | null
}
State Transitions
The feature handle transitions through these states:
Initial → Loading → Ready
↘ Disabled
↘ Error
| State | $isDisabled | $isReady | Component Behavior | Service Behavior |
|---|---|---|---|---|
| Initial | false |
false |
Renders null |
undefined |
| Ready | false |
true |
Renders component | Function available |
| Disabled | true |
false |
Renders null |
undefined |
| Error | false |
false |
Renders null |
undefined |