---
id: react/units
title: Units
---

Units change **how a number renders**, never which string is selected — that is a context-param job. If a value would appear in a message branch (`{gender, select, …}`) it's a param; if it goes to a number formatter it's a unit. Declaring categories: [stift.config.ts](../configuration/config-file.md).

## Value contract: store in the base unit

| Category | Base |
|---|---|
| length | `meter` |
| mass | `kilogram` |
| area | `hectare` |
| temperature | `celsius` |
| digital | `byte` |

Data and messages carry base-unit numbers; the library converts base → active unit at render. Never branch messages on a unit system and never convert in the message.

## Reading and setting units

```tsx
const { units, setUnit, resetUnit, resetAllUnits, getUnit, getUnitValues } = useUnits()

units.length // effective active unit for the current locale (or undefined)
getUnit('length') // the global override, if one exists (else undefined)
getUnitValues('length') // allowed units — config supportedValues

setUnit('length', 'kilometer') // global override; validated against supportedValues
resetUnit('length') // back to the per-locale default
resetAllUnits() // every category back to per-locale defaults
```

`units` is the **effective** per-category record for the active locale (per-locale defaults + global `setUnit` overrides), reactive. `setUnit` validates — an invalid unit is rejected and reported via the runtime hooks. Only `setUnit` overrides are persisted (`unitsPersistence`); defaults come from config. Unconfigured categories are invisible: no `units` entry, `setUnit` rejected, empty `getUnitValues`.

## Formatting

```tsx
const f = useFormatter()
f.formatLength(1000) // 1000 meter → active length unit, converted + localized
f.formatMass(2.5) // 2.5 kilogram → active mass unit
f.formatArea(3) // 3 hectare → active area unit
f.formatTemperature(25) // 25 celsius → active temperature unit
f.formatDigital(8 * 1024 ** 3) // bytes → active digital unit
```

Per-call overrides:

```tsx
f.formatLength(1000, { unit: 'mile', unitDisplay: 'short' }) // "621.4 mi"
f.formatMass(2.5, { unit: 'pound' }) // "5.5 lb"
```

`UnitFormatOptions = { unit?: string; unitDisplay?: 'long' | 'short' | 'narrow' }`. Values are always in the category base unit. Subtree-level overrides sit below per-call, above the active unit:

```tsx
useFormatter({ units: { length: 'mile' } })
```

## A preference UI

```tsx
function UnitPicker() {
  const { units, setUnit, resetUnit, getUnitValues } = useUnits()
  const values = getUnitValues('length') // typed: readonly RegisteredUnits['length'][]
  return (
    <select
      value={units.length ?? ''}
      onChange={(e) => (e.target.value ? setUnit('length', e.target.value) : resetUnit('length'))}
    >
      {values.map((u) => (
        <option key={u} value={u}>
          {u}
        </option>
      ))}
    </select>
  )
}
```

`getUnitValues` gives exactly the allowed units, so the picker can't offer an invalid choice. Wire `unitsPersistence` in `createStift` and the preference survives reloads.

## Common mistakes

- Passing a converted value to `formatLength` (double conversion) — always pass base-unit values.
- Branching a message on a unit system (`{unitSystem, select, …}`) — units never select strings.
- Calling `setUnit` with a unit outside `supportedValues` — rejected.
- Reading `units.length` for a category that isn't configured — absent by design.
- Forgetting `unitDisplay` for compact UI — long labels can overflow.

## Next

- Config declaration rules → [stift.config.ts](../configuration/config-file.md)
- Number/date/list formatting → [Translating & Values](./translating.md)
