Files
39e37998f9 refactor: optimize useLoadFeature with shared registry and reduced re-renders (#7154)
* 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.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* 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.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* 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.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* 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.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* 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 <dev@hanzo.ai>

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-02-12 16:05:20 +01:00
..

Feature Migration Codemod

A two-phase tool for migrating features to the v3 architecture (lazy loading + feature handles).

Overview

This tool helps automate the migration of features from the old architecture to the new v3 architecture documented in apps/web/docs/feature-architecture.md.

What it does

Phase 1 (Analyze):

  • Scans a feature directory
  • Discovers all exports (components, hooks, services)
  • Finds all consumer files
  • Analyzes current structure
  • Generates a migration config for review

Phase 2 (Execute):

  • Creates boilerplate files (contract.ts, feature.ts, index.ts)
  • Reorganizes file structure (moves files to appropriate folders)
  • Updates import statements in consumer files
  • Adds TODO comments for manual migration steps

What requires manual cleanup

  • Adjusting the public API in contract.ts (components and services only, NO hooks)
  • Completing the migration in consumer files:
    • Adding useLoadFeature() calls for components/services
    • Importing hooks directly from feature index
    • Converting component usage to feature.Component
    • Converting service usage to feature.service?.method() with $isReady checks
    • Removing unnecessary null checks for components
  • Keeping hooks lightweight (minimal imports, heavy logic in services)
  • Fixing type errors
  • Updating tests

Installation

From the repository root:

cd tools/codemods/migrate-feature
yarn install
yarn build

Usage

List all features

See which features are migrated and which aren't:

yarn migrate list

Phase 1: Analyze a feature

Interactive mode (recommended):

yarn migrate analyze --interactive

Analyze a specific feature:

yarn migrate analyze hypernative

This will:

  1. Scan the feature
  2. Show analysis results
  3. Prompt to save config to .codemod/{feature}.config.json

Phase 2: Execute migration

After reviewing the config, execute the migration:

yarn migrate execute hypernative

Or specify a custom config file:

yarn migrate execute --config .codemod/hypernative.config.json

Dry run (preview changes without modifying files):

yarn migrate execute hypernative --dry-run

Example Workflow

# 1. List features to see what needs migration
yarn migrate list

# 2. Analyze a feature
yarn migrate analyze hypernative

# 3. Review the generated config
cat .codemod/hypernative.config.json

# 4. Edit the config if needed (adjust public API, feature flag, etc.)
vim .codemod/hypernative.config.json

# 5. Execute migration (dry run first)
yarn migrate execute hypernative --dry-run

# 6. Execute for real
yarn migrate execute hypernative

# 7. Manual cleanup
# - Review generated files
# - Complete consumer file migrations
# - Fix type errors
# - Update tests

# 8. Verify
cd ../../..
yarn workspace @safe-global/web type-check
yarn workspace @safe-global/web lint
yarn workspace @safe-global/web test

Config File Format

The analysis phase generates a config file like this:

{
  "featureName": "hypernative",
  "featureFlag": "HYPERNATIVE",
  "publicAPI": {
    "components": ["HnMiniTxBanner", "HnQueueAssessmentBanner"],
    "hooks": ["useHnScanner", "useHnAssessment"],
    "services": ["hypernativeService"],
    "types": ["HnAssessment", "HnSeverity"],
    "constants": ["HN_API_URL"]
  },
  "structure": {
    "hasComponentsFolder": true,
    "hasHooksFolder": true,
    "hasServicesFolder": true,
    "hasStoreFolder": true,
    "hasUtilsFolder": false,
    "hasContextsFolder": false,
    "hasTypesFile": true,
    "hasConstantsFile": true,
    "hasReadme": true
  },
  "consumers": [
    {
      "filePath": "/path/to/consumer.tsx",
      "imports": [
        {
          "name": "HnMiniTxBanner",
          "type": "component",
          "importPath": "@/features/hypernative/components/HnMiniTxBanner",
          "isDefault": false,
          "isTypeOnly": false
        }
      ]
    }
  ]
}

You can edit this config before running the execute phase to:

  • Adjust which exports are public
  • Change the feature flag name
  • Skip certain files
  • Add custom notes

Manual Steps After Migration

After running the migration tool, you'll need to:

1. Review Generated Files

Check contract.ts, feature.ts, and index.ts to ensure they match your needs.

2. Complete Consumer Migrations

The tool adds TODO comments in consumer files. You need to:

// Before
import { MyComponent } from '@/features/myfeature/components/MyComponent'
import { useMyHook } from '@/features/myfeature/hooks/useMyHook'

function Consumer() {
  const data = useMyHook()
  return <MyComponent data={data} />
}

// After
import { MyFeature, useMyHook } from '@/features/myfeature'
import { useLoadFeature } from '@/features/__core__'

function Consumer() {
  const feature = useLoadFeature(MyFeature)

  // Hooks are imported directly (always loaded, not lazy)
  const data = useMyHook()

  // Components render via feature handle (lazy-loaded)
  // No null checks needed - proxy stubs handle it
  return <feature.MyComponent data={data} />
}

Important: Hooks are exported directly from index.ts (not lazy-loaded) to avoid Rules of Hooks violations. Services are accessed via the feature handle and should check $isReady before calling.

3. Fix Type Errors

Run type-check and fix any errors:

yarn workspace @safe-global/web type-check

4. Update Tests

Update test mocks to use the flat structure:

jest.mock('@/features/myfeature', () => ({
  MyFeature: {
    name: 'myfeature',
    useIsEnabled: () => true,
    load: () => Promise.resolve({
      default: {
        // Flat structure - components and services only (NO hooks)
        MyComponent: () => <div>Mock</div>,
        myService: jest.fn(),
      },
    }),
  },
  // Hooks are exported directly (always loaded, not in lazy-loaded feature)
  useMyHook: jest.fn(() => ({ data: 'mock' })),
}))

5. Run Tests

yarn workspace @safe-global/web test

6. Lint

yarn workspace @safe-global/web lint

Architecture Reference

For the complete architecture guide, see:

  • apps/web/docs/feature-architecture.md

Key principles:

  • Flat structure - no nested components/services in contract (hooks exported separately)
  • Hooks are NOT lazy-loaded - exported directly from index.ts to avoid Rules of Hooks violations
  • Proxy-based stubs - always returns an object for components/services, never null
  • Naming conventions - PascalCase (component), camelCase (service)
  • One dynamic import - feature.ts is lazy-loaded, use direct imports inside it (NO hooks in feature.ts)
  • typeof pattern - use typeof in contracts for IDE navigation

Troubleshooting

"Feature not found"

Make sure you're running the command from the repository root and the feature exists in apps/web/src/features/.

"Failed to transform imports"

The jscodeshift transform might fail on complex import patterns. You'll need to manually update those files.

Type errors after migration

This is expected. The tool generates boilerplate but you need to:

  1. Ensure all imports in feature.ts are correct
  2. Update consumer files to use the feature handle
  3. Fix any type mismatches

Development

To modify the tool:

  1. Edit TypeScript files in src/
  2. Rebuild: yarn build
  3. Test: yarn migrate --help

File Structure

migrate-feature/
├── src/
│   ├── index.ts           # CLI entry point
│   ├── types.ts           # Type definitions
│   ├── utils.ts           # Utility functions
│   ├── analyze.ts         # Phase 1: Analysis
│   ├── execute.ts         # Phase 2: Execution
│   ├── templates.ts       # Boilerplate generators
│   └── transforms/
│       ├── fileStructure.ts  # File reorganization
│       └── imports.ts        # Import updates
├── package.json
├── tsconfig.json
└── README.md

Contributing

When enhancing the tool:

  • Add new transforms to transforms/
  • Update templates in templates.ts
  • Add new CLI commands in index.ts
  • Update this README with new features