Files
atomic-design-poc/libs/shared/docs/fp-in-ui.mdx
T
ehoandClaude Sonnet 5 43dc3210cd refactor: move libs/shared/src/ui/ into atoms/molecules/organisms (RD-27)
The folder now equals the layer, as CLAUDE.md decision 2 requires. 33
directories move by git mv (25 flat, plus upload/'s 8 subfolders split
across all three layers). 28 distinct @shared/ui/* specifiers rewrite
across 73 files, longest-first. Five relative imports inside upload/
become @shared/ui aliases because their sibling now lives in a
different layer; two stay relative because both ends stay in the same
layer. Four .mdx docs get their seven broken story imports fixed;
atomic-design.mdx's page-shell import is untouched, because layout/
does not move.

No component, template, story title, or layer-tag comment changes.
That is RD-28's job.

Verified against the ticket's acceptance commands: the 26 flat
directories become exactly 3 layer folders with the counts the ticket
names, only three @shared/ui/* prefixes remain (atoms, molecules,
organisms), the .mdx import count holds at 7, and the relative-import
count inside ui/ drops from 7 to 2 as decision 4 requires. The
@shared/ui/ occurrence count moves from 200 to 205: decision 4
mandates turning 5 of those 7 relative imports into @shared/ui/*
aliases, which decision 3's "200 before, 200 after" check does not
account for. The 5-occurrence gap is exactly the 5 conversions decision
4 names, not a lost or duplicated specifier.

npm run ci --full passes: lint, typecheck, dep:check, format, tokens,
seam, both apps' + both libraries' tests, both apps' localized build,
audit, backend tests, all three generated-artifact drift checks, and
both Storybook instances' build + axe-core a11y suite (67+45 suites,
198+112 tests, all green).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 08:14:10 +02:00

64 lines
2.8 KiB
Plaintext

import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as AsyncStories from '../src/ui/molecules/async/async.stories';
<Meta title="Foundations/FP in the UI" />
# Functional programming in the UI
The components in this library are the _view_. Behind them, three small functional tools do
the heavy lifting — all so that **illegal states can't be represented**. This page is the
Storybook front door; the full narrative lives in `docs/reference/fp-tea-atomic-design.md`, and a
side-by-side "before/after" runs at the app's **`/concepts`** route.
## 1. `RemoteData<E,T>` — async has four states, not a boolean soup
`src/app/shared/application/remote-data.ts`. Instead of juggling `loading`, `error`, and
`data` flags (which permit "loading **and** error" nonsense), one tagged union:
`Loading | Empty | Failure | Success`. You combine sources with `map`/`map2`/`andThen` and
render it through the `async` molecule — exactly one of four templates shows, by
construction:
<Canvas of={AsyncStories.Loading} />
<Canvas of={AsyncStories.ErrorState} />
## 2. The Elm-style store — all state in one Model, changed only by pure `reduce`
`src/app/shared/application/store.ts` + the `*.machine.ts` files. State is one tagged-union
value; the template never mutates it, it `dispatch`es a message and a **pure**
`reduce(model, msg)` returns the next state. Side effects live in a _command_, never in the
reducer:
```ts
// reducer = "what the new state is" — pure, testable, no I/O
function reduce(model: Model, msg: Msg): Model { … }
// command = "go do it, then say what happened"
async function submit(...) {
const res = await http(...);
dispatch(res.ok ? { tag: 'Submitted' } : { tag: 'Failed', error: res.error });
}
```
Because state is one value, the whole thing is inspectable and every transition has a spec.
## 3. Parse, don't validate — raw input becomes a branded type once
`src/app/registratie/domain/value-objects/`. A `Postcode` is a distinct type from `string`,
mintable only through `parsePostcode`, which returns a `Result`. Once you hold the type, you
never re-check it — the type _is_ the proof. Compose the parse pipeline with the `Result`
combinators in `src/app/shared/kernel/fp.ts` (`map`, `mapErr`, `andThen`, `fold`) rather than
hand-branching `r.ok ? … : …` at every step.
```ts
parsePostcode(raw) // Result<string, Postcode>
|> mapErr(toLocalizedMessage) // swap raw msg → UI copy
|> map(toDomain) // only runs on success
```
## How it connects to atomic design
Atoms and molecules are pure view functions of their inputs; pages are the TEA runtime (the
"shell") that holds the store and wires effects. Same inward-pointing discipline as the
[layer rule](?path=/docs/foundations-atomic-design--docs), applied to state and effects
instead of imports.