# State and refresh

> Why widgets hold no values, when the library re-reads your suppliers, and the few things that do live inside built widgets.

URL: https://xichil.dev/docs/xis-mvui/v1/concepts/state-and-refresh/ · Updated: 2026-11-06 · Applies to: Fabric, Quilt, Legacy Fabric, Forge, NeoForge, MC 1.7.10 - 26.3

Widgets don't hold your values. They **read** them through suppliers and **write** changes back through
callbacks, and the library re-reads every supplier whenever the screen refreshes. Once that is clear, every
widget behaves predictably.

## The mod owns the state

```java
Toggle.builder("sounds")
        .label(Text.translatable("mymod.sounds", "Sounds"))
        .value(() -> config.sounds)          // read: what the toggle shows
        .onChange(v -> config.sounds = v)    // write: what a click asks for
        .build();
```

A click doesn't flip the toggle. It calls `onChange` with the opposite value, and then the screen shows
whatever `value` returns. Two things follow:

- An `onChange` that ignores the new value (or refuses it) leaves the widget unchanged.
- Anything else that changes `config.sounds` (another button, a `runAsync` callback) shows up on the toggle at
  the next refresh, with no extra code.

## When suppliers are re-read

The backend re-reads every supplier of the current screen:

1. when the screen opens,
2. after every entry action (a press, toggle, edit, selection…),
3. after every `runAsync` callback,
4. when you call `host.refresh()`.

Call `refresh()` yourself only when state changes outside those, for example inside a `runOnMainThread` task.

What counts as a supplier: labels and titles, values, `enabled`, `visible`, tooltips, `SelectList` rows,
`StringListEditor` items, and validators.

## Static or dynamic

Every supplier setter has a constant overload. Use the constant when it never changes:

```java
.label(Text.translatable("mymod.volume", "Volume"))                                // fixed
.label(() -> Text.translatable("mymod.volume.n", "Volume: %s", config.volume))     // re-read
.enabled(false)                                                                    // greyed out for good
.enabled(() -> config.sounds)                                                      // follows the toggle
```

## Keep suppliers fast and pure

Suppliers run on the main thread, on every refresh, so:

- No IO, no network, no heavy computation. Load data once (or with `runAsync`) and let the supplier read the
  loaded copy.
- No side effects. A supplier may run any number of times.
- Don't throw. A supplier that throws turns your menu off for the rest of the session
  ([Failure isolation](https://xichil.dev/docs/xis-mvui/v1/concepts/failure-isolation/index.md)).

## What lives in the widget

A few kinds of state belong to the built widget, not to your code:

| State | Where | Survives a refresh | Survives reopening the menu |
| --- | --- | --- | --- |
| typed text of `TextInput`, `NumberField`, `MultiLineInput` | the entry | yes | no: starts again from `initial` |
| a `Section`'s expanded state | the section | yes | no: starts from `expanded(…)`; remember it with `onToggle` |
| a `SelectList`'s selection | the list | yes, while its row exists | no |
| a `StatusLine`'s message | the status line | yes | no |

Typed text lives in the entry so that invalid input survives refreshes while the player fixes it; `onChange`
only ever receives valid text.

This is also why you build screens fresh in `create` and in button actions. A cached screen would carry the
last opening's typed text, sections and selection.

## Visibility

`visible(…)` decides whether an entry has a row at all. A hidden entry takes no space (the rows below move up),
none of its other suppliers are read, and it can't be used. A section's children are visible only while the
section is visible **and** expanded.

`enabled(…)` is different: a disabled widget keeps its row, shows its label, value and tooltip, and is drawn
greyed out.

## Next steps

- [Threads](https://xichil.dev/docs/xis-mvui/v1/concepts/threads/index.md): where suppliers and callbacks run.
- [Validate what players type](https://xichil.dev/docs/xis-mvui/v1/guides/validation/index.md): the typed-text exception in practice.
