Skip to content

feat: upload input v3 image preview improvement#263

Open
priscila-moneo wants to merge 2 commits into
mainfrom
feature/upload-input-v3-image-preview-improvement
Open

feat: upload input v3 image preview improvement#263
priscila-moneo wants to merge 2 commits into
mainfrom
feature/upload-input-v3-image-preview-improvement

Conversation

@priscila-moneo

@priscila-moneo priscila-moneo commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

ref: https://app.clickup.com/t/86ba8wmgh

Summary by CodeRabbit

  • New Features

    • Immediate image previews display when uploading files
  • Improvements

    • Enhanced loading performance for image previews (faster handling for local data:/blob: sources)
    • Improved preview handling during upload cancellations and errors (revokes unused previews and prevents carry-over)
    • Better preview persistence across component updates, including support for server-renamed uploads and out-of-order completions
  • Tests

    • Comprehensive test coverage for image preview behavior and loading scenarios

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@priscila-moneo, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 42 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b682662-2023-45cb-a48f-72e406355c0f

📥 Commits

Reviewing files that changed from the base of the PR and between ee35ca1 and 0061872.

📒 Files selected for processing (1)
  • src/components/inputs/upload-input-v3/index.js
📝 Walkthrough

Walkthrough

This PR updates upload thumbnail previews to create and cache blob URLs for new image uploads, render local URLs synchronously in ProgressiveImg, and revoke or reuse previews across cancel, error, completion, and parent-value update paths.

Changes

Blob URL Preview Caching and Lifecycle

Layer / File(s) Summary
ProgressiveImg data URL and blob URL optimization
src/components/progressive-img/index.js, src/components/progressive-img/__tests__/progressive-img.test.js
ProgressiveImg now treats data: and blob: URLs as immediately available, skips async image loading for them, and uses a local cancellation flag for remote loads. Tests cover local rendering, remote load states, fallback on error, and stale-load handling.
UploadInputV3 preview state and helpers
src/components/inputs/upload-input-v3/index.js
Adds filePreviews state, formatFileSize, shared row styling, and memoized extension/max-size values based on provided getters or mediaType fallbacks.
UploadInputV3 preview lifecycle management
src/components/inputs/upload-input-v3/index.js
Creates blob URLs for added image files, revokes them on removal or error, transfers them into filePreviews on completion, and switches parent-value syncing to useLayoutEffect.
UploadInputV3 preview rendering
src/components/inputs/upload-input-v3/index.js
Uploading rows render ProgressiveImg from previewUrl when present, and completed rows prefer cached filePreviews[filename] over the server preview URL.
UploadInputV3 image preview test suite
src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
Adds coverage for image-only previews, retained previews after renamed completions, revoke-on-cancel, out-of-order parallel uploads, and revoke-on-error behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smarcet
  • santipalenque

Poem

