React
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.
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
setUnitwith a unit outsidesupportedValues— rejected. - Reading
units.lengthfor a category that isn't configured — absent by design. - Forgetting
unitDisplayfor compact UI — long labels can overflow.
Next
- Config declaration rules → stift.config.ts
- Number/date/list formatting → Translating & Values