@thefoxieflow/signal-ctx 0.1.0 β†’ 0.1.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 ADDED
@@ -0,0 +1,379 @@
1
+ # signal-ctx
2
+
3
+ A tiny, signal-based state utility for React that **solves the `useContext` re-render problem** using
4
+ **`useSyncExternalStore`**.
5
+
6
+ If you’ve ever had this issue:
7
+
8
+ ```ts
9
+ { count, book }
10
+ // updating count re-renders book components 😑
11
+ ```
12
+
13
+ `signal-ctx` is designed specifically to fix that.
14
+
15
+ ---
16
+
17
+ ## ✨ Why signal-ctx?
18
+
19
+ ### ❌ The Problem with `useContext`
20
+
21
+ React Context subscribes to the **entire value**.
22
+
23
+ ```tsx
24
+ const { book } = useContext(StoreCtx)
25
+ ```
26
+
27
+ When **any property changes**, **every consumer re-renders** β€” even if they don’t use it.
28
+
29
+ This is not a bug. Context has **no selector mechanism**.
30
+
31
+ ---
32
+
33
+ ### βœ… The Solution
34
+
35
+ `signal-ctx`:
36
+
37
+ * Moves state **outside React**
38
+ * Uses **external subscriptions**
39
+ * Allows **selector-based updates**
40
+
41
+ So only the components that *actually use* the changed data re-render.
42
+
43
+ ---
44
+
45
+ ## ✨ Features
46
+
47
+ * ⚑ Signal-style state container
48
+ * 🎯 Selector-based subscriptions
49
+ * 🧡 React 18 concurrent-safe
50
+ * 🧩 Context-backed but not context-driven
51
+ * πŸ“¦ Very small bundle size
52
+ * 🌳 Tree-shakable
53
+ * 🧠 Explicit and predictable
54
+
55
+ ---
56
+
57
+ ## πŸ“¦ Installation
58
+
59
+ ```bash
60
+ npm install @thefoxieflow/signal-ctx
61
+ ```
62
+
63
+ > **Peer dependency:** React 18+
64
+
65
+ ---
66
+
67
+ ## 🧠 Core Idea
68
+
69
+ Context **does not store state**.
70
+
71
+ It stores a **stable signal reference**.
72
+
73
+ ```tsx
74
+ <Provider value={store} />
75
+ ```
76
+
77
+ The state lives **outside React**, and components subscribe **directly to the signal**.
78
+
79
+ ---
80
+
81
+ ## πŸ”Ή Signal Store
82
+
83
+ A signal is:
84
+
85
+ * A function that returns state
86
+ * Can be subscribed to
87
+ * Can be updated imperatively
88
+
89
+ ```ts
90
+ type Store<T> = {
91
+ (): T
92
+ subscribe(fn: () => void): () => void
93
+ set(action: SetStateAction<T>): void
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## πŸ”Ή Low-Level Hooks
100
+
101
+ ### `createStore(()=>{})`
102
+
103
+ ### `useStoreValue(store, selector?)`
104
+
105
+ Subscribe to a signal.
106
+
107
+ ```tsx
108
+ const count = useStoreValue(store, s => s.count)
109
+ ```
110
+
111
+ * Uses `useSyncExternalStore`
112
+ * Re-renders only when the selected value changes
113
+ * Selector is optional
114
+
115
+ ---
116
+
117
+ ### `useSetStore(store, selector?)`
118
+
119
+ Returns a setter function.
120
+
121
+ ```ts
122
+ const set = useSetStore(store)
123
+
124
+ set(prev => ({ ...prev, count: prev.count + 1 }))
125
+ ```
126
+
127
+ Scoped update:
128
+
129
+ ```ts
130
+ // book must be object for selector
131
+ const setBook = useSetSignal(store, s => s.book)
132
+
133
+
134
+ // put: update an object
135
+ setBook({
136
+ title : "book"
137
+ })
138
+
139
+ // patch: update partially
140
+ setBook((b)=>{
141
+ b.title = "book"
142
+ })
143
+ ```
144
+
145
+ ⚠️ Updates are **mutation-based**. Spread manually if you want immutability.
146
+
147
+ ---
148
+
149
+ ## πŸ”Ή Context-Based API
150
+
151
+ ### `createCtx(init)`
152
+
153
+ Creates a **context-backed signal store hook**.
154
+
155
+ ```ts
156
+ import { createCtx } from "@your-scope/signal-ctx"
157
+
158
+ export const useStore = createCtx(() => ({
159
+ count: 0,
160
+ book: { title: "1984" }
161
+ }))
162
+ ```
163
+
164
+ ---
165
+
166
+ ## πŸš€ Usage
167
+
168
+ ### 1. Create a Provider
169
+
170
+ ```tsx
171
+ // use default initial value from useStore
172
+ type Props = {
173
+ children: React.ReactNode
174
+ }
175
+
176
+ export function StoreProvider({ children }: Props) {
177
+ const Provider = useStore.provide()
178
+ return <Provider>{children}</Provider>
179
+ }
180
+
181
+ // overwrite value
182
+ export function StoreProvider({ children }: Props) {
183
+ const Provider = useStore.provide({
184
+ value: {
185
+ count: 10,
186
+ book: { title: "Brave New World" }
187
+ }
188
+ })
189
+
190
+ return <Provider>{children}</Provider>
191
+ }
192
+ ```
193
+
194
+ ```tsx
195
+ <StoreProvider>
196
+ <App />
197
+ </StoreProvider>
198
+ ```
199
+
200
+ ---
201
+
202
+ ### 2. Read only what you need
203
+
204
+ ```tsx
205
+ function Count() {
206
+ const count = useStore(s => s.count)
207
+ return <div>{count}</div>
208
+ }
209
+
210
+ function Book() {
211
+ const book = useStore(s => s.book)
212
+ return <div>{book.title}</div>
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ### 3. Update state
219
+
220
+ ```tsx
221
+ function Increment() {
222
+ const set = useStore.useSetter()
223
+
224
+ return (
225
+ <button onClick={() =>
226
+ set(s => ({ ...s, count: s.count + 1 }))
227
+ }>
228
+ +
229
+ </button>
230
+ )
231
+ }
232
+ ```
233
+
234
+ βœ… Updating `count` **does NOT re-render** `Book`.
235
+
236
+ ---
237
+
238
+ ## 🧩 Why This Works
239
+
240
+ * Context value never changes
241
+ * React does not re-render on context updates
242
+ * `useSyncExternalStore` compares selected snapshots
243
+ * Only changed selectors trigger re-renders
244
+
245
+ This is the **same model** used by:
246
+
247
+ * Redux `useSelector`
248
+ * Zustand selectors
249
+ * React’s official external store docs
250
+
251
+ ---
252
+
253
+ ## ⚠️ Important Rule
254
+
255
+ > **Never destructure the entire state.**
256
+ > Always select the smallest possible slice.
257
+
258
+ ❌ Bad:
259
+
260
+ ```ts
261
+ const state = useStore(s => s)
262
+ ```
263
+
264
+ βœ… Good:
265
+
266
+ ```ts
267
+ const count = useStore(s => s.count)
268
+ ```
269
+
270
+ ---
271
+
272
+ ## 🧩 Multiple Stores
273
+
274
+ You can create isolated stores using `storeName`.
275
+
276
+ ```tsx
277
+ type Props = {
278
+ children: React.ReactNode
279
+ storeName: string
280
+ initialValue?: { count: number; book: { title: string } }
281
+ }
282
+
283
+ export function StoreProvider({ children, storeName, initialValue }: Props) {
284
+ const Provider = useStore.provide({
285
+ storeName,
286
+ value: initialValue || { count: 0, book: { title: "1984" } }
287
+ })
288
+
289
+ return <Provider>{children}</Provider>
290
+ }
291
+ ```
292
+
293
+ ### Usage
294
+ ```tsx
295
+ <StoreProvider storeName="storeA" initialValue={{ count: 1, book: { title: "A" } }}>
296
+ <AppA />
297
+ <StoreProvider storeName="storeB" initialValue={{ count: 5, book: { title: "B" } }}>
298
+ <AppB />
299
+ </StoreProvider>
300
+ </StoreProvider>
301
+
302
+ function AppB(){
303
+ // book from parent context; parent = "storeA" so book.title = "A"
304
+ const book = useStore(s => s.book)
305
+
306
+ // but i want a book from storeA; so it requires storeName
307
+ // book.title = "B"
308
+ const storeABook = useStore(s => s.book,{ storeName :"storeA" })
309
+
310
+ // same apply to setter
311
+ const setBookStoreA = useStore.useSetter((s)=>s.book, {storeName : "storeA"})
312
+
313
+ const handleClick = (text)=>{
314
+ setCountStoreA((s) => {
315
+ s.title = "change A to AA"
316
+ return s
317
+ })
318
+ }
319
+ }
320
+ ```
321
+
322
+
323
+
324
+ Each store is independent.
325
+
326
+ ---
327
+
328
+ ## 🌐 Server-Side Rendering (SSR)
329
+
330
+ `signal-ctx` is SSR-safe.
331
+
332
+ * Uses `useSyncExternalStore`
333
+ * Identical snapshot logic on server & client
334
+ * No shared global state between requests
335
+
336
+ ---
337
+
338
+ ## ⚠️ Caveats
339
+
340
+ * No middleware
341
+ * No devtools
342
+ * No persistence
343
+ * Mutation-based updates by design
344
+
345
+ Best suited for:
346
+
347
+ * UI state
348
+ * App-level shared state
349
+ * Lightweight global stores
350
+
351
+ ---
352
+
353
+ ## πŸ§ͺ TypeScript
354
+
355
+ Fully typed with generics and inferred selectors.
356
+
357
+ ```ts
358
+ const count = useStore(s => s.count) // number
359
+ ```
360
+
361
+ ---
362
+
363
+ ## πŸ“„ License
364
+
365
+ MIT
366
+
367
+ ---
368
+
369
+ ## ⭐ Philosophy
370
+
371
+ `signal-ctx` is intentionally small.
372
+
373
+ It favors:
374
+
375
+ * Explicit ownership
376
+ * Predictable updates
377
+ * Minimal abstraction
378
+
379
+ If you understand React, you understand `signal-ctx`.
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- "use client";
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
package/dist/index.mjs CHANGED
@@ -1,5 +1,3 @@
1
- "use client";
2
-
3
1
  // src/index.tsx
4
2
  import {
5
3
  createContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thefoxieflow/signal-ctx",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Lightweight signal-based React context store",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",