🐰 Blobie the rabbit hops by with a grin,
Local previews pop up before uploads begin.
Revoke when they’re done, keep the right one in store,
Tiny images dance—no waiting anymore.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: improved image preview handling in UploadInputV3.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/upload-input-v3-image-preview-improvement

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/inputs/upload-input-v3/index.js`:
- Around line 182-194: The current preview assignment uses FIFO shift on
pendingPreviewsRef.current and assumes server-returned value order matches
upload order, causing mis-matches; update the logic to match previews to files
by a stable identifier instead of insertion order: ensure preview entries in
pendingPreviewsRef include the original client filename (e.g.,
preview.originalName or preview.clientFilename) at enqueue time, build a map
from that original name to dataURL, then iterate newFiles (from value) and for
each file try to match by comparing f.filename or f.originalName to the preview
map key (and use a heuristic like contains/endsWith if the server adds
prefixes); assign matched previews into updates and only fall back to shift()
for any remaining unmatched files; update the code paths around
pendingPreviewsRef, newFiles, and setFilePreviews to use this name-based
matching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d9718e3-5485-4d80-a2ed-9b8ff5300c12

📥 Commits

Reviewing files that changed from the base of the PR and between 2539b02 and 4610604.

📒 Files selected for processing (3)
  • src/components/inputs/upload-input-v3/dropzone-v3.js
  • src/components/inputs/upload-input-v3/index.js
  • src/components/progressive-img/index.js

Comment thread src/components/inputs/upload-input-v3/index.js Outdated
@priscila-moneo priscila-moneo force-pushed the feature/upload-input-v3-image-preview-improvement branch from 4610604 to 32e967f Compare June 5, 2026 21:25
Comment thread src/components/progressive-img/index.js
Comment thread src/components/inputs/upload-input-v3/index.js Outdated
Comment thread src/components/inputs/upload-input-v3/index.js Outdated
// Once the parent updates value, remove all completed files from uploadingFiles
useEffect(() => {
// Once the parent updates value, remove completed uploading files and assign local previews to new files
useLayoutEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't see the use case for this change, can you explain ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

useLayoutEffect runs synchronously before the browser paints, so when value updates, the completed uploading row is removed before the user sees anything. With useEffect it runs after paint, causing a one-frame flash where the file appears twice — once as "Complete" in the uploading section and once in the committed value section.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this just hides the symptom but does not address the re-render . which is caused by this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done the refactor you mention and prevValueRef, pendingPreviewsRef and the batched matching are all gone. But useLayoutEffect is still needed though: when value updates, both the completed uploading row and the new value row land in the same render, and without it the user sees them both for one frame before the cleanup runs. It's not hiding a symptom, it's preventing a real visual duplicate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm working on the last of your comments and will update this PR with the latest code, will tag you so you can review it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@priscila-moneo any update on this one ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

code already updated

Comment thread src/components/inputs/upload-input-v3/index.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/components/inputs/upload-input-v3/index.js (1)

210-214: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preview mapping is still ambiguous for same-size concurrent uploads.

At Line 212, matching uses only response.size, so two different files with identical sizes can receive swapped previews. Please map with a stable per-file identifier (client upload id echoed by server, or an explicit correlation token) instead of size-only matching.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/inputs/upload-input-v3/index.js` around lines 210 - 214, The
current preview-assignment logic in the setUploadingFiles callback uses
response.size to match upload entries (response.name/response.size and
entry.previewUrl), which causes wrong previews for concurrent same-size files;
change the protocol to use a stable per-file identifier (e.g. response.uploadId
or response.correlationId) returned by the server and store that id on the
client-side upload entries, then in setUploadingFiles find the entry by that
uploadId instead of size and call setFilePreviews with the stable key (e.g.
setFilePreviews(p => ({ ...p, [response.uploadId]: entry.previewUrl }))). Update
any code that creates upload entries to include the client upload id so matching
is deterministic.
🧹 Nitpick comments (2)
src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js (2)

350-358: ⚡ Quick win

Preserve and restore original URL methods instead of deleting globals.

At Line 356–Line 357, deleting global methods can pollute other tests. Store originals and restore them in afterEach for isolation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
around lines 350 - 358, The tests are deleting global URL methods which can leak
into other tests; modify the beforeEach/afterEach so beforeEach saves the
originals (e.g., const originalCreateObjectURL = URL.createObjectURL; const
originalRevokeObjectURL = URL.revokeObjectURL) then mocks with jest.fn in
beforeEach, and in afterEach restore them (URL.createObjectURL =
originalCreateObjectURL; URL.revokeObjectURL = originalRevokeObjectURL) instead
of using delete; update the beforeEach/afterEach in upload-input-v3.test.js
around the existing beforeEach/afterEach blocks referencing URL.createObjectURL
and URL.revokeObjectURL.

414-434: ⚡ Quick win

Add a collision test for parallel uploads with identical file sizes.

