@view-models/react 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,8 +53,8 @@ Use `useDerivedState` with the `derived` helper to compute values from your mode
53
53
  Derived mappers should be defined outside your components:
54
54
 
55
55
  ```tsx
56
- import { ViewModel } from "@view-models/core";
57
- import { useDerivedState, derived } from "@view-models/react";
56
+ import { ViewModel, derived } from "@view-models/core";
57
+ import { useDerivedState } from "@view-models/react";
58
58
 
59
59
  type TodoItem = {
60
60
  id: number;
package/dist/index.d.ts CHANGED
@@ -96,33 +96,6 @@ type Mapper<I, O> = (input: I) => O;
96
96
  type DerivedMapper<I, O> = Mapper<I, O> & {
97
97
  __brand: "derived";
98
98
  };
99
- /**
100
- * Creates a memoized derived state mapper function that caches results based on input identity.
101
- *
102
- * The memoization uses `Object.is` to compare inputs, making it ideal for use with
103
- * immutable state objects. When the input reference hasn't changed, the cached output
104
- * is returned without re-executing the function.
105
- *
106
- * This is the required way to create mapper functions for use with `useDerivedState`.
107
- *
108
- * @template I - The input type
109
- * @template O - The output type
110
- * @param fn - A pure function that transforms input to output
111
- * @returns A memoized derived mapper function
112
- *
113
- * @example
114
- * ```ts
115
- * const selectStats = derived(({ items }: AppState) => ({
116
- * total: items.reduce((sum, item) => sum + item.price, 0),
117
- * count: items.length
118
- * }));
119
- *
120
- * // Use with useDerivedState
121
- * const stats = useDerivedState(model, selectStats);
122
- * ```
123
- */
124
- declare const derived: <I, O>(fn: Mapper<I, O>) => DerivedMapper<I, O>;
125
-
126
99
  /**
127
100
  * A React hook that subscribes to a ViewModel and returns derived state.
128
101
  *
@@ -140,8 +113,8 @@ declare const derived: <I, O>(fn: Mapper<I, O>) => DerivedMapper<I, O>;
140
113
  *
141
114
  * @example
142
115
  * ```tsx
143
- * import { ViewModel } from "@view-models/core";
144
- * import { useDerivedState, derived } from "@view-models/react";
116
+ * import { ViewModel, derived } from "@view-models/core";
117
+ * import { useDerivedState } from "@view-models/react";
145
118
  *
