Ariakit
/

Component stores

Access and manipulate the state of Ariakit components in a performant way through component stores.

Overview

Component stores are lower-level React hooks exported by the @ariakit/react package. They allow you to read and write the state of Ariakit components. With them, you can control the value of a Combobox, the active item of a Menu, the open state of a Dialog, or the values in a Form, for example.

You instantiate the store by calling the component store hook within a React component:

function MyCombobox() {
const combobox = useComboboxStore();
}

Then, you should pass the store to the wrapping components of that module:

<Combobox store={combobox} />
<ComboboxPopover store={combobox}>
<ComboboxItem value="Apple" />
<ComboboxItem value="Banana" />
<ComboboxItem value="Orange" />

Passing the store to the child components, such as ComboboxItem, is optional.

Providing state to the store

Component stores accept an optional object as an argument. This object is used to initialize and control the state of the component.

Default state

Conventionally, when dealing with dynamic state, the initial value passed to the store has its property name prefixed with the word default. In this case, only the initial value will be considered. It doesn't have to be referentially stable between re-renders.

const form = useFormStore({
defaultValues: { name: "", email: "" },
});

State setters

Component stores may also accept callbacks for state changes. These functions conventionally bear the name of the state property they modify, prefixed with the word set. They are invoked with the new state whenever an update occurs.

These state setters serve various purposes, such as updating another state, executing side effects, or implementing features like onChange, onValuesChange, onToggle, onOpenChange, and so on.

const form = useFormStore({
defaultValues: { name: "", email: "" },
setValues(values) {
console.log(values);
},
});

Controlled state

You can take full control of the state by passing the exact property, without prefixes, to the store. In this case, the state will be considered controlled and the component will not update the state internally. It will only call the state setter. You can use this to implement a controlled component using React.useState:

const [values, setValues] = React.useState({ name: "", email: "" });
const form = useFormStore({ values, setValues });

You can also receive controlled props, such as value and onChange, from a parent component and pass them directly to the store:

const select = useSelectStore({
value: props.value,
setValue: props.onChange,
defaultValue: props.defaultValue,
});

Reading the state

Component stores in the @ariakit/react package expose a useState method. It's a custom React hook that you can use to read the state in a performant way.

Watching the entire state

Calling store.useState() without any arguments returns the entire state object and re-renders the component whenever the state changes.

function MyCombobox() {
const combobox = useComboboxStore();
const state = combobox.useState();
console.log(state.value);
}

Watching a specific state property

Alternatively, you can pass a string to store.useState() to read the value of a specific state property. The component will only re-render when the requested value changes.

const value = combobox.useState("value");
const isOpen = combobox.useState("open");

Computed values

Finally, store.useState() accepts a selector function as an argument. The function will receive the state as a parameter and should return a value, which can be computed inside the function body.

You're free to use other variables within this function. The selector will be called whenever the state is updated and on every render, but the component will only re-render when the returned value changes.

function MyComboboxItem({ store, id }) {
// This component will only re-render when isActive becomes true or false,
// rather than on any activeId change.
const isActive = store.useState((state) => state.activeId === id);
}

Reading the state on events

If you're reading the state inside an event handler, you don't need to use store.useState(). You can read the current state directly from the store using store.getState(), which won't trigger a re-render on the component.

const combobox = useComboboxStore();
function handleKeyDown(event) {
const { value } = combobox.getState();
console.log(value);
}

Writing the state

Component stores have a generic setState method that can be used to mutate any state in the store. This method shouldn't be called during render, but it's safe to call it inside an event handler or React effect callbacks.

const dialog = useDialogStore({ defaultOpen: false });
function onClick() {
dialog.setState("open", true);
}

The second parameter of store.setState() can be either the new state value or a function that receives the current state and returns the new state. This is useful when you need to update the state based on a previous value.

dialog.setState("open", (open) => !open);

Component stores may also expose specific methods to update the state. These methods are named after the state property they update. For example, store.setOpen() updates the open state.

const dialog = useDialogStore({ defaultOpen: false });
function onClick() {
// Equivalent to dialog.setState("open", true);
dialog.setOpen(true);
}

Like store.setState(), these methods can also receive a function:

dialog.setOpen((open) => !open);

For consistency, all the state setters that can be passed to the store as an argument are also exposed as methods.

For convenience, component stores may also expose methods that perform specific state updates:

dialog.show(); // dialog.setOpen(true)
dialog.hide(); // dialog.setOpen(false)
dialog.toggle(); // dialog.setOpen((open) => !open)

Using React Context

When you need to access the store in child components, passing it as a prop is usually the most straightforward approach. However, if the component is deeply nested within the component tree or if you're unable to pass the store as a prop for some reason, you can leverage React Context instead:

import { useFormContext, FormInput } from "@ariakit/react";
function RequiredInput(props) {
const form = useFormContext();
if (!form) {
throw new Error("RequiredInput must be used within a Form component");
}
form.useValidate(() => {
if (!form.getValue(props.name)) {
form.setError(props.name, "This field is required");
}
});
return <FormInput {...props} />;
}

Follow updates

Join 1,000+ subscribers and receive monthly updates with the latest improvements on Guide.

Read latest issue

No Spam. Unsubscribe at any time.