Skip to content
Draft
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/media-inline-dialog-footers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sanity-plugin-media": patch
---

Re-enable React Compiler for the asset details form. The underlying issue is fixed at the root: the form reset that runs when the dialog opens (and when the asset or tag is updated elsewhere) now keeps fields registered (`keepFieldsRef`), so registered string fields keep reporting changes under compiler memoization. The remaining dialog footers (and the confirm dialog header) are also no longer declared as inline components, so they no longer remount whenever form state changes.
2 changes: 2 additions & 0 deletions plugins/sanity-plugin-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"zod": "^3.25.76"
},
"devDependencies": {
"@rolldown/plugin-babel": "catalog:",
"@sanity/tsconfig": "catalog:",
"@sanity/tsdown-config": "catalog:",
"@testing-library/jest-dom": "catalog:",
Expand All @@ -79,6 +80,7 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react-file-icon": "^1.0.5",
"@vitejs/plugin-react": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"jsdom": "catalog:",
"react": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,6 @@ export default function Details({
creditLine,
locales,
}: DetailsProps) {
'use no memo'
// React Compiler memoizes the register() field JSX using asset-derived deps only.
// That can leave Save stuck disabled while the DOM still updates (uncontrolled inputs).
// Tags work because they use Controller; string fields use register and need this opt-out.

const hasLocales = locales && locales.length > 0
const [activeLocaleTab, setActiveLocaleTab] = useState(0)
const folderId = currentAsset?.opt?.media?.folder?._ref
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,15 @@ const DialogAssetEdit = (props: Props) => {
}
}, [getValues, lastRemovedTagIds, setValue])

// Reset react-hook-form local state on mount and every time the asset has been updated elsewhere
// Reset react-hook-form local state on mount and every time the asset has been updated elsewhere.
// `keepFieldsRef` is required with React Compiler: a plain `reset()` empties react-hook-form's
// internal field registry and relies on the `register()` calls re-running on the next render to
// re-register every field. The compiler memoizes the registered field JSX (keyed on the stable
// `register` reference), so without this option the fields stay unregistered after reset and
// edits to string fields no longer mark the form dirty (Save stays disabled).
useEffect(() => {
if (assetUpdatedPrev.current !== assetItem?.asset._updatedAt) {
reset(generateDefaultValues(assetItem?.asset))
reset(generateDefaultValues(assetItem?.asset), {keepFieldsRef: true})
}
assetUpdatedPrev.current = assetItem?.asset._updatedAt
}, [assetItem?.asset, generateDefaultValues, reset])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ const DialogConfirm = (props: Props) => {
handleClose()
}

