{"id":16974292,"url":"https://github.com/disjukr/bunja","last_synced_at":"2026-01-20T13:03:52.399Z","repository":{"id":242528843,"uuid":"808928428","full_name":"disjukr/bunja","owner":"disjukr","description":"State Lifetime Manager","archived":false,"fork":false,"pushed_at":"2025-10-17T14:46:36.000Z","size":12378,"stargazers_count":77,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-11-07T13:27:38.526Z","etag":null,"topics":["dependency-injection","jotai","lifetime","raii"],"latest_commit_sha":null,"homepage":"https://jotai.org/docs/third-party/bunja","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/disjukr.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-06-01T07:25:06.000Z","updated_at":"2025-10-28T03:25:19.000Z","dependencies_parsed_at":"2024-06-09T04:37:03.856Z","dependency_job_id":"41562482-0432-493a-9ead-e18d7f7d53c3","html_url":"https://github.com/disjukr/bunja","commit_stats":{"total_commits":20,"total_committers":2,"mean_commits":10.0,"dds":0.4,"last_synced_commit":"ebda78c18e56eae071bccccdd176ea62661386a3"},"previous_names":["disjukr/bunja"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/disjukr/bunja","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disjukr%2Fbunja","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disjukr%2Fbunja/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disjukr%2Fbunja/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disjukr%2Fbunja/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/disjukr","download_url":"https://codeload.github.com/disjukr/bunja/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/disjukr%2Fbunja/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28603412,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T12:01:53.233Z","status":"ssl_error","status_checked_at":"2026-01-20T12:01:46.545Z","response_time":117,"last_error":"SSL_read: 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":["dependency-injection","jotai","lifetime","raii"],"created_at":"2024-10-14T01:06:01.069Z","updated_at":"2026-01-20T13:03:52.387Z","avatar_url":"https://github.com/disjukr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bunja\n\nBunja is lightweight State Lifetime Manager.\\\nHeavily inspired by [Bunshi](https://github.com/saasquatch/bunshi).\n\n\u003e Definition: Bunja (分子 / 분자) - Korean for molecule, member or element.\n\n## Why is managing the lifetime of state necessary?\n\nGlobal state managers like jotai or signals offer the advantage of declaratively\ndescribing state and effectively reducing render counts, but they lack suitable\nmethods for managing resources with a defined start and end.\\\nFor example, consider establishing and closing a WebSocket connection or a modal\nform UI that appears temporarily and then disappears.\n\nBunja is a library designed to address these weaknesses.\\\nEach state defined with Bunja has a lifetime that begins when it is first\ndepended on somewhere in the render tree and ends when all dependencies\ndisappear.\n\nTherefore, when writing a state to manage a WebSocket, you only need to create a\nfunction that establishes the WebSocket connection and a disposal handler that\nterminates the connection.\\\nThe library automatically tracks the actual usage period and calls the init and\ndispose as needed.\n\n## So, do I no longer need jotai or other state management libraries?\n\nNo. Bunja focuses solely on managing the lifetime of state, so jotai and other\nstate management libraries are still valuable.\\\nYou can typically use jotai or something, and when lifetime management becomes\nnecessary, you can wrap those states with bunja.\n\n## How to use\n\nBunja basically provides two functions: `bunja` and `useBunja`.\\\nYou can use `bunja` to define a state with a finite lifetime and use the\n`useBunja` hook to access that state.\n\n### Defining a Bunja\n\nYou can define a bunja using the `bunja` function. When you access the defined\nbunja with the `useBunja` hook, a bunja instance is created.\\\nIf all components in the render tree that refer to the bunja disappear, the\nbunja instance is automatically destroyed.\n\nIf you want to trigger effects when the lifetime of a bunja starts and ends, you\ncan use the `bunja.effect` function.\n\n```ts\nimport { bunja } from \"bunja\";\nimport { useBunja } from \"bunja/react\";\n\nconst countBunja = bunja(() =\u003e {\n  const countAtom = atom(0);\n\n  bunja.effect(() =\u003e {\n    console.log(\"mounted\");\n    return () =\u003e console.log(\"unmounted\");\n  });\n\n  return { countAtom };\n});\n\nfunction MyComponent() {\n  const { countAtom } = useBunja(countBunja);\n  const [count, setCount] = useAtom(countAtom);\n  // Your component logic here\n}\n```\n\n### Defining a Bunja that relies on other Bunja\n\nIf you want to manage a state with a broad lifetime and another state with a\nnarrower lifetime, you can create a (narrower) bunja that depends on a (broader)\nbunja. For example, you can think of a bunja that manages the WebSocket\nconnection and disconnection, and another bunja that subscribes to a specific\nresource over the connected WebSocket.\n\nIn an application composed of multiple pages, you might want to subscribe to the\nFoo resource on page A and the Bar resource on page B, while using the same\nWebSocket connection regardless of which page you're on. In such a case, you can\nwrite the following code.\n\n```ts\n// To simplify the example, code for buffering and reconnection has been omitted.\nconst websocketBunja = bunja(() =\u003e {\n  let socket;\n  const send = (message) =\u003e socket.send(JSON.stringify(message));\n\n  const emitter = new EventEmitter();\n  const on = (handler) =\u003e {\n    emitter.on(\"message\", handler);\n    return () =\u003e emitter.off(\"message\", handler);\n  };\n\n  bunja.effect(() =\u003e {\n    socket = new WebSocket(\"...\");\n    socket.onmessage = (e) =\u003e emitter.emit(\"message\", JSON.parse(e.data));\n    return () =\u003e socket.close();\n  });\n\n  return { send, on };\n});\n\nconst resourceFooBunja = bunja(() =\u003e {\n  const { send, on } = bunja.use(websocketBunja);\n  const resourceFooAtom = atom();\n\n  bunja.effect(() =\u003e {\n    const off = on((message) =\u003e {\n      if (message.type === \"foo\") store.set(resourceAtom, message.value);\n    });\n    send(\"subscribe-foo\");\n    return () =\u003e {\n      send(\"unsubscribe-foo\");\n      off();\n    };\n  });\n\n  return { resourceFooAtom };\n});\n\nconst resourceBarBunja = bunja(() =\u003e {\n  const { send, on } = bunja.use(websocketBunja);\n  const resourceBarAtom = atom();\n  // ...\n});\n\nfunction PageA() {\n  const { resourceFooAtom } = useBunja(resourceFooBunja);\n  const resourceFoo = useAtomValue(resourceFooAtom);\n  // ...\n}\n\nfunction PageB() {\n  const { resourceBarAtom } = useBunja(resourceBarBunja);\n  const resourceBar = useAtomValue(resourceBarAtom);\n  // ...\n}\n```\n\nNotice that `websocketBunja` is not directly `useBunja`-ed. When you `useBunja`\neither `resourceFooBunja` or `resourceBarBunja`, since they depend on\n`websocketBunja`, it has the same effect as if `websocketBunja` were also\n`useBunja`-ed.\n\n\u003e [!NOTE]\n\u003e When a bunja starts, the initialization effect of the bunja with a broader\n\u003e lifetime is called first.\\\n\u003e Similarly, when a bunja ends, the cleanup effect of the bunja with the broader\n\u003e lifetime is called first.\\\n\u003e This behavior is aligned with how React's `useEffect` cleanup function is\n\u003e invoked, where the parent’s cleanup is executed before the child’s in the\n\u003e render tree.\n\u003e\n\u003e See: \u003chttps://github.com/facebook/react/issues/16728\u003e\n\n### Dependency injection using Scope\n\nYou can use a bunja for local state management.\\\nWhen you specify a scope as a dependency of the bunja, separate bunja instances\nare created based on the values injected into the scope.\n\n```ts\nimport { bunja, createScope } from \"bunja\";\n\nconst UrlScope = createScope();\n\nconst fetchBunja = bunja(() =\u003e {\n  const url = bunja.use(UrlScope);\n\n  const queryAtom = atomWithQuery((get) =\u003e ({\n    queryKey: [url],\n    queryFn: async () =\u003e (await fetch(url)).json(),\n  }));\n\n  return { queryAtom };\n});\n```\n\n#### Injecting dependencies via React context\n\nIf you bind a scope to a React context, bunjas that depend on the scope can\nretrieve values from the corresponding React context.\n\nIn the example below, there are two React instances (`\u003cChildComponent /\u003e`) that\nreference the same `fetchBunja`, but since each looks at a different context\nvalue, two separate bunja instances are also created.\n\n```tsx\nimport { createContext } from \"react\";\nimport { bunja, createScope } from \"bunja\";\nimport { bindScope } from \"bunja/react\";\n\nconst UrlContext = createContext(\"https://example.com/\");\nconst UrlScope = createScope();\nbindScope(UrlScope, UrlContext);\n\nconst fetchBunja = bunja(() =\u003e {\n  const url = bunja.use(UrlScope);\n\n  const queryAtom = atomWithQuery((get) =\u003e ({\n    queryKey: [url],\n    queryFn: async () =\u003e (await fetch(url)).json(),\n  }));\n\n  return { queryAtom };\n});\n\nfunction ParentComponent() {\n  return (\n    \u003c\u003e\n      \u003cUrlContext value=\"https://example.com/foo\"\u003e\n        \u003cChildComponent /\u003e\n      \u003c/UrlContext\u003e\n      \u003cUrlContext value=\"https://example.com/bar\"\u003e\n        \u003cChildComponent /\u003e\n      \u003c/UrlContext\u003e\n    \u003c/\u003e\n  );\n}\n\nfunction ChildComponent() {\n  const { queryAtom } = useBunja(fetchBunja);\n  const { data, isPending, isError } = useAtomValue(queryAtom);\n  // Your component logic here\n}\n```\n\nYou can use the `createScopeFromContext` function to handle both the creation of\nthe scope and the binding to the context in one step.\n\n```ts\nimport { createContext } from \"react\";\nimport { createScopeFromContext } from \"bunja/react\";\n\nconst UrlContext = createContext(\"https://example.com/\");\nconst UrlScope = createScopeFromContext(UrlContext);\n```\n\n#### Injecting dependencies directly into the scope\n\nYou might want to use a bunja directly within a React component where the values\nto be injected into the scope are created.\n\nIn such cases, you can use the second parameter of `useBunja` hook to inject\nvalues into the scope without wrapping the context separately.\n\n```tsx\nfunction MyComponent() {\n  const { queryAtom } = useBunja(\n    fetchBunja,\n    [UrlScope.bind(\"https://example.com/\")],\n  );\n  const { data, isPending, isError } = useAtomValue(queryAtom);\n  // Your component logic here\n}\n```\n\n##### Doing the same thing inside a bunja\n\nYou can use `bunja.fork` to inject scope values from within a bunja\ninitialization function.\n\n```ts\nconst myBunja = bunja(() =\u003e {\n  const fooData = bunja.fork(fetchBunja, [\n    UrlScope.bind(\"https://example.com/foo\"),\n  ]);\n  const barData = bunja.fork(fetchBunja, [\n    UrlScope.bind(\"https://example.com/bar\"),\n  ]);\n\n  return { fooData, barData };\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisjukr%2Fbunja","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdisjukr%2Fbunja","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisjukr%2Fbunja/lists"}