{"id":13565898,"url":"https://github.com/thomashoneyman/purescript-halogen-store","last_synced_at":"2026-03-06T20:32:02.761Z","repository":{"id":40557361,"uuid":"364746029","full_name":"thomashoneyman/purescript-halogen-store","owner":"thomashoneyman","description":"Global state management for Halogen","archived":false,"fork":false,"pushed_at":"2026-01-16T02:54:50.000Z","size":57,"stargazers_count":44,"open_issues_count":4,"forks_count":8,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-01-16T16:36:57.098Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PureScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thomashoneyman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["thomashoneyman"]}},"created_at":"2021-05-06T01:00:54.000Z","updated_at":"2025-05-26T11:05:47.000Z","dependencies_parsed_at":"2023-02-15T05:00:59.803Z","dependency_job_id":null,"html_url":"https://github.com/thomashoneyman/purescript-halogen-store","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/thomashoneyman/purescript-halogen-store","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-store","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-store/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-store/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-store/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomashoneyman","download_url":"https://codeload.github.com/thomashoneyman/purescript-halogen-store/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-store/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30196173,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"ssl_error","status_checked_at":"2026-03-06T18:57:34.882Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T13:01:57.432Z","updated_at":"2026-03-06T20:32:02.731Z","avatar_url":"https://github.com/thomashoneyman.png","language":"PureScript","funding_links":["https://github.com/sponsors/thomashoneyman"],"categories":["PureScript"],"sub_categories":[],"readme":"# Halogen Store\n\n[![CI](https://github.com/thomashoneyman/purescript-halogen-store/workflows/CI/badge.svg?branch=main)](https://github.com/thomashoneyman/purescript-halogen-store/actions?query=workflow%3ACI+branch%3Amain)\n[![Latest release](http://img.shields.io/github/release/thomashoneyman/purescript-halogen-store.svg)](https://github.com/thomashoneyman/purescript-halogen-store/releases)\n[![Maintainer: thomashoneyman](https://img.shields.io/badge/maintainer-thomashoneyman-teal.svg)](http://github.com/thomashoneyman)\n\nGlobal state management for Halogen. For a Hooks-first state management library, please see [Helix](https://github.com/katsujukou/purescript-halogen-helix).\n\n## Installation\n\nInstall `halogen-store` with Spago:\n\n```purs\nspago install halogen-store\n```\n\n## Quick Start\n\nThis library provides global state management for Halogen applications. A global or central state can help when many components need access to the same information, and threading those values through components via their inputs is either tedious or leads to an explosion of unnecessary fields in state.\n\nWriting applications with `halogen-store` comes down to three major steps, detailed in the next three sections:\n\n1. [Creating the store](#creating-the-store)\n2. [Using the store](#using-the-store)\n3. [Running the application](#running-the-application)\n\n### Creating the store\n\nFirst, we should create a central state for our application. This is called a \"store\" by convention.\n\n```purs\nmodule Basic.Store where\n\ntype Store = { count :: Int }\n\ninitialStore :: Store\ninitialStore = { count: 0 }\n```\n\nIn the same module we'll create an action type that represents an update to our store. This action type is similar to the action type you define in each of your Halogen components.\n\n```purs\ndata Action = Increment | Decrement\n```\n\nFinally, we should create a reducer: a function of type `store -\u003e action -\u003e store` that updates our central state when it receives an action. It's somewhat similar to the `handleAction` function you define in your Halogen components, but it can't perform effects.\n\n```purs\nreduce :: Store -\u003e Action -\u003e Store\nreduce store = case _ of\n  Increment -\u003e store { count = store.count + 1 }\n  Decrement -\u003e store { count = store.count - 1 }\n```\n\nIf you need to perform effects before or after updating the central state, then you can do that in the Halogen component which is performing the update.\n\nAs a brief aside: actions introduce some boilerplate to your application. If you want to stay bare-bones, then you can define your action type as a function `Store -\u003e Store`, and then you can implement your reducer as function application:\n\n```purs\ntype Action = Store -\u003e Store\n\nreduce :: Store -\u003e Action -\u003e Store\nreduce store k = k store\n```\n\nThis lets you write arbitrary `store -\u003e store` functions and send them to your central state.\n\n### Using the store\n\nWe can now use our store in our Halogen components. A component with access to the central store is called a \"connected\" component; a connected component can read, update, and subscribe to the store.\n\nA connected component requires a `MonadStore` constraint which specifies the store, action, and underlying monad types. We already defined our store and action types in the `Basic.Store` module, so we can reuse that in our component definition:\n\n```purs\nimport Basic.Store as BS\nimport Halogen.Store.Monad (class MonadStore)\n\ncomponent\n  :: forall q i o m\n   . MonadStore BS.Action BS.Store m\n  =\u003e H.Component q i o m\n```\n\nThe `MonadStore` class provides three methods:\n\n1. `getStore` retrieves the current value of the store.\n2. `updateStore` applies an action to the store to produce a new store, using our reducer.\n3. `emitSelected` produces an `Emitter` from the `halogen-subscriptions` library that will notify subscribers of the store's new value when it changes.\n\nWe can now use these methods anywhere we write `HalogenM` code -- for instance, in our `handleAction` function:\n\n```purs\nimport Basic.Store as BS\nimport Halogen.Store.Monad (updateStore)\n\nhandleAction = case _ of\n  Clicked -\u003e\n    -- This will increment our central store's count.\n    updateStore BS.Increment\n```\n\nIn practice it's common to send actions to the store with `updateStore`, but it's somewhat rare to use `getStore` or `emitSelected`. That's because there's an easy way to subscribe a component to the store and always keep its state in sync with the central store: the `connect` function.\n\nA component that uses `connect` function will receive the central state as part of its component input. That means it can use the central state with `initialState` and stay subscribed to all future state updates via the receiver. This is the easiest way to stay in sync with the store over time.\n\nFor example, the component below will receive the store's current value when it initializes and will receive the store's new value each time it changes:\n\n```purs\nimport Basic.Store as BS\nimport Data.Maybe (Maybe(..))\nimport Halogen as H\nimport Halogen.Store.Connect (Connected, connect)\nimport Halogen.Store.Select (selectAll)\n\ntype Input = Unit\n\ntype State = { count :: Int }\n\nderiveState :: Connected BS.Store Input -\u003e State\nderiveState { context, input } = { count: context.count }\n\ndata Action\n  = Receive (Connected BS.Store Input)\n\ncomponent\n  :: forall query input output m\n   . MonadStore BS.Action BS.Store m\n  =\u003e H.Component query input output m\ncomponent = connect selectAll $ H.mkComponent\n  { initialState: deriveState\n  , render: \\{ count } -\u003e ...\n  , eval: H.mkEval $ H.defaultEval\n      { handleAction = handleAction\n      , receive = Just \u003c\u003c\u003c Receive\n      }\n  }\n  where\n  handleAction = case _ of\n    Receive input -\u003e\n      H.put $ deriveState input\n```\n\nIn the real world we can't afford to update every connected component any time the central state changes; this would be incredibly inefficient. Instead, we want to only updated connected components when the bit of state they are concerned with has changed.\n\nWe can use a `Selector` to retrieve part of our central state and only be notified when the state we've selected has changed. In the previous example we used `selectAll` to just grab the entire store, but usually we'd write our own selector.\n\nImagine that our store actually contained dozens of fields in addition to the `count` field we've implemented, but we only want to subscribe to that field. Let's do that by adjusting our component from the last section.\n\n```purs\nimport Halogen.Store.Select (Selector, selectEq)\n\n-- We are no longer connected to the entire store; we're only connected to\n-- the `count` field, which is of type `Int` for our new context.\ntype Context = Int\n\nderiveState :: Connected Context Input -\u003e State\nderiveState { context, input } = { count: context }\n\nselectCount :: Selector BS.Store Context\nselectCount = selectEq \\store -\u003e store.count\n\ndata Action\n  = Receive (Connected Context Input)\n\ncomponent\n  :: forall query input output m\n   . MonadStore BS.Action BS.Store m\n  =\u003e H.Component query input output m\ncomponent = connect selectCount $ H.mkComponent\n  { initialState: deriveState\n  , ...\n  }\n```\n\nNow, even if other fields in our state are regularly changing, this component will only receive new input when the `count` field has changed.\n\n### Running the application\n\nWhen we run our application we'll need to satisfy our `MonadStore` constraints. Halogen components must always be run using the `Aff` monad, but our application needs to use a monad that supports `MonadStore`.\n\nTo solve this issue, we can use the `StoreT` transformer as the monad for our application, and then use `runStoreT` to transform it into `Aff`. (You're also welcome to define your own application monad, though I'd recommend defining it in terms of `StoreT`.)\n\nWe don't need to explicitly use `StoreT` in our component types; all we need to do is call `runStoreT` and supply an initial store, our reducer, and the component that requires the store. Let's see it in action:\n\n```purs\nmodule Main where\n\nimport Prelude\n\nimport Basic.Counter as Counter\nimport Basic.Store as BS\nimport Effect (Effect)\nimport Effect.Aff (launchAff_)\nimport Halogen.Aff as HA\nimport Halogen.Store.Monad (runStoreT)\nimport Halogen.VDom.Driver (runUI)\n\nmain :: Effect Unit\nmain = launchAff_ do\n  body \u003c- HA.awaitBody\n  root \u003c- runStoreT BS.initialStore BS.reduce Counter.component\n  runUI root unit body\n```\n\n### Using `halogen-store` with `halogen-hooks`\n\nIf you want to write your component with [Halogen Hooks](https://github.com/thomashoneyman/purescript-halogen-hooks) ,then you can use the `useSelector` hook to access the store.\n\n```purs\nmodule Main where\n\nimport Prelude\n\nimport Halogen.Hooks as Hooks\nimport Halogen.Store.Select (selectAll)\nimport Halogen.Store.UseSelector (useSelector)\n\ncomponent\n  :: forall q i o m\n   . MonadStore BS.Action BS.Store m\n  =\u003e H.Component q i o m\ncomponent = Hooks.component \\_ _ -\u003e Hooks.do\n  context \u003c- useSelector selectAll\n  Hooks.pure do\n    ...\n```\n\nUnlike `connect`, the context returned by `useSelector` has the type `Maybe store` because the hook does not have access to the store before it is initialized.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-store","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-store","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-store/lists"}