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:

text
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

text
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
                                                  UI

The 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.

typescript
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.

text
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

CapabilityModule filePage
ApiClient (cache/tags), mapperspokemon.api.tsapi-client
storage + selectors, minimal createSynapsepokemon.store.ts, pokemon.selectors.tscreate-synapse-basic
dispatcher (action/signal/apiActions/watcher)pokemon.dispatcher.tscreate-synapse-dispatcher, dispatcher-detailed
effects (validateMap/apiResult/fromRequest)pokemon.effects.tscreate-synapse-effects
dependencies (settingsStorage, async factory)pokemon.settings.ts, pokemon.synapse.tsdependencies
React: manual lift / providerPokemonAdvancedExample.tsxawait-synapse, synapse-ctx
framework-independent awaiter, SSR fast-pathsynapse-awaiter
event bus between modulesevent-bus
text