muya 2.2.9 → 2.3.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 +176 -173
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,268 +1,271 @@
|
|
|
1
|
-
|
|
2
1
|
# **Muya 🌀**
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
*A tiny, type-safe state manager for React with a dead-simple mental model:*
|
|
4
|
+
- **Create a state**
|
|
5
|
+
- **Read it in components**
|
|
6
|
+
- **Update it**
|
|
7
|
+
- **Derive more states when you need to**
|
|
5
8
|
|
|
6
9
|
---
|
|
7
10
|
|
|
8
11
|
[](https://github.com/samuelgja/muya/actions/workflows/build.yml)
|
|
9
|
-
[](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
|
|
13
|
+
[](https://bundlephobia.com/result?p=muya)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
15
|
+
## ✨ Highlights
|
|
16
|
+
- **2-call API**: `create` and `select` (plus a tiny hook `useValue`)
|
|
17
|
+
- **React-friendly**: internal batching; opt-in equality checks to skip renders
|
|
18
|
+
- **Type-first**: full TypeScript support
|
|
19
|
+
- **Lightweight**: built for small mental & bundle footprint
|
|
17
20
|
|
|
18
21
|
---
|
|
19
22
|
|
|
20
|
-
## 📦
|
|
21
|
-
|
|
22
|
-
Install with your favorite package manager:
|
|
23
|
+
## 📦 Install
|
|
23
24
|
```bash
|
|
24
25
|
bun add muya@latest
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
```bash
|
|
26
|
+
# or
|
|
27
|
+
npm i muya@latest
|
|
28
|
+
# or
|
|
30
29
|
yarn add muya@latest
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
---
|
|
34
33
|
|
|
35
|
-
##
|
|
34
|
+
## 🏃 Quick Start
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
```tsx
|
|
37
|
+
import { create } from 'muya'
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// 1) Make a state
|
|
40
|
+
const counter = create(0)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
function Counter() {
|
|
46
|
-
const count = useCounter(); // Call state directly
|
|
42
|
+
// 2) Read it inside React
|
|
43
|
+
export function Counter() {
|
|
44
|
+
const count = counter() // state is a hook
|
|
47
45
|
return (
|
|
48
46
|
<div>
|
|
49
|
-
<button onClick={() =>
|
|
47
|
+
<button onClick={() => counter.set(n => n + 1)}>+1</button>
|
|
50
48
|
<p>Count: {count}</p>
|
|
51
49
|
</div>
|
|
52
|
-
)
|
|
50
|
+
)
|
|
53
51
|
}
|
|
54
52
|
```
|
|
55
53
|
|
|
56
54
|
---
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Use `select` to derive a slice of the state:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
const state = create({ count: 0, value: 42 });
|
|
64
|
-
|
|
65
|
-
const countSlice = state.select((s) => s.count);
|
|
66
|
-
|
|
67
|
-
// Also async is possible, but Muya do not recommend
|
|
68
|
-
// It can lead to spaghetti re-renders code which is hard to maintain and debug
|
|
69
|
-
const asyncCountSlice = state.select(async (s) => {
|
|
70
|
-
const data = await fetchData();
|
|
71
|
-
return data.count;
|
|
72
|
-
});
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
### **Combine Multiple States**
|
|
56
|
+
## 🧩 Selecting / Deriving
|
|
78
57
|
|
|
79
|
-
|
|
58
|
+
Create derived states from one or many sources.
|
|
80
59
|
|
|
81
|
-
```
|
|
60
|
+
```ts
|
|
82
61
|
import { create, select } from 'muya'
|
|
83
62
|
|
|
84
|
-
const
|
|
85
|
-
const
|
|
63
|
+
const a = create(1)
|
|
64
|
+
const b = create(2)
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
66
|
+
// Single source
|
|
67
|
+
const doubleA = a.select(n => n * 2)
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
// Multiple sources
|
|
70
|
+
const sum = select([a, b], (x, y) => x + y)
|
|
71
|
+
```
|
|
93
72
|
|
|
94
|
-
|
|
95
|
-
const state = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b);
|
|
73
|
+
**Equality checks** (to avoid re-emits):
|
|
96
74
|
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
```ts
|
|
76
|
+
const obj = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b)
|
|
77
|
+
obj.set(p => ({ ...p, a: p.a + 1 })) // does not notify (b unchanged)
|
|
99
78
|
```
|
|
100
79
|
|
|
101
|
-
|
|
80
|
+
You can also add an equality function on a `select`:
|
|
102
81
|
|
|
103
|
-
```
|
|
104
|
-
const
|
|
82
|
+
```ts
|
|
83
|
+
const stable = select([a, b], (x, y) => x + y, (prev, next) => prev === next)
|
|
105
84
|
```
|
|
106
85
|
|
|
107
86
|
---
|
|
108
87
|
|
|
88
|
+
## 🎣 Using in Components
|
|
109
89
|
|
|
110
|
-
|
|
90
|
+
Muya states are callable hooks. Prefer that.
|
|
91
|
+
If you want a hook wrapper or slicing, use `useValue`.
|
|
111
92
|
|
|
112
|
-
|
|
93
|
+
```tsx
|
|
94
|
+
import { create, useValue } from 'muya'
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
Each state can be called as the hook directly
|
|
116
|
-
```typescript
|
|
117
|
-
const userState = create(0);
|
|
96
|
+
const user = create({ id: 'u1', name: 'Ada', admin: false })
|
|
118
97
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
// Option 1: call the state directly
|
|
99
|
+
function Profile() {
|
|
100
|
+
const u = user()
|
|
101
|
+
return <div>{u.name}</div>
|
|
122
102
|
}
|
|
123
|
-
```
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
function App() {
|
|
131
|
-
const user = useValue(userState); // Access state via hook
|
|
132
|
-
return <p>User: {user}</p>;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### **Option 3: Slice with Hook**
|
|
137
|
-
For efficient re-renders, `useValue` provides a slicing method.
|
|
138
|
-
```typescript
|
|
139
|
-
function App() {
|
|
140
|
-
const count = useValue(state, (s) => s.count); // Use selector in hook
|
|
141
|
-
return <p>Count: {count}</p>;
|
|
104
|
+
// Option 2: useValue for a selector
|
|
105
|
+
function OnlyName() {
|
|
106
|
+
const name = useValue(user, u => u.name)
|
|
107
|
+
return <div>{name}</div>
|
|
142
108
|
}
|
|
143
109
|
```
|
|
144
110
|
|
|
145
111
|
---
|
|
146
112
|
|
|
147
|
-
##
|
|
113
|
+
## ⚡ Async & Lazy (the 2-minute mental model)
|
|
148
114
|
|
|
149
|
-
|
|
115
|
+
**Immediate vs Lazy**
|
|
116
|
+
```ts
|
|
117
|
+
create(0) // immediate: value exists now
|
|
118
|
+
create(() => 0) // lazy: computed on first read (.get() / component render)
|
|
119
|
+
```
|
|
150
120
|
|
|
151
|
-
|
|
121
|
+
**Async sources**
|
|
122
|
+
```ts
|
|
123
|
+
async function fetchInitial() { return 0 }
|
|
124
|
+
create(fetchInitial) // lazy + async
|
|
125
|
+
create(Promise.resolve(0)) // immediate + async
|
|
126
|
+
```
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
|
|
128
|
+
**Setting with async state**
|
|
129
|
+
- `state.set(2)` **overrides immediately** (cancels previous pending promise)
|
|
130
|
+
- `state.set(prev => prev + 1)` **waits for the current promise to resolve** so `prev` is always the resolved value
|
|
155
131
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
132
|
+
**Async selectors**
|
|
133
|
+
```ts
|
|
134
|
+
const base = create(0)
|
|
135
|
+
const plusOne = base.select(async n => {
|
|
136
|
+
await doWork()
|
|
137
|
+
return n + 1
|
|
138
|
+
})
|
|
163
139
|
```
|
|
140
|
+
- Async selects **suspend** the first time (and when their upstream async value requires it)
|
|
141
|
+
- A sync selector reading an async parent will **suspend once** on initial load
|
|
164
142
|
|
|
165
|
-
|
|
143
|
+
> Tip: Prefer keeping selectors **sync** and performing async work **before** calling `set`. It keeps render trees predictable.
|
|
166
144
|
|
|
167
|
-
|
|
145
|
+
---
|
|
168
146
|
|
|
169
|
-
|
|
170
|
-
const derived = select([state1, state2], (s1, s2) => s1 + s2);
|
|
147
|
+
## 🧪 API (short and sweet)
|
|
171
148
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
149
|
+
### `create<T>(initial, isEqual?) => State<T>`
|
|
150
|
+
**State methods**
|
|
151
|
+
- `get(): T` — read current value (resolves lazy/async when needed)
|
|
152
|
+
- `set(value | (prev) => next)` — update value (batched)
|
|
153
|
+
- `listen(fn)` — subscribe, returns `unsubscribe`
|
|
154
|
+
- `select(selector, isEqual?)` — derive new state from this state
|
|
155
|
+
- `destroy()` — clear listeners and dispose
|
|
156
|
+
- `withName(name)` — add a debug label (DevTools)
|
|
179
157
|
|
|
180
|
-
###
|
|
158
|
+
### `select([states], derive, isEqual?) => State<R>`
|
|
159
|
+
Derive a state from one or multiple states.
|
|
181
160
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
const value = useValue(state, (s) => s.slice); // Optional selector
|
|
186
|
-
```
|
|
161
|
+
### `useValue(state, selector?)`
|
|
162
|
+
React hook to read a state or a slice.
|
|
187
163
|
|
|
188
164
|
---
|
|
189
165
|
|
|
190
|
-
##
|
|
166
|
+
## 🪵 DevTools
|
|
167
|
+
In **dev**, Muya auto-connects to Redux DevTools if present and reports state updates by name.
|
|
168
|
+
Use `state.withName('CartItems')` to make timelines readable.
|
|
191
169
|
|
|
192
|
-
|
|
193
|
-
- **Batch Updates**: Muya batches internal updates for better performance, reducing communication overhead similarly how react do.
|
|
194
|
-
- **Async Selectors / Derives**: Muya has support for async selectors / derives, but do not recommend to use as it can lead to spaghetti re-renders code which is hard to maintain and debug, if you want so, you can or maybe you should consider using other alternatives like `Jotai`.
|
|
170
|
+
---
|
|
195
171
|
|
|
172
|
+
## 🤝 Patterns & Anti-patterns
|
|
196
173
|
|
|
174
|
+
**✅ Good**
|
|
175
|
+
- Keep selectors pure and fast
|
|
176
|
+
- Use equality checks to avoid unnecessary updates
|
|
177
|
+
- Do async outside, then `set` synchronously
|
|
178
|
+
|
|
179
|
+
**⚠️ Be cautious**
|
|
180
|
+
- Deep async chains of selectors → harder to debug and may re-suspend often
|
|
181
|
+
- Long-running async work inside selectors → push it out of render path
|
|
197
182
|
|
|
198
|
-
`Muya` encourage use async updates withing sync state like this:
|
|
199
|
-
```typescript
|
|
200
|
-
const state = create({ data: null });
|
|
201
|
-
async function update() {
|
|
202
|
-
const data = await fetchData();
|
|
203
|
-
state.set({ data });
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
183
|
---
|
|
207
184
|
|
|
208
|
-
|
|
185
|
+
## 🧭 Examples
|
|
209
186
|
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
187
|
+
**Boolean flags derived from async state (suspends only once):**
|
|
188
|
+
```tsx
|
|
189
|
+
const user = create(fetchUser) // async lazy
|
|
190
|
+
const isAdmin = user.select(u => u.role === 'admin')
|
|
191
|
+
// First mount suspends while user loads, subsequent updates are instant
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Batching inside events:**
|
|
195
|
+
```ts
|
|
196
|
+
function onCheckout() {
|
|
197
|
+
cart.set(c => applyDiscount(c))
|
|
198
|
+
total.set(t => t - 10)
|
|
199
|
+
// Muya batches internally; React sees one commit
|
|
200
|
+
}
|
|
217
201
|
```
|
|
202
|
+
|
|
218
203
|
---
|
|
219
204
|
|
|
220
|
-
|
|
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.
|
|
205
|
+
## 🗃️ (Optional) Muya + SQLite Companion
|
|
222
206
|
|
|
207
|
+
If you’re using the companion `muya/sqlite` package, you can manage large, queryable lists with React-friendly pagination:
|
|
223
208
|
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
```ts
|
|
210
|
+
import { createSqliteState } from 'muya/sqlite'
|
|
211
|
+
import { useSqliteValue } from 'muya/sqlite'
|
|
227
212
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
type Person = { id: string; name: string; age: number }
|
|
214
|
+
|
|
215
|
+
const people = createSqliteState<Person>({
|
|
216
|
+
backend, // e.g. expo-sqlite / bunMemoryBackend
|
|
217
|
+
tableName: 'People',
|
|
218
|
+
key: 'id',
|
|
219
|
+
indexes: ['age'], // optional
|
|
220
|
+
})
|
|
231
221
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
// In React: stepwise fetching + where/order/limit
|
|
223
|
+
function PeopleList() {
|
|
224
|
+
const [rows, actions] = useSqliteValue(people, { sorBy: 'age', order: 'asc', limit: 50 }, [])
|
|
225
|
+
return (
|
|
226
|
+
<>
|
|
227
|
+
<ul>{rows.map(p => <li key={p.id}>{p.name}</li>)}</ul>
|
|
228
|
+
<button onClick={() => actions.next()}>Load more</button>
|
|
229
|
+
<button onClick={() => actions.reset()}>Reset</button>
|
|
230
|
+
</>
|
|
231
|
+
)
|
|
237
232
|
}
|
|
238
|
-
|
|
239
|
-
const state = create(initialLoad)
|
|
240
|
-
// or
|
|
241
|
-
const state = create(Promise.resolve(0))
|
|
233
|
+
```
|
|
242
234
|
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
**Quick ops**
|
|
236
|
+
```ts
|
|
237
|
+
await people.batchSet([{ id:'1', name:'Alice', age:30 }])
|
|
238
|
+
await people.set({ id:'2', name:'Bob', age:25 })
|
|
239
|
+
await people.delete('1')
|
|
240
|
+
const alice = await people.get('2', p => p.name) // 'Bob'
|
|
241
|
+
const count = await people.count({ where: { age: { gt: 20 } } })
|
|
242
|
+
for await (const p of people.search({ where: { name: { like: '%Ali%' }}})) { /* … */ }
|
|
245
243
|
```
|
|
246
244
|
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## ❓ FAQ
|
|
248
|
+
|
|
249
|
+
**Is Muya a replacement for Redux/Zustand/Jotai?**
|
|
250
|
+
No—Muya is intentionally tiny. If you need complex middleware, effects, or ecosystem plugins, those tools are great choices.
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
2. Call `.set((prev) => prev + 1)` will wait until previous promise is resolved, so previous value in set callback is always resolved.
|
|
252
|
+
**Can I use Suspense?**
|
|
253
|
+
Yes. Async states/selectors will suspend on first read (and when upstream requires it).
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
2. If the selector is async (`state.select(async (s) => s + 1)`) it will automatically use suspense mode, so on each change it firstly resolved promise (hit suspense) and then update the value.
|
|
257
|
-
3. If the selector is sync, but it's derived from async selector, it will also be in suspense mode - but it depends what the parent is, if the parent is async state, it will hit the suspense only once, on initial load, but if the parent is another async selector, it will hit suspense on each change.
|
|
255
|
+
**How do I avoid re-renders?**
|
|
256
|
+
Provide an `isEqual(prev, next)` to `create` or `select`, or select a smaller slice in `useValue`.
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
`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.
|
|
258
|
+
---
|
|
261
259
|
|
|
262
|
-
##
|
|
260
|
+
## 🧪 Testing Tips
|
|
261
|
+
- State reads/writes are synchronous, but **async sources/selectors** resolve over time. In tests, use `await waitFor(...)` around expectations that depend on async resolution.
|
|
263
262
|
|
|
264
|
-
|
|
265
|
-
If you enjoy `Muya`, please give it a ⭐️! :)
|
|
263
|
+
---
|
|
266
264
|
|
|
265
|
+
## 📜 License
|
|
266
|
+
MIT — if you like Muya, a ⭐️ is always appreciated!
|
|
267
267
|
|
|
268
|
+
---
|
|
268
269
|
|
|
270
|
+
### Changelog / Contributing
|
|
271
|
+
See repo issues and PRs. Keep changes small and measured—Muya’s value is simplicity.
|