146
119
  * type TodoState = {
147
120
  * items: Array<{ id: string; text: string; completed: boolean }>;
@@ -163,5 +136,4 @@ declare const derived: <I, O>(fn: Mapper<I, O>) => DerivedMapper<I, O>;
163
136
  */
164
137
  declare const useDerivedState: <S, D>(model: ViewModel<S>, mapper: DerivedMapper<S, D>) => D;
165
138
 
166
- export { derived, useDerivedState, useModelState };
167
- export type { DerivedMapper };
139
+ export { useDerivedState, useModelState };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{useSyncExternalStore as t}from"react";function r(r){return t(r.subscribe.bind(r),()=>r.state)}const e=(t,e)=>e(r(t)),n={},o=t=>{let r=n,e=n;return o=>r!==n&&Object.is(r,o)?e:e=t(r=o)};export{o as derived,e as useDerivedState,r as useModelState};
1
+ import{useSyncExternalStore as r}from"react";function t(t){return r(t.subscribe.bind(t),()=>t.state)}const o=(r,o)=>o(t(r));export{o as useDerivedState,t as useModelState};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/useModelState.ts","../src/useDerivedState.ts","../src/derived.ts"],"sourcesContent":["import { useSyncExternalStore } from \"react\";\nimport { ViewModel } from \"./ViewModel\";\n\n/**\n * A React hook that subscribes a component to a ViewModel's state updates.\n *\n * This hook connects a React component to a ViewModel instance, automatically\n * subscribing to state changes and triggering re-renders when the state updates.\n *\n * @template T - The state type, which must extend the State interface\n * @param model - The ViewModel instance to subscribe to\n * @returns The current state from the ViewModel\n *\n * @example\n * ```tsx\n * class CounterViewModel extends ViewModel<{ count: number }> {\n * increment = () => super.update({ count: super.state.count + 1 });\n * }\n *\n * function Counter() {\n * const counterModel = useMemo(() => new CounterViewModel({ count: 0 }), []);\n * const { count } = useModelState(counterModel);\n *\n * return (\n * <div>\n * <p>Count: {count}</p>\n * <button onClick={counterModel.increment}>+</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useModelState<T>(model: ViewModel<T>): T {\n return useSyncExternalStore(model.subscribe.bind(model), () => model.state);\n}\n","import { useModelState } from \"./useModelState\";\nimport { ViewModel } from \"./ViewModel\";\nimport { DerivedMapper } from \"./derived\";\n\n/**\n * A React hook that subscribes to a ViewModel and returns derived state.\n *\n * This hook combines `useModelState` with a derived mapper function to compute derived values\n * from the model's state. The component will re-render whenever the model state changes.\n *\n * The mapper must be created using the `derived` utility, which provides memoization\n * based on state identity to prevent unnecessary recalculations.\n *\n * @template S - The model state type\n * @template D - The derived state type\n * @param model - The ViewModel instance to subscribe to\n * @param mapper - A derived mapper function created with the `derived` utility\n * @returns The derived state computed from the current model state\n *\n * @example\n * ```tsx\n * import { ViewModel } from \"@view-models/core\";\n * import { useDerivedState, derived } from \"@view-models/react\";\n *\n * type TodoState = {\n * items: Array<{ id: string; text: string; completed: boolean }>;\n * };\n *\n * const todoModel = new ViewModel<TodoState>({ items: [] });\n *\n * // Create a derived mapper function\n * const selectCompletedCount = derived(({ items }: TodoState) => ({\n * completed: items.filter(item => item.completed).length,\n * total: items.length\n * }));\n *\n * function TodoStats() {\n * const stats = useDerivedState(todoModel, selectCompletedCount);\n * return <div>{stats.completed} of {stats.total} completed</div>;\n * }\n * ```\n */\nexport const useDerivedState = <S, D>(\n model: ViewModel<S>,\n mapper: DerivedMapper<S, D>,\n) => mapper(useModelState(model));\n","type Mapper<I, O> = (input: I) => O;\n\n/**\n * A branded type for derived state mapper functions created by the `derived` utility.\n * This type ensures that mapper functions are properly memoized before being used\n * with hooks like `useDerivedState`.\n */\nexport type DerivedMapper<I, O> = Mapper<I, O> & { __brand: \"derived\" };\n\nconst UNINITIALIZED = {};\n\n/**\n * Creates a memoized derived state mapper function that caches results based on input identity.\n *\n * The memoization uses `Object.is` to compare inputs, making it ideal for use with\n * immutable state objects. When the input reference hasn't changed, the cached output\n * is returned without re-executing the function.\n *\n * This is the required way to create mapper functions for use with `useDerivedState`.\n *\n * @template I - The input type\n * @template O - The output type\n * @param fn - A pure function that transforms input to output\n * @returns A memoized derived mapper function\n *\n * @example\n * ```ts\n * const selectStats = derived(({ items }: AppState) => ({\n * total: items.reduce((sum, item) => sum + item.price, 0),\n * count: items.length\n * }));\n *\n * // Use with useDerivedState\n * const stats = useDerivedState(model, selectStats);\n * ```\n */\nexport const derived = <I, O>(fn: Mapper<I, O>): DerivedMapper<I, O> => {\n let lastInput: I | typeof UNINITIALIZED = UNINITIALIZED;\n let lastOutput: O | typeof UNINITIALIZED = UNINITIALIZED;\n return ((input: I) => {\n if (lastInput !== UNINITIALIZED && Object.is(lastInput, input)) {\n return lastOutput as O;\n }\n return (lastOutput = fn((lastInput = input)));\n }) as DerivedMapper<I, O>;\n};\n"],"names":["useModelState","model","useSyncExternalStore","subscribe","bind","state","useDerivedState","mapper","UNINITIALIZED","derived","fn","lastInput","lastOutput","input","Object","is"],"mappings":"6CAgCM,SAAUA,EAAiBC,GAC/B,OAAOC,EAAqBD,EAAME,UAAUC,KAAKH,GAAQ,IAAMA,EAAMI,MACvE,CCQO,MAAMC,EAAkB,CAC7BL,EACAM,IACGA,EAAOP,EAAcC,ICpCpBO,EAAgB,CAAA,EA2BTC,EAAiBC,IAC5B,IAAIC,EAAsCH,EACtCI,EAAuCJ,EAC3C,OAASK,GACHF,IAAcH,GAAiBM,OAAOC,GAAGJ,EAAWE,GAC/CD,EAEDA,EAAaF,EAAIC,EAAYE"}
1
+ {"version":3,"file":"index.js","sources":["../src/useModelState.ts","../src/useDerivedState.ts"],"sourcesContent":["import { useSyncExternalStore } from \"react\";\nimport { ViewModel } from \"./ViewModel\";\n\n/**\n * A React hook that subscribes a component to a ViewModel's state updates.\n *\n * This hook connects a React component to a ViewModel instance, automatically\n * subscribing to state changes and triggering re-renders when the state updates.\n *\n * @template T - The state type, which must extend the State interface\n * @param model - The ViewModel instance to subscribe to\n * @returns The current state from the ViewModel\n *\n * @example\n * ```tsx\n * class CounterViewModel extends ViewModel<{ count: number }> {\n * increment = () => super.update({ count: super.state.count + 1 });\n * }\n *\n * function Counter() {\n * const counterModel = useMemo(() => new CounterViewModel({ count: 0 }), []);\n * const { count } = useModelState(counterModel);\n *\n * return (\n * <div>\n * <p>Count: {count}</p>\n * <button onClick={counterModel.increment}>+</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useModelState<T>(model: ViewModel<T>): T {\n return useSyncExternalStore(model.subscribe.bind(model), () => model.state);\n}\n","import { useModelState } from \"./useModelState\";\nimport { ViewModel } from \"./ViewModel\";\n\ntype Mapper<I, O> = (input: I) => O;\n\n/**\n * A branded type for derived state mapper functions created by the `derived` utility.\n * This type ensures that mapper functions are properly memoized before being used\n * with hooks like `useDerivedState`.\n */\ntype DerivedMapper<I, O> = Mapper<I, O> & { __brand: \"derived\" };\n\n/**\n * A React hook that subscribes to a ViewModel and returns derived state.\n *\n * This hook combines `useModelState` with a derived mapper function to compute derived values\n * from the model's state. The component will re-render whenever the model state changes.\n *\n * The mapper must be created using the `derived` utility, which provides memoization\n * based on state identity to prevent unnecessary recalculations.\n *\n * @template S - The model state type\n * @template D - The derived state type\n * @param model - The ViewModel instance to subscribe to\n * @param mapper - A derived mapper function created with the `derived` utility\n * @returns The derived state computed from the current model state\n *\n * @example\n * ```tsx\n * import { ViewModel, derived } from \"@view-models/core\";\n * import { useDerivedState } from \"@view-models/react\";\n *\n * type TodoState = {\n * items: Array<{ id: string; text: string; completed: boolean }>;\n * };\n *\n * const todoModel = new ViewModel<TodoState>({ items: [] });\n *\n * // Create a derived mapper function\n * const selectCompletedCount = derived(({ items }: TodoState) => ({\n * completed: items.filter(item => item.completed).length,\n * total: items.length\n * }));\n *\n * function TodoStats() {\n * const stats = useDerivedState(todoModel, selectCompletedCount);\n * return <div>{stats.completed} of {stats.total} completed</div>;\n * }\n * ```\n */\nexport const useDerivedState = <S, D>(\n model: ViewModel<S>,\n mapper: DerivedMapper<S, D>,\n) => mapper(useModelState(model));\n"],"names":["useModelState","model","useSyncExternalStore","subscribe","bind","state","useDerivedState","mapper"],"mappings":"6CAgCM,SAAUA,EAAiBC,GAC/B,OAAOC,EAAqBD,EAAME,UAAUC,KAAKH,GAAQ,IAAMA,EAAMI,MACvE,CCgBO,MAAMC,EAAkB,CAC7BL,EACAM,IACGA,EAAOP,EAAcC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@view-models/react",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "React integration for @view-models/core",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,7 +54,7 @@
54
54
  "@rollup/plugin-typescript": "^12.3.0",
55
55
  "@testing-library/react": "^16.1.0",
56
56
  "@types/react": "^18.3.18",
57
- "@view-models/core": "^4.0.0",
57
+ "@view-models/core": "^4.1.0",
58
58
  "@vitest/coverage-v8": "^2.1.8",
59
59
  "jsdom": "^26.0.0",
60
60
  "prettier": "^3.4.2",
package/src/index.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from "./useModelState.js";
2
2
  export * from "./useDerivedState.js";
3
- export * from "./derived.js";
@@ -1,6 +1,14 @@
1
1
  import { useModelState } from "./useModelState";
2
2
  import { ViewModel } from "./ViewModel";
3
- import { DerivedMapper } from "./derived";
3
+
4
+ type Mapper<I, O> = (input: I) => O;
5
+
6
+ /**
7
+ * A branded type for derived state mapper functions created by the `derived` utility.
8
+ * This type ensures that mapper functions are properly memoized before being used
9
+ * with hooks like `useDerivedState`.
10
+ */
11
+ type DerivedMapper<I, O> = Mapper<I, O> & { __brand: "derived" };
4
12
 
5
13
  /**
6
14
  * A React hook that subscribes to a ViewModel and returns derived state.
@@ -19,8 +27,8 @@ import { DerivedMapper } from "./derived";
19
27
  *
20
28
  * @example
21
29
  * ```tsx
22
- * import { ViewModel } from "@view-models/core";
23
- * import { useDerivedState, derived } from "@view-models/react";
30
+ * import { ViewModel, derived } from "@view-models/core";
31
+ * import { useDerivedState } from "@view-models/react";
24
32
  *
25
33
  * type TodoState = {
26
34
  * items: Array<{ id: string; text: string; completed: boolean }>;
package/src/derived.ts DELETED
@@ -1,46 +0,0 @@
1
- type Mapper<I, O> = (input: I) => O;
2
-
3
- /**
4
- * A branded type for derived state mapper functions created by the `derived` utility.
5
- * This type ensures that mapper functions are properly memoized before being used
6
- * with hooks like `useDerivedState`.
7
- */
8
- export type DerivedMapper<I, O> = Mapper<I, O> & { __brand: "derived" };
9
-
10
- const UNINITIALIZED = {};
11
-
12
- /**
13
- * Creates a memoized derived state mapper function that caches results based on input identity.
14
- *
15
- * The memoization uses `Object.is` to compare inputs, making it ideal for use with
16
- * immutable state objects. When the input reference hasn't changed, the cached output
17
- * is returned without re-executing the function.
18
- *
19
- * This is the required way to create mapper functions for use with `useDerivedState`.
20
- *
21
- * @template I - The input type
22
- * @template O - The output type
23
- * @param fn - A pure function that transforms input to output
24
- * @returns A memoized derived mapper function
25
- *
26
- * @example
27
- * ```ts
28
- * const selectStats = derived(({ items }: AppState) => ({
29
- * total: items.reduce((sum, item) => sum + item.price, 0),
30
- * count: items.length
31
- * }));
32
- *
33
- * // Use with useDerivedState
34
- * const stats = useDerivedState(model, selectStats);
35
- * ```
36
- */
37
- export const derived = <I, O>(fn: Mapper<I, O>): DerivedMapper<I, O> => {
38
- let lastInput: I | typeof UNINITIALIZED = UNINITIALIZED;
39
- let lastOutput: O | typeof UNINITIALIZED = UNINITIALIZED;
40
- return ((input: I) => {
41
- if (lastInput !== UNINITIALIZED && Object.is(lastInput, input)) {
42
- return lastOutput as O;
43
- }
44
- return (lastOutput = fn((lastInput = input)));
45
- }) as DerivedMapper<I, O>;
46
- };