{"id":20284166,"url":"https://github.com/clebert/loxia","last_synced_at":"2025-08-12T16:40:50.215Z","repository":{"id":73730713,"uuid":"317888426","full_name":"clebert/loxia","owner":"clebert","description":"Implementing JavaScript state machines using React Hooks.","archived":false,"fork":false,"pushed_at":"2023-03-26T22:45:55.000Z","size":895,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-04T14:44:39.890Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/clebert.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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,"zenodo":null}},"created_at":"2020-12-02T14:30:52.000Z","updated_at":"2024-09-24T07:31:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"d7de09ad-22fe-4a30-8fb2-73dae8c691b1","html_url":"https://github.com/clebert/loxia","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/clebert/loxia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clebert%2Floxia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clebert%2Floxia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clebert%2Floxia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clebert%2Floxia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clebert","download_url":"https://codeload.github.com/clebert/loxia/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clebert%2Floxia/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269740360,"owners_count":24467758,"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-10T02:00:08.965Z","response_time":71,"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":[],"created_at":"2024-11-14T14:18:33.748Z","updated_at":"2025-08-12T16:40:50.190Z","avatar_url":"https://github.com/clebert.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# loxia\n\n\u003e Implementing JavaScript state machines using React Hooks.\n\n## Installation\n\n```\nnpm install loxia\n```\n\n## Motivation\n\nI consider React Hooks to be particularly suitable for implementing state\nmachines. Based on this idea, I developed a\n[bookmark manager](https://github.com/clebert/bookmark.wtf). It's UI makes\nintensive use of the Hooks and patterns developed here and serves as a real\nworld test.\n\n## Hooks\n\nIn general, all Hooks in this library are built to be used with any React Hooks\ncompliant implementation. That would be, for example,\n[Batis](https://github.com/clebert/batis), Preact, and of course React. In the\nusage examples, Batis is always used, but as an alternative, the analogous usage\nwith React is shown as a comment.\n\n### `useTransition`\n\nA transition is a function with special runtime behavior that can be used to\nimplement the correct behavior of the transition methods of a state machine. A\ntransition executes the passed callback once and returns true if its\ndependencies have not changed, so it should depend on the state of the\nassociated state machine.\n\n\u003cdetails\u003e\n  \u003csummary\u003eUsage example\u003c/summary\u003e\n\n```ts\nimport {Host} from 'batis'; // import * as React from 'react';\nimport {createTransitionHook} from 'loxia';\n\nconst useTransition = createTransitionHook(Host /* React */);\n\nfunction useLock(): Lock {\n  const [locked, setLocked] = Host /* React */.useState(false);\n  const transition = useTransition(locked);\n\n  const lock = Host /* React */.useCallback(\n    () =\u003e transition(() =\u003e setLocked(true)),\n    [transition],\n  );\n\n  const unlock = Host /* React */.useCallback(\n    () =\u003e transition(() =\u003e setLocked(false)),\n    [transition],\n  );\n\n  return Host /* React */.useMemo(\n    () =\u003e (locked ? {locked, unlock} : {locked, lock}),\n    [locked],\n  );\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eType definitions\u003c/summary\u003e\n\n```ts\nfunction createTransitionHook(hooks: BatisHooks): UseTransition;\n```\n\n```ts\ntype UseTransition = (\n  ...dependencies: readonly [unknown, ...unknown[]]\n) =\u003e Transition;\n```\n\n```ts\ntype Transition = (callback?: () =\u003e void) =\u003e boolean;\n```\n\n\u003c/details\u003e\n\n### `useBinder`\n\nA binding is a function that is tied to the life cycle of the Hook or component\nit surrounds. Often React components are already unmounted and an associated\nasynchronous operation should no longer have any effect. It is therefore useful\nto bind the callback functions of `Promise.then`, `Promise.catch`, and also\n`setTimeout`.\n\n\u003cdetails\u003e\n  \u003csummary\u003eUsage example\u003c/summary\u003e\n\n```ts\nimport {Host} from 'batis'; // import * as React from 'react';\nimport {createBinderHook} from 'loxia';\n\nconst useBinder = createBinderHook(Host /* React */);\n\nfunction useExample() {\n  const bind = useBinder();\n\n  Host /* React */.useEffect(() =\u003e {\n    setTimeout(\n      bind(() =\u003e {\n        // ...\n      }),\n    );\n  });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eType definitions\u003c/summary\u003e\n\n```ts\nfunction createBinderHook(hooks: BatisHooks): UseBinder;\n```\n\n```ts\ntype UseBinder = () =\u003e Bind;\n```\n\n```ts\ntype Bind = \u003cTCallback extends (...args: any[]) =\u003e void\u003e(\n  callback: TCallback,\n) =\u003e Binding\u003cTCallback\u003e;\n```\n\n```ts\ntype Binding\u003cTCallback extends (...args: any[]) =\u003e void\u003e = (\n  ...args: Parameters\u003cTCallback\u003e\n) =\u003e boolean;\n```\n\n\u003c/details\u003e\n\n### `useReceiver`\n\nA receiver is a state machine which allows the reception of a signal in the form\nof a promise passed as an argument. A receiver is always in one of the following\nstates `receiving`, `successful`, or `failed`. As long as the reference to the\npassed promise remains the same, a receiver represents the state of the promise.\nWhen a reference to a new promise is passed, the old promise no longer affects\nthe receiver state.\n\nIt makes sense to use a receiver if an asynchronous operation is based on user\ninput. If the user input changes in the meantime and a new asynchronous\noperation overwrites the old one, the old one should no longer have any effect.\n\n\u003cdetails\u003e\n  \u003csummary\u003eUsage example\u003c/summary\u003e\n\n```ts\nimport {Host} from 'batis'; // import * as React from 'react';\nimport {createReceiverHook} from 'loxia';\n\nconst useReceiver = createReceiverHook(Host /* React */);\n\nfunction useAsyncJsonData(url) {\n  const signal = Host /* React */.useMemo(\n    () =\u003e fetch(url).then((response) =\u003e response.json()),\n    [url],\n  );\n\n  return useReceiver(signal);\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eType definitions\u003c/summary\u003e\n\n```ts\nfunction createReceiverHook(hooks: BatisHooks): UseReceiver;\n```\n\n```ts\ntype UseReceiver = \u003cTValue\u003e(signal: Promise\u003cTValue\u003e) =\u003e Receiver\u003cTValue\u003e;\n```\n\n```ts\ntype Receiver\u003cTValue\u003e =\n  | ReceivingReceiver\n  | SuccessfulReceiver\u003cTValue\u003e\n  | FailedReceiver;\n\ninterface ReceivingReceiver {\n  readonly state: 'receiving';\n}\n\ninterface SuccessfulReceiver\u003cTValue\u003e {\n  readonly state: 'successful';\n  readonly value: TValue;\n}\n\ninterface FailedReceiver {\n  readonly state: 'failed';\n  readonly error: unknown;\n}\n```\n\n\u003c/details\u003e\n\n### `useSender`\n\nA sender is a state machine which allows to send exactly one signal at a time.\n\n\u003cdetails\u003e\n  \u003csummary\u003eType definitions\u003c/summary\u003e\n\n```ts\nfunction createSenderHook(hooks: BatisHooks): UseSender;\n```\n\n```ts\ntype UseSender = () =\u003e Sender;\n```\n\n```ts\ntype Sender = IdleSender | SendingSender | FailedSender;\n\ninterface IdleSender {\n  readonly state: 'idle';\n\n  send(signal: Promise\u003cunknown\u003e): boolean;\n}\n\ninterface SendingSender {\n  readonly state: 'sending';\n}\n\ninterface FailedSender {\n  readonly state: 'failed';\n  readonly error: unknown;\n\n  send(signal: Promise\u003cunknown\u003e): boolean;\n}\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclebert%2Floxia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclebert%2Floxia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclebert%2Floxia/lists"}