Pokemon Advanced — the recipe: the whole data layer on PokeAPI
The final page of the chain. Every previous section dissected one brick on this same domain — here
they come together into one working module: ApiClient with caching → mappers → storage →
selectors → dispatcher → effects → createSynapse → React. This is the reference for how to split a
data-management layer into responsibility files and copy it into your own project.
Each section below links to the page where the corresponding brick is covered in detail.
Module structure
The whole domain lives in a single pokemon-advanced/ folder, one file = one responsibility:
pokemon-advanced/
pokemon.types.ts — domain types + request-state shape
pokemon.store.ts — initialState (store shape)
pokemon.settings.ts — external settings storage (a dependency)
pokemon.api.ts — ApiClient (endpoints, cache) + response mappers
pokemon.selectors.ts — derived values (class Selectors)
pokemon.dispatcher.ts — intents (class Dispatcher)
pokemon.effects.ts — side-effects on RxJS (class Effects)
pokemon.synapse.ts — assembly via createSynapse(factory)
index.ts — public exports
PokemonAdvancedExample.tsx / PokemonDemo.tsx — UI on top of the synapse
helpers.ts — small presentation utilities (typeColor)Data flow
UI (PokemonDemo)
│ store.actions.loadList() / selectPokemon(id) / setSearchQuery(q) / toggleFavorite(id)
▼
dispatcher (intents) ──► action$ ──► effects (RxJS)
│ apiActions/action/signal │ ofType → validateMap → fromRequest(api)
│ ▼
│ pokemon.api.ts (ApiClient + mappers)
│ │ apiResult(data) → mapListResponse / mapDetailsResponse
▼ ▼
└──────────► applyPokemonList / applyPokemonDetails / loadList.success ──► storage
│
selectors (filteredList, isLoading…) ◄┘
│ useSelector
▼
UIThe flow is one-way: the UI sends intents to the dispatcher, effects do side-effects and write the result to storage via actions, and selectors hand derived values back to the UI.
State holds both domain data and the request protocol (api.listRequest/detailsRequest with a
status). More on the request-state shape in create-synapse-effects.
export type ApiStatus = 'idle' | 'loading' | 'success' | 'error' | 'reset'
export interface ApiRequestState {
status: ApiStatus
error: string | null
}
export interface PokemonState {
api: {
listRequest: ApiRequestState
detailsRequest: ApiRequestState
}
pokemonList: PokemonBrief[]
offset: number
hasMore: boolean
selectedPokemonId: number | null
selectedPokemon: PokemonDetails | null
searchQuery: string
favorites: number[]
}pokemon.store.ts next to it is just initialState: PokemonState (both requests 'idle', lists empty).
The 5-state request protocol
The core of the dispatcher↔effects link: every request goes through a fixed lifecycle, and the UI
reads it through status selectors.
UI dispatch (loadList) -> status = 'idle' (no UI change)
|
effect: validateMap
|-- validation OK -> loadingAction -> status = 'loading' (spinner)
| |-- API OK -> apiResult(success) -> status = 'success' (data)
| \-- API ERR -> errorAction -> status = 'error' (error)
\-- validation FAIL -> skipAction -> status = 'reset' (no UI flicker)Map: capability → page
| Capability | Module file | Page |
|---|---|---|
| ApiClient (cache/tags), mappers | pokemon.api.ts | api-client |
| storage + selectors, minimal createSynapse | pokemon.store.ts, pokemon.selectors.ts | create-synapse-basic |
| dispatcher (action/signal/apiActions/watcher) | pokemon.dispatcher.ts | create-synapse-dispatcher, dispatcher-detailed |
| effects (validateMap/apiResult/fromRequest) | pokemon.effects.ts | create-synapse-effects |
| dependencies (settingsStorage, async factory) | pokemon.settings.ts, pokemon.synapse.ts | dependencies |
| React: manual lift / provider | PokemonAdvancedExample.tsx | await-synapse, synapse-ctx |
| framework-independent awaiter, SSR fast-path | — | synapse-awaiter |
| event bus between modules | — | event-bus |