In this post, we will take a look at Zag, a JavaScript library that employs the state machine approach to represent common component state patterns.
Using Zag allows you to create a design system with declarative, DRY, and simple state management logic by outsourcing most of its complexity to the library.
- Intro to Zag
- Why use Zag?
- What are state machines?
- Using out-of-the-box Zag state machines
- Building your own state machine
Intro to Zag
There’s an abundance of out-of-the-box UI libraries out there. Yet we often can’t use them because of the unique design requirements we must follow.
And while our components’ design may be unique, the functionality often isn’t. When creating your component from the ground up, you’ll have to write your state logic, reinventing the wheel in the process.
Enter Zag, a library that takes care of the component state logic for you so you can focus on making your components look great and leave the inner workings of their state to the state machines.
Zag provides state machines for the most common UI components, such as Menu, Accordion, Dialog, etc. You can find a comprehensive list of all the available state machines in their docs.
If you’re building your design system from scratch or have a project with lots of components that contain overlapping, yet slightly different logic, using Zag can save you time and headache.
For example, the same state machine can be used for both vertical and horizontal menus. Doing so allows you to share common state logic between components, keeping your design system DRY.
Why use Zag?
Now let’s cover why Zag might be the state machine solution for you:
- First of all, Zag is framework agnostic, so it works with React, Angular, Vue, or even vanilla JavaScript. Still, it does provide adapters for React, Solid, and Vue to make adoption easier if you happened to be using those frameworks
- Zag is completely unopinionated about how you style your components. You can follow whatever processes and workflows you’re used to. This is in contrast to many other UI libraries that come with their styling solutions that you have to learn and adopt
- You can introduce Zag to your project incrementally, adding state machines as you need them. That’s possible because each state machine is available as a separate NPM package
- The library is built with accessibility in mind and handles accessibility concerns like keyboard interactions, focus management, and aria roles for you
What are state machines?
To better appreciate the benefits of Zag and properly use the library, we need to understand the concept of state machines.
A state machine, also called a finite state machine, is a mathematical model of computation. It’s an abstract machine that can be in one of a finite number of states. The machine is in only one state at a time and can change from one state to another when triggered by an input (called an event).
In the React community, the state machines have been popularized by XState. They are often used to represent the logic of common components with complex behavior.
State machines are a natural fit for UI components because they allow you to model the different states that a component can be in and the events that can trigger a state change.
Using out-of-the-box Zag state machines
Now that we covered Zag and state machines, let’s see how we can use them in our project.
Sample usage example
Let’s try using a state machine for menus, one of the most common UI components.
Here’s what the code for adding a state machine to a React component looks like:
import * as menu from "@zag-js/menu"; import { useMachine, useSetup } from "@zag-js/react"; export default function Menu({ onSelect }: { onSelect: (id: string) => void }) { const [state, send] = useMachine( menu.machine({ onSelect: (id) => onSelect(id) }) ); const ref = useSetup({ send, id: "1" }); const api = menu.connect(state, send); return ( <div ref={ref}> <button {...api.triggerProps}> Actions <span aria-hidden>▾</span> </button> <div {...api.positionerProps}> <ul {...api.contentProps}> <li {...api.getItemProps({ id: "records" })}>Records</li> <li {...api.getItemProps({ id: "duplicate" })}>Duplicate</li> <li {...api.getItemProps({ id: "settins" })}>Settings</li> <li {...api.getItemProps({ id: "export" })}>Export...</li> </ul> </div> </div> ); }
In the code above, we’re using the useMachine
Hook to create a new instance of the state machine. The onSelect
callback that we pass will be triggered when an item is selected and will receive its id
.
Then, we call the useSetup
hook with an object that contains the id
of our menu and the send function from our state machine. useSetup
ensures that the component works in different environments (iframes, Electron, etc.). The function returns a ref
that we add to the root element of our component.
Note: id
needs to be a unique identifier.
Finally, we call the menu.connect
function with our state machine’s state and the send
function. connect
translates the machine’s state into JSX attributes and event handlers.
At this point, our state machine is ready to be used, and now we need to apply the JSX data stored in the api
variable.
The api
contains state logic for all of the inner components that make up a menu: trigger, positioner, content, and menu items. To apply the state machine logic to our HTML elements, we use the spread operator syntax.
And that’s all there’s to it. Now we have a functioning (although not the best looking) menu.
Styling
Zag is unopinionated about styling, and you have more control over how you want to style your components. You can use whatever CSS libraries you want or write your styles.
Each component usually has multiple parts that you can style separately. As we discussed before, the menu component has the following parts you can style: trigger, positioner, content, and menu items.
Zag automatically inserts the data-part
attribute to your component’s parts that you can use to target them for styling.
For example, here’s what a simplified HTML output of our menu component would look like:
<!--HTML--> <div> <button data-part="trigger" id="menu:1:trigger"> Actions <span aria-hidden="true">▾</span> </button> <div data-part="positioner" id="menu:1:popper"> <ul data-part="content" id="menu:1:content"> <li data-part="item" id="records"> Records </li> <li data-part="item" id="duplicate"> Duplicate </li> <li data-part="item" id="settins"> Settings </li> <li data-part="item" id="export"> Export... </li> </ul> </div> </div>
As you can see, each part has the data-part
attribute that can be used in CSS selectors.
For example, if we want to change the color of our menu items, we can write the following CSS:
[data-part="item"] { color: blue; }
When a component enters a certain state, Zag automatically adds an HTML
attribute to represent the current state of the component using data-ATTRIBUTE_NAME
, where ATTRIBUTE_NAME
is an attribute that represents the current state in the state machine.
For example, if a menu item is in a disabled state, you can target it in your CSS styles using:
[data-part="item"][data-disabled] { /* styles go here */ }
And that covers the basics of styling Zag components.
Adding custom event handlers
Typically you would add your event handlers whenever creating an instance of your machine, as we saw with our earlier menu example:
const [state, send] = useMachine( menu.machine({ onSelect: (id) => onSelect(id) }) );
However, if you want to add custom event handlers to specific parts of the component, you can do that as well using the mergeProps
utility function Zag provides:
const handleClick = () => { // do something here } const buttonProps = mergeProps(api.buttonProps, { onClick: handleClick, })
Building your own state machine
Now and then, you might run into a case that’s not covered by state machines provided by Zag. In that case, you can build your own state machine from scratch using Zag’s createMachine
function.
Defining your state machine model
Creating a state machine involves defining all the possible states and transitions for your component. For example, let’s say we want to create a machine that represents a simple on/off toggle button:
const machine = createMachine({ // initial state initial: "active", // the finite states states: { active: { on: { CLICK: { // go to inactive target: "inactive" } } }, inactive: { on: { CLICK: { // go to active target: "active" } } } } });
As you can see, we first defined the initial state of our machine. Then, for each state, we specified which events should trigger a transition to another state.
In this case, we only have one event (CLICK
) that can happen in both states and it will transition to the other state.
Creating connector function
Once we have our machine, we need to create a connector function that will take care of mapping the machine’s state to JSX props.
The connector function needs state
and send
arguments so it can access the machine’s current state and send events to the machine.
Here’s what the connector function for our on/off toggle button would look like:
function connect(state, send) { const active = state.matches("active"); return { active, buttonProps: { type: "button", role: "switch", "aria-checked": active, onClick() { send("CLICK"); } } }; }
Unfortunately, there’s no way to write one connect function that would work for all machines. You’ll have to create a connector function specific to each machine since the shape of the data in each machine’s state is different.
Connecting your components to your state machine
Finally, we need to instantiate our machine and use our connector to apply the machine’s state to our component:
import { useMachine } from "@zag-js/react"; import { machine, connect } from "./toggle"; function Toggle() { const [state, send] = useMachine(machine); const api = connect(state, send); return <button {...api.buttonProps}>{api.active ? "ON" : "OFF"}</button>; }
And that’s all there is to it.
Conclusion
In conclusion, using state machines and Zag to build your design system has a lot of benefits. State machines make your code more declarative and predictable.
Zag provides out-of-the-box state machine solutions for common use cases, allowing you to concentrate on the design of your components while outsourcing the state management to the library.
The post How to make your design system DRY with Zag appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/GuYWh7N
via Read more