Current coverage validates out-of-order responses but only with unique sizes. Add a case where two image uploads have the same size to catch preview mis-assignment regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`
around lines 414 - 434, Add a new unit test alongside the existing "correctly
maps previews for parallel uploads using response size" that reproduces a
collision where two uploads have identical size: use the same pattern of calling
dropzoneCallbacks.onAddedFile for two distinct filenames (e.g., 'sunset.jpg' and
'portrait.jpg') with identical size values, call
dropzoneCallbacks.onFileCompleted for both, then simulate server responses in
reverse order via dropzoneCallbacks.onUploadComplete with server filenames
(e.g., '246_portrait_abc123.jpg' and '246_sunset_def456.jpg'), rerender
UploadInputV3 with value containing those server filenames and sizes, and assert
that screen.getByRole('img', { name: ... }) returns the expected blob src
mapping to the original preview names (e.g., 'blob:portrait.jpg' and
'blob:sunset.jpg') to catch preview mis-assignment when sizes collide.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/inputs/upload-input-v3/index.js`:
- Line 70: Add a useEffect that revokes cached blob URLs on unmount and whenever
filePreviews changes: implement useEffect(() => { return () => {
Object.values(filePreviews).forEach(url => { if (url) URL.revokeObjectURL(url)
}) } }, [filePreviews]); this ensures any object URLs created earlier (look for
places that call URL.createObjectURL in the upload preview code around the
filePreviews setter) are revoked; also ensure any code path that replaces an
individual preview calls URL.revokeObjectURL on the old URL before overwriting
filePreviews via setFilePreviews.

---

Duplicate comments:
In `@src/components/inputs/upload-input-v3/index.js`:
- Around line 210-214: The current preview-assignment logic in the
setUploadingFiles callback uses response.size to match upload entries
(response.name/response.size and entry.previewUrl), which causes wrong previews
for concurrent same-size files; change the protocol to use a stable per-file
identifier (e.g. response.uploadId or response.correlationId) returned by the
server and store that id on the client-side upload entries, then in
setUploadingFiles find the entry by that uploadId instead of size and call
setFilePreviews with the stable key (e.g. setFilePreviews(p => ({ ...p,
[response.uploadId]: entry.previewUrl }))). Update any code that creates upload
entries to include the client upload id so matching is deterministic.

---

Nitpick comments:
In `@src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js`:
- Around line 350-358: The tests are deleting global URL methods which can leak
into other tests; modify the beforeEach/afterEach so beforeEach saves the
originals (e.g., const originalCreateObjectURL = URL.createObjectURL; const
originalRevokeObjectURL = URL.revokeObjectURL) then mocks with jest.fn in
beforeEach, and in afterEach restore them (URL.createObjectURL =
originalCreateObjectURL; URL.revokeObjectURL = originalRevokeObjectURL) instead
of using delete; update the beforeEach/afterEach in upload-input-v3.test.js
around the existing beforeEach/afterEach blocks referencing URL.createObjectURL
and URL.revokeObjectURL.
- Around line 414-434: Add a new unit test alongside the existing "correctly
maps previews for parallel uploads using response size" that reproduces a
collision where two uploads have identical size: use the same pattern of calling
dropzoneCallbacks.onAddedFile for two distinct filenames (e.g., 'sunset.jpg' and
'portrait.jpg') with identical size values, call
dropzoneCallbacks.onFileCompleted for both, then simulate server responses in
reverse order via dropzoneCallbacks.onUploadComplete with server filenames
(e.g., '246_portrait_abc123.jpg' and '246_sunset_def456.jpg'), rerender
UploadInputV3 with value containing those server filenames and sizes, and assert
that screen.getByRole('img', { name: ... }) returns the expected blob src
mapping to the original preview names (e.g., 'blob:portrait.jpg' and
'blob:sunset.jpg') to catch preview mis-assignment when sizes collide.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 060f9ed7-3bec-49bb-a60b-9791fd9adcfc

📥 Commits

Reviewing files that changed from the base of the PR and between 4610604 and ca65214.

📒 Files selected for processing (4)
  • src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
  • src/components/inputs/upload-input-v3/index.js
  • src/components/progressive-img/__tests__/progressive-img.test.js
  • src/components/progressive-img/index.js
✅ Files skipped from review due to trivial changes (1)
  • src/components/progressive-img/tests/progressive-img.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/progressive-img/index.js

Comment thread src/components/inputs/upload-input-v3/index.js

@santipalenque santipalenque left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, thank you @priscila-moneo

@smarcet

smarcet commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Memory leak: blob URLs not revoked on unmount

UploadInputV3 creates blob URLs in two places but has no cleanup when the component unmounts:

  1. filePreviews state — blob URLs are stored here (keyed by server filename) in wrappedOnUploadComplete. They are only revoked in handleRemove (user-initiated delete). If the user navigates away from the page while uploaded files are showing, those blob URLs persist until page reload.

  2. uploadingFiles[*].previewUrl — blob URLs created in handleAddedFile. If the component unmounts mid-upload, these are never revoked.

