Skip to content

fix(use-image): resolve race condition with cached images on Firefox/Safari#6041

Merged
wingkwong merged 2 commits into
heroui-inc:canaryfrom
brianatdetections:fix/use-image-race-condition
Dec 27, 2025
Merged

fix(use-image): resolve race condition with cached images on Firefox/Safari#6041
wingkwong merged 2 commits into
heroui-inc:canaryfrom
brianatdetections:fix/use-image-race-condition

Conversation

@brianatdetections
Copy link
Copy Markdown

@brianatdetections brianatdetections commented Dec 23, 2025

Closes #4534
Closes #2259

Summary

This PR fixes a race condition in the use-image hook that caused cached images to remain invisible (stuck at opacity-0) on Firefox and Safari. The issue has been reported multiple times and persists despite previous fix attempts.

Root Cause

The bug occurred because event handlers (onload/onerror) were attached in a separate useEffect that ran after setting the image src. For cached images, browsers may fire onload synchronously when src is set, causing the event to be missed entirely since the handler wasn't attached yet.

// Previous implementation (problematic)
useEffect(() => {
  imageRef.current.onload = ...  // Attached too late for cached images
}, [imageRef.current]);

const load = () => {
  img.src = src;  // Cached images fire onload HERE, synchronously
}

Solution

Attach handlers before setting src, ensuring callbacks are caught even for synchronously-loaded cached images:

const load = () => {
  img.onload = ...;   // Handlers first
  img.onerror = ...;
  img.src = src;      // Then set src
}

Additional improvements:

Test Coverage

Added 15 tests (previously 5) covering:

  • Basic loading states (pending, loading, loaded, failed)
  • Asynchronously cached images
  • Synchronously cached images (Firefox/Safari race condition)
  • Synchronous error for cached failed images
  • Dynamic src changes (undefined → valid, valid → different valid)
  • Bypass options (ignoreFallback, shouldBypassImageLoad)
  • Callback invocation (onLoad, onError)
  • Image properties (crossOrigin, srcSet, sizes, loading)

Verification

  • All 43 related tests pass (use-image, avatar, image components)
  • TypeScript compiles without errors
  • ESLint passes with zero warnings
  • Build succeeds

Breaking Changes

None. The hook's public API remains unchanged.


Thank you for maintaining HeroUI! Please let me know if you have any questions or would like any changes to this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Improved image loading reliability and eliminated race conditions with cached images
    • Better detection of successful vs failed loads via dimension checks
    • More robust handling of synchronous load/error callbacks
  • Tests

    • Expanded coverage for loading states, cached-image scenarios, dynamic src changes, and callback/property propagation

✏️ Tip: You can customize this high-level summary in your review settings.

…Safari

The bug occurred because event handlers (onload/onerror) were attached
AFTER setting the image src. For cached images, browsers fire onload
synchronously when src is set, causing the event to be missed and
images to remain stuck at opacity-0.