const Footer = () => (
// Plain JSX, not inline components: a new component identity per render remounts the
// footer/header subtrees whenever captured values change (see DialogAssetEdit for details).
const footer = (
<Box padding={3}>
<Flex justify="space-between">
<Button fontSize={1} mode="bleed" onClick={handleClose} text="Cancel" />
Expand All @@ -51,7 +53,7 @@ const DialogConfirm = (props: Props) => {
</Box>
)

const Header = () => (
const header = (
<Flex align="center">
<Box paddingX={1}>
<WarningOutlineIcon />
Expand All @@ -61,16 +63,7 @@ const DialogConfirm = (props: Props) => {
)

return (
<Dialog
animate
// oxlint-disable-next-line react/react-compiler
footer={<Footer />}
// oxlint-disable-next-line react/react-compiler
header={<Header />}
id="confirm"
onClose={handleClose}
width={1}
>
<Dialog animate footer={footer} header={header} id="confirm" onClose={handleClose} width={1}>
<Box paddingX={4} paddingY={4}>
<Stack space={3}>
{dialog?.title && <Text size={1}>{dialog.title}</Text>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ const DialogFolderCreate = (props: Props) => {
}
}, [creatingError, setError])

const Footer = () => (
// Plain JSX, not an inline component: a new component identity per render remounts the
// footer subtree whenever form state changes (see DialogAssetEdit for details).
const footer = (
<Box padding={3}>
<Flex justify="flex-end">
<FormSubmitButton
Expand All @@ -76,15 +78,7 @@ const DialogFolderCreate = (props: Props) => {
)

return (
<Dialog
animate
// oxlint-disable-next-line react/react-compiler
footer={<Footer />}
header="Create Folder"
id={id}
onClose={handleClose}
width={1}
>
<Dialog animate footer={footer} header="Create Folder" id={id} onClose={handleClose} width={1}>
<Box as="form" padding={4} onSubmit={handleSubmit(onSubmit)}>
<button style={{display: 'none'}} tabIndex={-1} type="submit" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ const DialogTagCreate = (props: Props) => {
}
}, [creatingError, setError])

const Footer = () => (
// Plain JSX, not an inline component: a new component identity per render remounts the
// footer subtree whenever form state changes (see DialogAssetEdit for details).
const footer = (
<Box padding={3}>
<Flex justify="flex-end">
{/* Submit button */}
Expand All @@ -79,15 +81,7 @@ const DialogTagCreate = (props: Props) => {
)

return (
<Dialog
animate
// oxlint-disable-next-line react/react-compiler
footer={<Footer />}
header="Create Tag"
id={id}
onClose={handleClose}
width={1}
>
<Dialog animate footer={footer} header="Create Tag" id={id} onClose={handleClose} width={1}>
{/* Form fields */}
<Box as="form" padding={4} onSubmit={handleSubmit(onSubmit)}>
{/* Hidden button to enable enter key submissions */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ const DialogTagEdit = (props: Props) => {
if (result && transition === 'update') {
// Regenerate snapshot
setTagSnapshot(result as Tag)
// Reset react-hook-form
reset(generateDefaultValues(result as Tag))
// Reset react-hook-form. `keepFieldsRef` keeps fields registered — with React Compiler
// the `register()` calls do not re-run after reset (see DialogAssetEdit for details).
reset(generateDefaultValues(result as Tag), {keepFieldsRef: true})
}
},
[reset, generateDefaultValues],
Expand Down Expand Up @@ -133,7 +134,9 @@ const DialogTagEdit = (props: Props) => {
}
}, [client, handleTagUpdate, tagItem?.tag])

const Footer = () => (
// Plain JSX, not an inline component: a new component identity per render remounts the
// footer subtree whenever form state changes (see DialogAssetEdit for details).
const footer = (
<Box padding={3}>
<Flex justify="space-between">
{/* Delete button */}
Expand Down Expand Up @@ -162,15 +165,7 @@ const DialogTagEdit = (props: Props) => {
}

return (
<Dialog
animate
// oxlint-disable-next-line react/react-compiler
footer={<Footer />}
header="Edit Tag"
id={id}
onClose={handleClose}
width={1}
>
<Dialog animate footer={footer} header="Edit Tag" id={id} onClose={handleClose} width={1}>
{/* Form fields */}
<Box as="form" padding={4} onSubmit={handleSubmit(onSubmit)}>
{/* Deleted notification */}
Expand Down
6 changes: 6 additions & 0 deletions plugins/sanity-plugin-media/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import pluginBabel from '@rolldown/plugin-babel'
import {reactCompilerPreset} from '@vitejs/plugin-react'
import {defineConfig} from 'vitest/config'

export default defineConfig({
// Run tests against React Compiler output so they exercise the same memoized
// code as the published build (`reactCompiler: true` in tsdown.config.ts uses
// the same plugin + preset combination).
plugins: [pluginBabel({presets: [reactCompilerPreset()]})],
oxc: {
jsx: {runtime: 'automatic'},
},
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ catalog:
'@portabletext/block-tools': ^5.1.12
'@portabletext/to-html': ^5.0.2
'@portabletext/types': ^4.0.2
'@rolldown/plugin-babel': ^0.2.3
'@sanity/asset-utils': ^2.3.0
'@sanity/client': ^7.26.0
'@sanity/color': ^3.0.8
Expand Down Expand Up @@ -47,6 +48,7 @@ catalog:
'@vanilla-extract/css': ^1.21.2
'@vanilla-extract/dynamic': ^2.1.5
'@vis.gl/react-google-maps': ^1.9.0
'@vitejs/plugin-react': ^6.0.5
'@vitest/coverage-v8': ^4.1.10
babel-plugin-react-compiler: ^1.0.0
date-fns: ^4.4.0
Expand Down
Loading