In a long-running SPA session where the user uploads images across multiple visits to this component without a full page reload, each unmount leaks the memory for all previewed images.

Suggested fix — add a cleanup effect using a ref to capture latest state at unmount time:

const filePreviewsRef = useRef(filePreviews);
filePreviewsRef.current = filePreviews;
const uploadingFilesRef = useRef(uploadingFiles);
uploadingFilesRef.current = uploadingFiles;

useEffect(() => {
  return () => {
    Object.values(filePreviewsRef.current).forEach(url => URL.revokeObjectURL(url));
    uploadingFilesRef.current.forEach(f => f.previewUrl && URL.revokeObjectURL(f.previewUrl));
  };
}, []);

(Using refs instead of deps on [] because a cleanup-on-unmount effect with [] captures stale initial state, while refs always reflect the latest values.)

@smarcet

smarcet commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Zero/undefined size breaks preview mapping and renders misleading file size

Two issues compound each other when a file's size is 0 or undefined:

1. formatFileSize uses a falsy guard (pre-existing, not introduced by this PR)

const formatFileSize = (bytes) => {
  if (!bytes) return '0 KB'; // catches 0, undefined, null, NaN
  ...
};

A file whose size is missing from the server response silently displays 0 KB instead of something explicit like or Unknown. Minor UX issue on its own.

2. The size-based preview matching in wrappedOnUploadComplete has no defense against size === 0 or size === undefined — introduced by this PR

const entry = prev.find(f => f.size === response.size && f.previewUrl);

If response.size comes back as 0 or undefined from the server, this find will match any uploading file that also has size === 0 or size === undefined and has a preview URL. In a multi-file upload where the server omits the size field or where two empty files are uploaded concurrently, all of them collapse to the same match — the first entry wins and every file gets assigned the same (wrong) blob URL.

Why these two are connected: the !bytes guard in formatFileSize exists precisely because size: 0 is a valid value that reaches the component. That same zero/undefined value is then used as an identity key for preview mapping with no validation.

Suggested fix: guard the preview mapping against falsy sizes before attempting the match:

const entry = response?.size
  ? prev.find(f => f.size === response.size && f.previewUrl)
  : null;

This ensures that a missing or zero size from the server never triggers a spurious preview assignment.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@priscila-moneo please re review

Signed-off-by: Priscila Moneo <priscila_moneo@hotmail.com.ar>
@priscila-moneo priscila-moneo force-pushed the feature/upload-input-v3-image-preview-improvement branch from ca65214 to ee35ca1 Compare June 24, 2026 22:27
@priscila-moneo priscila-moneo requested a review from smarcet June 24, 2026 22:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/inputs/upload-input-v3/index.js`:
- Around line 182-185: The upload cleanup in upload-input-v3 is removing every
completed row whenever value changes, which can hide other finished uploads that
the parent has not received yet. Update wrappedOnUploadComplete and the
useLayoutEffect cleanup to track each row’s upload/response identity, then only
clear the specific uploading row whose completed response is actually present in
value. Use the existing uploading row state in upload-input-v3 and the
completion handler logic around wrappedOnUploadComplete to locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fd40445-f0a8-473b-a7f9-862a57f894c4

📥 Commits

Reviewing files that changed from the base of the PR and between ca65214 and ee35ca1.

📒 Files selected for processing (4)
  • src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js
  • src/components/inputs/upload-input-v3/index.js
  • src/components/progressive-img/__tests__/progressive-img.test.js
  • src/components/progressive-img/index.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/progressive-img/tests/progressive-img.test.js
  • src/components/inputs/upload-input-v3/tests/upload-input-v3.test.js

Comment thread src/components/inputs/upload-input-v3/index.js
Signed-off-by: Priscila Moneo <priscila_moneo@hotmail.com.ar>
@priscila-moneo priscila-moneo force-pushed the feature/upload-input-v3-image-preview-improvement branch from 860645a to 0061872 Compare June 24, 2026 22:36
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.

3 participants