{"id":26221685,"url":"https://github.com/snowflyt/troza","last_synced_at":"2025-04-17T08:46:56.982Z","repository":{"id":280607485,"uuid":"942577635","full_name":"Snowflyt/troza","owner":"Snowflyt","description":"Intuitive state management for React and Vanilla, easier than ever.","archived":false,"fork":false,"pushed_at":"2025-03-10T14:51:47.000Z","size":1069,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-29T06:04:48.016Z","etag":null,"topics":["react","state","state-management","store","type-safe","typesafe","typescript","typescript-react","vanilla-javascript","vanilla-js"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/troza","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Snowflyt.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":"2025-03-04T10:24:17.000Z","updated_at":"2025-03-20T21:52:36.000Z","dependencies_parsed_at":"2025-03-04T10:35:46.735Z","dependency_job_id":"ac516be0-4929-404e-a96f-d82890987415","html_url":"https://github.com/Snowflyt/troza","commit_stats":null,"previous_names":["snowflyt/troza"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftroza","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftroza/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftroza/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftroza/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Snowflyt","download_url":"https://codeload.github.com/Snowflyt/troza/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249326666,"owners_count":21251754,"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":["react","state","state-management","store","type-safe","typesafe","typescript","typescript-react","vanilla-javascript","vanilla-js"],"created_at":"2025-03-12T16:29:12.281Z","updated_at":"2025-04-17T08:46:56.960Z","avatar_url":"https://github.com/Snowflyt.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eTroza\u003c/h1\u003e\n\n`npm install troza` makes _intuitive_ state management easier than ever.\n\n[![downloads](https://img.shields.io/npm/dm/troza.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://www.npmjs.com/package/troza)\n[![version](https://img.shields.io/npm/v/troza.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://www.npmjs.com/package/troza)\n[![minzipped size](https://img.shields.io/bundlephobia/minzip/troza.svg?label=bundle%20size\u0026style=flat\u0026colorA=000000\u0026colorB=000000)](https://bundlephobia.com/package/troza)\n[![coverage status](https://img.shields.io/coverallsCoverage/github/Snowflyt/troza?branch=main\u0026style=flat\u0026colorA=000000\u0026colorB=000000)](https://coveralls.io/github/Snowflyt/troza?branch=main)\n[![license](https://img.shields.io/npm/l/troza.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://github.com/Snowflyt/troza)\n\nTroza is a lightweight, TypeScript-friendly state management library with easy composability.\n\n- A single **immutable** state tree with mutable-style updates.\n- **Auto dependency tracking** for **computed states** and your components.\n- Direct action access on your store—no extra hooks required.\n\nYou can try a live demo [here](https://githubbox.com/Snowflyt/troza/tree/main/examples/demo-react).\n\n### First create a store\n\n```typescript\nimport { create } from \"troza\";\n\nconst counterStore = create({\n  count: 0,\n  incBy(by: number) {\n    this.count += by;\n  },\n});\n\nexport default counterStore;\n```\n\n### Then use it in your components, and that’s it!\n\n```tsx\nimport { useStore } from \"troza/react\";\nimport counterStore from \"../stores/counter\";\n\n// Actions are directly accessible via `store.action()`\nconst { incBy } = counterStore;\n\nfunction Counter() {\n  // Only re-render when `count` changes\n  const { count } = useStore(counterStore);\n  return \u003cdiv\u003eCount: {count}\u003c/div\u003e;\n}\n\nfunction CounterControls() {\n  return \u003cbutton onClick={() =\u003e incBy(1)}\u003eOne up\u003c/button\u003e;\n}\n```\n\nAlso, check out the [`hookify`](#create-hooks-for-stores-to-avoid-boilerplate) utility to create custom hooks for your stores—this enhances compatibility with React DevTools and reduces boilerplate code.\n\n## Recipes\n\n### Create computed states\n\nTroza supports automatically cached computed states. You can define these computed states using the `get` helper function:\n\n```typescript\nimport { create, get } from \"troza\";\n\nconst counterStore = create({\n  count: 0,\n  [get(\"doubled\")]() {\n    return this.count * 2;\n  },\n  [get(\"quadrupled\")]() {\n    // Computed states can be accessed within other computed states\n    return this.doubled * 2;\n  },\n  increment() {\n    // ...or within actions\n    if (this.quadrupled \u003e 10) {\n      throw new Error(\"Counter too high\");\n    }\n    this.count++;\n  },\n});\n```\n\nThe syntax is similar to standard JavaScript getters, but by using the `get` helper, Troza ensures a better TypeScript experience.\n\nComputed states are **cached** and only re-evaluated when their dependencies change. You can access computed states just like regular state values in your components, without worrying about unnecessary re-renders:\n\n```tsx\nconst store = create({\n  count: 0,\n  nums: [1, 2, 3],\n  [get(\"oddNums\")]() {\n    // Only re-run when `nums` changes\n    return this.nums.filter((num) =\u003e num % 2 === 0);\n  },\n});\n\nfunction MyComponent() {\n  // Does not re-render when unrelated state (e.g., `count`) changes\n  const { oddNums } = useStore(store);\n  // ...\n}\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eCaveat:\u003c/strong\u003e Computed states are not allowed to mutate the state\u003c/summary\u003e\n\nComputed states cannot mutate the state because the state passed to the computed state is read-only. For example, the following code will not work:\n\n```typescript\nconst todoStore = create({\n  loading: false,\n  todoId: 1,\n  async [get(\"todo\")]() {\n    // This will throw an error\n    this.loading = true;\n    // The rest of the function will never run\n    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${this.todoId}`);\n    const todo = await response.json();\n    this.loading = false;\n    return todo;\n  },\n});\n\nfunction TodoList() {\n  const todo = use(useStore(todoStore).todo);\n  // ...\n}\n```\n\nAnd you’ll see an error like:\n\n```text\nUncaught (in promise) TypeError: setting getter-only property \"loading\"\n```\n\nIf you need to update the state in a computed state, consider using an action instead—actions can read and write to the state and can also return a value like a computed state.\n\n\u003c/details\u003e\n\n### Create hooks for stores to avoid boilerplate\n\n`useStore` is a straightforward way to use the store, but you can also create custom hooks using `hookify` to make your code more friendly to React DevTools and reduce boilerplate.\n\n```typescript\nimport { create } from \"troza\";\nimport { hookify } from \"troza/react\";\n\nconst counterStore = create({\n  /* ... */\n});\n\nexport default counterStore;\n\nexport const useCounterStore = hookify(\"count\", bookStore);\n```\n\nThe first argument of `hookify` is the store’s name, which helps during debugging. You can omit this argument if it’s not needed.\n\nThen, you can use `useCounterStore` in your components:\n\n```typescript\nimport { useCounterStore } from \"../stores/counter\";\n\nfunction Counter() {\n  const { count } = useCounterStore();\n  // ...\n}\n```\n\n### Using selectors\n\nIf you prefer [Zustand](https://github.com/pmndrs/zustand)-like selectors over relying solely on auto dependency tracking, that’s totally fine. You can pass an optional selector function to `useStore` (or your custom hook) to explicitly pick the state your component depends on:\n\n```typescript\nfunction BookList() {\n  // Select a single state\n  const readingBook = useStore(bookStore, (state) =\u003e state.reading);\n\n  // Select multiple states\n  const [readingBook, readBooks] = useStore(bookStore, (state) =\u003e [\n    state.readingBook,\n    state.readBooks,\n  ]);\n\n  // Derive state directly in the selector, eliminating the need for `useMemo`\n  const tomes = useBookStore((state) =\u003e\n    // Re-run only when `bookshelf.books` changes\n    state.bookshelf.books.filter((book) =\u003e book.pages \u003e= 300),\n  );\n\n  // ...\n}\n```\n\nBy using selectors, you explicitly define which parts of the state your component relies on instead of depending solely on auto dependency tracking. However, auto dependency tracking is still active inside selectors, so you can safely select multiple states without causing unnecessary re-renders.\n\nKeep in mind that since selectors are memoized, they should depend solely on the store state—not on external variables (e.g., `props` or states returned by `useState` or `useReducer`). If you need to use external variables, consider defining an unmemoized version of `useStore` yourself.\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see the unmemoized version of \u003ccode\u003euseStore\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport type { Store } from \"troza\";\n\nexport function useUnmemoizedStore\u003c\n  State extends object,\n  Computed extends object,\n  Actions extends Record\u003cstring, (...args: never) =\u003e unknown\u003e,\n  const Selected = State,\n\u003e(\n  store: Store\u003cState, Computed, Actions\u003e,\n  selector?: (state: Readonly\u003cState \u0026 Computed\u003e) =\u003e Selected,\n): Selected {\n  selector = selector || ((state) =\u003e state as unknown as Selected);\n  return useSyncExternalStore(\n    (onStoreChange) =\u003e store.$subscribe(onStoreChange),\n    () =\u003e selector(store.$get() as any),\n    () =\u003e selector(store.$getInitialState() as any),\n  );\n}\n```\n\n\u003c/details\u003e\n\n### Directly accessing state and computed states\n\nBesides calling actions on your store, you can also directly read state and computed states from the store object. For example:\n\n```typescript\nconst store = create({\n  count: 0,\n  [get(\"doubled\")]() {\n    return this.count * 2;\n  },\n});\n\nconsole.log(store.count); // 0\nstore.count++;\nconsole.log(store.count); // 1\nconsole.log(store.doubled); // 2\n```\n\nWhile this direct access is possible, you still have to use `useStore` or `useCounterStore` to subscribe to state changes for proper re-rendering. For example, the following `Counter` component won’t re-render when `store.count` changes:\n\n```tsx\nconst counterStore = create({ count: 0 });\n\nfunction Counter() {\n  return \u003cdiv\u003e{store.count}\u003c/div\u003e;\n}\n\nfunction CounterControls() {\n  return \u003cbutton onClick={() =\u003e store.count++}\u003eOne up\u003c/button\u003e;\n}\n```\n\n**Caveat:** Direct access is provided to simplify updating a single state value without a dedicated action. However, Troza cannot batch updates when multiple state changes are made via direct access, which can hurt performance and lead to unexpected behavior. It’s best to use actions for multiple state updates.\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see an example of anti-intuitive behavior caused by direct access\u003c/summary\u003e\n\nConsider the following example:\n\n```typescript\nconst store = create({ nums: [1, 2, 3] });\n\nconst f = () =\u003e {\n  const nums = store.nums;\n  nums.push(4);\n  nums.push(5);\n};\n\nfunction MyComponent() {\n  const { nums } = useStore(store);\n  console.log(nums);\n  // ...\n}\n```\n\nYou might expect `[1, 2, 3, 4, 5]` when `f` is called, but the actual result is `[1, 2, 3, 4]`:\n\n- `const nums = state.nums` retrieves a proxy of the `store.nums` array.\n- `nums.push(4)` mutates the proxy and **eagerly flushes** the changes to the actual state.\n- Each flush creates a new state instead of mutating the original one in Troza; here, `store.nums` becomes `[1, 2, 3, 4]`.\n- However, `nums` still holds the old proxy, so `nums.push(5)` mutates that disconnected proxy.\n\nIn simple cases, you can avoid this by not caching the array in a variable and directly calling `store.nums.push(4)` and `store.nums.push(5)`. In more complex scenarios, this anti-intuitive behavior can be challenging to debug.\n\nTo batch updates manually, you can use `store.$set`, `store.$patch` or `store.$update` (see the [async actions section](#async-actions)). However, the best practice is to avoid directly mutating state when you need to make multiple changes—instead, use actions, as they batch updates until the action is complete, preventing this anti-intuitive behavior:\n\n```typescript\nconst store = create({\n  nums: [1, 2, 3],\n  // Everything works as expected\n  f() {\n    const nums = this.nums;\n    nums.push(4);\n    nums.push(5);\n  },\n});\n```\n\n\u003c/details\u003e\n\n### Anonymous actions\n\nTroza provides a way to directly invoke a function as an action without defining it as a named action. This is useful for one-off actions:\n\n```typescript\nconst store = create({ name: \"John Doe\", count: 0 });\n\nstore.$act(function () {\n  this.name = \"Jane Doe\";\n  this.count++;\n});\n```\n\nIt is also possible to define actions in a more functional style without the need to unifying actions inside the store object, by making use of `$act`, but it is not very recommended and violates Troza’s design principles.\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see an example of defining actions in a functional style\u003c/summary\u003e\n\nYou can create a helper function like this:\n\n```typescript\nconst createAction =\n  \u003c\n    State extends object,\n    Computed extends object,\n    Actions extends Record\u003cstring, (...args: never) =\u003e unknown\u003e,\n    Args extends unknown[],\n    R,\n  \u003e(\n    store: Store\u003cState, Computed, Actions\u003e,\n    fn: (\n      state: ThisParameterType\u003cParameters\u003cStore\u003cState, Computed, Actions\u003e[\"$act\"]\u003e[0]\u003e,\n      ...args: Args\n    ) =\u003e R,\n  ) =\u003e\n  (...args: Args): R =\u003e\n    store.$act(function () {\n      return fn(this as any, ...args);\n    });\n```\n\nAnd then define actions like this:\n\n```typescript\nconst incBy = createAction(counterStore, (state, by: number) =\u003e {\n  state.count += by;\n});\n```\n\nOr if you prefer a cleaner syntax, try this version:\n\n```typescript\nconst createDef =\n  \u003c\n    State extends object,\n    Computed extends object,\n    Actions extends Record\u003cstring, (...args: never) =\u003e unknown\u003e,\n  \u003e(\n    store: Store\u003cState, Computed, Actions\u003e,\n  ) =\u003e\n  \u003cArgs extends unknown[], R\u003e(\n    fn: (\n      state: ThisParameterType\u003cParameters\u003cStore\u003cState, Computed, Actions\u003e[\"$act\"]\u003e[0]\u003e,\n      ...args: Args\n    ) =\u003e R,\n  ) =\u003e\n  (...args: Args): R =\u003e\n    store.$act(function () {\n      return fn(this as any, ...args);\n    });\n```\n\nAnd define actions like this:\n\n```typescript\nconst def = createDef(counterStore);\n\nconst incBy = def((state, by: number) =\u003e {\n  state.count += by;\n});\n```\n\nWhile this approach is possible, it is not recommended because it violates Troza’s design principles. Troza is designed to be used with actions defined directly on the store object, which makes it easier to understand and maintain the code.\n\nAlso, such syntax does not handle well with generic TypeScript functions, as it requires manual type annotations for `state` when you define an action with generic type parameters:\n\n```typescript\nconst f = def(\u003cT\u003e(state, value: T) =\u003e {\n  //               ~~~~~\n  // Parameter 'state' implicitly has an 'any' type.ts(7006)\n});\n```\n\n\u003c/details\u003e\n\n### Async actions\n\nYou can define async actions in Troza without any extra effort:\n\n```typescript\nconst todoStore = create({\n  loading: true,\n  todoId: 1,\n  todo: null as { id: number; title: string; completed: boolean } | null,\n  async fetchTodo() {\n    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${this.todoId}`);\n    const todo = await response.json();\n    this.loading = false;\n    this.todo = todo;\n  },\n});\n```\n\nHowever, async actions prevent Troza from batching updates, which usually won’t cause extra re-renders but will trigger `$subscribe` on every update (see [the later section](#using-in-vanilla-javascript)). If you prefer to batch updates manually, you can use `this.$set`, `this.$patch` or `this.$update` (which are also accessible via `store.$set`, `store.$patch` and `store.$update`).\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see an example of using \u003ccode\u003ethis.$set\u003c/code\u003e, \u003ccode\u003ethis.$patch\u003c/code\u003e and \u003ccode\u003ethis.$update\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nconst todoStore = create({\n  loading: true,\n  todoId: 1,\n  todo: null as { id: number; title: string; completed: boolean } | null,\n  async fetchTodos() {\n    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${this.todoId}`);\n    const todo = await response.json();\n    // `$update` enables mutable-style updates\n    this.$update((state) =\u003e {\n      state.loading = false;\n      state.todo = todo;\n    });\n    // Or\n    this.$set({ loading: false, todoId: this.todoId, todo });\n    // Or\n    this.$set((prev) =\u003e ({\n      ...prev,\n      loading: false,\n      todo: todo.slice(0, 10),\n    }));\n    // `$patch` is similar to `$set` but updates with partial state\n    this.$patch({ loading: false, todo });\n    this.$patch((prev) =\u003e ({ loading: !prev.loading, todo });\n  },\n});\n```\n\nNote that you don’t need to use `this.$set`, `this.$patch` or `this.$update` if you are using a synchronous action, as Troza will automatically batch updates for you.\n\n\u003c/details\u003e\n\n### Async computed states\n\nWhile the example in the [async actions section](#async-actions) is a possible way for fetching data, you can define an async computed state for a simpler and cleaner approach:\n\n```typescript\nconst todoStore = create({\n  todoId: 1,\n  async [get(\"todo\")]() {\n    // Only re-run when `todoId` changes\n    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${this.todoId}`);\n    return await response.json();\n  },\n});\n```\n\nAnd use it with React suspense and the new `use` hook introduced in React 19:\n\n```tsx\nimport { Suspense, use } from \"react\";\nimport { useTodoStore } from \"../stores/todo\";\n\nfunction Todo() {\n  const todo = use(useTodoStore((state) =\u003e state.todo));\n  return \u003cdiv\u003e{todo.title}\u003c/div\u003e;\n}\n\nfunction App() {\n  return (\n    \u003cSuspense fallback={\u003cdiv\u003eLoading...\u003c/div\u003e}\u003e\n      \u003cTodo /\u003e\n    \u003c/Suspense\u003e\n  );\n}\n```\n\n### Slices pattern\n\nTroza creates stores as plain objects, making them easy to compose. You can create slices for different parts of your store and then combine them using object spread:\n\n```typescript\nconst counterSlice = {\n  count: 0,\n  increment() {\n    this.count++;\n  },\n};\n\nconst nameSlice = {\n  name: \"John Doe\",\n  changeName(name: string) {\n    this.name = name;\n  },\n};\n\nconst store = create({ ...counterSlice, ...nameSlice });\n```\n\nThis is ideal when you want to share common logic across multiple stores or break a large store into smaller, manageable parts.\n\nFor instance, if many of your stores require similar loading logic with an `isLoading` state and a `showLoading` flag to prevent UI flickering, you can create a reusable slice for that logic:\n\n```typescript\nconst loadingSlice = {\n  isLoading: false,\n  showLoading: false,\n  startLoading() {\n    this.isLoading = true;\n    // Set `showLoading` after a delay to avoid UI flickering\n    setTimeout(() =\u003e {\n      if (!this.isLoading) return;\n      this.showLoading = this.isLoading;\n    }, 300);\n  },\n  stopLoading() {\n    this.isLoading = false;\n    this.showLoading = false;\n  },\n};\n\nconst myStore = create({\n  ...loadingSlice,\n  async loadSomething() {\n    this.startLoading();\n    // Load something...\n    this.stopLoading();\n    return loadedData;\n  },\n});\n```\n\nFor better TypeScript type inference of computed states, you can use the `slice` helper function to create slices. It simply returns the `slice` object as is but helps with type inference:\n\n```typescript\nimport { get, slice } from \"troza\";\n\nconst counterSlice = slice({\n  count: 0,\n  [get(\"doubled\")]() {\n    return this.count * 2;\n  },\n  increment() {\n    if (this.doubled \u003e 10) {\n      //     ^ TypeScript will infer `this.doubled` as a number\n      throw new Error(\"Counter too high\");\n    }\n    this.count++;\n  },\n});\n```\n\nIt is recommended to use the `slice` helper even in JavaScript-only projects, as it provides better editor completion.\n\n### Using in vanilla JavaScript\n\nTroza is a universal library, not tied to any specific framework, so you can use it in vanilla JavaScript as well. The React bindings are just simple wrappers around the core library.\n\nSuppose you have a store like this:\n\n```typescript\nconst store = create({\n  count: 0,\n  [get(\"doubled\")]() {\n    return this.count * 2;\n  },\n  incBy(by: number) {\n    this.count += by;\n  },\n\n  bookshelf: {\n    books: [\n      { title: \"Refactoring\", pages: 448, read: true },\n      { title: \"Clean Code\", pages: 464, read: false },\n    ],\n  },\n  [get(\"readBooks\")]() {\n    return this.bookshelf.books.filter((book) =\u003e book.read);\n  },\n  markRead(title: string) {\n    this.bookshelf.books.find((book) =\u003e book.title === title)?.read = true;\n  },\n  addBook(title: string, pages: number) {\n    const book = { title, pages, read: false };\n    this.bookshelf.books.push(book);\n    return book;\n  },\n});\n```\n\nYou can use the following methods to interact with the store:\n\n- `$act`: Invoke a function as an action directly on the store.\n- `$get`: Retrieve the current state.\n- `$getInitialState`: Retrieve the initial state.\n- `$set`: Set the state directly.\n- `$patch`: Set the state with partial updates.\n- `$update`: Update the state with mutable-style updates (batched).\n- `$subscribe`: Subscribe to state changes.\n- Actions: Call actions directly on the store.\n\nFor example:\n\n```typescript\nconst state1 = store.$get();\n// { count: 0, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n\n// Call an action directly\nstore.incBy(2);\n\nconst state2 = store.$get();\n// { count: 2, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n\n// Each action creates a new state\nconsole.log(state1 === state2); // false\nconsole.log(state1); // { count: 0, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n\n// Computed states are cached and only recalculated when their dependencies change\nconsole.log(state1.doubled === state2.doubled); // false\nconsole.log(state1.readBooks === state2.readBooks); // true\n\n// Subscribe to changes\nconst unsubscribe = store.$subscribe((state, prevState) =\u003e {\n  console.log(\"State changed\\nm:\", prevState, \"\\n\", state);\n});\n\nstore.inc();\n// State changed\n// from: { count: 2, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n// to: { count: 3, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\nstore.addBook(\"JavaScript: The Definitive Guide\", 706);\n// State changed\n// from: { count: 3, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n// to: { count: 3, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\nstore.markRead(\"JavaScript: The Definitive Guide\");\n// State changed\n// from: { count: 3, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n// to: { count: 3, bookshelf: [...], doubled: [Getter], readBooks: [Getter] }\n```\n\n### Using `$subscribe` with selector\n\nThe `$subscribe` method can also accept a selector function to listen only to changes in a specific part of the state:\n\n```typescript\n$subscribe(subscriber): () =\u003e void;\n$subscribe(selector, subscriber): () =\u003e void;\n```\n\nThe selector passed to `$subscribe` is memoized based on its auto-tracked dependencies, meaning it only re-runs when the selected state changes.\n\n```typescript\nstore.$subscribe(\n  (state) =\u003e state.count,\n  (count, prevCount) =\u003e {\n    console.log(\"Count changed:\", count, prevCount);\n  },\n);\n```\n\nThis behavior is similar to [`watch`](https://vuejs.org/guide/essentials/watchers.html#watch-source-types) in Vue 3—except that Troza always operates on immutable objects rather than reactive ones.\n\nRemember that the same rules for selectors in `useStore` apply here, so avoid using external variables within the selector.\n\n### Watch for changes in a specific part of the state\n\nYou might notice an interesting pattern in `$subscribe`—you can pass an empty subscriber function and rely solely on the selector to trigger side effects:\n\n```typescript\nstore.$subscribe((state) =\u003e {\n  document.querySelector(\"#count\").innerText = state.count;\n}, () =\u003e {});\n```\n\nDue to the memoized nature of the selector, it only re-runs when the selected part of the state changes, thereby avoiding unnecessary re-renders. This is similar to `useEffect` in React, but without needing a dependency array—or more precisely, [`watchEffect`](https://vuejs.org/guide/essentials/watchers.html#watcheffect) in Vue 3.\n\nFor improved code readability and to access the previous state, Troza provides a `$watch` method:\n\n```typescript\nconst unwatch = store.$watch((state, prevState) =\u003e {\n  document.querySelector(\"#count\").innerText = state.count;\n});\n```\n\nFor React users, the React bindings offer a `useWatch` hook that automatically unsubscribes when the component unmounts:\n\n```typescript\nimport { useWatch } from \"troza/react\";\nimport userStore from \"../stores/user\";\n\nfunction UserProfile() {\n  const [userProfile, setUserProfile] = React.useState(null);\n\n  useWatch(userStore, async (state, prevState) =\u003e {\n    const profile = await fetchUserProfile(state.userId);\n    setUserProfile(profile);\n  });\n\n  // ...\n}\n```\n\nYou might have noticed that `useWatch` is exactly an alias of `useStore` that allows you to access the previous state, but it is more readable and explicitly intended for watching changes.\n\nNote that, like subscribers, watchers are only attempted to be triggered after the next state change rather than immediately upon creation. Therefore, avoid using them to initialize state or trigger side effects that should run as soon as the component mounts.\n\n### Redux DevTools\n\nInstall the [Redux DevTools Chrome extension](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) to use the DevTools middleware.\n\n```typescript\nimport { devtools } from \"troza/middleware\";\n\nconst counterStore = create(devtools({ count: 0, ... }));\nconst useCounterStore = hookify(\"count\", counterStore);\n```\n\nBy default, Troza groups all stores into a single global group for each application. That is to say, you can see all stores in a single Redux DevTools tab. As is shown in the screenshot below. Note that computed states are also shown in the DevTools, with a `~getter:` prefix.\n\n![DevTools middleware screenshot](./docs/devtools-screenshot.png)\n\nAll store with the same group name will be grouped together as a Redux DevTools tab. To specify a custom group name for a store, you can pass it as the second argument to `devtools`:\n\n```typescript\nconst counterStore = create(devtools({ count: 0, ... }, { group: \"Counter\" }));\n```\n\nYou can also specify the name of the store in the DevTools by passing a `name` option:\n\n```typescript\nconst counterStore = create(devtools({ count: 0, ... }, { name: \"count\" }));\n```\n\nYou don’t have to pass this option if you already used the `hookify` utility, as it automatically sets the store name for you. If the name of a store cannot be determined, it will be displayed as “anonymous”.\n\n### Use Troza more than a store\n\nTroza is primarily designed for managing state across your entire application (i.e., a store), but it’s flexible enough to be used in other ways.\n\nFor example, you can define a `useTroza` hook as a `useState` replacement, and use it like this:\n\n```tsx\nfunction Counter() {\n  const state = useTroza({ count: 0 });\n\n  return (\n    \u003cdiv\u003e\n      \u003cspan\u003eCount: {state.count}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e state.count++}\u003eOne up\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eClick to see the implementation of \u003ccode\u003euseTroza\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nexport function useTroza\u003cSlice extends object\u003e(\n  sliceFactory: () =\u003e Parameters\u003ctypeof create\u003cSlice\u003e\u003e[0],\n): ReturnType\u003ctypeof create\u003cSlice\u003e\u003e;\nexport function useTroza\u003cSlice extends object\u003e(\n  slice: Parameters\u003ctypeof create\u003cSlice\u003e\u003e[0],\n): ReturnType\u003ctypeof create\u003cSlice\u003e\u003e;\nexport function useTroza(slice: object) {\n  const store = React.useMemo(() =\u003e create(typeof slice === \"function\" ? slice() : slice), []);\n  React.useSyncExternalStore(\n    (onStoreChange) =\u003e store.$subscribe(onStoreChange),\n    () =\u003e store.$get(),\n    () =\u003e store.$getInitialState(),\n  );\n  return store;\n}\n```\n\n\u003c/details\u003e\n\nWhile this approach might seem attractive, remember that Troza is not specifically designed for such use cases. The demonstrated `useTroza` hook is just for illustration and isn’t included in the official library.\n\nIf this pattern really appeals to you, consider using [Valtio](https://github.com/pmndrs/valtio), which provides a similar API as the illustration above.\n\n## FAQ\n\n### How should I organize my store?\n\nTroza is not opinionated about how to structure your stor. You can use the slice pattern to create one global store or create multiple stores for different parts of your application.\n\nBelow is an example of organizing multiple stores in your project:\n\n```text\n├── src\n│   ├── components\n│   │   ├── Counter.tsx\n│   │   └── BookList.tsx\n│   ├── stores\n│   │   ├── counter.ts\n│   │   └── book.ts\n│   ├── App.tsx\n│   └── main.tsx\n├── package.json\n├── package-lock.json\n└── tsconfig.json\n```\n\nIn each file in the `stores` folder, you can create a store like this:\n\n```typescript\nimport { create } from \"troza\";\nimport { hookify } from \"troza/react\";\n\nconst counterStore = create({\n  /* ... */\n});\n\nexport default counterStore;\n\nexport const useCounterStore = hookify(\"counter\", counterStore);\n```\n\n### Why `this` everywhere?\n\nThough `this` in JavaScript sometimes gets a bad rep, not in Troza. Under the hood, Troza statically binds `this` to the store rather than dynamically binding it to the function context. This design lets you destructure actions directly from the store without manually binding `this`.\n\nUsing `this` makes the syntax cleaner and more TypeScript-friendly. Without it, you’d have to write something like `actionName: (state) =\u003e (...args) =\u003e {}` for every action or use less TypeScript-friendly patterns that require manual type annotations.\n\n### What’s the magic behind auto dependency tracking and mutable-style updates?\n\nUnder the hood, Troza uses two distinct proxy systems for different purposes:\n\n- **Auto Dependency Tracking:** Troza leverages [proxy-compare](https://github.com/dai-shi/proxy-compare) in computed states (and selectors) to track dependencies and re-run them only when those dependencies change.\n- **Mutable-Style Updates:** For applying mutable-style updates on an immutable state tree, Troza employs a custom proxy system (implemented in `src/index.ts`). Previously, Immer was used, but it was replaced for better integration and reduced bundle size.\n\nThe proxy system is largely inspired by [Valtio](https://github.com/pmndrs/valtio), another proxy-based state management library, but with some key differences:\n\n- Troza maintains an immutable state tree internally and applies updates in a mutable style. Each “mutation” is recorded in a temporary draft and then used to create a new state when the action completes, rather than directly mutating the state and notifying subscribers as Valtio does.\n- Troza lazily proxies the state to avoid unnecessary overhead, whereas Valtio eagerly proxies the entire state deeply.\n- Troza only proxies arrays and plain objects, while Valtio also proxies class instances. Although Valtio’s approach can be beneficial, it may cause issues when proxying objects that shouldn’t be proxied (e.g., DOM nodes). Troza opts for simplicity, even if it means sacrificing a bit of flexibility.\n\n### How does Troza compare to other state management libraries?\n\nTroza is designed to be:\n\n- **Immutable** since it still maintains an immutable state tree under the hood.\n- **Intuitive** with mutable-style updates and auto dependency tracking.\n- **Powerful** with its special support for computed states.\n- **Encapsulated** by keeping state, computed states, and actions all in one place.\n- **Composable** via straightforward syntax for slices.\n- **TypeScript-friendly** with minimal type annotations required.\n\nCompared to [Zustand](https://github.com/pmndrs/zustand), Troza offers:\n\n- Support for cached computed states.\n- Actions directly accessible on the store object.\n- Intuitive syntax with mutable-style updates and auto dependency tracking.\n- Enhanced TypeScript friendliness with fewer manual type annotations.\n- Easier composition with a straightforward slice pattern.\n\nCompared to [Valtio](https://github.com/pmndrs/valtio), Troza provides:\n\n- Built-in support for _cached_ computed states.\n- An encapsulated syntax with state, computed states, and actions all together.\n- An immutable state tree under the hood rather than directly mutation.\n\nHowever, note that Troza isn’t always the best choice for every application. If the proxy-based approach feels too magical or if you want to avoid any extra performance overhead, [Zustand](https://github.com/pmndrs/zustand) remains a simple and powerful alternative. For tiny, localized states instead of a centralized store, you might also consider [Jotai](https://github.com/pmndrs/jotai), and [Valtio](https://github.com/pmndrs/valtio) can be a great choice if you need more flexibility and control.\n\n## License\n\nThis project is licensed under the Mozilla Public License Version 2.0 (MPL 2.0).\nFor details, please refer to the `LICENSE` file.\n\nIn addition to the open-source license, a commercial license is available for proprietary use.\nIf you modify this library and do not wish to open-source your modifications, or if you wish to use the modified library as part of a closed-source or proprietary project, you must obtain a commercial license.\n\nFor details, see `COMMERCIAL_LICENSE.md`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnowflyt%2Ftroza","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsnowflyt%2Ftroza","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnowflyt%2Ftroza/lists"}