{"id":18441063,"url":"https://github.com/silentcatd/react-state-view-controller","last_synced_at":"2025-04-07T22:31:59.022Z","repository":{"id":205697413,"uuid":"714869640","full_name":"SilentCatD/react-state-view-controller","owner":"SilentCatD","description":"A clean state management library for React/React Native. Effectively separate logic and UI","archived":false,"fork":false,"pushed_at":"2024-03-18T02:46:36.000Z","size":852,"stargazers_count":3,"open_issues_count":6,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-06T23:11:38.854Z","etag":null,"topics":["bloc","clean-architecture","model-view-controller","mvc","react","state","state-management","typescript","view-controller"],"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/SilentCatD.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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}},"created_at":"2023-11-06T02:23:28.000Z","updated_at":"2025-01-10T09:39:12.000Z","dependencies_parsed_at":"2023-11-06T03:00:28.897Z","dependency_job_id":"d6ea606f-4de7-47a1-8f2b-2a6fcd3a3745","html_url":"https://github.com/SilentCatD/react-state-view-controller","commit_stats":null,"previous_names":["silentcatd/react-state-view-controller","silentcatd/react-view-controller"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SilentCatD%2Freact-state-view-controller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SilentCatD%2Freact-state-view-controller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SilentCatD%2Freact-state-view-controller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SilentCatD%2Freact-state-view-controller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SilentCatD","download_url":"https://codeload.github.com/SilentCatD/react-state-view-controller/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247741096,"owners_count":20988336,"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":["bloc","clean-architecture","model-view-controller","mvc","react","state","state-management","typescript","view-controller"],"created_at":"2024-11-06T06:34:40.601Z","updated_at":"2025-04-07T22:31:54.012Z","avatar_url":"https://github.com/SilentCatD.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# React State-View-Controller\n\n[![NPM version][npm-image]][npm-url]\n[![codecov][codecov-image]][codecov-url]\n[![npm bundle size][npm-bundle-size-image]][npm-bundle-size-url]\n[![Build][github-build]][github-build-url]\n[![License][github-license]][github-license-url]\n\n[codecov-url]: https://codecov.io/github/SilentCatD/react-state-view-controller\n[codecov-image]: https://codecov.io/github/SilentCatD/react-state-view-controller/graph/badge.svg?token=QVRSQ5QI4D\n[npm-url]: https://www.npmjs.com/package/react-state-view-controller\n[npm-image]: https://img.shields.io/npm/v/react-state-view-controller\n[npm-bundle-size-image]: https://img.shields.io/bundlephobia/min/react-state-view-controller\n[npm-bundle-size-url]: https://bundlephobia.com/package/react-state-view-controller\n[github-license]: https://img.shields.io/github/license/SilentCatD/react-state-view-controller\n[github-license-url]: https://github.com/SilentCatD/react-scoped-provider/blob/main/LICENSE\n[github-build]: https://github.com/SilentCatD/react-state-view-controller/actions/workflows/publish.yml/badge.svg\n[github-build-url]: https://github.com/SilentCatD/react-state-view-controller/actions/workflows/publish.yml\n\n## Overview\n\nIn my opinion, a robust state management system should effectively adhere to the following criteria:\n\n1. **Effective Separation of Logic and UI**\n\n   These two aspects should not be tightly coupled. While it may be acceptable for a single or simple component, when dealing with complex components, screens, or features (comprising multiple screens), the controlling logic should be separated. This separation brings several benefits:\n\n   - Easier Dependency Injection (DI): We can create a network of interdependent `Controller` with clearly defined coupling relationships, adhering to the Component Principle.\n   - State Lifecycle: A `State`/`Model` managing a `View` should align with the `View`'s lifecycle. When the screen is mounted, the `State` and `Controller` managed it is created and evolves throughout the `View` lifecycle, triggering UI layer re-rendering when it changes. When the `View` is unmounted, the managing `Controller` and `State` should be disposed of, cleaning up any unnecessary resources.\n   - Abstraction: It hides the complexity of logic, allowing the UI to trigger a logic flow by simply calling a function in the `Controller`, which may involve complex interactions, operations through APIs, or interactions with other Controllers, and emit `State` as necessary.\n   - Reusability: One of the advantages is the ease of reusing logic between different `Controllers`.\n\n2. **Dependency Injection (DI)**\n\n   A strong state management system should provide an effective mechanism for dependency injection. The `Controller` should be easily accessible for any of the children within its scope, especially those currently managed by it. The `Context` API is a good example of this mechanism, as it allows access to data without having to pass props down to every child component.\n\n3. **Scoped Re-render Trigger and Re-render Filter**\n\n   Regardless of how effectively the framework optimizes this process (e.g., comparing props in `React`), the trigger for the rebuild process should be optimized. It's better if we have more control over where and when this process occurs, even though it's just a trigger for UI re-rendering.\n\n   We should be able to specify where re-rendering may occur and set conditions for it. For example, we might have a component that only re-renders when the `a` property of the `State` object is greater than `5`.\n\n## Usage\n\nThis library is created to address the concerns mentioned above.\n\n## Controller\n\n### Overview\n\nWhen defining a `Controller` for a React view or view group, adopting a subclass approach brings several advantages:\n\n#### 1. Inter-Controller Subscription\n\n`Controller` instances can seamlessly subscribe to and depend on each other, enabling internal triggers for state emissions.\n\n#### 2. Inheritance and Extensibility\n\nUtilizing inheritance facilitates the extension of a `Controller`, promoting the creation of more reusable code.\n\n#### 3. Property Manipulation\n\nCreating, accessing, and modifying properties within a `Controller` becomes a straightforward process.\n\n### State Management\n\nThe core of the state management revolves around the `State` object, responsible for holding data crucial for UI rendering. The UI utilizes the `State` object during its rendering process.\n\n### Base Controller Class\n\nExplore some of the available functions in the base `Controller` class. The provided functionality eliminates the need for redundant code, unless customization is desired.\n\n```ts\nabstract class Controller\u003cT\u003e {\n  // The initial state of the UI\n  constructor(initialState: T)\n\n  // The observable to subscribe to if necessary\n  // Will fire new states and notifying listeners.\n  public get observable(): Observable\u003cT\u003e\n\n  // The current state that the controller is maintaining\n  public get state(): T\n\n  // Emit a new state and trigger all listeners.\n  // Will be merged with existed state\n  protected emit(state: Partial\u003cT\u003e)\n\n  // Override if necessary to clean up any resources.\n  // Will be called when attached view unmounted\n  public async dispose(): Promise\u003cvoid\u003e\n}\n```\n\nA `Controller` will directly interact with the `State` object to mutate it, indirectly causing UI re-rendering.\n\nTo create a new `Controller`, you need to extend the `Controller` class, which provides methods for state mutation and notifying listeners. In the example below, the `CounterController` has `{count: 0}` as its `initialState`. It updates it through methods like `increaseCounter` or `decreaseCounter` using `emit(newState)`.\n\n```ts\nimport { Controller } from 'react-state-view-controller'\n\ntype CounterState = {\n  count: number\n}\n\nclass CounterController extends Controller\u003cCounterState\u003e {\n  constructor() {\n    super({ count: 0 })\n  }\n  increaseCounter() {\n    // Use `emit` to update new state.\n    this.emit({ count: this.state.count + 1 })\n  }\n  decreaseCounter() {\n    this.emit({ count: this.state.count - 1 })\n  }\n}\n```\n\nDo note that the `newState` object must be different from the old `State`. Otherwise, the `Controller` will just skip it. This optimization avoids unnecessary state updates.\n\nWhen using `emit`, the `{...this.state}` is not needed to copy other contents of old state. Object passed in the `emit(newState)` function will be merge with the existed current state.\n\nOne of many common patterns is to handle all of the necessary logic to fetch data from an API in the `Controller`, then emit the data from within the `Controller`. For example:\n\n```ts\nimport { Controller } from 'react-state-view-controller'\n\ntype CounterState = {\n  loading: boolean\n  count: number\n}\n\nclass CounterController extends Controller\u003cCounterState\u003e {\n  ///\n  async fetchCounter() {\n    this.emit({ loading: true })\n\n    // some function to fetch data\n    const newCounter = await fetchDataFromSource()\n\n    // emit new state with changed data\n    this.emit({ loading: false, count: newCounter })\n  }\n  ///\n}\n```\n\nAs you can see, we don't need to worry about UI-related code here in this `async` operation. It is the UI's responsibility to subscribe to changes in the `State` object and render accordingly.\n\nIn the UI, we can then check for the `loading` property of the `State` object and render a `LoadingScreen` if necessary.\n\n### Provider and Dependency Injection (DI)\n\nA `Controller` manages states for a group of child components, and so it must be provided to them for further interaction. This is similar to the `Context` API. In fact, this library uses the `Context` API internally for DI.\n\nTo provide a group of child components with a `Controller`, we can use `ControllerProvider`:\n\n```tsx\n\u003cControllerProvider source={() =\u003e new CounterController()}\u003e\n  \u003cCounterComponent /\u003e\n  \u003cButtonComponent /\u003e\n\u003c/ControllerProvider\u003e\n```\n\nThe `source` parameter take in a function to create a `Controller` and keep it with `useRef` throughout the component's life-span. When the component unmounted, a clean up function will be called automatically, trigger the `dispose` function defined inside the `Controller` class, allowing resources clean up when a `Controller` is not needed anymore.\n\nAnother way to provide a `Controller` to children components would be using `ControllerProvider` but pass to the `source` param an instance of `Controller` instead of a create function:\n\n```tsx\n// existed isntance else where, you would have to manage the references and cleanup yourself though\n\u003cControllerProvider source={counterControllerInstance}\u003e\n  \u003cCounterComponent /\u003e\n  \u003cButtonComponent /\u003e\n\u003c/ControllerProvider\u003e\n```\n\nBut be aware that `Controller` provided this way won't be kept with `useRef` like the first approach, clean up function also won't be called when this `ControllerProvider` unmounted.\n\nThe `CounterComponent` and `ButtonComponent` will now have access to the `CounterController`.\n\nInside the `ButtonComponent` or `CounterComponent`, you can get the provided `CounterController` instance through the `useProvider` hook:\n\n```tsx\nimport { useProvider } from 'react-state-view-controller'\n\nconst ButtonComponent = () =\u003e {\n  // pass in the type of class\n  const controller = useProvider(CounterController)\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e controller.increaseCounter()}\u003eIncrease\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e controller.decreaseCounter()}\u003eDecrease\u003c/button\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\nYou can interact with the provided `CounterController` as needed. Please note that this will get the nearest provided `CounterController`, and if it can't find one (e.g., if a `CounterController` is not provided to this scope), an error will be thrown.\n\nThis hook should not cause re-render when new `State` from `CounterController` is emitted.\n\nThe `Controller` instance passed to `ControllerProvider` will have it's name extracted and used as a resource-key for later query, this mean if you provide a class with the name `A`, you have to use the exact type to query it with `useProvider` hook.\n\nBut for cases that one type of `Controller` may has many implemmentations, subclass-ing each other, and we only want the dependent/client of this controller know it's base type, we can use the `ctor` param in `ProviderController` to overwrite the query type.\n\n```tsx\n// query type will now become `TestController`\n// else where:\n// useProvider(TestController)\n\u003cControllerProvider ctor={TestController} source={()=\u003e SubclassOfTestController()}\u003e\n  \u003cScreen /\u003e\n\u003c/ControllerProvider\u003e,\n```\n\n#### Context hell\n\nNow, we can observe that a wrapper like this indents the file slightly. However, when multiple `ControllerProvider` components are present, the file may appear disorganized.\n\nFor instance, you might encounter a structure like this:\n\n```tsx\n\u003cControllerProvider create={() =\u003e new CounterController()}\u003e\n  \u003cControllerProvider create={() =\u003e new CounterController2()}\u003e\n    \u003cControllerProvider create={() =\u003e new CounterController3()}\u003e\n      \u003cControllerProvider create={() =\u003e new CounterController4()}\u003e\n        \u003cApp /\u003e\n      \u003c/ControllerProvider\u003e,\n    \u003c/ControllerProvider\u003e,\n  \u003c/ControllerProvider\u003e,\n\u003c/ControllerProvider\u003e,\n```\n\nIn such cases, the `MultiProvider` component can be utilized to reduce indentation:\n\n```tsx\nimport { MultiProvider } from 'react-state-view-controller';\n\n\u003cMultiProvider\n  providers={[\n    \u003cControllerProvider create={() =\u003e new CounterController()} /\u003e,\n    \u003cControllerProvider create={() =\u003e new CounterController2()} /\u003e,\n    \u003cControllerProvider create={() =\u003e new CounterController3()} /\u003e,\n    \u003cControllerProvider create={() =\u003e new CounterController4()} /\u003e,\n  ]}\n\u003e\n  \u003cApp /\u003e\n\u003c/MultiProvider\u003e,\n\n```\n\nBoth representations are equivalent. The nested components wrapped each other, granting access to the above provided resources if needed.\n\nIn situations where a single `ControllerProvider` requires access to already provided `Controller` to depend on it, consider separating it into a distinct component:\n\n```tsx\nimport { useProvider } from 'react-state-view-controller'\n\nconst DependentProvider = ({ children }) =\u003e {\n  // We can get the CounterController here because it's provided is above this component.\n  const controller = useProvider(CounterController)\n\n  return \u003cControllerProvider create={() =\u003e new DependentController(controller)}\u003e{children}\u003c/ControllerProvider\u003e\n}\n```\n\nThen this component can then be used as follows:\n\n```tsx\n\u003cMultiProvider\n  providers={[\n    \u003cControllerProvider create={() =\u003e new CounterController()} /\u003e,\n    \u003cDependentProvider /\u003e,\n    \u003cControllerProvider create={() =\u003e new CounterController3()} /\u003e,\n    \u003cControllerProvider create={() =\u003e new CounterController4()} /\u003e,\n  ]}\n\u003e\n  \u003cApp /\u003e\n\u003c/MultiProvider\u003e,\n```\n\n### useAutoDispose\n\nWhile `ControllerProvider` provides an effective way of DI, there are times that this is unecessary, as we just simply want to separate logic of a small view to a controller, dependent child components won't go that deeply nested, but still want to benefit from the auto-cleanup feature.\n\nIn this case, we can use the `useAutoDispose` hook:\n\n```tsx\nconst controller = useAutoDispose(() =\u003e new CounterController())\n```\n\nThis `controller` instance will be kept with `useRef`, making it persist between re-render and will auto call the `dispose` function when current component is unmounted.\n\nController created this way can also be feed into the `source` param of `ProviderController` mentioned above, specifically the second approach, which the `source` param take in an instance. Because the responsibility of persisting the controller instance and auto cleaning up feature is handled already by `useAutoDispose`.\n\n### Builder\n\nTo trigger the re-render process when new State is emitted from `Controller` within the same scope, you can use the `Builder` component.\n\n```tsx\n\u003cBuilder\n  // specify the class type to query\n  source={CounterController}\n  buildWhen={(prev, curr) =\u003e {\n    // Optional: If this function is provided and returns `false`, the re-render trigger will be skipped.\n    // We are provided with the previous state - the state that the component is using for rendering,\n    // and the new state, which will potentially be used for rendering if we return true or omit\n    // this function entirely.\n  }}\n\u003e\n  {(state: number, controller: CounterController) =\u003e {\n    // render content based on state\n    return \u003cView\u003e\u003c/View\u003e\n  }}\n\u003c/Builder\u003e\n```\n\nThis component can also take in directly an instance of `Controller` as the `source` instead of a type.\n\n```tsx\n\u003cBuilder\n  // passing an instance\n  source={counterInstance}\n  buildWhen={(prev, curr) =\u003e true}\n\u003e\n  {(state: number) =\u003e {\n    // render content based on state\n    return \u003cView\u003e\u003c/View\u003e\n  }}\n\u003c/Builder\u003e\n```\n\nThere are hooks for all of this as well, in case you don't need to scope the re-render, but rather need to\nrender a whole component, or you just prefer hook.\n\n```ts\nimport { useBuilder } from 'react-state-view-controller'\n\n// buildWhen is also provided\n// type\nconst [state, controller] = useBuilder(CounterController, (prev, curr) =\u003e true)\n// or instance\nconst state = useBuilder(counterInstance)\n```\n\n### Selector\n\nUsually, we don't need to watch for changes in the whole `State`, but rather just a portion of it, `Selector` can be used to do state-filtering.\n\n```tsx\n// only re-render when state.count5 changed\n\u003cSelector source={MultiCounterController} selector={(state) =\u003e state.count5}\u003e\n  {(value, controller) =\u003e {\n    // render content based on selected value\n    return \u003cView\u003e\u003c/View\u003e\n  }}\n\u003c/Selector\u003e\n```\n\nIt also support direct controller instance variant\n\n```tsx\n\u003cSelector source={multiCounterInstance} selector={(state) =\u003e state.count5}\u003e\n  {(value) =\u003e {\n    // render content based on selected value\n    return \u003cView\u003e\u003c/View\u003e\n  }}\n\u003c/Selector\u003e\n```\n\nAlternatively, we can use the `useSelector` hook\n\n```ts\nimport { useSelector } from 'react-state-view-controller'\n\n// only trigger re-render when `state.count5` changed\nconst [value, controller] = useSelector(MultiCounterController, (state) =\u003e state.count5)\n// or the equivalent\nconst value = useSelector(multiCounterInstance, (state) =\u003e state.count5)\n```\n\n### Listener\n\nFor triggering actions on the UI without causing a re-render, you can use this Component:\n\n```tsx\n\u003cListener\n  // specify the type\n  source={CounterController}\n  // callback when new state emitted\n  listener={(state: number, controller: CounterController) =\u003e console.log(state)}\n  // for state filter, similar with Builder\n  listenWhen={(prev: number, current: number) =\u003e true}\n\u003e\n  \u003cView\u003e\u003c/View\u003e\n\u003c/Listener\u003e\n```\n\nYou will be provided with the `listener` callback to be called when the State changes. There is also `listenWhen`, similar to `Builder.buildWhen`, to filter changes in `State` to listen to as needed.\n\nThe controller instance variant:\n\n```tsx\n\u003cListener\n  source={counterInstance}\n  listener={(state: number) =\u003e console.log(state)}\n  listenWhen={(prev: number, current: number) =\u003e true}\n\u003e\n  \u003cView\u003e\u003c/View\u003e\n\u003c/Listener\u003e\n```\n\nThe hooks for this feature:\n\n```tsx\nimport { useListener } from 'react-state-view-controller'\n\nconst controller = useListener(\n  CounterController,\n  (state: number, controller: CounterController) =\u003e console.log(state), // callback when state changed\n  (prev, curr) =\u003e true, // callback filter\n)\n// or\nconst controller = useListener(\n  counterInstance,\n  (state) =\u003e console.log(state), // callback when state changed\n  (prev, curr) =\u003e true, // callback filter\n)\n```\n\nNote that this hook is not intended by default to cause re-render, it just simply triggers callback\n\n## For other types\n\nOf course, a view `Controller` is not the only thing that we need to DI in typical production app, we also have services, repositories, models,... or other types of data.\n\nTo acommondate these, you can check out the [react-scoped-provider](https://www.npmjs.com/package/react-scoped-provider) library, which is fully compatible with this library, in fact, the `useProvider` hook is just re-import and re-export from it.\n\n## Conclusion\n\n[Example](https://github.com/SilentCatD/react-state-view-controller/tree/main/example)\n\nPlease open an issue if you spot any problems or for discussions. I would be happy to receive feedback.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsilentcatd%2Freact-state-view-controller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsilentcatd%2Freact-state-view-controller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsilentcatd%2Freact-state-view-controller/lists"}