{"id":23121897,"url":"https://github.com/pj/hacn","last_synced_at":"2025-08-17T01:32:04.815Z","repository":{"id":57114883,"uuid":"314400233","full_name":"pj/hacn","owner":"pj","description":"A \"monad\" or DSL for creating React components using Fable and F# computation expressions","archived":false,"fork":false,"pushed_at":"2023-04-25T01:01:32.000Z","size":1106,"stargazers_count":34,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-08T17:15:04.613Z","etag":null,"topics":["computation-expressions","fable","fsharp","monadic","react","react-hooks"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-20T00:06:07.000Z","updated_at":"2024-02-25T10:55:53.000Z","dependencies_parsed_at":"2024-11-13T16:41:32.580Z","dependency_job_id":"ec24b0be-2a27-4988-8eaa-d11eb616f115","html_url":"https://github.com/pj/hacn","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/pj/hacn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pj%2Fhacn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pj%2Fhacn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pj%2Fhacn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pj%2Fhacn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pj","download_url":"https://codeload.github.com/pj/hacn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pj%2Fhacn/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269606692,"owners_count":24446239,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-09T02:00:10.424Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["computation-expressions","fable","fsharp","monadic","react","react-hooks"],"created_at":"2024-12-17T07:17:26.780Z","updated_at":"2025-08-17T01:32:03.978Z","avatar_url":"https://github.com/pj.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Introduction\n\nHacn is a DSL for creating React components using Fable, an F# to Javascript compiler and F# computation expressions. It's intended to make it easy to write complex interactive components without using callbacks.\n\nIf you're familiar with functional programming languages like Haskell and Scala you can think of it as an attempt to write a React monad, though it probably isn't technically a monad. It draws inspiration from React Hooks, algebraic effects, Redux sagas and is similar in concept to crank js.\n\nIt's written on top of react to make it possible to easily integrate with existing components and potentially to integrate into existing projects.\n\nYou can see TodoMVC written in it [here](https://github.com/pj/hacn-todomvc)\n\n## Installation\n\nTo install into your F# project:\n\n```\ndotnet add package Hacn\n```\n\n## Usage\n\nHacn uses the type `Hacn.Types.Operation` to represent actions and effects that control the execution and rendering of a hacn react component. It's easiest to think of this as being like the `Promise` type in javascript, with some extras to handle things like rendering.\n\nIn the same was as async/await is used to combine promises in javascript, Hacn uses F# computation expressions to sequence operations. If you're not familar with how computation expressions work it might be helpful to read a [tutorial](https://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/) on async programming in F#, since Hacn shares some of the same concepts. \n\nUnlike async/await, control flow in a Hacn component isn't simply linear with one operation following another. Operations have state and can signal that they have changed causing all the operations after them to re-execute. Another way of thinking about this is as a kind of implicit goto.\n\nTo create a component you use the `react { ... }` expression builder syntax. Hacn components can be included in regular Fable React components:\n\n```fsharp\nmodule User\n\nopen Hacn.Core\nopen Hacn.Operations\n\nlet Element = \n  react {\n    let! props = Props\n    do! Render(\n      Html.div [\n        prop.text props.Message\n      ]\n    )\n  }\n\nlet User =\n  React.functionComponent(\n    fun props -\u003e \n      Html.div [\n        Element {Message = \"Hello\"}\n      ]\n  ) \n```\n\n`Props` and `Render` are operations and handle rendering, capturing results, props, state, timeouts and eventually things like fetches. They implement the `Perform` interface from the `Operation` typed union, see the section on writing operations below.\n\n## Operations\n\nBuilt in operations are currently defined in `Hacn.Operations`, work is still ongoing to document all these properly and expand them to include things like data fetching.\n\n### Props \n\nThe `Props` operation handles react props, basically it restarts the sequence of operations from the point where `Props` is used when props changes.\n\n```fsharp\nmodule Element\n\nopen Hacn.Core\nopen Hacn.Operations\n\ntype ElementProps = [\n  Message: string\n]\n\nlet Element = \n  react {\n    let! props = Props\n  }\n```\n\nProps uses a shallow comparison to compare fields, you can perform your own comparison by implementing an operation that uses the `PerformProps` interface. See the section on writing operations below.\n\n### Rendering and Capturing Values.\n\nHacn uses a library called Feliz for html, basic rendering is handled using the `Render` operation:\n\n```fsharp\nmodule Element\n\nopen Hacn.Core\nopen Hacn.Operations\n\nlet Element = \n  react {\n    do! Render(\n      Html.div [\n        prop.text \"Hello World!\"\n      ]\n    )\n  }\n```\n\nIt's also possible to capture dom events and \"return\" them from the `Render` operation into the sequence of events:\n\n```fsharp\nmodule Element\n\nopen Hacn.Core\nopen Hacn.Operations\n\nlet Element = \n  react {\n    let! newValue = Render(\n      Html.div [\n        prop.text value\n        prop.children [\n          Html.input [\n            prop.captureValueChange\n          ]\n        ]\n      ]\n    )\n    console.log newValue\n  }\n```\n\nCapture can also be performced manually using the `RenderCapture` operation.\n\n```fsharp\nmodule Element\n\nopen Hacn.Core\nopen Hacn.Operations\n\nlet Element = \n  react {\n    let! newValue = RenderCapture(\n      fun capture -\u003e \n        Html.div [\n          prop.text value\n          prop.children [\n            Html.input [\n              prop.onChange (\n                fun (keyEvent: Browser.Types.Event) -\u003e \n                  let inputElement = box keyEvent.target :?\u003e HTMLInputElement\n                  capture (inputElement.value)\n                )\n            ]\n          ]\n        ]\n    )\n    console.log newValue\n  }\n```\n\nHacn always rerenders the react element that was rendered, so after capturing the results the Html.input returning onChange events will be rendered again.\n\n### State\n\nThe `State` operation works similarly to the `useState` react hook, except it returns a function to create an operation which updates the state:\n\n```fsharp\nmodule Element\n\nopen Hacn.Core\nopen Hacn.Operations\n\nlet Element = \n  react {\n    let! value, updateValue = State \"Start\"\n    let! newValue = RenderCapture(\n      fun capture -\u003e \n        Html.div [\n          prop.text value\n          prop.children [\n            Html.input [\n              prop.onChange (\n                fun (keyEvent: Browser.Types.Event) -\u003e \n                  let inputElement = box keyEvent.target :?\u003e HTMLInputElement\n                  capture (inputElement.value)\n                )\n            ]\n          ]\n        ]\n    )\n\n    if newValue = \"End\" then\n      do! updateValue \"Done\"\n  }\n```\n\nNB: setting the state always triggers the sequence to restart from the point that state was used, regardless of whether the value was changed.\n\n### Calling passed in functions\n\nThe `Call` operation can be used to call functions that are passed in from parent components and also any kind of effect that doesn't update operation state or trigger rerenders.\n\nHacn component props need to implement the equality interface, this isn't automatically generated for props that contain functions so you have to implement it yourself:\n\n```fsharp\n[\u003cCustomEquality; NoComparison\u003e]\ntype TestProps = { TestFunc: string -\u003e unit }\n  with \n    override _.Equals __ = false\n    override _.GetHashCode() = 1\n\nlet Test = \n  react {\n    let! props = Props\n    do! Call (fun () -\u003e (props.TestFunc \"Test\") )\n  }\n```\n\nNB: the implementation of equality isn't actually used by Fable, but is required for type checking.\n\n### Alpha features\n\nThe following features and operations need some additional work and testing.\n\n#### Composition\n\nThe `react` expression builder returns a react component rather than an `Operation`, which means you can't easily compose sequences of operations.\n\nThere is a separate `hacn` builder that allows creation of composable operations:\n\n```fsharp\nlet clickBlocker () = hacn {\n  do! Render Html.div [\n    prop.text \"Continue?\"\n    prop.captureClick ()\n  ]\n}\nlet App = \n  react {\n    let! props = Props\n    do! clickBlocker ()\n    do! Render Html.div [\n      prop.testId \"clicked\"\n      prop.text \"Element Clicked!\"\n    ]\n  }\n```\n\n#### If/Then\n\nConditionals can be used:\n\n```fsharp\ntype CombineProps = {\n  ShowBlocker: bool\n}\n\nlet App = \n  react {\n    let! props = Props\n    if props.ShowBlocker then\n      do! Render Html.div [\n        prop.text \"Continue?\"\n        prop.captureClick ()\n      ]\n\n    do! Render Html.div [\n      prop.text \"Done!\"\n    ]\n  }\n```\n\n#### Error handling\n\nThere is some support for catching errors in child components and in operations. Check the section below for how to trigger errors in operations.\n\n```fsharp\ntype TestException(message:string) =\n   inherit Exception(message, null)\n\nlet errorComponent = React.functionComponent (\n    fun _ -\u003e \n      raise (TestException \"Something's wrong\")\n      []\n    )\nlet App = \n  react {\n    try \n      let! x = Props\n      do! Render errorComponent []\n    with\n    | e -\u003e \n      do! Render Html.div [\n        prop.testId \"error\"\n        prop.text e.Exception.Message\n      ]\n  }\n```\n\n### Writing operations\n\nThe interface for wrting operations is defined by the `Operation` typed union in `Hacn.Types`. The `Perform` record interface is the primary way to write operations that do things like call hooks and wrap functions like the `setTimeout` function. The `PerformProps` interface is used to create operations with custom props change detection logic.\n\nAll the other cases are used internally e.g. `End` marks the end of the sequence of operations and `Control` and `ControlProps` are used internally to make typechecking work.\n\nEach operation has an associated state associated with it that is passed into the functions of the `Perform`. The operation state is currently typed as `obj` (basically untyped) so it has to be cast to the type you want it to be before using it.\n\nThe `Perform` type case takes a record that contains two functions, `PreProcess` and `GetResult`:\n\n```fsharp\ntype PerformData\u003c'props, 'returnType when 'props: equality\u003e =\n{ \nPreProcess: obj option -\u003e obj option;\nGetResult: CaptureReturn -\u003e obj option -\u003e PerformResult\u003c'props, 'returnType\u003e;\n}\n```\n\nThe `PreProcess` function is mainly for operations that wrap hooks and therefore need to be run every time in the same order a Hacn component renders. The function takes the current operation state if it exists and returns an updated state if something has changed. Returning a value causes the Hacn runtime to restart the sequence of operations at that point.\n\nAs an example wrapping the `useRef` hook is defined as follows:\n\n```fsharp\nlet Ref (initialValue: 'returnType option) =\nPerform({ \nPreProcess = fun operationState -\u003e \n  let currentRef = Hooks.useRef(initialValue)\n  let castOperationState: 'returnType option = unbox operationState\n  match castOperationState with\n  | Some(_) -\u003e None\n  | None -\u003e Some(currentRef :\u003e obj)\nGetResult = fun _ operationState -\u003e \n  let castOperationState: (('returnType option) IRefValue) option = unbox operationState\n  match castOperationState with\n  | Some(existingRef) -\u003e \n    PerformContinue(\n      {\n        Element = None\n        Effect = None\n        LayoutEffect = None\n      }, \n      existingRef\n    )\n  | None -\u003e failwith \"should not happen\"\n})\n```\n\nThe `GetResult` method is the main method for handling operation logic in Hacn. It has to handle a number of different scenarios for how operations are written, so it ends up being a bit complicated. \n\nThe two parameters it takes are `capture`, which is for updating the operation state from effects and dom events. The second is the current operation state if it has been set.\n\nThe `capture` parameter is a function that takes a function to update the operation state. The type of the update function is `StateUpdater` and takes the current state (if any) and returns one of the following to update the operation state:\n- Keep - keep the existing state as is.\n- Erase - set the operation state to `None`.\n- Replace - update the state to the value of replace.\n- SetException - used to set the exception on operations, so that they can be handled with try/with.\n\nThe return type for `GetResult` is the `PerformResult` typed union, which has two cases - `PerformWait` which causes hacn to wait for an effect or capture to update the operation state and `PerformContinue` which causes hacn to return a value to the sequence of operations.\n\nBoth cases include a record of type `OperationData`, which includes several fields for controlling hacn:\n- The `Element` field which is what the operation should render.\n- The `Effect` and `LayoutEffect` fields for any side effects e.g. setTimeout. Both `Effect` and `LayoutEffect` work the same, with `Effect` being run in a `useEffect` hook and `LayoutEffect` being run in a `useLayoutEffect` hook.\n- `OperationState` - immediately updates the operation state without triggering a rerender - mostly used for memoization.\n\nEffects are functions that can be used to update the operation state and goto to its location in the sequence of events. This causes its `GetResult` method to be called again, possibly to return the updated result with `PerformContinue`.\n\nEffects can return a function to dispose of any resources when the sequence of operations goes to a previous operation e.g. props change and all operations forward of the `Props` operation need to be reset. Typically operations set their operation state to None in dispose, though this isn't enforced by default (yet). The dispose function returns a state updater like the `capture` function passed into `GetResult`\n\nAs an example here is the `Timeout` operation:\n\n```fsharp\nlet Timeout time = \nPerform({ \nPreProcess = fun _ -\u003e None;\nGetResult = fun captureResult operationState -\u003e \n  match operationState with\n  | Some(_) -\u003e \n    PerformContinue(\n      {\n        Element = None; \n        Effect = None;\n        LayoutEffect = None\n      }, \n      ()\n    )\n  | None -\u003e \n    let timeoutEffect () =\n      let timeoutCallback () =\n        captureResult Replace(() :\u003e obj)\n      let timeoutID = Fable.Core.JS.setTimeout timeoutCallback time\n\n      Some(fun _ -\u003e \n        Fable.Core.JS.clearTimeout timeoutID\n        None\n      )\n      \n    PerformWait(\n      {\n        Element = None\n        Effect = Some(timeoutEffect)\n        LayoutEffect = None\n      }\n    )\n})\n```\n\n## Roadmap \n\n- [ ] Fully document operations and architecture.\n- [ ] Create website.\n- [ ] User Guide.\n- [x] Implement error handling to allow try/catch\n  - [ ] Handle errors within Wait and WaitAny\n  - [ ] Preprocess for error handling\n  - [ ] Error handling in dispose\n- [ ] Test combinations of combine/compose/try/with\n- [ ] Allow returning error as value (rather than try/with)\n- [ ] Graphql generation with Snowflaque \n- [ ] See if operation state can be made typesafe.\n- [ ] Fix type of `Operation` so that it doesn't include separate Perform/Control types.\n- [ ] Create compiler that takes a hacn component and compiles a hooks based element out of it.\n\n## Authors\n\n- **Paul Johnson** - [pj](https://github.com/pj)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpj%2Fhacn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpj%2Fhacn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpj%2Fhacn/lists"}