Skip to main content
View Zag.js on Github
Join the Discord server

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.machine is no longer a function, it is passed directly to useMachine.

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 value prop.

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", })

Fixed

  • Menu: Fix issue where context menu doesn't update positioning on subsequent right clicks.

  • Avatar: Fix issue where api.setSrc doesn't work.

  • File Upload: Fix issue where drag-and-drop doesn't work when directory is true.

  • Carousel

    • Fix issue where initial page is not working.
    • Fix issue where pagination sync broken after using dots indicators.

Removed

  • General

    • Removed useActor hook in favor of useMachine everywhere.
    • Removed open.controlled in favor of defaultOpen and open props.
  • Pagination

    • api.setCount is removed in favor of explicitly setting the count prop.
  • Select, Combobox

    • api.setCollection is removed in favor of explicitly setting the collection prop.

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

  • activities is now renamed to effects

  • prop, context and refs are now explicitly passed to the machine. Prior to this everything was pass to the context object which was quite expensive (performance wise).

  • The syntax for watch has changed significantly, refer to the new machines to learn how it works. It is somewhat similar to how useEffect works in react.

  • createMachine is just an identity function, it doesn't do anything. The machine work is now moved to the framework useMachine hook.

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

Proudly made in🇳🇬by Segun Adebayo

Copyright © 2025
On this page