{"id":13529751,"url":"https://github.com/AmadeusITGroup/tansu","last_synced_at":"2025-04-01T17:30:49.452Z","repository":{"id":36953834,"uuid":"319979964","full_name":"AmadeusITGroup/tansu","owner":"AmadeusITGroup","description":"tansu is a lightweight, push-based framework-agnostic state management library. It borrows the ideas and APIs originally designed and implemented by Svelte stores and extends them with computed and batch.","archived":false,"fork":false,"pushed_at":"2025-03-04T16:25:35.000Z","size":4618,"stargazers_count":91,"open_issues_count":8,"forks_count":10,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-28T01:10:36.302Z","etag":null,"topics":["agnostic","angular","computed","derived","interop","model","observable","reactive","readable","signal","signals","state","state-management","store","svelte","writable"],"latest_commit_sha":null,"homepage":"https://amadeusitgroup.github.io/tansu/","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/AmadeusITGroup.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}},"created_at":"2020-12-09T14:22:19.000Z","updated_at":"2025-03-04T15:47:54.000Z","dependencies_parsed_at":"2023-02-18T09:31:19.568Z","dependency_job_id":"d3b26d80-c058-46fa-ad2d-d549f5a9207c","html_url":"https://github.com/AmadeusITGroup/tansu","commit_stats":{"total_commits":153,"total_committers":8,"mean_commits":19.125,"dds":0.4444444444444444,"last_synced_commit":"59ca0cc1ce3631a3aa4fe16958cbfd7377dd4b5a"},"previous_names":["amadeusitgroup/ngx-tansu"],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmadeusITGroup%2Ftansu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmadeusITGroup%2Ftansu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmadeusITGroup%2Ftansu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AmadeusITGroup%2Ftansu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AmadeusITGroup","download_url":"https://codeload.github.com/AmadeusITGroup/tansu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246547388,"owners_count":20794970,"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":["agnostic","angular","computed","derived","interop","model","observable","reactive","readable","signal","signals","state","state-management","store","svelte","writable"],"created_at":"2024-08-01T07:00:39.097Z","updated_at":"2025-04-01T17:30:49.445Z","avatar_url":"https://github.com/AmadeusITGroup.png","language":"TypeScript","funding_links":[],"categories":["Recently Updated","State Management"],"sub_categories":["[Jul 15, 2024](/content/2024/07/15/README.md)","Other State Libraries"],"readme":"# Tansu\n\n[![npm](https://img.shields.io/npm/v/@amadeus-it-group/tansu)](https://www.npmjs.com/package/@amadeus-it-group/tansu)\n![build](https://github.com/AmadeusITGroup/tansu/workflows/ci/badge.svg)\n[![codecov](https://codecov.io/gh/AmadeusITGroup/tansu/branch/master/graph/badge.svg)](https://codecov.io/gh/AmadeusITGroup/tansu)\n\nTansu is a lightweight, push-based framework-agnostic state management library.\nIt borrows the ideas and APIs originally designed and implemented by [Svelte stores](https://github.com/sveltejs/rfcs/blob/master/text/0002-reactive-stores.md)\nand extends them with `computed` and `batch`.\n\nMain characteristics:\n\n* small conceptual surface with expressive and very flexible API (functional and class-based);\n* can be used to create \"local\" (module-level or component-level), collaborating stores;\n* can handle both immutable and mutable data;\n* results in compact code with the absolute minimum of boilerplate.\n\nImplementation wise, it is a tiny (1300 LOC) library without any external dependencies.\n\n## Installation\n\nYou can add Tansu to your project by installing the `@amadeus-it-group/tansu` package using your favorite package manager, ex.:\n\n* `yarn add @amadeus-it-group/tansu`\n* `npm install @amadeus-it-group/tansu`\n\n## Usage\n\nCheck out the [Tansu API documentation](https://amadeusitgroup.github.io/tansu/).\n\nThe functional part of the API to manage your reactive state can be categorized into three distinct groups:\n\n  - Base store: `writable`\n  - Computed stores: `derived`, `computed`, `readable`\n  - Utilities: `batch`, `asReadable`, `asWritable`\n\n### writable\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/writable.html)\n\n**Writable: A Fundamental Building Block**\n\n\nA `writable` serves as the foundational element of a \"store\" – a container designed to encapsulate a value, enabling observation and modification of its state. You can change the internal value using the `set` or `update` methods.\n\nTo receive notifications whenever the value undergoes a change, the `subscribe()` method, paired with a callback function, can be employed.\n\n#### Basic usage\n\n```typescript\nimport {writable} from \"@amadeus-it-group/tansu\";\nconst value$ = writable(0);\n\nconst unsubscribe = values$.subscribe((value) =\u003e {\n  console.log(`value = ${value}`);\n});\n\nvalue$.set(1);\nvalue$.update((value) =\u003e value + 1);\n```\n\noutput:\n\n```text\n  value = 0\n  value = 1\n  value = 2\n```\n\n#### Setup and teardown\n\nThe writable's second parameter allows for receiving notifications when at least one subscriber subscribes or when there are no more subscribers.\n\n```typescript\nimport {writable} from \"@amadeus-it-group/tansu\";\n\nconst value$ = writable(0, () =\u003e {\n  console.log('At least one subscriber');\n\n  return () =\u003e {\n    console.log('No more subscriber');\n  }\n});\n\nconst unsubscribe = values$.subscribe((value) =\u003e {\n  console.log(`value = ${value}`);\n});\n\nvalue$.set(1);\nunsubscribe();\n```\n\noutput:\n\n```text\n  At least one subscriber\n  value = 0\n  value = 1\n  No more subscriber\n```\n\n### derived\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/derived.html)\n\nA `derived` store calculates its value based on one or more other stores provided as parameters.\nSince its value is derived from other stores, it is a read-only store and does not have any `set` or `update` methods.\n\n#### Single store\n\n```typescript\nimport {writable, derived} from \"@amadeus-it-group/tansu\";\n\nconst value$ = writable(1);\nconst double$ = derived(value$, (value) =\u003e value * 2);\n\ndouble$.subscribe((double) =\u003e console.log('Double value', double));\nvalue$.set(2);\n```\n\noutput:\n\n```text\nDouble value 2\nDouble value 4\n```\n\n#### Multiple stores\n\n```typescript\nimport {writable, derived} from \"@amadeus-it-group/tansu\";\n\nconst a$ = writable(1);\nconst b$ = writable(1);\nconst sum$ = derived([a$, b$], ([a, b]) =\u003e a + b);\n\nsum$.subscribe((sum) =\u003e console.log('Sum', sum));\na$.set(2);\n```\n\noutput:\n\n```text\nSum 2\nSum 3\n```\n\n#### Asynchronous set\n\nA `derived` can directly manipulate its value using the set method instead of relying on the returned value of the provided function.\nThis flexibility allows you to manage asynchronous operations or apply filtering logic before updating the observable's value.\n\n```typescript\nimport {writable, derived} from \"@amadeus-it-group/tansu\";\n\nconst a$ = writable(0);\nconst asynchronousDouble$ = derived(a$, (a, set) =\u003e {\n  const plannedLater = setTimeout(() =\u003e set(a * 2));\n  return () =\u003e {\n    // This clean-up function is called if there is no listener anymore,\n    // or if the value of a$ changed\n    // In this case, the function passed to setTimeout should not be called\n    // (if it was not called already)\n    clearTimeout(plannedLater);\n  };\n}, -1);\n\nconst evenOnly$ = derived(a$, (a, set) =\u003e {\n  if (a % 2 === 0) {\n      set(a);\n  }\n}, \u003cnumber | undefined\u003eundefined);\n\nasynchronousDouble$.subscribe((double) =\u003e console.log('Double (asynchronous)', double));\nevenOnly$.subscribe((value) =\u003e console.log('Even', value));\n\na$.set(1);\na$.set(2);\n```\n\noutput:\n\n```text\nDouble (asynchronous) -1\nEven 0\nEven 2\nDouble (asynchronous) 4\n```\n\n\n### computed\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/computed.html)\n\nA `computed` store is another variant of a derived store, with the following characteristics:\n\n  - **Implicit Dependencies:** Unlike in a derived store, there is no requirement to explicitly declare dependencies.\n\n  - **Dynamic Dependency Listening:** Dependencies are determined based on their usage. This implies that a dependency not actively used is not automatically \"listened\" to, optimizing resource utilization.\n\n#### Switch map\n\nThis capability to subscribe/unsubscribe to the dependency allows to create switch maps in a natural way.\n\n```typescript\nimport {writable, computed} from \"@amadeus-it-group/tansu\";\n\nconst switchToA$ = writable(true);\nconst a$ = writable(1);\nconst b$ = writable(0);\n\nconst computedValue$ = computed(() =\u003e {\n  if (switchToA$()) {\n    console.log('Return a$');\n    return a$();\n  } else {\n    console.log('Return b$');\n    return b$();\n  }\n});\n\ncomputedValue$.subscribe((value) =\u003e console.log('Computed value:', value));\na$.set(2);\nswitchToA$.set(false);\na$.set(3);\na$.set(4);\nswitchToA$.set(true);\n\n```\n\noutput:\n\n```text\nReturn a$\nComputed value: 1\nReturn a$\nComputed value: 2\nReturn b$\nComputed value: 0\nReturn a$\nComputed value: 4\n```\n\nWhen `switchToA$.set(false)` is called, the subscription to `a$` is canceled, which means that subsequent changes to `a$` will no longer trigger the calculation., which is only performed again when switchToA$ is set back to true.\n\n### readable\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/readable.html)\n\nSimilar to Svelte stores, this function generates a store where the value cannot be externally modified.\n\n```typescript\nimport {readable} from '@amadeus-it-group/tansu';\n\nconst time = readable(new Date(), (set) =\u003e {\n  const interval = setInterval(() =\u003e {\n    set(new Date());\n  }, 1000);\n\n  return () =\u003e clearInterval(interval);\n});\n```\n\n### derived vs computed\n\nWhile derived and computed may appear similar, they exhibit distinct characteristics that can significantly impact effectiveness based on use-cases:\n\n- **Declaration of Dependencies:**\n  - `computed`: No explicit declaration of dependencies is required, providing more flexibility in code composition.\n  - `derived`: Requires explicit declaration of dependencies.\n\n- **Performance:**\n  - `computed`: Better performance by re-running the function only based on changes in the stores involved in the last run.\n  - `derived`: Re-run the function each time a dependent store changes.\n\n- **Asynchronous State:**\n  - `computed`: Unable to manage asynchronous state.\n  - `derived`: Can handle asynchronous state with the `set` method.\n\n- **Skipping Value Emission:**\n  - `computed`: Does not provide a mechanism to skip emitting values.\n  - `derived`: Allows skipping the emission of values by choosing not to call the provided `set` method.\n\n- **Setup and Teardown:**\n  - `computed`: Lacks explicit setup and teardown methods.\n  - `derived`: Supports setup and teardown methods, allowing actions such as adding or removing DOM listeners.\n  - When the last listener unsubscribes and then subscribes again, the derived function is rerun due to its setup-teardown functionality. In contrast, a computed provides the last value without recomputing if dependencies haven't changed in the meantime.\n\nWhile `computed` feels more intuitive in many use-cases, `derived` excels in scenarios where `computed` falls short, particularly in managing asynchronous state and providing more granular control over value emissions.\n\nCarefully choosing between them based on specific requirements enhances the effectiveness of state management in your application.\n\n### Getting the value\n\nThere are three ways for getting the value of a store:\n\n```typescript\nimport {writable, get} from \"@amadeus-it-group/tansu\";\n\nconst count$ = writable(1);\nconst unsubscribe = count$.subscribe((count) =\u003e {\n  // Will be called with the updated value synchronously first, then each time count$ changes.\n  // `unsubscribe` must be called to prevent future calls.\n  console.log(count);\n});\n\n// A store is also a function that you can call to get the instant value.\nconsole.log(count$());\n\n// Equivalent to\nconsole.log(get(count$));\n```\n\n\u003e [!NOTE]\n\u003e Getting the instant value implies the subscription and unsubription on the store:\n\u003e   - It can be important to know in case of setup/teardown functions.\n\u003e   - In the same scope, prefer to store the value once in a local variable instead of calling `store$()` several times.\n\u003e\n\u003e When called inside a reactive context (i.e. inside a computed), getting the value serves to know and \"listen\" the dependent stores.\n\n\n### batch\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/batch.html)\n\nContrary to other libraries like Angular with signals or Svelte with runes, where the callback of a subscription is executed asynchronously (usually referenced as an \"effect\"), we have maintained the constraint of synchronicity between the store changes and their subscriptions in Tansu.\n\nWhile it is acceptable for these frameworks to defer these calls since their goals are well-known in advance (to optimize their final rendering), this is not the case for Tansu, where the goal is to be adaptable to any situation.\n\nThe problem with synchronous subscriptions is that it can create \"glitches\". Subscribers and computed store callbacks that are run too many times can create incorrect intermediate values.\n\nSee for example the [asymmetric diamond dependency problem](https://github.com/AmadeusITGroup/tansu/pull/31), which still exists in Svelte stores, while it has been fixed in Tansu.\n\nThere is also another use case.\n\nLet's have a look at the following example:\n\n```typescript\nimport {writable, derived} from '@amadeus-it-group/tansu';\n\nconst firstName = writable('Arsène');\nconst lastName = writable('Lupin');\nconst fullName = derived([firstName, lastName], ([a, b]) =\u003e `${a} ${b}`);\nfullName.subscribe((name) =\u003e console.log(name)); // logs any change to fullName\nfirstName.set('Sherlock');\nlastName.set('Holmes');\nconsole.log('Process end');\n```\n\noutput:\n\n```text\nArsène Lupin\nSherlock Lupin\nSherlock Holmes\nProcess end\n```\n\nThe fullName store successively went through different states, including an inconsistent one, as `Sherlock Lupin` does not exist! Even if it can be seen as just an intermediate state, it is **fundamental** for a state management to only manage consistent data in order to prevent issues and optimize the code.\n\nIn Tansu, the `batch` is available to defer **synchronously** the subscribers calls, and de facto the dependent `derived` or `computed` calculation to solve all kind of multiple dependencies issues.\n\nThe previous example is resolved this way:\n\n```typescript\nimport {writable, derived, computed, batch} from '@amadeus-it-group/tansu';\n\nconst firstName = writable('Arsène');\nconst lastName = writable('Lupin');\nconst fullName = derived([firstName, lastName], ([a, b]) =\u003e `${a} ${b}`);\n// note that the fullName store could alternatively be create with computed:\n// const fullName = computed(() =\u003e `${firstName()} ${lastName()}`);\nfullName.subscribe((name) =\u003e console.log(name)); // logs any change to fullName\nbatch(() =\u003e {\n    firstName.set('Sherlock');\n    lastName.set('Holmes');\n});\nconsole.log('Process end');\n```\n\noutput:\n\n```text\nArsène Lupin\nSherlock Holmes\nProcess end\n```\n\u003e [!NOTE]\n\u003e - Retrieving the immediate value of a store (using any of the three methods mentioned earlier: calling `subscribe` with a subscriber that will be called with the value synchronously, using `get` or calling the store as a function) always provides the value based on the up-to-date values of all dependent stores (even if this requires re-computations of a computed or a derived inside the batch)\n\u003e - all calls to subscribers (excluding the first synchronous call during the subscribe process) are deferred until the end of the batch\n\u003e - if a subscriber has already been notified of a new value inside the batch (for example, when it is a new subscriber registered within the batch), it won't be notified again at the end of the batch if the value remains unchanged. Subscribers are invoked only when the store's value has changed since their last call.\n\u003e - `batch` can be called inside `batch`. The subscriber calls are performed at the end of the first batch, synchronously.\n\n\n### asReadable\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/asReadable.html)\n\n`asReadable` returns a new store that exposes only the essential elements needed for subscribing to the store. It also includes any extra methods passed as parameters.\n\nThis is useful and widely used to compose a custom store:\n\n  - The first parameter is the writable store,\n  - The second parameter is an object to extend the readable store returned.\n\n```typescript\nimport {writable, asReadable} from \"@amadeus-it-group/tansu\";\n\nfunction createCounter(initialValue: number) {\n  const store$ = writable(initialValue);\n\n  return asReadable(store$, {\n    increment: () =\u003e store$.update((value) =\u003e value + 1),\n    decrement: () =\u003e store$.update((value) =\u003e value - 1),\n    reset: () =\u003e store$.set(initialValue)\n  });\n}\n\nconst counter$ = createCounter(0);\n\ncounter$.subscribe((value) =\u003e console.log('Value: ', value));\n\ncounter$.increment();\ncounter$.reset();\ncounter$.set(2); // Error, set does not exist\n```\noutput:\n```text\nValue: 0\nValue: 1\nValue: 0\n(Error thrown !)\n```\n\n### asWritable\n\n[api documentation](https://amadeusitgroup.github.io/tansu/functions/asWritable.html)\n\n`asWritable` is almost the same as an `asReadable`, with the key difference being its implementation of the [WritableSignal](https://amadeusitgroup.github.io/tansu/interfaces/WritableSignal.html) interface.\n\nIt's useful when you want to connect your computed store to the original one, or implement a custom `set` method. The `set` method can be passed directly in the second parameter or within an object, similar to the usage in `asReadable`.\n\nFor example:\n\n```typescript\nimport {writable, asWritable} from \"@amadeus-it-group/tansu\";\n\nconst number$ = writable(1);\nconst double$ = computed(() =\u003e number$() * 2);\nconst writableDouble$ = asWritable(double$, (doubleNumber) =\u003e {\n  number$.set(doubleNumber / 2);\n});\n/* equivalent to:\n  const writableDouble$ = asWritable(double$, {\n    set: (doubleNumber) =\u003e number$.set(doubleNumber / 2)\n  });\n*/\n\nwritableDouble$.subscribe((value) =\u003e console.log('Value: ', value));\n\nwritableDouble$.set(2); // Nothing is triggered here, as number$ is already set with 1\nwritableDouble$.set(4);\n\n```\noutput:\n```text\nValue: 2\nValue: 4\n```\n\n## Integration in frameworks\n\n### Tansu works well with the Svelte framework\n\nTansu is designed to be and to remain fully compatible with Svelte.\n\n### Tansu works well with the Angular ecosystem\n\nHere is an example of an Angular component using a Tansu store:\n\n```typescript\nimport { Component } from \"@angular/core\";\nimport { AsyncPipe } from '@angular/common';\nimport { Store, computed, get } from \"@amadeus-it-group/tansu\";\n\n// A store is a class extending Store from Tansu\nclass CounterStore extends Store\u003cnumber\u003e {\n  constructor() {\n    super(0); // initialize store's value (state)\n  }\n\n  // implement state manipulation logic as regular methods\n  increment() {\n    // create new state based on the current state\n    this.update(value =\u003e value + 1);\n  }\n\n  reset() {\n    // replace the entire state with a new value\n    this.set(0);\n  }\n}\n\n@Component({\n  selector: \"my-app\",\n  template: `\n    \u003cbutton (click)=\"counter$.increment()\"\u003e+\u003c/button\u003e \u003cbr /\u003e\n\n    \u003c!-- store values can be displayed in a template with the standard async pipe --\u003e\n    Counter: {{ counter$ | async }} \u003cbr /\u003e\n    Double counter: {{ doubleCounter$ | async }} \u003cbr /\u003e\n  `,\n  standalone: true,\n  imports: [AsyncPipe]\n})\nexport class App {\n  //  A store can be instantiated directly or registered in the DI container\n  counter$ = new CounterStore();\n\n  // One can easily create computed values by specifying a transformation function\n  doubleCounter$ = computed(() =\u003e 2 * get(this.counter$));\n}\n```\n\nWhile being fairly minimal, this example demonstrates most of the Tansu APIs with Angular.\n\n* works with the standard `async` pipe out of the box\n* stores can be registered in the dependency injection (DI) container at any level (module or component injector)\n* stores can be used easily with rxjs because they implement the `InteropObservable` interface\n* conversely, rxjs observables (or any object implementing the `InteropObservable` interface) can easily be used with Tansu (e.g. in Tansu `computed` or `derived`).\n\n## Contributing to the project\n\nPlease check the [DEVELOPER.md](DEVELOPER.md) for documentation on building and testing the project on your local development machine.\n\n## Credits and the prior art\n\n* [Svelte](https://github.com/sveltejs/rfcs/blob/master/text/0002-reactive-stores.md) gets all the credit for the initial idea and the API design.\n* [NgRx component stores](https://hackmd.io/zLKrFIadTMS2T6zCYGyHew?view) solve a similar problem with a different approach.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAmadeusITGroup%2Ftansu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAmadeusITGroup%2Ftansu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAmadeusITGroup%2Ftansu/lists"}