{"id":15792682,"url":"https://github.com/nfour/xcomponent","last_synced_at":"2025-04-01T13:34:03.344Z","repository":{"id":256148941,"uuid":"854436451","full_name":"nfour/xcomponent","owner":"nfour","description":"A mobx \u0026 react microframework","archived":false,"fork":false,"pushed_at":"2024-10-18T01:53:03.000Z","size":351,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-20T14:11:16.213Z","etag":null,"topics":["mobx","react","typescript"],"latest_commit_sha":null,"homepage":"","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/nfour.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":"2024-09-09T07:04:21.000Z","updated_at":"2024-10-18T01:53:07.000Z","dependencies_parsed_at":"2024-09-18T07:53:23.562Z","dependency_job_id":"7a158c7e-16fd-4929-95ea-0c9c1cc5f511","html_url":"https://github.com/nfour/xcomponent","commit_stats":{"total_commits":88,"total_committers":1,"mean_commits":88.0,"dds":0.0,"last_synced_commit":"08235bbd53441461143afc2f7e27d6da0fe39b05"},"previous_names":["nfour/xcomponent"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Fxcomponent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Fxcomponent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Fxcomponent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nfour%2Fxcomponent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nfour","download_url":"https://codeload.github.com/nfour/xcomponent/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246604612,"owners_count":20804100,"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","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":["mobx","react","typescript"],"created_at":"2024-10-04T23:02:47.667Z","updated_at":"2025-04-01T13:34:03.335Z","avatar_url":"https://github.com/nfour.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# XComponent\n\nA microframework that combines MobX and React to solve common performance, state management, and lifecycle issues.\n\n+ [Install](#install)\n+ [Features](#features)\n+ [Usage](#usage)\n  + [Basic Component](#basic-component)\n  + [Inline State](#inline-state)\n  + [Lifecycle Hooks](#lifecycle-hooks)\n  + [Component Composition](#component-composition)\n+ [API](#api)\n  + [Core](#core)\n  + [Models](#models)\n    + [Value](#value)\n    + [AsyncValue](#asyncvalue)\n    + [BoxedValue](#boxedvalue)\n    + [BoolValue](#boolvalue)\n+ [Documentation](#documentation)\n+ [License](#license)\n\n\n## Install\n\n```bash\npnpm add @n4s/xcomponent\n```\n\n## Features\n\n- Drop-in replacement for MobX `observer`\n- Built-in state management patterns\n- Simplified lifecycle hooks\n- Component composition utilities\n- Helper models for common use cases\n\n## Usage\n\n### Basic Component\n\nWhen NOT using a compile plugin to auto-wrap components for observability:\n\n```tsx\n// BEFORE: MobX Observer\nimport { observer } from 'mobx-react-lite'\nconst MyComponent = observer((props: { someProp: number }) =\u003e \u003c\u003e{props.someProp}\u003c/\u003e)\n\n// AFTER: XComponent\nimport { X } from '@n4s/xcomponent'\nconst MyComponent = X((props: { someProp: number }) =\u003e \u003c\u003e{props.someProp}\u003c/\u003e)\n```\n\nIf you ARE using a compile plugin to auto-wrap, you can omit the HOC wrapper:\n\n```tsx\nexport const MyComponent = (props: { someProp: number }) =\u003e {\n  const state = X.useState(props, (p) =\u003e class {\n    foo = new Value(0)\n\n    get computed() {\n      return this.foo.value + p.someProp\n    }\n  })\n\n  return \u003c\u003e{state.computed}\u003c/\u003e\n}\n```\n\n### Inline State\n\n```tsx\nimport { X, Value } from '@n4s/xcomponent'\n\nconst Counter = () =\u003e {\n  const state = X.useState(() =\u003e class {\n    count = new Value(0)\n    get doubledCount() {\n      return this.count.value * 2\n    }\n\n    increment = () =\u003e this.count.set(this.count.value + 1)\n  })\n\n  return (\n    \u003c\u003e\n      Count: {state.count.value}, Doubled Count: {state.doubledCount}\n      \u003cbutton onClick={state.increment}\u003e+\u003c/button\u003e\n    \u003c/\u003e\n  )\n}\n\n/**\n * This demonstrates taking in props, using them observably within X.useState.\n * \n * \u003cObservablePropsCounter multiplier={2.5} initialCount={0} /\u003e\n */\nconst ObservablePropsCounter = (props: { initialCount: number, multiplier: number }) =\u003e {\n  const state = X.useState(props, (props) =\u003e class {\n    count = new Value(props.initialCount)\n    get multipliedCount() {\n      return this.count.value * props.multiplier // props.multiplier is observable!\n    }\n\n    increment = () =\u003e this.count.set(this.count.value + 1)\n  })\n\n  return (\n    \u003c\u003e\n      Count: {state.count.value}, Multiplied Count: {state.multipliedCount}\n      \u003cbutton onClick={state.increment}\u003e+\u003c/button\u003e\n    \u003c/\u003e\n  )\n}\n```\n\n### Lifecycle Hooks\n\nThe goal of this library is to avoid using hooks from `react` during normal state management operations, thus the below lifecycle hooks are provided.\n\n```tsx\nX.useOnMounted(() =\u003e {\n  // Called when component mounts\n})\n\nX.useOnUnmounted(() =\u003e {\n  // Called when component unmounts\n})\n\nX.useReaction(\n  () =\u003e state.someValue,\n  (newValue) =\u003e {\n    // Called on first render, and whenever observable dependencies change\n  }\n)\n\nX.useAutorun(() =\u003e {\n  // Called on first render, and whenever observable dependencies change\n})\n```\n\n\n### Component Composition\n\nIn the below examples you can see how to create a `Dialog` component with `Header` and `Body` subcomponents.\n\n```tsx\nconst Dialog = X(({ children }) =\u003e (\n  \u003cdiv className={Dialog.classes.dialog} \u003e{children}\u003c/div\u003e\n)).with({\n  Header: X(({ children }) =\u003e (\n    \u003cheader className={Dialog.classes.header} \u003e{children}\u003c/header\u003e\n  )),\n  Body: X(({ children }) =\u003e (\n    \u003cdiv className={Dialog.classes.body}\u003e{children}\u003c/div\u003e\n  )),\n  classes: {\n    dialog: 'dialog',\n    header: 'dialog-header',\n    body: 'dialog-body',\n  }\n})\n\n// Usage\n\u003cDialog css={{\n  // Can also ovveride using the classes we defined.\n  [`.${Dialog.classes.dialog}`]: {\n    background: 'white',\n    padding: '1rem',\n  }\n}}\u003e\n  \u003cDialog.Header\u003eTitle\u003c/Dialog.Header\u003e\n  \u003cDialog.Body\u003eContent\u003c/Dialog.Body\u003e\n\u003c/Dialog\u003e\n```\n\n\n## API\n\n### Core\n\n- `X\u003cProps\u003e()` - Create an observed component with type support\n- `X.useState()` - Create component-scoped state\n- `X.useOnMounted()` - Mount lifecycle hook\n- `X.useOnUnmounted()` - Unmount lifecycle hook\n- `X.useReaction()` - MobX reaction hook\n- `X.useAutorun()` - MobX autorun hook\n\n### Models\n\n- `Value\u003cT\u003e` - Observable value container\n- `AsyncValue\u003cT\u003e` - Async state container with pending/error/value states\n- `BoxedValue\u003cT\u003e` - Encapsulated observable with custom getter/setter\n- `BoolValue` - Boolean value with toggle utilities\n\n\n#### Value\n\nThe `Value` class is effectively `observable.box` of interface `{ value: T, set: (value: T) =\u003e void }`.\n\nFeatures:\n- Type inferrence\n- Async mobx actions (no need to wrap in `runInAction` or use `flow` generators)\n- Terseness\n- Avoids reading `value` until necessary during prop-passing\n- Supports two way binding patterns\n\n```tsx\nconst selectedFruit = new Value\u003c'banana'|'apple'|undefined\u003e(undefined)\nselectedFruit.set('test') // TS error\nselectedFruit.set('banana') // Valid\nselectedFruit.value // 'banana'\n```\n\n####  AsyncValue\n\nThink of `react-query` for this one. It is a `Value` that can be in a loading state, and can be awaited.\n\n\nFeatures:\n- Ergonomic types\n- Async mobx actions\n- Queuing\n- Promise cancellation\n- Pending state\n- Error state\n- Success state\n- Progress state (eg. for uploads)\n\n```tsx\nasync function fetchFiles(c: { userId: string; foo: number }): Promise\u003c{ name: string }[]\u003e {\n  return []\n}\n\nclass ExampleModel {\n  constructor() { makeAutoObservable(this) }\n  activeUserId = '22'\n  files = new AsyncValue(async ({ foo }: { foo: number }) =\u003e\n    fetchFiles({ userId: this.activeUserId, foo })\n  )\n}\n\nconst example = new ExampleModel()\nexample.files.value?.[0]?.name // undefined - missing data\nawait example.files.query({ foo: 22 }) // foo is strongly typed, inferred!\nexample.files.value?.[0]?.name // 'myFile.txt' - has data!\nexample.files.error // undefined - no error\nexample.files.isPending // false - we already awaited it\n\nconst v = new AsyncValue(() =\u003e fetchUsersList())\nv.value // undefined\nconst promise = v.query() // Don't need to provide params as none are defined\nv.isPending // true\nawait promise\nv.isPending // false\nv.value // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]\n```\n\n\n####  BoxedValue\n\nVery similar to `Value`, however, allows for the getter and setter to be defined seperately, and additionally encapsulates the observable value inside the closure.\n\n```tsx\n\nconst blah = { something: 'banana' }\n\nconst somethingFromUri = new BoxedValue(\n  // getter\n  () =\u003e uriRoutes.someRoute.search.something,\n  // setter\n  (newValue) =\u003e uriRoutes.someRoute.push((uri) =\u003e ({ search: { something: newValue } })),\n)\n\nsomethingFromUri.value // 'foo'\nsomethingFromUri.set('bar')\nsomethingFromUri.value // 'bar'\n\n// Here we omit the setter, so the value is read-only\n// This is effectively just a container encapsulating the value\nconst somethingWrappedToOptimizeObservability = new BoxedValue(\n  () =\u003e blah.something,\n)\n\nsomethingWrappedToOptimizeObservability.value // 'banana'\nsomethingWrappedToOptimizeObservability.set('banana') // does nothing, because no setter\n  \n```\n\n####  BoolValue\n\nA `Value` that is specifically for boolean values. It has a few additional methods to make working with booleans easier.\n\n```tsx\n\nconst isOpen = new BoolValue(true)\n\nisOpen.toggle() // false\nisOpen.toggle() // true\nisOpen.setFalse()\nisOpen.value // false\nisOpen.isTrue // false\nisOpen.setTrue()\nisOpen.isTrue // true\nisOpen.value // false\n\nconst Example = X(() =\u003e \n  \u003c\u003e\n    \u003cbutton onClick={isOpen.toggle}\u003eOpen\u003c/button\u003e\n    \u003cDialog onClose={isOpen.setFalse}\u003e...\u003c/Dialog\u003e\n  \u003c/\u003e\n)\n```\n\n## Documentation\n\n- [Conventions](./Conventions.md)\n  - Plug this into your AI instructions as prompts.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnfour%2Fxcomponent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnfour%2Fxcomponent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnfour%2Fxcomponent/lists"}