context-scoped-state 0.0.10 → 0.0.12
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/dist/index.js +76 -67
- package/dist/lib/Store.d.ts +3 -4
- package/dist/lib/Store.d.ts.map +1 -1
- package/dist/lib/createStore.d.ts +6 -3
- package/dist/lib/createStore.d.ts.map +1 -1
- package/package.json +1 -1
- package/README.md +0 -233
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as C } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import h, { useSyncExternalStore as H } from "react";
|
|
3
3
|
var m = function(e, r) {
|
|
4
4
|
return m = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(t, n) {
|
|
5
5
|
t.__proto__ = n;
|
|
@@ -7,7 +7,7 @@ var m = function(e, r) {
|
|
|
7
7
|
for (var o in n) Object.prototype.hasOwnProperty.call(n, o) && (t[o] = n[o]);
|
|
8
8
|
}, m(e, r);
|
|
9
9
|
};
|
|
10
|
-
function
|
|
10
|
+
function v(e, r) {
|
|
11
11
|
if (typeof r != "function" && r !== null)
|
|
12
12
|
throw new TypeError("Class extends value " + String(r) + " is not a constructor or null");
|
|
13
13
|
m(e, r);
|
|
@@ -85,8 +85,8 @@ var S = (function() {
|
|
|
85
85
|
if (this._parentage = null, Array.isArray(s))
|
|
86
86
|
try {
|
|
87
87
|
for (var u = w(s), c = u.next(); !c.done; c = u.next()) {
|
|
88
|
-
var
|
|
89
|
-
|
|
88
|
+
var f = c.value;
|
|
89
|
+
f.remove(this);
|
|
90
90
|
}
|
|
91
91
|
} catch (a) {
|
|
92
92
|
r = { error: a };
|
|
@@ -110,8 +110,8 @@ var S = (function() {
|
|
|
110
110
|
if (P) {
|
|
111
111
|
this._finalizers = null;
|
|
112
112
|
try {
|
|
113
|
-
for (var
|
|
114
|
-
var V =
|
|
113
|
+
for (var b = w(P), p = b.next(); !p.done; p = b.next()) {
|
|
114
|
+
var V = p.value;
|
|
115
115
|
try {
|
|
116
116
|
A(V);
|
|
117
117
|
} catch (a) {
|
|
@@ -122,7 +122,7 @@ var S = (function() {
|
|
|
122
122
|
n = { error: a };
|
|
123
123
|
} finally {
|
|
124
124
|
try {
|
|
125
|
-
|
|
125
|
+
p && !p.done && (o = b.return) && o.call(b);
|
|
126
126
|
} finally {
|
|
127
127
|
if (n) throw n.error;
|
|
128
128
|
}
|
|
@@ -169,7 +169,7 @@ function A(e) {
|
|
|
169
169
|
}
|
|
170
170
|
var $ = {
|
|
171
171
|
Promise: void 0
|
|
172
|
-
},
|
|
172
|
+
}, W = {
|
|
173
173
|
setTimeout: function(e, r) {
|
|
174
174
|
for (var t = [], n = 2; n < arguments.length; n++)
|
|
175
175
|
t[n - 2] = arguments[n];
|
|
@@ -180,21 +180,21 @@ var $ = {
|
|
|
180
180
|
},
|
|
181
181
|
delegate: void 0
|
|
182
182
|
};
|
|
183
|
-
function
|
|
184
|
-
|
|
183
|
+
function q(e) {
|
|
184
|
+
W.setTimeout(function() {
|
|
185
185
|
throw e;
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
-
function
|
|
188
|
+
function T() {
|
|
189
189
|
}
|
|
190
190
|
function y(e) {
|
|
191
191
|
e();
|
|
192
192
|
}
|
|
193
193
|
var F = (function(e) {
|
|
194
|
-
|
|
194
|
+
v(r, e);
|
|
195
195
|
function r(t) {
|
|
196
196
|
var n = e.call(this) || this;
|
|
197
|
-
return n.isStopped = !1, t ? (n.destination = t, R(t) && t.add(n)) : n.destination =
|
|
197
|
+
return n.isStopped = !1, t ? (n.destination = t, R(t) && t.add(n)) : n.destination = J, n;
|
|
198
198
|
}
|
|
199
199
|
return r.create = function(t, n, o) {
|
|
200
200
|
return new O(t, n, o);
|
|
@@ -221,7 +221,7 @@ var F = (function(e) {
|
|
|
221
221
|
this.unsubscribe();
|
|
222
222
|
}
|
|
223
223
|
}, r;
|
|
224
|
-
})(S),
|
|
224
|
+
})(S), D = (function() {
|
|
225
225
|
function e(r) {
|
|
226
226
|
this.partialObserver = r;
|
|
227
227
|
}
|
|
@@ -253,42 +253,42 @@ var F = (function(e) {
|
|
|
253
253
|
}
|
|
254
254
|
}, e;
|
|
255
255
|
})(), O = (function(e) {
|
|
256
|
-
|
|
256
|
+
v(r, e);
|
|
257
257
|
function r(t, n, o) {
|
|
258
258
|
var i = e.call(this) || this, s;
|
|
259
259
|
return l(t) || !t ? s = {
|
|
260
260
|
next: t ?? void 0,
|
|
261
261
|
error: n ?? void 0,
|
|
262
262
|
complete: o ?? void 0
|
|
263
|
-
} : s = t, i.destination = new
|
|
263
|
+
} : s = t, i.destination = new D(s), i;
|
|
264
264
|
}
|
|
265
265
|
return r;
|
|
266
266
|
})(F);
|
|
267
267
|
function d(e) {
|
|
268
|
-
|
|
268
|
+
q(e);
|
|
269
269
|
}
|
|
270
|
-
function
|
|
270
|
+
function G(e) {
|
|
271
271
|
throw e;
|
|
272
272
|
}
|
|
273
|
-
var
|
|
273
|
+
var J = {
|
|
274
274
|
closed: !0,
|
|
275
|
-
next:
|
|
276
|
-
error:
|
|
277
|
-
complete:
|
|
278
|
-
},
|
|
275
|
+
next: T,
|
|
276
|
+
error: G,
|
|
277
|
+
complete: T
|
|
278
|
+
}, K = (function() {
|
|
279
279
|
return typeof Symbol == "function" && Symbol.observable || "@@observable";
|
|
280
280
|
})();
|
|
281
|
-
function
|
|
281
|
+
function L(e) {
|
|
282
282
|
return e;
|
|
283
283
|
}
|
|
284
|
-
function
|
|
285
|
-
return e.length === 0 ?
|
|
284
|
+
function Q(e) {
|
|
285
|
+
return e.length === 0 ? L : e.length === 1 ? e[0] : function(t) {
|
|
286
286
|
return e.reduce(function(n, o) {
|
|
287
287
|
return o(n);
|
|
288
288
|
}, t);
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
-
var
|
|
291
|
+
var I = (function() {
|
|
292
292
|
function e(r) {
|
|
293
293
|
r && (this._subscribe = r);
|
|
294
294
|
}
|
|
@@ -326,12 +326,12 @@ var T = (function() {
|
|
|
326
326
|
}, e.prototype._subscribe = function(r) {
|
|
327
327
|
var t;
|
|
328
328
|
return (t = this.source) === null || t === void 0 ? void 0 : t.subscribe(r);
|
|
329
|
-
}, e.prototype[
|
|
329
|
+
}, e.prototype[K] = function() {
|
|
330
330
|
return this;
|
|
331
331
|
}, e.prototype.pipe = function() {
|
|
332
332
|
for (var r = [], t = 0; t < arguments.length; t++)
|
|
333
333
|
r[t] = arguments[t];
|
|
334
|
-
return
|
|
334
|
+
return Q(r)(this);
|
|
335
335
|
}, e.prototype.toPromise = function(r) {
|
|
336
336
|
var t = this;
|
|
337
337
|
return r = k(r), new r(function(n, o) {
|
|
@@ -363,7 +363,7 @@ var z = M(function(e) {
|
|
|
363
363
|
e(this), this.name = "ObjectUnsubscribedError", this.message = "object unsubscribed";
|
|
364
364
|
};
|
|
365
365
|
}), B = (function(e) {
|
|
366
|
-
|
|
366
|
+
v(r, e);
|
|
367
367
|
function r() {
|
|
368
368
|
var t = e.call(this) || this;
|
|
369
369
|
return t.closed = !1, t.currentObservers = null, t.observers = [], t.isStopped = !1, t.hasError = !1, t.thrownError = null, t;
|
|
@@ -385,8 +385,8 @@ var z = M(function(e) {
|
|
|
385
385
|
var c = u.value;
|
|
386
386
|
c.next(t);
|
|
387
387
|
}
|
|
388
|
-
} catch (
|
|
389
|
-
o = { error:
|
|
388
|
+
} catch (f) {
|
|
389
|
+
o = { error: f };
|
|
390
390
|
} finally {
|
|
391
391
|
try {
|
|
392
392
|
u && !u.done && (i = s.return) && i.call(s);
|
|
@@ -436,13 +436,13 @@ var z = M(function(e) {
|
|
|
436
436
|
var n = this, o = n.hasError, i = n.thrownError, s = n.isStopped;
|
|
437
437
|
o ? t.error(i) : s && t.complete();
|
|
438
438
|
}, r.prototype.asObservable = function() {
|
|
439
|
-
var t = new
|
|
439
|
+
var t = new I();
|
|
440
440
|
return t.source = this, t;
|
|
441
441
|
}, r.create = function(t, n) {
|
|
442
442
|
return new U(t, n);
|
|
443
443
|
}, r;
|
|
444
|
-
})(
|
|
445
|
-
|
|
444
|
+
})(I), U = (function(e) {
|
|
445
|
+
v(r, e);
|
|
446
446
|
function r(t, n) {
|
|
447
447
|
var o = e.call(this) || this;
|
|
448
448
|
return o.destination = t, o.source = n, o;
|
|
@@ -461,7 +461,7 @@ var z = M(function(e) {
|
|
|
461
461
|
return (o = (n = this.source) === null || n === void 0 ? void 0 : n.subscribe(t)) !== null && o !== void 0 ? o : Y;
|
|
462
462
|
}, r;
|
|
463
463
|
})(B), N = (function(e) {
|
|
464
|
-
|
|
464
|
+
v(r, e);
|
|
465
465
|
function r(t) {
|
|
466
466
|
var n = e.call(this) || this;
|
|
467
467
|
return n._value = t, n;
|
|
@@ -486,8 +486,10 @@ var z = M(function(e) {
|
|
|
486
486
|
})(B);
|
|
487
487
|
class et {
|
|
488
488
|
_stateSubject;
|
|
489
|
-
constructor() {
|
|
490
|
-
this.
|
|
489
|
+
constructor(r) {
|
|
490
|
+
this._stateSubject = new N(
|
|
491
|
+
this.getInitialState(r)
|
|
492
|
+
);
|
|
491
493
|
}
|
|
492
494
|
getState() {
|
|
493
495
|
return this._stateSubject.value;
|
|
@@ -495,7 +497,6 @@ class et {
|
|
|
495
497
|
state$() {
|
|
496
498
|
return this._stateSubject.asObservable();
|
|
497
499
|
}
|
|
498
|
-
state;
|
|
499
500
|
setState(r) {
|
|
500
501
|
const t = typeof r == "function" ? r(this._stateSubject.value) : r;
|
|
501
502
|
this._stateSubject.next(t);
|
|
@@ -506,10 +507,14 @@ class et {
|
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
509
|
function nt(e) {
|
|
509
|
-
const r =
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
const r = class extends e {
|
|
511
|
+
state;
|
|
512
|
+
}, t = h.createContext(
|
|
513
|
+
void 0
|
|
514
|
+
);
|
|
515
|
+
function n() {
|
|
516
|
+
const o = h.useContext(t);
|
|
517
|
+
if (!o)
|
|
513
518
|
throw new Error(
|
|
514
519
|
`Store hook used outside of its Context provider.
|
|
515
520
|
|
|
@@ -521,38 +526,42 @@ Then wrap your component with:
|
|
|
521
526
|
<YourComponent />
|
|
522
527
|
</useYourStore.Context>`
|
|
523
528
|
);
|
|
524
|
-
const
|
|
525
|
-
(
|
|
526
|
-
const
|
|
527
|
-
return () =>
|
|
529
|
+
const i = h.useCallback(
|
|
530
|
+
(u) => {
|
|
531
|
+
const c = o.state$().subscribe(u);
|
|
532
|
+
return () => c.unsubscribe();
|
|
528
533
|
},
|
|
529
|
-
|
|
530
|
-
|
|
534
|
+
[o]
|
|
535
|
+
), s = H(
|
|
536
|
+
i,
|
|
537
|
+
() => o.getState(),
|
|
538
|
+
() => o.getState()
|
|
531
539
|
// getServerSnapshot for SSR
|
|
532
540
|
);
|
|
533
|
-
return
|
|
541
|
+
return o.state = s, o;
|
|
534
542
|
}
|
|
535
|
-
return
|
|
536
|
-
children:
|
|
543
|
+
return n.Context = function({
|
|
544
|
+
children: i,
|
|
545
|
+
value: s
|
|
537
546
|
}) {
|
|
538
|
-
const [
|
|
539
|
-
return /* @__PURE__ */ C(
|
|
540
|
-
},
|
|
541
|
-
children:
|
|
542
|
-
state:
|
|
547
|
+
const [u] = h.useState(() => new r(s));
|
|
548
|
+
return /* @__PURE__ */ C(t.Provider, { value: u, children: i });
|
|
549
|
+
}, n.MockContext = function({
|
|
550
|
+
children: i,
|
|
551
|
+
state: s
|
|
543
552
|
}) {
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
return new
|
|
547
|
-
class
|
|
553
|
+
const u = () => {
|
|
554
|
+
if (s === void 0)
|
|
555
|
+
return new r();
|
|
556
|
+
const f = class extends r {
|
|
548
557
|
getInitialState() {
|
|
549
|
-
return
|
|
558
|
+
return s;
|
|
550
559
|
}
|
|
551
|
-
}
|
|
552
|
-
return new
|
|
553
|
-
},
|
|
554
|
-
return /* @__PURE__ */ C(
|
|
555
|
-
},
|
|
560
|
+
};
|
|
561
|
+
return new f();
|
|
562
|
+
}, c = h.useRef(u());
|
|
563
|
+
return /* @__PURE__ */ C(t.Provider, { value: c.current, children: i });
|
|
564
|
+
}, n;
|
|
556
565
|
}
|
|
557
566
|
export {
|
|
558
567
|
et as Store,
|
package/dist/lib/Store.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
declare abstract class Store<T
|
|
2
|
-
protected abstract getInitialState(): T;
|
|
1
|
+
declare abstract class Store<T, C = Partial<T>> {
|
|
2
|
+
protected abstract getInitialState(contextValue?: C): T;
|
|
3
3
|
private readonly _stateSubject;
|
|
4
|
-
constructor();
|
|
4
|
+
constructor(contextValue?: C);
|
|
5
5
|
getState(): T;
|
|
6
6
|
state$(): import('rxjs').Observable<T>;
|
|
7
|
-
state: T;
|
|
8
7
|
protected setState(newState: T | ((currentState: T) => T)): void;
|
|
9
8
|
protected patchState(partialState: Partial<T> | ((currentState: T) => Partial<T>)): void;
|
|
10
9
|
}
|
package/dist/lib/Store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../../src/lib/Store.ts"],"names":[],"mappings":"AAEA,uBAAe,KAAK,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"Store.d.ts","sourceRoot":"","sources":["../../src/lib/Store.ts"],"names":[],"mappings":"AAEA,uBAAe,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IACpC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC;IAEvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,YAAY,CAAC,EAAE,CAAC;IAM5B,QAAQ,IAAI,CAAC;IAIb,MAAM;IAIN,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI;IAQhE,SAAS,CAAC,UAAU,CAClB,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAC3D,IAAI;CAQR;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { Store } from './Store';
|
|
3
|
-
declare function createStoreHook<T extends Store<any>>(storeClass: new () => T): {
|
|
4
|
-
(): T
|
|
5
|
-
|
|
3
|
+
declare function createStoreHook<T extends Store<any, any>>(storeClass: new (contextValue?: any) => T): {
|
|
4
|
+
(): T & {
|
|
5
|
+
readonly state: ReturnType<T["getState"]>;
|
|
6
|
+
};
|
|
7
|
+
Context({ children, value, }: {
|
|
6
8
|
children: React.ReactNode;
|
|
9
|
+
value?: T extends Store<any, infer C> ? C : never;
|
|
7
10
|
}): import("react/jsx-runtime").JSX.Element;
|
|
8
11
|
MockContext({ children, state, }: {
|
|
9
12
|
children: React.ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createStore.d.ts","sourceRoot":"","sources":["../../src/lib/createStore.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,iBAAS,eAAe,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"createStore.d.ts","sourceRoot":"","sources":["../../src/lib/createStore.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,iBAAS,eAAe,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,EAChD,UAAU,EAAE,KAAK,YAAY,CAAC,EAAE,GAAG,KAAK,CAAC;;;;kCAyDtC;QACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;QAC1B,KAAK,CAAC,6BAvD2C,CAAC,cAuDzB;KAC1B;sCAWE;QACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;QAC1B,KAAK,CAAC,4BAAY;KACnB;EAwBF;AACD,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="logo.svg" alt="context-scoped-state logo" width="120" height="120">
|
|
3
|
-
<h1>context-scoped-state</h1>
|
|
4
|
-
<p><strong>State management that respects component boundaries.</strong></p>
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
Unlike global state libraries (Redux, Zustand), `context-scoped-state` keeps your state where it belongs — scoped to the component tree that needs it. Each context provider creates an independent store instance, making your components truly reusable and your tests truly isolated.
|
|
8
|
-
|
|
9
|
-
## Why Scoped State?
|
|
10
|
-
|
|
11
|
-
Global state is convenient, but it comes with hidden costs:
|
|
12
|
-
|
|
13
|
-
- **Testing nightmares** — State leaks between tests, requiring complex cleanup
|
|
14
|
-
- **Component coupling** — Reusing components means sharing their global state
|
|
15
|
-
- **Implicit dependencies** — Components magically depend on global singletons
|
|
16
|
-
|
|
17
|
-
`context-scoped-state` solves this by leveraging React's Context API the right way. Same API simplicity, but with proper encapsulation.
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install context-scoped-state
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
yarn add context-scoped-state
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pnpm add context-scoped-state
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
> **Peer Dependencies:** React 18+
|
|
34
|
-
|
|
35
|
-
## Try it Online
|
|
36
|
-
|
|
37
|
-
[](https://stackblitz.com/github/HarshRohila/context-scoped-state/tree/master/examples/playground)
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
### 1. Create Your Store (one file, one export)
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
// counterStore.ts
|
|
45
|
-
import { Store, createStoreHook } from 'context-scoped-state';
|
|
46
|
-
|
|
47
|
-
class CounterStore extends Store<{ count: number }> {
|
|
48
|
-
protected getInitialState() {
|
|
49
|
-
return { count: 0 };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
increment() {
|
|
53
|
-
// Callback-based: receives current state, returns new state
|
|
54
|
-
this.setState(state => ({ count: state.count + 1 }));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
decrement() {
|
|
58
|
-
// Direct value: pass the new state directly
|
|
59
|
-
this.setState({ count: this.getState().count - 1 });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// This single export is all you need
|
|
64
|
-
export const useCounterStore = createStoreHook(CounterStore);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 2. Use in Your App
|
|
68
|
-
|
|
69
|
-
```tsx
|
|
70
|
-
import { useCounterStore } from './counterStore';
|
|
71
|
-
|
|
72
|
-
function Counter() {
|
|
73
|
-
const counterStore = useCounterStore();
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div>
|
|
77
|
-
<span>{counterStore.state.count}</span>
|
|
78
|
-
<button onClick={() => counterStore.increment()}>+</button>
|
|
79
|
-
<button onClick={() => counterStore.decrement()}>-</button>
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function App() {
|
|
85
|
-
return (
|
|
86
|
-
<useCounterStore.Context>
|
|
87
|
-
<Counter />
|
|
88
|
-
</useCounterStore.Context>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
That's it. One hook export gives you the hook and its `.Context` provider. No extra setup needed.
|
|
94
|
-
|
|
95
|
-
### Partial State Updates with patchState
|
|
96
|
-
|
|
97
|
-
For stores with multiple properties, use `patchState` to update only specific fields:
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
class UserStore extends Store<{ name: string; age: number; email: string }> {
|
|
101
|
-
protected getInitialState() {
|
|
102
|
-
return { name: '', age: 0, email: '' };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
updateName(name: string) {
|
|
106
|
-
// Only updates name, preserves age and email
|
|
107
|
-
this.patchState({ name });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
incrementAge() {
|
|
111
|
-
// Callback-based: receives current state, returns partial update
|
|
112
|
-
this.patchState(state => ({ age: state.age + 1 }));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
- `setState` — Replaces the entire state
|
|
118
|
-
- `patchState` — Merges partial updates into existing state
|
|
119
|
-
|
|
120
|
-
## Examples
|
|
121
|
-
|
|
122
|
-
### Independent Nested Stores
|
|
123
|
-
|
|
124
|
-
Each `Context` creates a completely independent store instance. Perfect for reusable widget patterns:
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
function PlayerScore() {
|
|
128
|
-
const store = useScoreStore();
|
|
129
|
-
return <span>Score: {store.state.score}</span>;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function Game() {
|
|
133
|
-
return (
|
|
134
|
-
<div>
|
|
135
|
-
{/* Player 1 has their own score */}
|
|
136
|
-
<useScoreStore.Context>
|
|
137
|
-
<h2>Player 1</h2>
|
|
138
|
-
<PlayerScore />
|
|
139
|
-
</useScoreStore.Context>
|
|
140
|
-
|
|
141
|
-
{/* Player 2 has their own score */}
|
|
142
|
-
<useScoreStore.Context>
|
|
143
|
-
<h2>Player 2</h2>
|
|
144
|
-
<PlayerScore />
|
|
145
|
-
</useScoreStore.Context>
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Both players have completely independent state — no configuration needed.
|
|
152
|
-
|
|
153
|
-
### Testing with MockContext
|
|
154
|
-
|
|
155
|
-
Test components in any state without complex setup:
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
import { render, screen } from '@testing-library/react';
|
|
159
|
-
|
|
160
|
-
test('shows warning when balance is low', () => {
|
|
161
|
-
render(
|
|
162
|
-
<useAccountStore.MockContext state={{ balance: 5, currency: 'USD' }}>
|
|
163
|
-
<AccountStatus />
|
|
164
|
-
</useAccountStore.MockContext>,
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
expect(screen.getByText('Low balance warning')).toBeInTheDocument();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('shows normal status when balance is healthy', () => {
|
|
171
|
-
render(
|
|
172
|
-
<useAccountStore.MockContext state={{ balance: 1000, currency: 'USD' }}>
|
|
173
|
-
<AccountStatus />
|
|
174
|
-
</useAccountStore.MockContext>,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
expect(screen.queryByText('Low balance warning')).not.toBeInTheDocument();
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
No mocking libraries. No global state cleanup. Just render with the state you need.
|
|
182
|
-
|
|
183
|
-
## Why context-scoped-state Over Other Libraries?
|
|
184
|
-
|
|
185
|
-
| Feature | context-scoped-state | Redux | Zustand |
|
|
186
|
-
| ---------------------- | -------------------- | ---------------------------- | -------------- |
|
|
187
|
-
| **Scoped by default** | Yes | No | No |
|
|
188
|
-
| **Multiple instances** | Automatic | Manual wiring | Manual wiring |
|
|
189
|
-
| **Test isolation** | Built-in MockContext | Requires setup | Requires reset |
|
|
190
|
-
| **Boilerplate** | Low | High | Low |
|
|
191
|
-
| **Type safety** | Full | Requires setup | Good |
|
|
192
|
-
| **Learning curve** | Just classes | Actions, reducers, selectors | Simple |
|
|
193
|
-
|
|
194
|
-
### The Core Difference
|
|
195
|
-
|
|
196
|
-
**Global state libraries** make you fight against React's component model. You end up with:
|
|
197
|
-
|
|
198
|
-
- Selector functions to prevent re-renders
|
|
199
|
-
- Complex test fixtures to reset global state
|
|
200
|
-
- Workarounds for component reusability
|
|
201
|
-
|
|
202
|
-
**context-scoped-state** works _with_ React:
|
|
203
|
-
|
|
204
|
-
- State lives in the component tree, just like React intended
|
|
205
|
-
- Each provider = new instance, automatically
|
|
206
|
-
- Testing is just rendering with different props
|
|
207
|
-
|
|
208
|
-
### When to Use What
|
|
209
|
-
|
|
210
|
-
**Use context-scoped-state when:**
|
|
211
|
-
|
|
212
|
-
- Building reusable components with internal state
|
|
213
|
-
- You want test isolation without extra setup
|
|
214
|
-
- State naturally belongs to a subtree, not the whole app
|
|
215
|
-
|
|
216
|
-
**Need global state?** Just place the Context at your app root — same API, app-wide access.
|
|
217
|
-
|
|
218
|
-
### Why Not Just Use useState or useReducer?
|
|
219
|
-
|
|
220
|
-
**vs useState:**
|
|
221
|
-
|
|
222
|
-
- `useState` binds state directly to the component — poor separation of concerns and hard to test since you can't easily set a component to a specific state
|
|
223
|
-
- Lifting state up with `useState` requires refactoring components and passing props; with `context-scoped-state`, just move the Context wrapper up the tree
|
|
224
|
-
|
|
225
|
-
**vs useReducer:**
|
|
226
|
-
|
|
227
|
-
- No action types, switch statements, or dispatch boilerplate
|
|
228
|
-
- Just call methods directly: `store.increment()` instead of `dispatch({ type: 'INCREMENT' })`
|
|
229
|
-
- Full TypeScript autocomplete for your actions
|
|
230
|
-
|
|
231
|
-
---
|
|
232
|
-
|
|
233
|
-
**context-scoped-state** — Because not all state needs to be global.
|