Browse docs

React

Units

ReactVanilla

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.

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

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

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:

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:

useFormatter({ units: { length: 'mile' } })

A preference UI

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