muya 2.0.0 → 2.0.2
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 +43 -11
- package/cjs/index.js +1 -1
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/create.test.tsx +67 -1
- package/src/create.ts +24 -4
- package/src/debug/development-tools.ts +0 -1
- package/src/types.ts +2 -2
- package/types/types.d.ts +2 -2
package/README.md
CHANGED
|
@@ -9,10 +9,6 @@ Muya is simple and lightweight react state management library.
|
|
|
9
9
|
[](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
|
|
10
10
|
[](https://bundlephobia.com/result?p=muya)
|
|
11
11
|
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## 🚀 **Key Features**
|
|
16
12
|
|
|
17
13
|
- **Simplified API**: Only `create` and `select`.
|
|
18
14
|
- **Batch Updates**: Built-in batching ensures efficient `muya` state updates internally.
|
|
@@ -43,14 +39,14 @@ yarn add muya@latest
|
|
|
43
39
|
```typescript
|
|
44
40
|
import { create } from 'muya';
|
|
45
41
|
|
|
46
|
-
const
|
|
42
|
+
const useCounter = create(0);
|
|
47
43
|
|
|
48
44
|
// Access in a React component
|
|
49
45
|
function Counter() {
|
|
50
|
-
const count =
|
|
46
|
+
const count = useCounter(); // Call state directly
|
|
51
47
|
return (
|
|
52
48
|
<div>
|
|
53
|
-
<button onClick={() =>
|
|
49
|
+
<button onClick={() => useCounter.set((prev) => prev + 1)}>Increment</button>
|
|
54
50
|
<p>Count: {count}</p>
|
|
55
51
|
</div>
|
|
56
52
|
);
|
|
@@ -80,9 +76,11 @@ const asyncCountSlice = state.select(async (s) => {
|
|
|
80
76
|
|
|
81
77
|
### **Combine Multiple States**
|
|
82
78
|
|
|
83
|
-
Combine multiple states into a derived state:
|
|
79
|
+
Combine multiple states into a derived state via `select` method:
|
|
84
80
|
|
|
85
81
|
```typescript
|
|
82
|
+
import { create, select } from 'muya'
|
|
83
|
+
|
|
86
84
|
const state1 = create(1);
|
|
87
85
|
const state2 = create(2);
|
|
88
86
|
|
|
@@ -114,7 +112,7 @@ const derived = select([state1, state2], (s1, s2) => s1 + s2, (prev, next) => pr
|
|
|
114
112
|
Access state directly or through `useValue` hook:
|
|
115
113
|
|
|
116
114
|
### **Option 1: Access State Directly**
|
|
117
|
-
|
|
115
|
+
Each state can be called as the hook directly
|
|
118
116
|
```typescript
|
|
119
117
|
const userState = create(0);
|
|
120
118
|
|
|
@@ -125,7 +123,7 @@ function App() {
|
|
|
125
123
|
```
|
|
126
124
|
|
|
127
125
|
### **Option 2: Use the Hook**
|
|
128
|
-
|
|
126
|
+
Or for convenience, there is `useValue` method
|
|
129
127
|
```typescript
|
|
130
128
|
import { useValue } from 'muya';
|
|
131
129
|
|
|
@@ -136,7 +134,7 @@ function App() {
|
|
|
136
134
|
```
|
|
137
135
|
|
|
138
136
|
### **Option 3: Slice with Hook**
|
|
139
|
-
|
|
137
|
+
For efficient re-renders, `useValue` provides a slicing method.
|
|
140
138
|
```typescript
|
|
141
139
|
function App() {
|
|
142
140
|
const count = useValue(state, (s) => s.count); // Use selector in hook
|
|
@@ -219,6 +217,40 @@ const asyncState = state.select(async (s) => {
|
|
|
219
217
|
```
|
|
220
218
|
---
|
|
221
219
|
|
|
220
|
+
### Lazy resolution
|
|
221
|
+
`Muya` can be used in `immediate` mode or in `lazy` mode. When create a state with just plain data, it will be in immediate mode, but if you create a state with a function, it will be in lazy mode. This is useful when you want to create a state that is executed only when it is accessed for the first time.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// immediate mode, so no matter what, this value is already stored in memory
|
|
226
|
+
const state = create(0)
|
|
227
|
+
|
|
228
|
+
// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
|
|
229
|
+
const state = create(() => 0)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
And in async:
|
|
233
|
+
```typescript
|
|
234
|
+
// we can create some initial functions like this
|
|
235
|
+
async function initialLoad() {
|
|
236
|
+
return 0
|
|
237
|
+
}
|
|
238
|
+
// immediate mode, so no matter what, this value is already stored in memory
|
|
239
|
+
const state = create(initialLoad)
|
|
240
|
+
// or
|
|
241
|
+
const state = create(Promise.resolve(0))
|
|
242
|
+
|
|
243
|
+
// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
|
|
244
|
+
const state = create(() => Promise.resolve(0))
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
And when setting state when initial value is promise, set is always sync.
|
|
248
|
+
But as in react there are two methods how to set a state. Directly `.set(2)` or with a function `.set((prev) => prev + 1)`.
|
|
249
|
+
|
|
250
|
+
So how `set` state will behave with async initial value?
|
|
251
|
+
1. Directly call `.set(2)` will be sync, and will set the value to 2 (it will cancel the initial promise)
|
|
252
|
+
2. Call `.set((prev) => prev + 1)` will wait until previous promise is resolved, so previous value in set callback is always resolved.
|
|
253
|
+
|
|
222
254
|
### Debugging
|
|
223
255
|
`Muya` in dev mode automatically connects to the `redux` devtools extension if it is installed in the browser. For now devtool api is simple - state updates.
|
|
224
256
|
|
package/cjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var w=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var M=(e,t)=>{for(var r in t)w(e,r,{get:t[r],enumerable:!0})},z=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of R(t))!L.call(e,o)&&o!==r&&w(e,o,{get:()=>t[o],enumerable:!(n=F(t,o))||n.enumerable});return e};var H=e=>z(w({},"__esModule",{value:!0}),e);var K={};M(K,{EMPTY_SELECTOR:()=>g,create:()=>U,select:()=>b,shallow:()=>q,useValue:()=>E});module.exports=H(K);var g=e=>e;function p(e){return e instanceof Promise}function k(e){return typeof e=="function"}function C(e){return e instanceof Map}function V(e){return e instanceof Set}function O(e){return Array.isArray(e)}function d(e,t){return e===t?!0:!!Object.is(e,t)}function P(e){return typeof e=="function"}function I(e){return e instanceof DOMException&&e.name==="StateAbortError"}function D(e){return e instanceof Error}function f(e){return e===void 0}function j(e,t){t&&t.abort();let r=new AbortController,{signal:n}=r;return{promise:new Promise((i,s)=>{n.addEventListener("abort",()=>{s(new DOMException("Promise was aborted","StateAbortError"))}),e.then(i).catch(s)}),controller:r}}function S(e,t=d){if(!f(e.current)){if(!f(e.previous)&&t(e.current,e.previous))return!1;e.previous=e.current}return!0}function m(e,t,r){if(!p(r))return r;e.abortController&&e.abortController.abort();let{promise:n,controller:o}=j(r,e.abortController);return e.abortController=o,n.then(i=>{e.current=i,t()}).catch(i=>{I(i)||(e.current=i,t())})}function v(){let e=new Map,t=new Set,r=performance.now(),n=!1;function o(){let s=performance.now(),a=s-r,{size:u}=t;if(a<.2&&u>0&&u<10){r=s,i();return}n||(n=!0,Promise.resolve().then(()=>{n=!1,r=performance.now(),i()}))}function i(){if(t.size===0)return;let s=new Set;for(let a of t){if(e.has(a.id)){s.add(a.id);let{onResolveItem:u}=e.get(a.id);u&&u(a.value)}t.delete(a)}if(t.size>0){o();return}for(let a of s)e.get(a)?.onFinish()}return{add(s,a){return e.set(s,a),()=>{e.delete(s)}},schedule(s,a){t.add({value:a,id:s}),o()}}}function b(e,t,r){let n={};function o(){let c=e.map(l=>l.get());return t(...c)}function i(){if(f(n.current)){let c=o();n.current=m(n,a.emitter.emit,c)}return n.current}let s=[];for(let c of e){let l=c.emitter.subscribe(()=>{T.schedule(a.id,null)});s.push(l)}let a=h({destroy(){for(let c of s)c();u(),a.emitter.clear(),n.current=void 0},get:i}),u=T.add(a.id,{onFinish(){let c=o();n.current=m(n,a.emitter.emit,c),S(n,r)&&a.emitter.emit()}});return a}var y=require("react");function E(e,t=g){let{emitter:r}=e,n=(0,y.useSyncExternalStore)(e.emitter.subscribe,()=>t(r.getSnapshot()),()=>t(r.getInitialSnapshot?r.getInitialSnapshot():r.getSnapshot()));if((0,y.useDebugValue)(n),p(n)||D(n))throw n;return n}function G(e,t){let r=new Set,n=[];return{clear:()=>{for(let o of n)o();r.clear()},subscribe:o=>(r.add(o),()=>{r.delete(o)}),emit:(...o)=>{for(let i of r)i(...o)},contains:o=>r.has(o),getSnapshot:e,getInitialSnapshot:t,getSize:()=>r.size,subscribeToOtherEmitter(o){let i=o.subscribe(()=>{this.emit()});n.push(i)}}}var N=0;function _(){return N++}function h(e){let{get:t,destroy:r,set:n}=e,o=!!n,i=function(s){return E(i,s)};return i.isSet=o,i.id=_(),i.emitter=G(t),i.destroy=r,i.listen=function(s){return this.emitter.subscribe(()=>{s(t())})},i.withName=function(s){return this.stateName=s,this},i.select=function(s,a=d){return b([i],s,a)},i.get=t,i.set=n,i}var T=v();function U(e,t=d){let r={};function n(){try{if(f(r.current)){let u=k(e)?e():e,c=m(r,s.emitter.emit,u);return r.current=c,r.current}return r.current}catch(u){r.current=u}return r.current}async function o(u,c){await u;let l=c(r.current),x=m(r,s.emitter.emit,l);r.current=x}function i(u){let c=n(),l=P(u);if(l&&p(c)){o(c,u);return}r.abortController&&r.abortController.abort();let x=l?u(c):u,A=m(r,s.emitter.emit,x);r.current=A}let s=h({get:n,destroy(){n(),a(),s.emitter.clear(),r.current=void 0},set(u){T.schedule(s.id,u)}}),a=T.add(s.id,{onFinish(){r.current=n(),S(r,t)&&s.emitter.emit()},onResolveItem:i});return k(e)||n(),s}function q(e,t){if(e==t)return!0;if(typeof e!="object"||e==null||typeof t!="object"||t==null)return!1;if(C(e)&&C(t)){if(e.size!==t.size)return!1;for(let[o,i]of e)if(!Object.is(i,t.get(o)))return!1;return!0}if(V(e)&&V(t)){if(e.size!==t.size)return!1;for(let o of e)if(!t.has(o))return!1;return!0}if(O(e)&&O(t)){if(e.length!==t.length)return!1;for(let[o,i]of e.entries())if(!Object.is(i,t[o]))return!1;return!0}let r=Object.keys(e),n=Object.keys(t);if(r.length!==n.length)return!1;for(let o of r)if(!Object.prototype.hasOwnProperty.call(t,o)||!Object.is(e[o],t[o]))return!1;return!0}
|
package/esm/create.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{canUpdate as
|
|
1
|
+
import{canUpdate as p,handleAsyncUpdate as u}from"./utils/common";import{isEqualBase as V,isFunction as i,isPromise as h,isSetValueFunction as b,isUndefined as y}from"./utils/is";import{createScheduler as C}from"./scheduler";import{subscribeToDevelopmentTools as w}from"./debug/development-tools";import{createState as v}from"./create-state";const l=C();function I(c,m=V){const e={};function o(){try{if(y(e.current)){const t=i(c)?c():c,n=u(e,r.emitter.emit,t);return e.current=n,e.current}return e.current}catch(t){e.current=t}return e.current}async function d(t,n){await t;const a=n(e.current),s=u(e,r.emitter.emit,a);e.current=s}function f(t){const n=o(),a=b(t);if(a&&h(n)){d(n,t);return}e.abortController&&e.abortController.abort();const s=a?t(n):t,S=u(e,r.emitter.emit,s);e.current=S}const r=v({get:o,destroy(){o(),T(),r.emitter.clear(),e.current=void 0},set(t){l.schedule(r.id,t)}}),T=l.add(r.id,{onFinish(){e.current=o(),p(e,m)&&r.emitter.emit()},onResolveItem:f});return i(c)||o(),w(r),r}export{I as create,l as stateScheduler};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isPromise as
|
|
1
|
+
import{isPromise as r,isState as a}from"../utils/is";const o=window?.__REDUX_DEVTOOLS_EXTENSION__?.connect({name:"CustomState",trace:!0});o&&o.init({message:"Initial state"});function p(e){if(!o)return;const{message:t,type:n,value:s,name:i}=e;r(s)||o.send(i,{value:s,type:n,message:t},n)}function m(e,t){return n=>{p({name:e,type:t,value:n,message:"update"})}}function S(e){}export{S as subscribeToDevelopmentTools};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { create } from '../create'
|
|
2
2
|
import { waitFor } from '@testing-library/react'
|
|
3
3
|
import { longPromise } from './test-utils'
|
|
4
|
+
import { isPromise } from '../utils/is'
|
|
4
5
|
|
|
5
6
|
describe('create', () => {
|
|
6
7
|
it('should get basic value states', async () => {
|
|
@@ -56,9 +57,16 @@ describe('create', () => {
|
|
|
56
57
|
})
|
|
57
58
|
})
|
|
58
59
|
|
|
59
|
-
it('should initialize state with a
|
|
60
|
+
it('should initialize state with a lazy value', () => {
|
|
60
61
|
const initialValue = jest.fn(() => 10)
|
|
61
62
|
const state = create(initialValue)
|
|
63
|
+
expect(initialValue).not.toHaveBeenCalled()
|
|
64
|
+
expect(state.get()).toBe(10)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should initialize state with direct lazy value', () => {
|
|
68
|
+
const initialValue = jest.fn(() => 10)
|
|
69
|
+
const state = create(initialValue())
|
|
62
70
|
expect(initialValue).toHaveBeenCalled()
|
|
63
71
|
expect(state.get()).toBe(10)
|
|
64
72
|
})
|
|
@@ -156,4 +164,62 @@ describe('create', () => {
|
|
|
156
164
|
expect(listener).toHaveBeenCalledWith(2)
|
|
157
165
|
})
|
|
158
166
|
})
|
|
167
|
+
|
|
168
|
+
it('should resolve immediately when state is promise', async () => {
|
|
169
|
+
const promiseMock = jest.fn(() => longPromise(100))
|
|
170
|
+
const state1 = create(promiseMock())
|
|
171
|
+
expect(promiseMock).toHaveBeenCalled()
|
|
172
|
+
state1.set((value) => {
|
|
173
|
+
// set with callback will be executed later when promise is resolved
|
|
174
|
+
expect(isPromise(value)).toBe(false)
|
|
175
|
+
return value + 1
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(state1.get()).toBe(1)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
state1.set(2)
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(state1.get()).toBe(2)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
state1.set((value) => {
|
|
188
|
+
expect(isPromise(value)).toBe(false)
|
|
189
|
+
return value + 1
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(state1.get()).toBe(3)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should resolve lazy when state is promise', async () => {
|
|
198
|
+
const promiseMock = jest.fn(() => longPromise(100))
|
|
199
|
+
const state1 = create(promiseMock)
|
|
200
|
+
expect(promiseMock).not.toHaveBeenCalled()
|
|
201
|
+
state1.set((value) => {
|
|
202
|
+
// set with callback will be executed later when promise is resolved
|
|
203
|
+
expect(isPromise(value)).toBe(false)
|
|
204
|
+
return value + 1
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(state1.get()).toBe(1)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
state1.set(2)
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(state1.get()).toBe(2)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
state1.set((value) => {
|
|
217
|
+
expect(isPromise(value)).toBe(false)
|
|
218
|
+
return value + 1
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(state1.get()).toBe(3)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
159
225
|
})
|
package/src/create.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canUpdate, handleAsyncUpdate } from './utils/common'
|
|
2
|
-
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
3
|
-
import type { Cache, DefaultValue, IsEqual, SetValue, State } from './types'
|
|
2
|
+
import { isEqualBase, isFunction, isPromise, isSetValueFunction, isUndefined } from './utils/is'
|
|
3
|
+
import type { Cache, DefaultValue, IsEqual, SetStateCb, SetValue, State } from './types'
|
|
4
4
|
import { createScheduler } from './scheduler'
|
|
5
5
|
import { subscribeToDevelopmentTools } from './debug/development-tools'
|
|
6
6
|
import { createState } from './create-state'
|
|
@@ -19,21 +19,37 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
|
|
|
19
19
|
const value = isFunction(initialValue) ? initialValue() : initialValue
|
|
20
20
|
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, value)
|
|
21
21
|
cache.current = resolvedValue
|
|
22
|
+
|
|
23
|
+
return cache.current
|
|
22
24
|
}
|
|
23
25
|
return cache.current
|
|
24
26
|
} catch (error) {
|
|
25
27
|
cache.current = error as T
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
return cache.current
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
async function handleAsyncSetValue(previousPromise: Promise<T>, value: SetStateCb<T>) {
|
|
34
|
+
await previousPromise
|
|
35
|
+
const newValue = value(cache.current as Awaited<T>)
|
|
36
|
+
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
37
|
+
cache.current = resolvedValue
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function setValue(value: SetValue<T>) {
|
|
41
|
+
const previous = getValue()
|
|
42
|
+
const isFunctionValue = isSetValueFunction(value)
|
|
43
|
+
|
|
44
|
+
if (isFunctionValue && isPromise(previous)) {
|
|
45
|
+
handleAsyncSetValue(previous as Promise<T>, value)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
31
48
|
if (cache.abortController) {
|
|
32
49
|
cache.abortController.abort()
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
const
|
|
36
|
-
const newValue = isSetValueFunction(value) ? value(previous) : value
|
|
52
|
+
const newValue = isFunctionValue ? value(previous as Awaited<T>) : value
|
|
37
53
|
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
|
|
38
54
|
cache.current = resolvedValue
|
|
39
55
|
}
|
|
@@ -62,6 +78,10 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
|
|
|
62
78
|
onResolveItem: setValue,
|
|
63
79
|
})
|
|
64
80
|
|
|
81
|
+
if (!isFunction(initialValue)) {
|
|
82
|
+
getValue()
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
subscribeToDevelopmentTools(state)
|
|
66
86
|
return state
|
|
67
87
|
}
|
|
@@ -47,6 +47,5 @@ export function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>) {
|
|
|
47
47
|
type = 'derived'
|
|
48
48
|
}
|
|
49
49
|
const name = state.stateName?.length ? state.stateName : `${type}(${state.id.toString()})`
|
|
50
|
-
sendToDevelopmentTools({ name, type, value: state.get(), message: 'initial' })
|
|
51
50
|
return state.listen(developmentToolsListener(name, type))
|
|
52
51
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Emitter } from './utils/create-emitter'
|
|
2
2
|
|
|
3
3
|
export type IsEqual<T = unknown> = (a: T, b: T) => boolean
|
|
4
|
-
export type SetStateCb<T> = (value:
|
|
4
|
+
export type SetStateCb<T> = (value: Awaited<T>) => Awaited<T>
|
|
5
5
|
export type SetValue<T> = SetStateCb<T> | Awaited<T>
|
|
6
6
|
export type DefaultValue<T> = T | (() => T)
|
|
7
|
-
export type Listener<T> = (listener: (value
|
|
7
|
+
export type Listener<T> = (listener: (value: T) => void) => () => void
|
|
8
8
|
export interface Cache<T> {
|
|
9
9
|
current?: T
|
|
10
10
|
previous?: T
|
package/types/types.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Emitter } from './utils/create-emitter';
|
|
2
2
|
export type IsEqual<T = unknown> = (a: T, b: T) => boolean;
|
|
3
|
-
export type SetStateCb<T> = (value:
|
|
3
|
+
export type SetStateCb<T> = (value: Awaited<T>) => Awaited<T>;
|
|
4
4
|
export type SetValue<T> = SetStateCb<T> | Awaited<T>;
|
|
5
5
|
export type DefaultValue<T> = T | (() => T);
|
|
6
|
-
export type Listener<T> = (listener: (value
|
|
6
|
+
export type Listener<T> = (listener: (value: T) => void) => () => void;
|
|
7
7
|
export interface Cache<T> {
|
|
8
8
|
current?: T;
|
|
9
9
|
previous?: T;
|