State management
The apps avoid a global state library. State falls into three categories, most
of it living in @enode/core.
1. Prefetched reference data — external stores
Caches like metrics, text-content, user-app-settings each use the same
pattern (packages/core/src/<domain>/):
- a module-level store (
store.ts:Map+ listener set +loadX/clearX), - read through
useSyncExternalStorehooks (hooks.ts), - an
<XLoader/>mounted in each app'slayout.tsx.
Rule: don't add React state for prefetched reference data — extend the store. These return DTOs stay read-only by design.
2. Training write path — domain models
The session / set / rep / measurement write path uses the domain models in
packages/core/src/training/:
models.ts—WorkoutSession,WorkoutSet,WorkoutRep,Measurement.- immutable set/session mutations.
- builds reps from sensor events.
Models map to wire DTOs via core's api/mappers (toSessionCreateDto) only
at upload time, inside api/.
3. Local UI / feature state — hooks
App feature views keep transient UI state in co-located use-* hooks and React
state. State resets use the render-time prev-value pattern, not setState in
useEffect.
Read path vs. write path (summary)
| Direction | Representation | Crosses to DTO… |
|---|---|---|
| Read (reference data) | return DTOs, read-only | n/a |
| Write (training) | training/ domain models | only inside core's api/, at upload |
TODO: the workout return-DTO side becomes a domain model when workout editing (editable items/blocks) is built — deferred, not rejected.