@view-models/react 3.0.0 → 4.1.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 +0 -74
- package/dist/index.d.ts +2 -51
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +0 -1
- package/src/useModelState.ts +2 -3
- package/src/useDerivedState.ts +0 -54
package/README.md
CHANGED
|
@@ -46,80 +46,6 @@ function Counter() {
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
### Derived State
|
|
50
|
-
|
|
51
|
-
Use `useDerivedState` with the `derived` helper to compute values from your model state. The `derived` function creates a memoized mapper that uses reference equality (`Object.is`) to cache results, which works perfectly with immutable state.
|
|
52
|
-
|
|
53
|
-
Derived mappers should be defined outside your components:
|
|
54
|
-
|
|
55
|
-
```tsx
|
|
56
|
-
import { ViewModel, derived } from "@view-models/core";
|
|
57
|
-
import { useDerivedState } from "@view-models/react";
|
|
58
|
-
|
|
59
|
-
type TodoItem = {
|
|
60
|
-
id: number;
|
|
61
|
-
text: string;
|
|
62
|
-
completed: boolean;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type TodoState = Readonly<{
|
|
66
|
-
items: TodoItem[];
|
|
67
|
-
filter: "all" | "active" | "completed";
|
|
68
|
-
}>;
|
|
69
|
-
|
|
70
|
-
class TodoViewModel extends ViewModel<TodoState> {
|
|
71
|
-
// ... methods to modify state
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const todoModel = new TodoViewModel({ items: [], filter: "all" });
|
|
75
|
-
|
|
76
|
-
// Create derived mappers using the derived helper
|
|
77
|
-
const selectStats = derived(({ items }): TodoState) => ({
|
|
78
|
-
total: items.length,
|
|
79
|
-
completed: items.filter((item) => item.completed).length,
|
|
80
|
-
active: items.filter((item) => !item.completed).length,
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
function TodoStats() {
|
|
84
|
-
// Use the derived mapper with useDerivedState
|
|
85
|
-
const stats = useDerivedState(todoModel, selectStats);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div>
|
|
89
|
-
<p>Total: {stats.total}</p>
|
|
90
|
-
<p>Completed: {stats.completed}</p>
|
|
91
|
-
<p>Active: {stats.active}</p>
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
You can create multiple derived mappers for different parts of your state:
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
// Create derived mappers outside your components
|
|
101
|
-
const selectFilteredItems = derived(({ items, filter }: TodoState) =>
|
|
102
|
-
items.filter((item) => {
|
|
103
|
-
if (filter === "active") return !item.completed;
|
|
104
|
-
if (filter === "completed") return item.completed;
|
|
105
|
-
return true;
|
|
106
|
-
}),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
function TodoList() {
|
|
110
|
-
// The mapper only re-runs when the state reference changes
|
|
111
|
-
const filteredItems = useDerivedState(todoModel, selectFilteredItems);
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<ul>
|
|
115
|
-
{filteredItems.map((item) => (
|
|
116
|
-
<li key={item.id}>{item.text}</li>
|
|
117
|
-
))}
|
|
118
|
-
</ul>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
49
|
### Creating view models inside components
|
|
124
50
|
|
|
125
51
|
When you need to create a view model from within a React component, use `useMemo` to ensure the model is only created once.
|
package/dist/index.d.ts
CHANGED
|
@@ -85,55 +85,6 @@ interface ViewModel<T> {
|
|
|
85
85
|
* }
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
|
-
declare
|
|
88
|
+
declare const useModelState: <T>(model: ViewModel<T>) => T;
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* A branded type for derived state mapper functions created by the `derived` utility.
|
|
93
|
-
* This type ensures that mapper functions are properly memoized before being used
|
|
94
|
-
* with hooks like `useDerivedState`.
|
|
95
|
-
*/
|
|
96
|
-
type DerivedMapper<I, O> = Mapper<I, O> & {
|
|
97
|
-
__brand: "derived";
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* A React hook that subscribes to a ViewModel and returns derived state.
|
|
101
|
-
*
|
|
102
|
-
* This hook combines `useModelState` with a derived mapper function to compute derived values
|
|
103
|
-
* from the model's state. The component will re-render whenever the model state changes.
|
|
104
|
-
*
|
|
105
|
-
* The mapper must be created using the `derived` utility, which provides memoization
|
|
106
|
-
* based on state identity to prevent unnecessary recalculations.
|
|
107
|
-
*
|
|
108
|
-
* @template S - The model state type
|
|
109
|
-
* @template D - The derived state type
|
|
110
|
-
* @param model - The ViewModel instance to subscribe to
|
|
111
|
-
* @param mapper - A derived mapper function created with the `derived` utility
|
|
112
|
-
* @returns The derived state computed from the current model state
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```tsx
|
|
116
|
-
* import { ViewModel, derived } from "@view-models/core";
|
|
117
|
-
* import { useDerivedState } from "@view-models/react";
|
|
118
|
-
*
|
|
119
|
-
* type TodoState = {
|
|
120
|
-
* items: Array<{ id: string; text: string; completed: boolean }>;
|
|
121
|
-
* };
|
|
122
|
-
*
|
|
123
|
-
* const todoModel = new ViewModel<TodoState>({ items: [] });
|
|
124
|
-
*
|
|
125
|
-
* // Create a derived mapper function
|
|
126
|
-
* const selectCompletedCount = derived(({ items }: TodoState) => ({
|
|
127
|
-
* completed: items.filter(item => item.completed).length,
|
|
128
|
-
* total: items.length
|
|
129
|
-
* }));
|
|
130
|
-
*
|
|
131
|
-
* function TodoStats() {
|
|
132
|
-
* const stats = useDerivedState(todoModel, selectCompletedCount);
|
|
133
|
-
* return <div>{stats.completed} of {stats.total} completed</div>;
|
|
134
|
-
* }
|
|
135
|
-
* ```
|
|
136
|
-
*/
|
|
137
|
-
declare const useDerivedState: <S, D>(model: ViewModel<S>, mapper: DerivedMapper<S, D>) => D;
|
|
138
|
-
|
|
139
|
-
export { useDerivedState, useModelState };
|
|
90
|
+
export { useModelState };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useSyncExternalStore as
|
|
1
|
+
import{useSyncExternalStore as o}from"react";const r=r=>o(r.subscribe.bind(r),()=>r.state);export{r 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"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/useModelState.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 const useModelState = <T>(model: ViewModel<T>): T =>\n useSyncExternalStore(model.subscribe.bind(model), () => model.state);\n"],"names":["useModelState","model","useSyncExternalStore","subscribe","bind","state"],"mappings":"6CAgCO,MAAMA,EAAoBC,GAC/BC,EAAqBD,EAAME,UAAUC,KAAKH,GAAQ,IAAMA,EAAMI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@view-models/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "React integration for @view-models/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@testing-library/react": "^16.1.0",
|
|
56
56
|
"@types/react": "^18.3.18",
|
|
57
57
|
"@view-models/core": "^4.1.0",
|
|
58
|
+
"@view-models/react": "^3.0.0",
|
|
58
59
|
"@vitest/coverage-v8": "^2.1.8",
|
|
59
60
|
"jsdom": "^26.0.0",
|
|
60
61
|
"prettier": "^3.4.2",
|
package/src/index.ts
CHANGED
package/src/useModelState.ts
CHANGED
|
@@ -30,6 +30,5 @@ import { ViewModel } from "./ViewModel";
|
|
|
30
30
|
* }
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
}
|
|
33
|
+
export const useModelState = <T>(model: ViewModel<T>): T =>
|
|
34
|
+
useSyncExternalStore(model.subscribe.bind(model), () => model.state);
|
package/src/useDerivedState.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useModelState } from "./useModelState";
|
|
2
|
-
import { ViewModel } from "./ViewModel";
|
|
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" };
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A React hook that subscribes to a ViewModel and returns derived state.
|
|
15
|
-
*
|
|
16
|
-
* This hook combines `useModelState` with a derived mapper function to compute derived values
|
|
17
|
-
* from the model's state. The component will re-render whenever the model state changes.
|
|
18
|
-
*
|
|
19
|
-
* The mapper must be created using the `derived` utility, which provides memoization
|
|
20
|
-
* based on state identity to prevent unnecessary recalculations.
|
|
21
|
-
*
|
|
22
|
-
* @template S - The model state type
|
|
23
|
-
* @template D - The derived state type
|
|
24
|
-
* @param model - The ViewModel instance to subscribe to
|
|
25
|
-
* @param mapper - A derived mapper function created with the `derived` utility
|
|
26
|
-
* @returns The derived state computed from the current model state
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```tsx
|
|
30
|
-
* import { ViewModel, derived } from "@view-models/core";
|
|
31
|
-
* import { useDerivedState } from "@view-models/react";
|
|
32
|
-
*
|
|
33
|
-
* type TodoState = {
|
|
34
|
-
* items: Array<{ id: string; text: string; completed: boolean }>;
|
|
35
|
-
* };
|
|
36
|
-
*
|
|
37
|
-
* const todoModel = new ViewModel<TodoState>({ items: [] });
|
|
38
|
-
*
|
|
39
|
-
* // Create a derived mapper function
|
|
40
|
-
* const selectCompletedCount = derived(({ items }: TodoState) => ({
|
|
41
|
-
* completed: items.filter(item => item.completed).length,
|
|
42
|
-
* total: items.length
|
|
43
|
-
* }));
|
|
44
|
-
*
|
|
45
|
-
* function TodoStats() {
|
|
46
|
-
* const stats = useDerivedState(todoModel, selectCompletedCount);
|
|
47
|
-
* return <div>{stats.completed} of {stats.total} completed</div>;
|
|
48
|
-
* }
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
export const useDerivedState = <S, D>(
|
|
52
|
-
model: ViewModel<S>,
|
|
53
|
-
mapper: DerivedMapper<S, D>,
|
|
54
|
-
) => mapper(useModelState(model));
|