Skip to main content

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 useSyncExternalStore hooks (hooks.ts),
  • an <XLoader/> mounted in each app's layout.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.tsWorkoutSession, 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)

DirectionRepresentationCrosses to DTO…
Read (reference data)return DTOs, read-onlyn/a
Write (training)training/ domain modelsonly 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.