Migration Guide
After years of refinement and iteration, we've cemented Zag to work seamlessly across major JavaScript frameworks.
Now, we're taking things to the next level by focusing on:
- Performance: Improving the runtime and rendering performance of every component
- Bundle Size: Reducing the gross bundle size of each component + framework adapters
We achieved this by moving from an external store to native reactive primitives provided by each framework.
Our rigorous performance testing, which involved stress-testing with 10,000 instances of the same component, revealed roughly 1.5x - 4x performance improvements across components. View Breakdown
Changed
The major changes are quite simple, and are listed below:
useMachine
useMachine now returns a service object instead of a tuple of
[state, send].
This change is the same across all components. Using "find and replace" will help you migrate faster.
Before
const [state, send] = useMachine(avatar.machine({ id: useId() }))
After
const service = useMachine(avatar.machine, { id: useId() })
Notice that
avatar.machineis no longer a function, it is passed directly touseMachine.
Caveats
Due to the switch from <component>.machine() as a function to
<component>.machine as an object, the TS inference is limited for generic
components like combobox and select.
To help with this, we've exported an equivalent <component>.Machine type to
help with the casting.
useMachine(combobox.machine as combobox.Machine<Item>)
Controlled vs Uncontrolled value
Managing controlled and uncontrolled values is a fairly common need in most component libraries.
Previously, we handled this by providing initial and controlled context to the machine.
/// 👇🏻 Default value const [state, send] = useMachine(numberInput.machine({ value: "10" }), { context: { // 👇🏻 Controlled value value: "10", }, })
This can be initially confusing to users and is error prone.
Now, we've moved the logic to the machine itself. Allowing users to explicitly provide a default value and a controlled value.
const service = useMachine(numberInput.machine, { // 👇🏻 Default value defaultValue: "10", // 👇🏻 Controlled value value: "10", })
This change applies all component with some form of
valueprop.
Controlled vs Uncontrolled open
Previously, we handled controlled and uncontrolled open states by providing
initial open state and an additional open.controlled property.
// 👇🏻 Default value const [state, send] = useMachine(dialog.machine({ open: true }), { context: { // 👇🏻 Controlled value open: true, "open.controlled": true, }, })
Now, we've moved the logic to the machine itself. Allowing users to explicitly provide a default and controlled open state.
const service = useMachine(dialog.machine, { // 👇🏻 Default value defaultOpen: true, // 👇🏻 Controlled value open: true, })
Typings
<component>.Context is now renamed to <component>.Props
Before
import * as accordion from "@zag-js/accordion" interface Props extends accordion.Context {}
After
import * as accordion from "@zag-js/accordion" interface Props extends accordion.Props {}
Toast
The toast component new requires that you create a toast store (or manager), and pass that store to the toast group machine.
This store is to be used in userland to create and manage toasts.
Refer to the toast documentation for more details.
Before
const [state, send] = useMachine( toast.group.machine({ overlap: false, placement: "bottom", }), ) const toaster = toast.group.connect(state, send, normalizeProps) // propagate the `toaster` via context and use it in your app. toaster.create({ title: "Hello", description: "World", })
After
const toaster = toast.createStore({ overlap: false, placement: "bottom", }) const service = useMachine(toast.group.machine, { store: toaster, }) // use the `toaster` store to create and manage toasts. No need for context. toaster.create({ title: "Hello", description: "World", })
For Solid.js users, we recommend using <Key> exported from @zag-js/solid
when mapping over the toasts, instead of the <For> component.
Fixed
-
Menu: Fix issue where context menu doesn't update positioning on subsequent right clicks.
-
Avatar: Fix issue where
api.setSrcdoesn't work. -
File Upload: Fix issue where drag-and-drop doesn't work when
directoryistrue. -
Carousel
- Fix issue where initial page is not working.
- Fix issue where pagination sync broken after using dots indicators.
Removed
-
General
- Removed
useActorhook in favor ofuseMachineeverywhere. - Removed
open.controlledin favor ofdefaultOpenandopenprops.
- Removed
-
Pagination
api.setCountis removed in favor of explicitly setting thecountprop.
-
Select, Combobox
api.setCollectionis removed in favor of explicitly setting thecollectionprop.
Performance
We measured the mount performance of 10k instances of each component, and compared the before and after.
Avatar
Result: ~27% faster mount time and ~99% faster update time
#Before
{phase: 'mount', duration: 1007.3000000119209} {phase: 'update', duration: 890.4000000357628}
#After:
{phase: 'mount', duration: 736.9999999403954} {phase: 'update', duration: 1.899999976158142}
Accordion
Result: ~61% faster mount time and no update time
Before
{phase: 'mount', duration: 2778.4999997913837} {phase: 'update', duration: 2.3000000715255737}
After
{phase: 'mount', duration: 1079.0000001490116}
Collapsible
Result: ~65% faster mount time and no update time
Before
{phase: 'mount', duration: 834.4000000357628} {phase: 'update', duration: 2.1999999284744263}
After
{phase: 'mount', duration: 290.3000001013279}
Dialog
Result: ~80% faster mount time and no update time
Before
{phase: 'mount', duration: 688.9000000357628} {phase: 'update', duration: 2.0000000298023224}
After
{phase: 'mount', duration: 135.50000008940697}
Editable
Result: ~56% faster mount time and no update time
Before
{phase: 'mount', duration: 1679.500000089407} {phase: 'update', duration: 2.0000000298023224}
After
{phase: 'mount', duration: 737.5999999940395}
Tooltip
Result: ~82% faster mount time and no update time
Before
{phase: 'mount', duration: 797.7999999821186} {phase: 'update', duration: 2.5999999940395355}
After
{phase: 'mount', duration: 139.9000000357628}
Presence
Result: ~64% faster mount time and eliminated update time
Before
{ phase: "mount", duration: 1414 } { phase: "update", duration: 0 }
After
{ phase: "mount", duration: 502 }
Tabs
Result: ~6% faster mount time
Before
{ phase: "mount", duration: 4120 } { phase: "update", duration: 2014 }
After
{ phase: "mount", duration: 3880 } { phase: "nested-update", duration: 3179 }
Bundle Size
We've made significant strides in reducing the bundle size of the overall library. The core package powers all components. It is now less than 2KB minified, a whopping 98% reduction in size.
Before: 13.78 KB
After: 1.52 KB
Contributors Notes
-
activitiesis now renamed toeffects -
prop,contextandrefsare now explicitly passed to the machine. Prior to this everything was pass to thecontextobject which was quite expensive (performance wise). -
The syntax for
watchhas changed significantly, refer to the new machines to learn how it works. It is somewhat similar to howuseEffectworks in react. -
createMachineis just an identity function, it doesn't do anything. The machine work is now moved to the frameworkuseMachinehook.
Thank you
We'd like to thank the following contributors for their help in making this release possible:
-
Segun Adebayo for leading the charge and making such engineering feats possible.
-
Christian Schroeter for providing valuable feedback and suggestions to improve the library.
Edit this page on GitHub