Skip to content

Commit 342a3a1

Browse files
authored
Add synchronization options to store subscriptions for immediate notifications (#29)
1 parent ab74c56 commit 342a3a1

25 files changed

Lines changed: 623 additions & 158 deletions

File tree

.changeset/warm-icons-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codebelt/classy-store": minor
3+
---
4+
5+
Add sync option

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ function AddButton() {
5555
}
5656
```
5757

58+
For React controlled inputs, opt the field subscription into synchronous timing so React can preserve caret position and IME composition:
59+
60+
```tsx
61+
const name = useClassyStore(formStore, (state) => state.name, {sync: true});
62+
63+
return (
64+
<input
65+
value={name}
66+
onChange={(event) => formStore.setName(event.target.value)}
67+
/>
68+
);
69+
```
70+
5871
## 📦 Installation
5972

6073
```bash

bun.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"dependencies": {
1111
"@codebelt/classy-store": "workspace:*",
12-
"next": "16.1.6",
12+
"next": "16.2.9",
1313
"react": "19.2.3",
1414
"react-dom": "19.2.3"
1515
},

examples/nextjs/src/components/dashboard/DashboardOverview.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function DashboardOverview() {
6868
count: state.recipeCount,
6969
avgTime: state.averageTotalTime,
7070
}),
71-
shallowEqual,
71+
{isEqual: shallowEqual},
7272
);
7373

7474
const shoppingStats = useClassyStore(
@@ -77,7 +77,7 @@ export function DashboardOverview() {
7777
total: state.totalItems,
7878
unchecked: state.uncheckedCount,
7979
}),
80-
shallowEqual,
80+
{isEqual: shallowEqual},
8181
);
8282

8383
const plannerStats = useClassyStore(
@@ -86,7 +86,7 @@ export function DashboardOverview() {
8686
meals: state.totalMealsPlanned,
8787
pct: state.completionPercentage,
8888
}),
89-
shallowEqual,
89+
{isEqual: shallowEqual},
9090
);
9191

9292
const cards = [
@@ -127,7 +127,7 @@ export function DashboardOverview() {
127127
code={`const stats = useClassyStore(recipeStore, (state) => ({
128128
count: state.recipeCount,
129129
avgTime: state.averageTotalTime,
130-
}), shallowEqual);`}
130+
}), {isEqual: shallowEqual});`}
131131
/>
132132
</div>
133133

examples/nextjs/src/components/editor/RecipeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function RecipeEditor() {
1717
if (!historyRef.current) {
1818
historyRef.current = withHistory(store, {limit: 30});
1919
}
20-
const snap = useClassyStore(store);
20+
const snap = useClassyStore(store, {sync: true});
2121

2222
return (
2323
<div className="space-y-4">

examples/nextjs/src/components/recipes/RecipeCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function RecipeCard({recipeId}: {recipeId: string}) {
2727
ingredientCount: r.ingredients.length,
2828
};
2929
},
30-
shallowEqual,
30+
{isEqual: shallowEqual},
3131
);
3232

3333
if (!recipe) return null;

examples/nextjs/src/components/shopping/ShoppingStats.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ShoppingStats() {
1414
checked: state.checkedCount,
1515
lastAction: state.lastAction,
1616
}),
17-
shallowEqual,
17+
{isEqual: shallowEqual},
1818
);
1919

2020
return (
@@ -27,7 +27,7 @@ export function ShoppingStats() {
2727
unchecked: state.uncheckedCount,
2828
checked: state.checkedCount,
2929
lastAction: state.lastAction,
30-
}), shallowEqual);`}
30+
}), {isEqual: shallowEqual});`}
3131
/>
3232
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
3333
<div className="bg-card border border-border rounded-lg p-4">

examples/react/src/demos/reactivity/ReactiveFundamentalsDemo.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ import {CounterStore, counterStore} from '../../stores/counterStore';
99

1010
const batchStore = createClassyStore(new CounterStore());
1111

12+
class DemoFormStore {
13+
name = 'Alice';
14+
15+
setName(value: string) {
16+
this.name = value;
17+
}
18+
19+
reset() {
20+
this.name = 'Alice';
21+
}
22+
}
23+
24+
const formStore = createClassyStore(new DemoFormStore());
25+
1226
const names = ['World', 'Bun', 'React', 'Store'];
1327
let nameIndex = 0;
1428

@@ -44,6 +58,17 @@ function BatchCard() {
4458
// 1000 mutations → 1 render (microtask batching)
4559
const count = useClassyStore(batchStore, (state) => state.count);
4660
return <div>{count}</div>;
61+
}
62+
63+
function NameInput() {
64+
const name = useClassyStore(formStore, (state) => state.name, { sync: true });
65+
66+
return (
67+
<input
68+
value={name}
69+
onChange={(event) => formStore.setName(event.target.value)}
70+
/>
71+
);
4772
}`;
4873

4974
function CountCard() {
@@ -100,13 +125,34 @@ function BatchCard() {
100125
);
101126
}
102127

128+
function NameInputCard() {
129+
const name = useClassyStore(formStore, (state) => state.name, {sync: true});
130+
const renders = useRenderCount();
131+
132+
return (
133+
<div className="flex-1 flex items-center justify-between gap-3 bg-emerald-500/10 border border-emerald-500/20 rounded-lg px-3 py-2.5">
134+
<label className="flex-1 min-w-0">
135+
<span className="block text-[10px] text-zinc-400 uppercase tracking-wide mb-1">
136+
Name Input
137+
</span>
138+
<input
139+
value={name}
140+
onChange={(event) => formStore.setName(event.target.value)}
141+
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-sm text-zinc-100 outline-none focus:border-emerald-400"
142+
/>
143+
</label>
144+
<RenderBadge count={renders} />
145+
</div>
146+
);
147+
}
148+
103149
export function ReactiveFundamentalsDemo() {
104150
const [lastBatch, setLastBatch] = useState(false);
105151

106152
return (
107153
<DemoContainer
108154
title="Reactive Fundamentals"
109-
description="Render isolation via selectors + microtask batching + mutable deep updates — all in one store."
155+
description="Render isolation via selectors + controlled inputs + microtask batching + mutable deep updates — all in one store."
110156
codeTabs={[
111157
{label: 'Store', code: STORE_CODE, language: 'typescript'},
112158
{label: 'Component', code: COMPONENT_CODE},
@@ -115,6 +161,7 @@ export function ReactiveFundamentalsDemo() {
115161
<div className="flex flex-col gap-3 mb-4">
116162
<CountCard />
117163
<NameCard />
164+
<NameInputCard />
118165
<BatchCard />
119166
</div>
120167

@@ -144,6 +191,7 @@ export function ReactiveFundamentalsDemo() {
144191
onClick={() => {
145192
counterStore.reset();
146193
batchStore.reset();
194+
formStore.reset();
147195
setLastBatch(false);
148196
}}
149197
>

examples/react/src/demos/shallow-equal/ShallowEqualDemo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const name = useClassyStore(profileStore, (state) => ({
4444
const name = useClassyStore(
4545
profileStore,
4646
(state) => ({ first: state.firstName, last: state.lastName }),
47-
shallowEqual
47+
{ isEqual: shallowEqual }
4848
);`;
4949

5050
const firstNames = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'];
@@ -82,7 +82,7 @@ function NameCardWith() {
8282
const name = useClassyStore(
8383
profileStore,
8484
(state) => ({first: state.firstName, last: state.lastName}),
85-
shallowEqual,
85+
{isEqual: shallowEqual},
8686
);
8787
const renders = useRenderCount();
8888

0 commit comments

Comments
 (0)