Skip to content

Latest commit

 

History

History
238 lines (165 loc) · 14.1 KB

MIGRATION.md

File metadata and controls

238 lines (165 loc) · 14.1 KB

Migration guides

If you upgrade directly from v2 to v4, complete the v2-to-v3 steps first, and then complete the v3-to-v4 steps.

@sanity/ui: from v3 to v4

v4 modernizes the runtime without redesigning components. If your app is already on current tooling, the migration is one stylesheet import plus some import path updates — TypeScript walks you through the rest: every removed or moved API is kept in the types as a @deprecated tombstone whose message names its replacement.

Not ready to upgrade? v3 is maintained on the v3 branch: 3.x fixes land there and are published under the release-v3 npm dist-tag.

Work through the checklist top to bottom:

  1. Upgrade the runtime — Node.js 22.12+, React 19.2+, styled-components 6.1+, ESM-only
  2. Import the stylesheet — import '@sanity/ui/styles.css'
  3. Update imports for moved components
  4. Replace removed deprecated APIs
  5. Less common removals
  6. Behavior changes to review
  7. Update test assertions on closed tooltip and popover content — check visibility, not existence

1. Upgrade the runtime

  • ESM-only. The CommonJS build (.cjs / .d.cts) and the require export condition are removed — the package now ships ESM only. This is not a code change for consumers: modern Node.js supports require(esm), so require('@sanity/ui') keeps working on the supported Node.js versions.
  • Node.js 22.12 or later (engines is >=22.12, matching sanity).
  • React 19.2 or later — peer dependencies are react: ^19.2 and react-dom: ^19.2. The 19.2 floor is set by the new <Activity> component, which shipped in React 19.2.0 and which v4 uses to keep closed Tooltip and Popover content mounted. Components are compiled with the React Compiler targeting React 19, using React's built-in runtime.
  • styled-components 6.1 or later — it stays a peer dependency, now declared as ^6.1. v4.0.0 already required v6 — it imports the named styled export and v6-only types such as ExecutionContext, neither of which exist in v5 — but the peer range was left at ^5.2 || ^6 and the 4.0.0 release notes forgot to mention the dropped v5 support. The corrected range also excludes 6.0.x, which ships the Babel macro (and with it the entire @babel/* toolchain) as runtime dependencies until 6.1.0 removed it, and whose last release, 6.0.9, was published without its type declarations.
  • TypeScript: use an exports-aware moduleResolution (node16, nodenext, or bundler). v4 resolves everything through package exports and no longer ships typesVersions, so the legacy moduleResolution: "node" (node10) mode cannot resolve the types for the subpath entry points.
npm install @sanity/ui@^4 react@^19.2 react-dom@^19.2 styled-components@^6.1

2. Import the stylesheet

Static styles (the ones that don't depend on theme or props) are extracted into a stylesheet at build time instead of being injected at runtime, and the stylesheet is not loaded automatically.

Add this import once, next to where the app renders <ThemeProvider>:

import '@sanity/ui/styles.css'

Without it, SrOnly, Spinner and internal text-overflow styling render unstyled. These components have moved off styled-components — more components follow in future 4.x minors, and UI5 ships without styled-components or any other runtime CSS-in-JS.

The stylesheet must load before styled-components' <style data-styled> tag. It has no cascade layers, so when one of your styled-components overrides, such as styled(Button), has the same specificity as the static style it overrides, the rule that loads last wins. Importing the stylesheet before anything renders gives the right order; see Load order.

3. Update imports for moved components

Components with heavy dependencies moved to dedicated subpath entry points, so importing @sanity/ui no longer pulls in @floating-ui/react-dom, motion or react-refractor — even without bundler treeshaking.

Entry point Exports
@sanity/ui/toast Toast, ToastProvider, useToast
@sanity/ui/popover Popover
@sanity/ui/tooltip Tooltip, TooltipDelayGroupProvider, useTooltipDelayGroup
@sanity/ui/menu Menu, MenuButton, MenuDivider, MenuGroup, MenuItem
@sanity/ui/autocomplete Autocomplete
@sanity/ui/breadcrumbs Breadcrumbs
@sanity/ui/code Code

Prop, context and message types moved along with their components (PopoverProps, ToastParams, MenuItemProps, AutocompleteState, TooltipDelayGroupContextValue, …).

-import {MenuButton, ToastProvider, useToast} from '@sanity/ui'
+import {MenuButton} from '@sanity/ui/menu'
+import {ToastProvider, useToast} from '@sanity/ui/toast'

You don't need to hunt these down by hand: every moved symbol is still declared on the root entry as a @deprecated never tombstone, so TypeScript reports the new location at each usage instead of a bare "does not exist" error.

4. Replace removed deprecated APIs

Everything that was deprecated during v3 is removed in v4. Removed props stay on the public types as @deprecated never (you get the migration message, not a mystery error), and removed hooks are still exported but throw when called.

Removed Use instead
space prop gap
Grid columns / rows / column* / row* props gridTemplateColumns / gridTemplateRows / gridColumn* / gridRow*
Menu focusFirst / focusLast props shouldFocus
MenuButton top-level popover props popover={{…}}
Popover boundaryElement prop floatingBoundary / referenceBoundary (plus BoundaryElementProvider for max-width / constrainSize)
Tooltip allowedAutoPlacements prop fallbackPlacements
useClickOutside useClickOutsideEvent
useElementRect useElementSize
useForwardedRef useRef + useImperativeHandle
useArrayProp(value) Array.isArray(value) ? value : [value]
ConditionalWrapper inline the conditional wrapping logic

5. Less common removals

  • @sanity/ui/_visual-editing is removed. Import from @sanity/ui instead — except the moved components (Menu, MenuDivider, MenuGroup, MenuItem, Popover), which now live on @sanity/ui/menu and @sanity/ui/popover (see step 3). The slim private subset is no longer needed: displayName side effects are gone, so unused components tree-shake out of consuming bundles.
  • The internal _ResizeObserver export is removed along with the @juggle/resize-observer polyfill. Use the global ResizeObserver.

6. Behavior changes to review

  • Tooltip and Popover keep closed content mounted with React's <Activity>, preserving internal state and pre-rendering hidden content. With animate enabled, hiding is deferred until exit animations finish. Popovers with recursive content must gate the recursion on open — otherwise they render an infinitely deep hidden tree. Tests that assert on closed content need updating, see step 7.
  • All components are plain function components that take ref as a regular prop (React 19 semantics). Refs keep working exactly as before, but components no longer pass react-is checks such as isForwardRef.

7. Update test assertions on closed tooltip and popover content

<Activity mode="hidden"> keeps closed content in the DOM and hides it with display: none. "Closed" therefore means "not visible", not "not in the document": assertions that check whether content exists have to check its visibility instead. This applies to unit tests and end-to-end tests alike.

End-to-end tests (Playwright):

-await expect(page.getByText('Tooltip content')).toHaveCount(0)
+await expect(page.getByText('Tooltip content')).toBeHidden()

Unit tests (Testing Library and jest-dom):

-expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
+expect(screen.getByText('Tooltip content')).not.toBeVisible()

Assertions on open content are unchanged, and so are queries that skip inaccessible elements: display: none keeps closed content out of the accessibility tree, so Testing Library's *ByRole queries and Playwright's getByRole() still don't match it (unless you opt in with hidden: true / includeHidden: true).

React renders hidden activities at a lower priority than visible ones, so closed content isn't guaranteed to be in the DOM yet — the most robust assertions accept both "absent" and "present but hidden". Playwright's toBeHidden() does that by definition. jest-dom's toBeVisible() requires an element, so query first and only assert on what you find:

function expectHidden(text: string) {
  const element = screen.queryByText(text)

  if (element) {
    expect(element).not.toBeVisible()
  }
}

@sanity/ui: from v2 to v3

v3 updates the Node.js requirement and the syntax-highlighting dependencies. Most applications only need to update Node.js and @sanity/ui. Applications that use react-refractor or refractor directly must also update those imports.

Work through the checklist top to bottom:

  1. Upgrade Node.js
  2. Upgrade @sanity/ui
  3. Update direct react-refractor usage
  4. Test syntax highlighting

1. Upgrade Node.js

v2 supports Node.js 14 or later. v3 supports these Node.js versions:

  • >=20.19 <22
  • >=22.12

Node.js 22.0 through 22.11 is not supported.

Check the installed version:

node --version

Upgrade Node.js before you install v3 if the installed version is outside the supported ranges.

2. Upgrade @sanity/ui

Install the latest v3 release:

npm install @sanity/ui@^3

You do not need to change code that only uses the Code component from @sanity/ui. Its internal implementation already supports the updated syntax-highlighting dependencies.

3. Update direct react-refractor usage

v3 updates these dependencies:

  • refractor: 4.9.0 → 5.0.0
  • react-refractor: 2.2.0 → 4.0.0

This update addresses CVE-2024-53382.

If your application imports react-refractor or refractor directly, make these changes:

  • Use the named Refractor export instead of the default export.
  • Import languages from refractor/<language> instead of refractor/lang/<language>.
  • Use the separate registerLanguage and hasLanguage functions instead of methods on Refractor.
-import Refractor from 'react-refractor'
-import javascript from 'refractor/lang/javascript'
+import {Refractor, hasLanguage, registerLanguage} from 'react-refractor'
+import javascript from 'refractor/javascript'

-Refractor.registerLanguage(javascript)
-const registered = Refractor.hasLanguage('javascript')
+registerLanguage(javascript)
+const registered = hasLanguage('javascript')

Complete example:

// v2
import Refractor from 'react-refractor'
import javascript from 'refractor/lang/javascript'
import json from 'refractor/lang/json'

Refractor.registerLanguage(javascript)
Refractor.registerLanguage(json)

function MyComponent() {
  return <Refractor language="javascript" value="const x = 1" />
}
// v3
import {Refractor, registerLanguage} from 'react-refractor'
import javascript from 'refractor/javascript'
import json from 'refractor/json'

registerLanguage(javascript)
registerLanguage(json)

function MyComponent() {
  return <Refractor language="javascript" value="const x = 1" />
}

4. Test syntax highlighting

Test each code path that uses syntax highlighting. Confirm that each registered language still works.

For more information: