Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/some-ducks-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Fix File/Blob equality in change detection
12 changes: 12 additions & 0 deletions packages/form-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,18 @@ export function evaluate<T>(objA: T, objB: T) {
return false
}

// Blob (and File, which extends Blob) objects have no own enumerable keys,
// so the generic key-comparison below would incorrectly consider any two
// Blob/File instances as equal. Fall back to referential identity (already
// handled by Object.is above, which returned false).
if (
typeof Blob !== 'undefined' &&
objA instanceof Blob &&
objB instanceof Blob
) {
return false
}

if (objA instanceof Date && objB instanceof Date) {
return objA.getTime() === objB.getTime()
}
Expand Down
26 changes: 26 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4101,6 +4101,32 @@ it('should generate a formId if not provided', () => {
expect(form.formId.length).toBeGreaterThan(1)
})

it('should detect file value changes when setting a different File', () => {
const form = new FormApi({
defaultValues: {
avatar: undefined as File | undefined,
},
})

form.mount()

const firstFile = new File(['first'], 'first.png', { type: 'image/png' })
form.setFieldValue('avatar', firstFile)
expect(form.getFieldValue('avatar')).toBe(firstFile)
expect(form.getFieldValue('avatar')).toBeInstanceOf(File)
expect(form.getFieldValue('avatar')!.name).toBe('first.png')

const secondFile = new File(['second'], 'second.png', { type: 'image/png' })
form.setFieldValue('avatar', secondFile)
expect(form.getFieldValue('avatar')).toBe(secondFile)
expect(form.getFieldValue('avatar')).toBeInstanceOf(File)
expect(form.getFieldValue('avatar')!.name).toBe('second.png')

// Setting the same file reference again should keep the same value
form.setFieldValue('avatar', secondFile)
expect(form.getFieldValue('avatar')).toBe(secondFile)
})

describe('form api event client', () => {
it('should have debug disabled', () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
Expand Down
31 changes: 31 additions & 0 deletions packages/form-core/tests/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,37 @@ describe('evaluate', () => {
const setB = new Set([1, 2, 4])
expect(evaluate(setA, setB)).toEqual(false)
})

it('should test equality between File/Blob objects', () => {
// Same reference should be equal
const file1 = new File(['content'], 'test.txt', { type: 'text/plain' })
expect(evaluate(file1, file1)).toEqual(true)

// Different File objects with same metadata should NOT be equal
// (referential identity β€” the user picked a new file)
const file2 = new File(['content'], 'test.txt', { type: 'text/plain' })
expect(evaluate(file1, file2)).toEqual(false)

// Different File objects with different metadata
const file3 = new File(['other'], 'other.txt', { type: 'image/png' })
expect(evaluate(file1, file3)).toEqual(false)

// Blob objects
const blob1 = new Blob(['data'], { type: 'application/octet-stream' })
const blob2 = new Blob(['data'], { type: 'application/octet-stream' })
expect(evaluate(blob1, blob1)).toEqual(true)
expect(evaluate(blob1, blob2)).toEqual(false)

// File inside an object structure
const obj1 = { avatar: file1 }
const obj2 = { avatar: file2 }
expect(evaluate(obj1, obj2)).toEqual(false)

// Same file ref inside an object structure
const obj3 = { avatar: file1 }
const obj4 = { avatar: file1 }
expect(evaluate(obj3, obj4)).toEqual(true)
})
})

describe('concatenatePaths', () => {
Expand Down
Loading