Changes:
- Attach handlers BEFORE setting src to catch synchronous callbacks
- Check both naturalWidth AND naturalHeight (per CodeRabbit review on #4523)
- Handle synchronous error callbacks for failed cached images
- Add comprehensive test coverage (15 tests) including:
  - Synchronous cached image success (Firefox/Safari race condition)
  - Synchronous cached image error
  - Dynamic src changes
  - All props (crossOrigin, srcSet, sizes, loading)
  - Callback invocation verification

Reproduction and investigation performed using Claude Opus 4.5.

Fixes #4534, #2259
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Dec 23, 2025

🦋 Changeset detected

Latest commit: e7b5fe1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@heroui/use-image Patch
@heroui/image Patch
@heroui/avatar Patch
@heroui/react Patch
@heroui/user Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 23, 2025

@brianathere is attempting to deploy a commit to the HeroUI Inc Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 23, 2025

Walkthrough

Fixes a race condition in the use-image hook by attaching image load/error handlers before setting the src attribute, properly handling cached and synchronously-loaded images, and validating image dimensions. Includes comprehensive test expansion covering basic loading states, cached image scenarios, dynamic source changes, and callback invocations.

Changes

Cohort / File(s) Summary
Test Suite Expansion
packages/hooks/use-image/__tests__/use-image.test.tsx
Reorganized tests into nested describe blocks covering basic loading states (pending → loading → loaded/failed), cached images (async and sync cached behavior including synchronous onload/onerror), dynamic src changes, bypass options (ignoreFallback / shouldBypassImageLoad), callbacks (onLoad/onError), and image property propagation (crossOrigin, srcSet, sizes, loading). Adds custom global Image mocks to simulate sync/async behavior and property mutations.
Hook Implementation
packages/hooks/use-image/src/index.ts
Replaced effect-based handler wiring with a memoized load() + flush() approach. Image creation deferred until load(). Attach onload/onerror handlers before assigning src to avoid cached-image race conditions; apply crossOrigin, srcSet, sizes, and loading before src. Detect already-complete images by checking img.complete and both naturalWidth and naturalHeight to determine loaded vs failed. Cleans up previous handlers when reloading.
Release Notes
.changeset/fix-use-image-race-condition.md
Patch release note documenting the race condition fix: handler ordering, dimension validation, synchronous cached-image handling, added tests, and links to related fixes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing a race condition with cached images on Firefox/Safari in the use-image hook.
Description check ✅ Passed The description covers all required sections: issue references, summary, root cause explanation, solution with code examples, test coverage details, verification steps, and breaking change status.
Linked Issues check ✅ Passed The PR successfully addresses both linked issues: #4534 (cached images stuck at opacity-0) through synchronous callback handling, and #2259 (loading attribute not working) through proper handler attachment and attribute propagation.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the use-image hook race condition and improving test coverage. No unrelated modifications were introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59f0ddf and e7b5fe1.

📒 Files selected for processing (3)
  • .changeset/fix-use-image-race-condition.md
  • packages/hooks/use-image/__tests__/use-image.test.tsx
  • packages/hooks/use-image/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/hooks/use-image/tests/use-image.test.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-27T21:52:33.324Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.324Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.

Applied to files:

  • packages/hooks/use-image/src/index.ts
📚 Learning: 2025-10-25T17:11:59.338Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.338Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/hooks/use-image/src/index.ts
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/hooks/use-image/src/index.ts
🔇 Additional comments (8)
.changeset/fix-use-image-race-condition.md (1)

1-19: Excellent documentation of the race condition fix.

The changeset clearly articulates the root cause, solution, and browser-specific context. The detailed explanation will help maintainers understand this critical timing fix.

packages/hooks/use-image/src/index.ts (7)

7-7: LGTM: Import addition for new callback usage.

The useCallback import is correctly added to support the new flush and load callback implementations.


90-90: Correct initialization to prevent early image creation.

Initializing imageRef as null (instead of eagerly creating new Image()) prevents race conditions during hydration and ensures the image is only created when load() is called with proper handler setup.


94-100: Well-designed cleanup function.

The flush callback correctly clears previous image handlers and resets the ref. The empty dependency array is appropriate since it only accesses imageRef.current.


106-109: Proper cleanup before creating new image.

Calling flush() before creating a new Image() ensures any previous image's handlers are cleared, preventing stale callbacks from interfering with the new load.


111-122: Critical fix: Handlers attached before src assignment.

This is the core solution to the race condition. Attaching onload/onerror handlers BEFORE setting img.src ensures cached images firing synchronous callbacks are properly caught. Both handlers correctly call flush() for cleanup.


124-144: Correct property assignment order and comprehensive complete-image handling.

Properties are set after handlers but before src (which triggers the load). The "already-complete" check correctly validates both naturalWidth and naturalHeight to distinguish successful loads from failures, catching any remaining synchronous scenarios.


147-158: Dependency array correctly includes all referenced values.

The dependency array now includes ignoreFallback (line 154), which addresses the previous review comment. This ensures the load callback is recreated when ignoreFallback changes, preventing stale closures. All other dependencies are also correctly listed.

Note: This resolves the issue flagged in the past review comments.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/hooks/use-image/__tests__/use-image.test.tsx (1)

220-362: Comprehensive property propagation tests.

The custom mocks with getter/setter pairs correctly capture property assignments, verifying that crossOrigin, srcSet, sizes, and loading are properly set on the image element before src is assigned.

Minor: The async keyword on these tests (lines 221, 263, 321) is unnecessary since they don't use await. Consider removing them for consistency:

-    it("sets crossOrigin property on image element", async () => {
+    it("sets crossOrigin property on image element", () => {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5131d7 and 59f0ddf.

📒 Files selected for processing (3)
  • .changeset/fix-use-image-race-condition.md
  • packages/hooks/use-image/__tests__/use-image.test.tsx
  • packages/hooks/use-image/src/index.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-27T21:52:33.324Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.324Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.

Applied to files:

  • packages/hooks/use-image/src/index.ts
📚 Learning: 2025-10-25T17:11:59.338Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.338Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/hooks/use-image/src/index.ts
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/hooks/use-image/src/index.ts
🧬 Code graph analysis (1)
packages/hooks/use-image/__tests__/use-image.test.tsx (3)
packages/hooks/use-image/src/index.ts (1)
  • useImage (75-160)
packages/utilities/test-utils/src/mocks/image.ts (1)
  • mockImage (5-48)
packages/utilities/test-utils/src/mocks/index.ts (1)
  • mocks (3-5)
🔇 Additional comments (9)
.changeset/fix-use-image-race-condition.md (1)

1-18: Well-documented changeset with clear root cause analysis.

The changeset provides excellent documentation of the bug fix, including the root cause, solution, and references to the relevant issues. The patch version bumps are appropriate for this bug fix.

packages/hooks/use-image/src/index.ts (3)

94-100: Clean implementation of the flush function.

The flush function correctly clears event handlers before nulling the ref, preventing potential memory leaks and stale callbacks. The empty dependency array is appropriate since imageRef is a stable ref object.


111-131: Core race condition fix is correctly implemented.

The handler attachment order (handlers → properties → src) properly addresses the race condition. Calling flush() within handlers ensures cleanup happens regardless of sync/async callback timing.


136-144: Proper fallback handling for already-complete cached images.

The check for both naturalWidth and naturalHeight correctly handles the case where cached images may already be complete when src is assigned. This aligns with the HTML specification for detecting successfully loaded images.

Note: If the synchronous onload handler fires before this check, both the handler's setStatus("loaded") and the returned "loaded" status will trigger state updates, but React will deduplicate identical state updates, so this is harmless.

packages/hooks/use-image/__tests__/use-image.test.tsx (5)

16-38: Solid coverage of basic loading state transitions.

The tests correctly validate the pending → loading → loaded/failed state machine. The async patterns with waitFor are appropriate for testing the hook's asynchronous behavior.


48-95: Excellent test for the Firefox/Safari race condition.

This test directly validates the core fix by simulating synchronous onload firing when src is set. The custom mock with getter/setter for src accurately replicates cached image behavior. Good use of try/finally for cleanup.


141-182: Good coverage of dynamic src change scenarios.

The tests properly validate state transitions when src changes. The mock reset at lines 170-171 correctly simulates a fresh image load for the new URL.


184-198: Correct validation of bypass options.

Both ignoreFallback and shouldBypassImageLoad correctly short-circuit to return "loaded" immediately, and the tests validate this synchronous behavior.


200-218: Proper validation of callback invocations.

The tests verify that onLoad and onError callbacks are invoked at the appropriate times during the image loading lifecycle.

Comment thread packages/hooks/use-image/src/index.ts Outdated
Address CodeRabbit review feedback: the `ignoreFallback` prop was used
inside the `load` callback (line 104) but was missing from the dependency
array, creating a stale closure bug.

Without this fix, if `ignoreFallback` changes from `true` to `false`
dynamically, the `load` callback would retain the stale `true` value,
preventing the image from ever loading.

Changes:
- Add `ignoreFallback` to useCallback dependency array
- Add tests for dynamic `ignoreFallback` changes (both directions)
- Update changeset to document this fix

Verification performed using Claude Opus 4.5.
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 27, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
heroui Ready Ready Preview, Comment Dec 27, 2025 5:57am
heroui-sb Ready Ready Preview, Comment Dec 27, 2025 5:57am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Dec 27, 2025

Open in StackBlitz

@heroui/accordion

npm i https://pkg.pr.new/@heroui/accordion@6041

@heroui/alert

npm i https://pkg.pr.new/@heroui/alert@6041

@heroui/autocomplete

npm i https://pkg.pr.new/@heroui/autocomplete@6041

@heroui/avatar

npm i https://pkg.pr.new/@heroui/avatar@6041

@heroui/badge

npm i https://pkg.pr.new/@heroui/badge@6041

@heroui/breadcrumbs

npm i https://pkg.pr.new/@heroui/breadcrumbs@6041

@heroui/button

npm i https://pkg.pr.new/@heroui/button@6041

@heroui/calendar

npm i https://pkg.pr.new/@heroui/calendar@6041

@heroui/card

npm i https://pkg.pr.new/@heroui/card@6041

@heroui/checkbox

npm i https://pkg.pr.new/@heroui/checkbox@6041

@heroui/chip

npm i https://pkg.pr.new/@heroui/chip@6041

@heroui/code

npm i https://pkg.pr.new/@heroui/code@6041

@heroui/date-input

npm i https://pkg.pr.new/@heroui/date-input@6041

@heroui/date-picker

npm i https://pkg.pr.new/@heroui/date-picker@6041

@heroui/divider

npm i https://pkg.pr.new/@heroui/divider@6041

@heroui/drawer

npm i https://pkg.pr.new/@heroui/drawer@6041

@heroui/dropdown

npm i https://pkg.pr.new/@heroui/dropdown@6041

@heroui/form

npm i https://pkg.pr.new/@heroui/form@6041

@heroui/image

npm i https://pkg.pr.new/@heroui/image@6041

@heroui/input

npm i https://pkg.pr.new/@heroui/input@6041

@heroui/input-otp

npm i https://pkg.pr.new/@heroui/input-otp@6041

@heroui/kbd

npm i https://pkg.pr.new/@heroui/kbd@6041

@heroui/link

npm i https://pkg.pr.new/@heroui/link@6041

@heroui/listbox

npm i https://pkg.pr.new/@heroui/listbox@6041

@heroui/menu

npm i https://pkg.pr.new/@heroui/menu@6041

@heroui/modal

npm i https://pkg.pr.new/@heroui/modal@6041

@heroui/navbar

npm i https://pkg.pr.new/@heroui/navbar@6041

@heroui/number-input

npm i https://pkg.pr.new/@heroui/number-input@6041

@heroui/pagination

npm i https://pkg.pr.new/@heroui/pagination@6041

@heroui/popover

npm i https://pkg.pr.new/@heroui/popover@6041

@heroui/progress

npm i https://pkg.pr.new/@heroui/progress@6041

@heroui/radio

npm i https://pkg.pr.new/@heroui/radio@6041

@heroui/ripple

npm i https://pkg.pr.new/@heroui/ripple@6041

@heroui/scroll-shadow

npm i https://pkg.pr.new/@heroui/scroll-shadow@6041

@heroui/select

npm i https://pkg.pr.new/@heroui/select@6041

@heroui/skeleton

npm i https://pkg.pr.new/@heroui/skeleton@6041

@heroui/slider

npm i https://pkg.pr.new/@heroui/slider@6041

@heroui/snippet

npm i https://pkg.pr.new/@heroui/snippet@6041

@heroui/spacer

npm i https://pkg.pr.new/@heroui/spacer@6041

@heroui/spinner

npm i https://pkg.pr.new/@heroui/spinner@6041

@heroui/switch

npm i https://pkg.pr.new/@heroui/switch@6041

@heroui/table

npm i https://pkg.pr.new/@heroui/table@6041

@heroui/tabs

npm i https://pkg.pr.new/@heroui/tabs@6041

@heroui/toast

npm i https://pkg.pr.new/@heroui/toast@6041

@heroui/tooltip

npm i https://pkg.pr.new/@heroui/tooltip@6041

@heroui/user

npm i https://pkg.pr.new/@heroui/user@6041

@heroui/react

npm i https://pkg.pr.new/@heroui/react@6041

@heroui/system

npm i https://pkg.pr.new/@heroui/system@6041

@heroui/system-rsc

npm i https://pkg.pr.new/@heroui/system-rsc@6041

@heroui/theme

npm i https://pkg.pr.new/@heroui/theme@6041

@heroui/use-aria-accordion

npm i https://pkg.pr.new/@heroui/use-aria-accordion@6041

@heroui/use-aria-accordion-item

npm i https://pkg.pr.new/@heroui/use-aria-accordion-item@6041

@heroui/use-aria-button

npm i https://pkg.pr.new/@heroui/use-aria-button@6041

@heroui/use-aria-link

npm i https://pkg.pr.new/@heroui/use-aria-link@6041

@heroui/use-aria-modal-overlay

npm i https://pkg.pr.new/@heroui/use-aria-modal-overlay@6041

@heroui/use-aria-multiselect

npm i https://pkg.pr.new/@heroui/use-aria-multiselect@6041

@heroui/use-aria-overlay

npm i https://pkg.pr.new/@heroui/use-aria-overlay@6041

@heroui/use-callback-ref

npm i https://pkg.pr.new/@heroui/use-callback-ref@6041

@heroui/use-clipboard

npm i https://pkg.pr.new/@heroui/use-clipboard@6041

@heroui/use-data-scroll-overflow

npm i https://pkg.pr.new/@heroui/use-data-scroll-overflow@6041

@heroui/use-disclosure

npm i https://pkg.pr.new/@heroui/use-disclosure@6041

@heroui/use-draggable

npm i https://pkg.pr.new/@heroui/use-draggable@6041

@heroui/use-form-reset

npm i https://pkg.pr.new/@heroui/use-form-reset@6041

@heroui/use-image

npm i https://pkg.pr.new/@heroui/use-image@6041

@heroui/use-infinite-scroll

npm i https://pkg.pr.new/@heroui/use-infinite-scroll@6041

@heroui/use-intersection-observer

npm i https://pkg.pr.new/@heroui/use-intersection-observer@6041

@heroui/use-is-mobile

npm i https://pkg.pr.new/@heroui/use-is-mobile@6041

@heroui/use-is-mounted

npm i https://pkg.pr.new/@heroui/use-is-mounted@6041

@heroui/use-measure

npm i https://pkg.pr.new/@heroui/use-measure@6041

@heroui/use-pagination

npm i https://pkg.pr.new/@heroui/use-pagination@6041

@heroui/use-real-shape

npm i https://pkg.pr.new/@heroui/use-real-shape@6041

@heroui/use-ref-state

npm i https://pkg.pr.new/@heroui/use-ref-state@6041

@heroui/use-resize

npm i https://pkg.pr.new/@heroui/use-resize@6041

@heroui/use-safe-layout-effect

npm i https://pkg.pr.new/@heroui/use-safe-layout-effect@6041

@heroui/use-scroll-position

npm i https://pkg.pr.new/@heroui/use-scroll-position@6041

@heroui/use-ssr

npm i https://pkg.pr.new/@heroui/use-ssr@6041

@heroui/use-theme

npm i https://pkg.pr.new/@heroui/use-theme@6041

@heroui/use-update-effect

npm i https://pkg.pr.new/@heroui/use-update-effect@6041

@heroui/use-viewport-size

npm i https://pkg.pr.new/@heroui/use-viewport-size@6041

@heroui/aria-utils

npm i https://pkg.pr.new/@heroui/aria-utils@6041

@heroui/dom-animation

npm i https://pkg.pr.new/@heroui/dom-animation@6041

@heroui/framer-utils

npm i https://pkg.pr.new/@heroui/framer-utils@6041

@heroui/react-rsc-utils

npm i https://pkg.pr.new/@heroui/react-rsc-utils@6041

@heroui/react-utils

npm i https://pkg.pr.new/@heroui/react-utils@6041

@heroui/shared-icons

npm i https://pkg.pr.new/@heroui/shared-icons@6041

@heroui/shared-utils

npm i https://pkg.pr.new/@heroui/shared-utils@6041

@heroui/stories-utils

npm i https://pkg.pr.new/@heroui/stories-utils@6041

@heroui/test-utils

npm i https://pkg.pr.new/@heroui/test-utils@6041

commit: e7b5fe1

@wingkwong wingkwong merged commit 5ec842b into heroui-inc:canary Dec 27, 2025
10 checks passed
AnYiEE added a commit to AnYiEE/touhou-mystia-izakaya-assistant that referenced this pull request Jan 5, 2026
wingkwong added a commit that referenced this pull request Jan 29, 2026
* fix(theme): hide password reveal button (#5990)

* fix(link): link overriding role (#5999)

* fix(link): allow overriding role

* chore(link): changeset

* chore(deps): upgrade react-aria (v1.14.0) (#5996)

* chore(deps): upgrade react-aria (v1.14.0)

* refactor(react): group client components

* fix(dropdown): keyDown test cases

* chore(react): rollback

* fix(theme): default transition-duration (#6011)

* fix(theme): default transitionDuration

* chore(deps): bump @heroui/theme peer dep

* ci(changesets): version packages (#5991)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(use-image): resolve race condition with cached images on Firefox/Safari (#6041)

* fix(use-image): resolve race condition with cached images on Firefox/Safari

The bug occurred because event handlers (onload/onerror) were attached
AFTER setting the image src. For cached images, browsers fire onload
synchronously when src is set, causing the event to be missed and
images to remain stuck at opacity-0.

Changes:
- Attach handlers BEFORE setting src to catch synchronous callbacks
- Check both naturalWidth AND naturalHeight (per CodeRabbit review on #4523)
- Handle synchronous error callbacks for failed cached images
- Add comprehensive test coverage (15 tests) including:
  - Synchronous cached image success (Firefox/Safari race condition)
  - Synchronous cached image error
  - Dynamic src changes
  - All props (crossOrigin, srcSet, sizes, loading)
  - Callback invocation verification

Reproduction and investigation performed using Claude Opus 4.5.

Fixes #4534, #2259

* fix(use-image): add ignoreFallback to useCallback dependencies

Address CodeRabbit review feedback: the `ignoreFallback` prop was used
inside the `load` callback (line 104) but was missing from the dependency
array, creating a stale closure bug.

Without this fix, if `ignoreFallback` changes from `true` to `false`
dynamically, the `load` callback would retain the stale `true` value,
preventing the image from ever loading.

Changes:
- Add `ignoreFallback` to useCallback dependency array
- Add tests for dynamic `ignoreFallback` changes (both directions)
- Update changeset to document this fix

Verification performed using Claude Opus 4.5.

---------

Co-authored-by: Brian Meek <brian@current.space>

* fix(docs): broken links in Form page (#6077)

* fix(pagination): improve layout for large page counts (#6034)

* fix(pagination): improve layout for large page counts/style of paagination compnents

* fix(pagination): refine item sizing to balance small and large page numbers

* ci(changesets): add pagination sizing changeset

* fix(pagination): ensure cursor fully covers button without changing radius

* chore(changeset): revise message and add issue numbers

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix(date-picker): open date-picker when clicking border (#6084)

* fix(date-picker): add pointer interaction to open date picker on wrapper click

* chore: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(accordion): click behaviour for dynamically generated accordion items (#6133)

* fix(accordion): add collection state

* chore: add changeset

* fix: update change set

* refactor(theme): remove flat dependency (#6157)

* chore(deps): remove flat library

* refactor(theme): replace flatten from flat

* chore: add changeset

* fix(listbox): prevent option focus from start/end content slots (#6060)

* fix(listbox): prevent option focus from start/end content slots

* test: add test code about when clicking
non-interactive slot content, and prevented focus leakage from interactive
start/end content such as buttons.

* docs: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(system-rsc): extendVariants & compound variants types (#5847)

* fix(extendVariants): return component type error

* fix(CompoundVariants): correct type inference for extended/compound variants and composition

* test: cover compound/extend inference; enforce CP required props

* fix(types): correct CompoundVariants class value inference

Replaces the conditional ClassProp logic with a simpler,
consistent form to fix incorrect slot value inference.

Before:
  ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>

After:
  ClassProp<ClassValue | GetSuggestedValues<S>>

This ensures GetSuggestedValues<S> is used for slot-aware variants
and avoids duplicated conditional branches for undefined slots.

* fix(system-rsc): correct slot detection in getSlots()

* fix(types): make ExtendVariants props optional and guard V[key] with NonNullable

Simplifies the return type of ExtendVariants to ensure no required props
are enforced at the HOC level. This aligns with the intended API contract
where extended components expose all props as optional.

- All keys (CP ∪ V) are optional
- Preserve CP type hints and booleanized V values
- Added NonNullable<V[key]> guard to prevent undefined indexing

* test(extendVariants): add compoundVariants integration test

* fix(system-rsc): getSlots() brief JSDoc comment added

* test(extendVariants):  new styles - extended & fixed styles - original tests for slots component

* test(extendVariants): fixed slot component variant styles extended test

* fix(types): avoid leaking React internals by removing PropsWithoutRef

Replace PropsWithoutRef with explicit Exclude<'ref'> in mapped keys and
intersect with RefAttributes<InferRef<C>>. This prevents @types/react’s
internal UNDEFINED_VOID_ONLY from leaking into the public .d.ts and fixes
declaration emit for components like extended Autocomplete.

* chore(changeset): add patch for extendVariants and CompoundVariants type fix

* chore(system-rsc): add changeset for getSlots() slot detection fix

* refactor(types): unify slot value inference via GetSuggestedValues<S> for consistent variant typing

* fix(extendVariants): improved as-prop handling and exclude classNames from SuggestedVariants

* fix(system-rsc): add polymorphic 'as' prop support to extendVariants

* chore(system-rsc): add missing tests

---------

Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* chore(docs): update meta (#6168)

* ci(changesets): version packages (#6059)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

---------

Co-authored-by: Hayato Hasegawa <hase1225hayato@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
Co-authored-by: brianatdetections <brian@detections.ai>
Co-authored-by: Brian Meek <brian@current.space>
Co-authored-by: Dominik Hryshaiev <domhryshaiev@gmail.com>
Co-authored-by: creative-atish <149860680+atishkr25@users.noreply.github.com>
Co-authored-by: KyZy7 <29321162+KyZy7@users.noreply.github.com>
Co-authored-by: Deepansh Bhargava <deepansh940@gmail.com>
Co-authored-by: KumJungMin <37934668+KumJungMin@users.noreply.github.com>
Co-authored-by: Bohdan Kulinchenko <35272606+ITBoomBKStudio@users.noreply.github.com>
Co-authored-by: doki- <1335902682@qq.com>
jrgarciadev added a commit that referenced this pull request Feb 10, 2026
* fix(theme): hide password reveal button (#5990)

* fix(link): link overriding role (#5999)

* fix(link): allow overriding role

* chore(link): changeset

* chore(deps): upgrade react-aria (v1.14.0) (#5996)

* chore(deps): upgrade react-aria (v1.14.0)

* refactor(react): group client components

* fix(dropdown): keyDown test cases

* chore(react): rollback

* fix(theme): default transition-duration (#6011)

* fix(theme): default transitionDuration

* chore(deps): bump @heroui/theme peer dep

* ci(changesets): version packages (#5991)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(use-image): resolve race condition with cached images on Firefox/Safari (#6041)

* fix(use-image): resolve race condition with cached images on Firefox/Safari

The bug occurred because event handlers (onload/onerror) were attached
AFTER setting the image src. For cached images, browsers fire onload
synchronously when src is set, causing the event to be missed and
images to remain stuck at opacity-0.

Changes:
- Attach handlers BEFORE setting src to catch synchronous callbacks
- Check both naturalWidth AND naturalHeight (per CodeRabbit review on #4523)
- Handle synchronous error callbacks for failed cached images
- Add comprehensive test coverage (15 tests) including:
  - Synchronous cached image success (Firefox/Safari race condition)
  - Synchronous cached image error
  - Dynamic src changes
  - All props (crossOrigin, srcSet, sizes, loading)
  - Callback invocation verification

Reproduction and investigation performed using Claude Opus 4.5.

Fixes #4534, #2259

* fix(use-image): add ignoreFallback to useCallback dependencies

Address CodeRabbit review feedback: the `ignoreFallback` prop was used
inside the `load` callback (line 104) but was missing from the dependency
array, creating a stale closure bug.

Without this fix, if `ignoreFallback` changes from `true` to `false`
dynamically, the `load` callback would retain the stale `true` value,
preventing the image from ever loading.

Changes:
- Add `ignoreFallback` to useCallback dependency array
- Add tests for dynamic `ignoreFallback` changes (both directions)
- Update changeset to document this fix

Verification performed using Claude Opus 4.5.

---------

Co-authored-by: Brian Meek <brian@current.space>

* fix(docs): broken links in Form page (#6077)

* fix(pagination): improve layout for large page counts (#6034)

* fix(pagination): improve layout for large page counts/style of paagination compnents

* fix(pagination): refine item sizing to balance small and large page numbers

* ci(changesets): add pagination sizing changeset

* fix(pagination): ensure cursor fully covers button without changing radius

* chore(changeset): revise message and add issue numbers

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix(date-picker): open date-picker when clicking border (#6084)

* fix(date-picker): add pointer interaction to open date picker on wrapper click

* chore: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(accordion): click behaviour for dynamically generated accordion items (#6133)

* fix(accordion): add collection state

* chore: add changeset

* fix: update change set

* refactor(theme): remove flat dependency (#6157)

* chore(deps): remove flat library

* refactor(theme): replace flatten from flat

* chore: add changeset

* fix(listbox): prevent option focus from start/end content slots (#6060)

* fix(listbox): prevent option focus from start/end content slots

* test: add test code about when clicking
non-interactive slot content, and prevented focus leakage from interactive
start/end content such as buttons.

* docs: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(system-rsc): extendVariants & compound variants types (#5847)

* fix(extendVariants): return component type error

* fix(CompoundVariants): correct type inference for extended/compound variants and composition

* test: cover compound/extend inference; enforce CP required props

* fix(types): correct CompoundVariants class value inference

Replaces the conditional ClassProp logic with a simpler,
consistent form to fix incorrect slot value inference.

Before:
  ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>

After:
  ClassProp<ClassValue | GetSuggestedValues<S>>

This ensures GetSuggestedValues<S> is used for slot-aware variants
and avoids duplicated conditional branches for undefined slots.

* fix(system-rsc): correct slot detection in getSlots()

* fix(types): make ExtendVariants props optional and guard V[key] with NonNullable

Simplifies the return type of ExtendVariants to ensure no required props
are enforced at the HOC level. This aligns with the intended API contract
where extended components expose all props as optional.

- All keys (CP ∪ V) are optional
- Preserve CP type hints and booleanized V values
- Added NonNullable<V[key]> guard to prevent undefined indexing

* test(extendVariants): add compoundVariants integration test

* fix(system-rsc): getSlots() brief JSDoc comment added

* test(extendVariants):  new styles - extended & fixed styles - original tests for slots component

* test(extendVariants): fixed slot component variant styles extended test

* fix(types): avoid leaking React internals by removing PropsWithoutRef

Replace PropsWithoutRef with explicit Exclude<'ref'> in mapped keys and
intersect with RefAttributes<InferRef<C>>. This prevents @types/react’s
internal UNDEFINED_VOID_ONLY from leaking into the public .d.ts and fixes
declaration emit for components like extended Autocomplete.

* chore(changeset): add patch for extendVariants and CompoundVariants type fix

* chore(system-rsc): add changeset for getSlots() slot detection fix

* refactor(types): unify slot value inference via GetSuggestedValues<S> for consistent variant typing

* fix(extendVariants): improved as-prop handling and exclude classNames from SuggestedVariants

* fix(system-rsc): add polymorphic 'as' prop support to extendVariants

* chore(system-rsc): add missing tests

---------

Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* chore(docs): update meta (#6168)

* ci(changesets): version packages (#6059)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

---------

Co-authored-by: Hayato Hasegawa <hase1225hayato@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
Co-authored-by: brianatdetections <brian@detections.ai>
Co-authored-by: Brian Meek <brian@current.space>
Co-authored-by: Dominik Hryshaiev <domhryshaiev@gmail.com>
Co-authored-by: creative-atish <149860680+atishkr25@users.noreply.github.com>
Co-authored-by: KyZy7 <29321162+KyZy7@users.noreply.github.com>
Co-authored-by: Deepansh Bhargava <deepansh940@gmail.com>
Co-authored-by: KumJungMin <37934668+KumJungMin@users.noreply.github.com>
Co-authored-by: Bohdan Kulinchenko <35272606+ITBoomBKStudio@users.noreply.github.com>
Co-authored-by: doki- <1335902682@qq.com>
jrgarciadev added a commit that referenced this pull request Feb 18, 2026
* fix(theme): hide password reveal button (#5990)

* fix(link): link overriding role (#5999)

* fix(link): allow overriding role

* chore(link): changeset

* chore(deps): upgrade react-aria (v1.14.0) (#5996)

* chore(deps): upgrade react-aria (v1.14.0)

* refactor(react): group client components

* fix(dropdown): keyDown test cases

* chore(react): rollback

* fix(theme): default transition-duration (#6011)

* fix(theme): default transitionDuration

* chore(deps): bump @heroui/theme peer dep

* ci(changesets): version packages (#5991)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(use-image): resolve race condition with cached images on Firefox/Safari (#6041)

* fix(use-image): resolve race condition with cached images on Firefox/Safari

The bug occurred because event handlers (onload/onerror) were attached
AFTER setting the image src. For cached images, browsers fire onload
synchronously when src is set, causing the event to be missed and
images to remain stuck at opacity-0.

Changes:
- Attach handlers BEFORE setting src to catch synchronous callbacks
- Check both naturalWidth AND naturalHeight (per CodeRabbit review on #4523)
- Handle synchronous error callbacks for failed cached images
- Add comprehensive test coverage (15 tests) including:
  - Synchronous cached image success (Firefox/Safari race condition)
  - Synchronous cached image error
  - Dynamic src changes
  - All props (crossOrigin, srcSet, sizes, loading)
  - Callback invocation verification

Reproduction and investigation performed using Claude Opus 4.5.

Fixes #4534, #2259

* fix(use-image): add ignoreFallback to useCallback dependencies

Address CodeRabbit review feedback: the `ignoreFallback` prop was used
inside the `load` callback (line 104) but was missing from the dependency
array, creating a stale closure bug.

Without this fix, if `ignoreFallback` changes from `true` to `false`
dynamically, the `load` callback would retain the stale `true` value,
preventing the image from ever loading.

Changes:
- Add `ignoreFallback` to useCallback dependency array
- Add tests for dynamic `ignoreFallback` changes (both directions)
- Update changeset to document this fix

Verification performed using Claude Opus 4.5.

---------

Co-authored-by: Brian Meek <brian@current.space>

* fix(docs): broken links in Form page (#6077)

* fix(pagination): improve layout for large page counts (#6034)

* fix(pagination): improve layout for large page counts/style of paagination compnents

* fix(pagination): refine item sizing to balance small and large page numbers

* ci(changesets): add pagination sizing changeset

* fix(pagination): ensure cursor fully covers button without changing radius

* chore(changeset): revise message and add issue numbers

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix(date-picker): open date-picker when clicking border (#6084)

* fix(date-picker): add pointer interaction to open date picker on wrapper click

* chore: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(accordion): click behaviour for dynamically generated accordion items (#6133)

* fix(accordion): add collection state

* chore: add changeset

* fix: update change set

* refactor(theme): remove flat dependency (#6157)

* chore(deps): remove flat library

* refactor(theme): replace flatten from flat

* chore: add changeset

* fix(listbox): prevent option focus from start/end content slots (#6060)

* fix(listbox): prevent option focus from start/end content slots

* test: add test code about when clicking
non-interactive slot content, and prevented focus leakage from interactive
start/end content such as buttons.

* docs: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(system-rsc): extendVariants & compound variants types (#5847)

* fix(extendVariants): return component type error

* fix(CompoundVariants): correct type inference for extended/compound variants and composition

* test: cover compound/extend inference; enforce CP required props

* fix(types): correct CompoundVariants class value inference

Replaces the conditional ClassProp logic with a simpler,
consistent form to fix incorrect slot value inference.

Before:
  ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>

After:
  ClassProp<ClassValue | GetSuggestedValues<S>>

This ensures GetSuggestedValues<S> is used for slot-aware variants
and avoids duplicated conditional branches for undefined slots.

* fix(system-rsc): correct slot detection in getSlots()

* fix(types): make ExtendVariants props optional and guard V[key] with NonNullable

Simplifies the return type of ExtendVariants to ensure no required props
are enforced at the HOC level. This aligns with the intended API contract
where extended components expose all props as optional.

- All keys (CP ∪ V) are optional
- Preserve CP type hints and booleanized V values
- Added NonNullable<V[key]> guard to prevent undefined indexing

* test(extendVariants): add compoundVariants integration test

* fix(system-rsc): getSlots() brief JSDoc comment added

* test(extendVariants):  new styles - extended & fixed styles - original tests for slots component

* test(extendVariants): fixed slot component variant styles extended test

* fix(types): avoid leaking React internals by removing PropsWithoutRef

Replace PropsWithoutRef with explicit Exclude<'ref'> in mapped keys and
intersect with RefAttributes<InferRef<C>>. This prevents @types/react’s
internal UNDEFINED_VOID_ONLY from leaking into the public .d.ts and fixes
declaration emit for components like extended Autocomplete.

* chore(changeset): add patch for extendVariants and CompoundVariants type fix

* chore(system-rsc): add changeset for getSlots() slot detection fix

* refactor(types): unify slot value inference via GetSuggestedValues<S> for consistent variant typing

* fix(extendVariants): improved as-prop handling and exclude classNames from SuggestedVariants

* fix(system-rsc): add polymorphic 'as' prop support to extendVariants

* chore(system-rsc): add missing tests

---------

Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* chore(docs): update meta (#6168)

* ci(changesets): version packages (#6059)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(system-rsc): extendVariants rendering behavior with as (#6215)

* fix(system-rsc): fix components rendering with 'as' prop

* fix(system-rsc): fix compoundVariants and slots inheritance in extendVariants

* fix(system-rsc): extendVariants test file cleaned

* chore(deps): bump RA dependencies (#6221)

* chore(deps): bump RA dependencies

* chore(date-picker): revise test cases

* fix(button): correct disableRipple prop precedence (#6199)

* ci(changesets): version packages (#6227)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

---------

Co-authored-by: Hayato Hasegawa <hase1225hayato@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
Co-authored-by: brianatdetections <brian@detections.ai>
Co-authored-by: Brian Meek <brian@current.space>
Co-authored-by: Dominik Hryshaiev <domhryshaiev@gmail.com>
Co-authored-by: creative-atish <149860680+atishkr25@users.noreply.github.com>
Co-authored-by: KyZy7 <29321162+KyZy7@users.noreply.github.com>
Co-authored-by: Deepansh Bhargava <deepansh940@gmail.com>
Co-authored-by: KumJungMin <37934668+KumJungMin@users.noreply.github.com>
Co-authored-by: Bohdan Kulinchenko <35272606+ITBoomBKStudio@users.noreply.github.com>
Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: Chris Nowak <krzysztofmareknowak@gmail.com>
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.

[BUG] - Cached avatar don't reset opacity 0 [BUG] - Image component: loading="lazy" not working

3 participants