{"id":14966914,"url":"https://github.com/abhishiv/alfama","last_synced_at":"2026-03-05T09:31:51.590Z","repository":{"id":119237536,"uuid":"609925559","full_name":"abhishiv/alfama","owner":"abhishiv","description":"⚡ Fine grained reactive UI library with no-compiler and no-magic, explicit subscriptions for signals, and first class support for HMR at ~9kb","archived":false,"fork":false,"pushed_at":"2024-10-20T22:39:54.000Z","size":197,"stargazers_count":9,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-04T16:15:42.812Z","etag":null,"topics":["fine-grained","framework-ui","javascript","jsx","observable","reactive","reactiveui","ui-library"],"latest_commit_sha":null,"homepage":"https://alfama.vercel.app","language":"TypeScript","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/abhishiv.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":"2023-03-05T16:41:50.000Z","updated_at":"2025-05-29T17:07:36.000Z","dependencies_parsed_at":"2024-11-14T22:33:49.996Z","dependency_job_id":"2e6336bf-91e3-466d-996d-22c191dfcfd6","html_url":"https://github.com/abhishiv/alfama","commit_stats":{"total_commits":94,"total_committers":1,"mean_commits":94.0,"dds":0.0,"last_synced_commit":"8695b9f3d38f09722a205d0cd01d70c0ed73c294"},"previous_names":["abhishiv/stallone","abhishiv/alfama","abhishiv/rocky7","abhishiv/dylan"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/abhishiv/alfama","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abhishiv%2Falfama","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abhishiv%2Falfama/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abhishiv%2Falfama/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abhishiv%2Falfama/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/abhishiv","download_url":"https://codeload.github.com/abhishiv/alfama/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abhishiv%2Falfama/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30117683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-05T08:19:04.902Z","status":"ssl_error","status_checked_at":"2026-03-05T08:17:37.148Z","response_time":93,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5: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":["fine-grained","framework-ui","javascript","jsx","observable","reactive","reactiveui","ui-library"],"created_at":"2024-09-24T13:37:08.636Z","updated_at":"2026-03-05T09:31:51.571Z","avatar_url":"https://github.com/abhishiv.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# alfama\n\n⚡ Fine grained reactive UI Library.\n\n[![Version](https://img.shields.io/npm/v/alfama.svg?color=success\u0026style=flat-square)](https://www.npmjs.com/package/alfama)\n[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)\n[![Build Status](https://github.com/abhishiv/alfama/actions/workflows/ci.yml/badge.svg)](https://github.com/abhishiv/alfama/actions/workflows/ci.yml)\n![Badge size](https://deno.bundlejs.com/?q=alfama\u0026config={%22analysis%22:undefined}\u0026badge=)\n\n**npm**: `npm i alfama`\n\n**cdn**: https://cdn.jsdelivr.net/npm/alfama/+esm\n\n---\n\n#### Features\n\n- **Rich and Complete:** From support for `SVG` to popular patterns like `dangerouslySetInnerHTML`, `ref` to `\u003cFragment\u003e` and `\u003cPortal /\u003e` alfama has you covered.\n- **Small:** Fully featured at `~9kB` gzip.\n- **Truly reactive and fine grained:** Unlike VDOM libraries which use diffing to compute changes, it uses fine grained updates to target only the DOM which needs to update.\n- **No Magic:** Explicit subscriptions obviate the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) methods found in other fine grained reactive libraries like solid/sinuous. _Importantly, many feel that this also makes your code easy to reason about._\n- **Signals and Stores:** Signals for primitives and Stores for deeply nested objects/arrays.\n- **First class HMR:** Preserves Signals/Stores across HMR loads for a truly stable HMR experience.\n- **DevEx:** no compile step needed if you want: choose your view syntax: `h` for plain javascript or `\u003cJSX/\u003e` for babel/typescript.\n\n#### Backers \u0026 Sponsors\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n      \u003cimg style=\"display: block;\" width=\"25px\" height=\"25px\"  src=\"https://www.grati.co/images/logo5.png\" /\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      \u003ca href=\"https://www.grati.co\"\u003egrati.co\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      Next-Generation IDE\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n#### Ecosystem\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\n      \u003ca href=\"https://github.com/abhishiv/alfama-router\"\u003ealfama-router\u003c/a\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      Router with a familiar react-router like API\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n#### Example\n\n[Counter - Codesandbox](https://codesandbox.io/s/counter-demo-alfama-t7ift3?file=/src/index.tsx)\n\n```tsx\n/** @jsx h **/\nimport { component, h, render } from \"alfama\";\n\n// 1) The signal/wire/store functions are passed as a param to\n// component definition\nconst Page = component(\"HomePage\", (props, { signal, wire }) =\u003e {\n  // 2) Named signals for stable HMR\n  const [count, setCount] = signal(\"count\", 0);\n  // or $count = signal(\"count\", 0) and then $count.get/$count.set\n\n  // 3) Most importantly: wire reactivity to signals\n  // with explicit subscription using the $ token param\n  // NB: also makes code easy to reason about and prevents those pesky untrack/sample related errors\n  const $doubleCount = wire(($) =\u003e count($) * 2);\n\n  return (\n    \u003cdiv id=\"home\"\u003e\n      \u003cp\u003eHey, {props.name}\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e setCount(count() + 1)}\u003e\n        Increment / {wire(count)})\n      \u003c/button\u003e\n      \u003cp\u003eDouble count = {$doubleCount}\u003c/p\u003e\n    \u003c/div\u003e\n  );\n});\n\nrender(\u003cPage name=\"John Doe\" /\u003e, document.body);\n```\n\n## Motivation\n\nThis library is at its core inspired by [haptic](https://github.com/heyheyhello/haptic) that in particular it also favours manual subscription model instead of automatic subscriptions model. This oblivates the need of [`sample`](https://github.com/luwes/sinuous/blob/8d1aa0cdb8a25e6bfcdf34f022523564a9adb533/src/observable.js#L34-L49)/[`untrack`](https://github.com/vobyjs/voby#untrack) found in almost all other reactive libraries.\n\nAlso it borrows the nomenclature of aptly named Signal and Wire from haptic.\n\nIt's also influenced by Sinuous, Solid, \u0026 S.js\n\n## API\n\n### Core\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003esignal\u003c/strong\u003e: create a signal\u003c/summary\u003e\n\n```tsx\nexport const HomePage = component\u003c{ name: string }\u003e(\n  \"HomePage\",\n  (props, { signal, wire }) =\u003e {\n    const [count, setCount] = signal(\"count\", 0);\n    //.. rest of component\n  }\n);\n```\n\n\u003c/details\u003e\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003ewire\u003c/strong\u003e: create a wire\u003c/summary\u003e\n\n```tsx\n\u003cdiv id=\"home\"\u003e\n  \u003cbutton\n    onclick={() =\u003e {\n      setCount(count() + 1);\n    }}\n  \u003e\n    Increment to {wire(($) =\u003e $(count))}\n  \u003c/button\u003e\n\u003c/div\u003e\n```\n\n\u003c/details\u003e\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003estore\u003c/strong\u003e: create a store to hold object/arrays\u003c/summary\u003e\n\n```tsx\nexport const Todos = component(\"Todos\", (props, { signal, wire, store }) =\u003e {\n  const $todos = store(\"todos\", {\n    items: [{ task: \"Do Something\" }, { task: \"Do Something else\" }],\n  });\n  return (\n    \u003cul\u003e\n      \u003cEach\n        cursor={$todos.items}\n        renderItem={(cursor) =\u003e {\n          return \u003cli\u003e{cursor().task}\u003c/li\u003e;\n        }}\n      \u003e\u003c/Each\u003e\n    \u003c/ul\u003e\n  );\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003edefineContext\u003c/strong\u003e: define context value\u003c/summary\u003e\n\n```tsx\nexport const RouterContext = defineContext\u003cRouterObject\u003e(\"RouterObject\");\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003esetContext\u003c/strong\u003e: set context value\u003c/summary\u003e\n\n```tsx\nconst BrowserRouter = component(\"Router\", (props, { setContext, signal }) =\u003e {\n  setContext(\n    RouterContext,\n    signal(\"router\", createRouter(window.history, window.location))\n  );\n  return props.children;\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003egetContext\u003c/strong\u003e: get context value\u003c/summary\u003e\n\n```tsx\nconst Link = component(\"Link\", (props: any, { signal, wire, getContext }) =\u003e {\n  const router = getContext(RouterContext);\n  //... rest of component\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eonMount\u003c/strong\u003e: triggered on mount\u003c/summary\u003e\n\n```tsx\nexport const Prosemirror = component(\"Prosemirror\", (props, { onMount }) =\u003e {\n  onMount(() =\u003e {\n    console.log(\"component mounted\");\n  });\n  // ...\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eonUnmount\u003c/strong\u003e: triggered on unmount\u003c/summary\u003e\n\n```tsx\nexport const Prosemirror = component(\"Prosemirror\", (props, { onUnmount }) =\u003e {\n  onUnmount(() =\u003e {\n    console.log(\"component unmounted\");\n  });\n  // ...\n});\n```\n\n\u003c/details\u003e\n\n### Helper Components\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhen\u003c/strong\u003e: reactive if\u003c/summary\u003e\n\n```tsx\n\u003cWhen\n  condition={($) =\u003e count($) \u003e 5}\n  views={{\n    true: () =\u003e {\n      return \u003cdiv key=\"true\"\u003e\"TRUE\"\u003c/div\u003e;\n    },\n    false: () =\u003e {\n      return \u003cdiv key=\"false\"\u003e\"FALSE\"\u003c/div\u003e;\n    },\n  }}\n\u003e\u003c/When\u003e\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eEach\u003c/strong\u003e: reactive map\u003c/summary\u003e\n\n```tsx\n\u003cEach\n  cursor={$todos.items}\n  renderItem={(cursor) =\u003e {\n    return \u003cli\u003e{wire(cursor().task)}\u003c/li\u003e;\n  }}\n\u003e\u003c/Each\u003e\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003ePortal\u003c/strong\u003e: mount outside of render tree\u003c/summary\u003e\n\n```tsx\nexport const PortalExample = component(\"PortalExample\", (props, utils) =\u003e {\n  const [active, setActive] = utils.signal(\"active\", false);\n  return (\n    \u003cdiv\u003e\n      \u003cbutton\n        onClick={(e) =\u003e {\n          setActive(!active());\n        }}\n      \u003e\n        toggle modal\n      \u003c/button\u003e\n      \u003cWhen\n        condition={($) =\u003e active($)}\n        views={{\n          true: () =\u003e {\n            return (\n              \u003cPortal mount={document.body}\u003e\n                \u003cdiv style=\"position: fixed; max-width: 400px; max-height: 50vh; background: white; padding: 7px; width: 100%; border: 1px solid #000;top: 0;\"\u003e\n                  \u003ch1\u003ePortal\u003c/h1\u003e\n                \u003c/div\u003e\n              \u003c/Portal\u003e\n            );\n          },\n          false: () =\u003e {\n            return \"\";\n          },\n        }}\n      \u003e\u003c/When\u003e\n    \u003c/div\u003e\n  );\n});\n```\n\n\u003c/details\u003e\n\n## Reciepes\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHMR\u003c/strong\u003e\u003c/summary\u003e\n\n```tsx\n/** @jsx h **/\nimport { h, render } from \"alfama\";\nimport { Layout } from \"./index\";\n\nconst renderApp = ({ Layout }: { Layout: typeof Layout }) =\u003e\n  render(\u003cLayout /\u003e, document.getElementById(\"app\")!);\n\nwindow.addEventListener(\"load\", () =\u003e renderApp({ Layout }));\n\nif (import.meta.hot) {\n  import.meta.hot.accept(\"./index\", (newModule) =\u003e {\n    if (newModule) renderApp(newModule as unknown as { Layout: typeof Layout });\n  });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eRefs\u003c/strong\u003e\u003c/summary\u003e\n\n```tsx\n/** @jsx h **/\n\nexport const Prosemirror = component(\"Prosemirror\", (props, { onUnmount }) =\u003e {\n  let container: Element | undefined = undefined;\n  let prosemirror: EditorView | undefined = undefined;\n  onUnmount(() =\u003e {\n    if (prosemirror) {\n      prosemirror.destroy();\n    }\n  });\n  return (\n    \u003cdiv\n      style=\"\n    height: 100%;    position: absolute; width: 100%;\"\n      ref={(el) =\u003e {\n        container = el;\n        if (container) {\n          prosemirror = setupProsemirror(container);\n        }\n      }}\n    \u003e\u003c/div\u003e\n  );\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003edangerouslySetInnerHTML\u003c/strong\u003e\u003c/summary\u003e\n\n```tsx\n/** @jsx h **/\n\u003cdiv dangerouslySetInnerHTML={{ __html: `\u003c!-- any HTML you want --\u003e` }} /\u003e\n```\n\n\u003c/details\u003e\n\n## Concepts\n\n### Signals\n\nThese are reactive read/write variables who notify subscribers when they've been written to. They act as dispatchers in the reactive system.\n\n```tsx\nconst [count, setCount] = signal(\"count\", 0);\n\ncount(); // Passive read (read-pass)\nsetCount(1); // Write\n\n// also possible to use get/set on signal instead of tuples\nconst $count = signal(\"count\", 0);\n$count.get();\n$count.set(5);\n```\n\nThe subscribers to signals are wires, which will be introduced later. They subscribe by read-subscribing the signal.\n\n### Stores\n\nStores are for storing nested arrays/objects and also act as dispatchers in the reactive system. And like signals, stores can also be read subsribed by wires. Outside of wires, they can be read via `reify` function. Writes can be done via `produce` function immer style.\n\n```tsx\nconst val = { name: \"Jane\", friends: [{ id: \"1\", name: \"John\" }] };\nconst $profile = store(\"profile\", val);\n\n// Passive read (read-pass)\nconst friends = reify($profile.friends);\nconsole.log(friends.length);\n// Write\nproduce($profile.friends, (friends) =\u003e {\n  friends.push({ id: \"2\", name: \"John Doe 2\" });\n});\n```\n\n### Wires\n\nThese are task runners who subscribe to signals/stores and react to writes. They hold a function (the task) and manage its subscriptions, nested wires, run count, and other metadata. The wire provides a `$` token to the function call that, at your discretion as the developer, can use to read-subscribe to signals.\n\n```tsx\nwire(($) =\u003e {\n  // Explicitly subscribe to count signal getter using the subtoken \"$\"\n  const countValue = $(count);\n\n  // also possible to subscribe to a stores using \"$\" subtoken\n  const friendsCount = $($profile.friends);\n  return countValue + friendsCount;\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabhishiv%2Falfama","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabhishiv%2Falfama","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabhishiv%2Falfama/lists"}