react-state-custom 1.0.22 → 1.0.24
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/.github/copilot-instructions.md +17 -2
- package/.github/workflows/deploy.yml +56 -0
- package/API_DOCUMENTATION.md +132 -3
- package/README.md +69 -1
- package/dist/dev-tool/DataViewComponent.d.ts +6 -0
- package/dist/dev-tool/DevTool.d.ts +5 -0
- package/dist/dev-tool/DevToolState.d.ts +9 -0
- package/dist/dev-tool/StateLabelRender.d.ts +2 -0
- package/dist/dev-tool/useHighlight.d.ts +11 -0
- package/dist/dev.d.ts +0 -1
- package/dist/examples/Playground.d.ts +1 -0
- package/dist/examples/cart/app.d.ts +1 -0
- package/dist/examples/cart/index.d.ts +3 -0
- package/dist/examples/cart/state.d.ts +23 -0
- package/dist/examples/cart/view.d.ts +4 -0
- package/dist/examples/counter/app.d.ts +1 -0
- package/dist/examples/counter/index.d.ts +2 -0
- package/dist/examples/counter/state.d.ts +6 -0
- package/dist/examples/counter/view.d.ts +2 -0
- package/dist/examples/form/app.d.ts +1 -0
- package/dist/examples/form/index.d.ts +3 -0
- package/dist/examples/form/state.d.ts +16 -0
- package/dist/examples/form/view.d.ts +4 -0
- package/dist/examples/timer/app.d.ts +1 -0
- package/dist/examples/timer/index.d.ts +2 -0
- package/dist/examples/timer/state.d.ts +11 -0
- package/dist/examples/timer/view.d.ts +4 -0
- package/dist/examples/todo/app.d.ts +1 -0
- package/dist/examples/todo/index.d.ts +3 -0
- package/dist/examples/todo/state.d.ts +17 -0
- package/dist/examples/todo/view.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +297 -397
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/react-state-custom.css +1 -1
- package/dist/state-utils/ctx.d.ts +3 -2
- package/package.json +7 -2
- package/src/dev-tool/DataViewComponent.tsx +17 -0
- package/src/dev-tool/DevTool.css +134 -0
- package/src/{DevTool.tsx → dev-tool/DevTool.tsx} +3 -2
- package/src/dev-tool/DevToolState.tsx +78 -0
- package/src/dev-tool/StateLabelRender.tsx +38 -0
- package/src/dev-tool/useHighlight.tsx +56 -0
- package/src/dev.tsx +4 -11
- package/src/examples/Playground.tsx +180 -0
- package/src/examples/cart/app.tsx +16 -0
- package/src/examples/cart/index.ts +3 -0
- package/src/examples/cart/state.ts +67 -0
- package/src/examples/cart/view.tsx +62 -0
- package/src/examples/counter/app.tsx +14 -0
- package/src/examples/counter/index.ts +2 -0
- package/src/examples/counter/state.ts +22 -0
- package/src/examples/counter/state.tsx?raw +0 -0
- package/src/examples/counter/view.tsx +20 -0
- package/src/examples/form/app.tsx +16 -0
- package/src/examples/form/index.ts +3 -0
- package/src/examples/form/state.ts +58 -0
- package/src/examples/form/view.tsx +53 -0
- package/src/examples/timer/app.tsx +16 -0
- package/src/examples/timer/index.ts +2 -0
- package/src/examples/timer/state.ts +43 -0
- package/src/examples/timer/view.tsx +26 -0
- package/src/examples/todo/app.tsx +16 -0
- package/src/examples/todo/index.ts +3 -0
- package/src/examples/todo/state.ts +54 -0
- package/src/examples/todo/view.tsx +47 -0
- package/src/index.ts +2 -1
- package/src/state-utils/ctx.ts +36 -13
- package/src/vite-env.d.ts +6 -0
- package/tsconfig.json +12 -3
- package/vite.config.dev.ts +6 -1
- package/dist/DevTool.d.ts +0 -4
- package/dist/DevToolState.d.ts +0 -15
- package/dist/Test.d.ts +0 -1
- package/src/DevTool.css +0 -192
- package/src/DevToolState.tsx +0 -319
- package/src/Test.tsx +0 -97
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export function useHighlight(filterString: string) {
|
|
4
|
+
const highlight = useMemo(
|
|
5
|
+
() => buildRegex(filterString
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.split(" "), 'gi'),
|
|
8
|
+
[filterString]
|
|
9
|
+
);
|
|
10
|
+
return { highlight };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeRegex(str: string) {
|
|
14
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildRegex(words: string[], flags = 'gi') {
|
|
18
|
+
const pattern = words.map(escapeRegex).join('|');
|
|
19
|
+
return new RegExp(`(${pattern})`, flags);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function markByToken(text: string, regex: RegExp) {
|
|
23
|
+
const result = [];
|
|
24
|
+
let last = 0;
|
|
25
|
+
for (const match of text.matchAll(regex)) {
|
|
26
|
+
const [m] = match;
|
|
27
|
+
const start = match.index;
|
|
28
|
+
if (start > last) result.push(text.slice(last, start));
|
|
29
|
+
result.push(<mark key={start}>{m}</mark>);
|
|
30
|
+
last = start + m.length;
|
|
31
|
+
}
|
|
32
|
+
if (last < text.length) result.push(text.slice(last));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const highlightCtx = React.createContext<{ highlight?: RegExp }>({
|
|
37
|
+
highlight: undefined
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const HightlightWrapper: React.FC<{ highlight: string, children: any }> = ({ children, highlight }) => {
|
|
41
|
+
return <highlightCtx.Provider value={useHighlight(highlight)}>
|
|
42
|
+
{children}
|
|
43
|
+
</highlightCtx.Provider>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const HighlightString: React.FC<{ text: string; }> = ({ text }) => {
|
|
47
|
+
const { highlight } = useContext(highlightCtx)
|
|
48
|
+
|
|
49
|
+
const render = useMemo(
|
|
50
|
+
() => highlight ? markByToken(text, highlight) : text,
|
|
51
|
+
[text, highlight]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return <>{render}</>;
|
|
55
|
+
|
|
56
|
+
};
|
package/src/dev.tsx
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { createRoot } from 'react-dom/client'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { Test } from './Test'
|
|
2
|
+
import { Playground } from './examples/Playground'
|
|
3
|
+
import "react-obj-view/dist/react-obj-view.css"
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<>
|
|
10
|
-
<DevToolContainer />
|
|
11
|
-
<Test/>
|
|
12
|
-
<AutoRootCtx/>
|
|
13
|
-
</>
|
|
14
|
-
)
|
|
6
|
+
createRoot(document.getElementById('root')!)
|
|
7
|
+
.render(<Playground />)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Sandpack } from '@codesandbox/sandpack-react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import counterState from "./counter/state.ts?raw"
|
|
6
|
+
import counterView from "./counter/view.tsx?raw"
|
|
7
|
+
import counterApp from "./counter/app.tsx?raw"
|
|
8
|
+
import todoState from "./todo/state.ts?raw"
|
|
9
|
+
import todoView from "./todo/view.tsx?raw"
|
|
10
|
+
import todoApp from "./todo/app.tsx?raw"
|
|
11
|
+
import timerState from "./timer/state.ts?raw"
|
|
12
|
+
import timerView from "./timer/view.tsx?raw"
|
|
13
|
+
import timerApp from "./timer/app.tsx?raw"
|
|
14
|
+
import formState from "./form/state.ts?raw"
|
|
15
|
+
import formView from "./form/view.tsx?raw"
|
|
16
|
+
import formApp from "./form/app.tsx?raw"
|
|
17
|
+
import cartState from "./cart/state.ts?raw"
|
|
18
|
+
import cartView from "./cart/view.tsx?raw"
|
|
19
|
+
import cartApp from "./cart/app.tsx?raw"
|
|
20
|
+
|
|
21
|
+
const devToolCode = `
|
|
22
|
+
|
|
23
|
+
import { ObjectView } from "react-obj-view"
|
|
24
|
+
import "react-obj-view/dist/react-obj-view.css"
|
|
25
|
+
|
|
26
|
+
export const DataView: DataViewComponent = ({ name, value }) => {
|
|
27
|
+
return <ObjectView
|
|
28
|
+
{...{ name, value }}
|
|
29
|
+
expandLevel={1}
|
|
30
|
+
/>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const updateImport = (code: string) => {
|
|
37
|
+
return code.replaceAll(
|
|
38
|
+
`from '../../index'`,
|
|
39
|
+
`from 'react-state-custom'`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const injectRootCtx = (code: string) => {
|
|
45
|
+
return [
|
|
46
|
+
`import { AutoRootCtx, DevToolContainer } from 'react-state-custom';`,
|
|
47
|
+
`import 'react-state-custom/dist/react-state-custom.css';`,
|
|
48
|
+
`import { DataView } from './dataview.tsx';`,
|
|
49
|
+
code
|
|
50
|
+
.replaceAll(
|
|
51
|
+
`{/* <AutoRootCtx/> */}`,
|
|
52
|
+
`<AutoRootCtx/>`
|
|
53
|
+
).replaceAll(
|
|
54
|
+
`{/* <DevToolContainer Component={DataView} /> */}`,
|
|
55
|
+
`<DevToolContainer Component={DataView} style={{ left:"20px", bottom:"10px", right:"unset"}}/>`,
|
|
56
|
+
)
|
|
57
|
+
].join("\n")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const examples = {
|
|
61
|
+
counter: {
|
|
62
|
+
title: 'Counter Example',
|
|
63
|
+
description: 'A simple counter demonstrating basic state management with increment, decrement, and reset operations.',
|
|
64
|
+
state: updateImport(counterState),
|
|
65
|
+
view: updateImport(counterView),
|
|
66
|
+
app: injectRootCtx(updateImport(counterApp)),
|
|
67
|
+
},
|
|
68
|
+
todo: {
|
|
69
|
+
title: 'Todo List Example',
|
|
70
|
+
description: 'Multiple independent todo lists showing how contexts can be scoped by parameters.',
|
|
71
|
+
state: updateImport(todoState),
|
|
72
|
+
view: updateImport(todoView),
|
|
73
|
+
app: injectRootCtx(updateImport(todoApp)),
|
|
74
|
+
},
|
|
75
|
+
form: {
|
|
76
|
+
title: 'Form Example',
|
|
77
|
+
description: 'Form validation example with multiple independent form instances. Shows real-time validation and error handling.',
|
|
78
|
+
state: updateImport(formState),
|
|
79
|
+
view: updateImport(formView),
|
|
80
|
+
app: injectRootCtx(updateImport(formApp)),
|
|
81
|
+
},
|
|
82
|
+
timer: {
|
|
83
|
+
title: 'Timer Example',
|
|
84
|
+
description: 'Multiple independent timers with millisecond precision demonstrating side effects.',
|
|
85
|
+
state: updateImport(timerState),
|
|
86
|
+
view: updateImport(timerView),
|
|
87
|
+
app: injectRootCtx(updateImport(timerApp)),
|
|
88
|
+
},
|
|
89
|
+
cart: {
|
|
90
|
+
title: 'Shopping Cart Example',
|
|
91
|
+
description: 'Shopping cart with product selection and quantity management. Shows how to handle derived state (total, itemCount) and complex state updates.',
|
|
92
|
+
state: updateImport(cartState),
|
|
93
|
+
view: updateImport(cartView),
|
|
94
|
+
app: injectRootCtx(updateImport(cartApp)),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Playground = () => {
|
|
99
|
+
const [activeExample, setActiveExample] = useState<keyof typeof examples>('counter')
|
|
100
|
+
const example = examples[activeExample]
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={{ padding: '2rem', maxWidth: '1400px', margin: '0 auto' }}>
|
|
104
|
+
<h1>React State Custom - Interactive Playground</h1>
|
|
105
|
+
<p style={{ color: '#666', marginBottom: '2rem' }}>
|
|
106
|
+
Edit the code below and see the changes in real-time!
|
|
107
|
+
</p>
|
|
108
|
+
|
|
109
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '2rem', flexWrap: 'wrap' }}>
|
|
110
|
+
{(Object.keys(examples) as Array<keyof typeof examples>).map((key) => (
|
|
111
|
+
<button
|
|
112
|
+
key={key}
|
|
113
|
+
onClick={() => setActiveExample(key)}
|
|
114
|
+
style={{
|
|
115
|
+
padding: '0.5rem 1rem',
|
|
116
|
+
background: activeExample === key ? '#007bff' : '#e0e0e0',
|
|
117
|
+
color: activeExample === key ? 'white' : 'black',
|
|
118
|
+
border: 'none',
|
|
119
|
+
borderRadius: '4px',
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
textTransform: 'capitalize'
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{key}
|
|
125
|
+
</button>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
130
|
+
<h2>{example.title}</h2>
|
|
131
|
+
<p style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
132
|
+
{example.description}
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
<Sandpack
|
|
138
|
+
template="react-ts"
|
|
139
|
+
theme="light"
|
|
140
|
+
files={{
|
|
141
|
+
'/App.tsx': example.app,
|
|
142
|
+
'/state.ts': example.state,
|
|
143
|
+
'/view.tsx': example.view,
|
|
144
|
+
'/dataview.tsx': devToolCode,
|
|
145
|
+
}}
|
|
146
|
+
options={{
|
|
147
|
+
// showNavigator: true,
|
|
148
|
+
showTabs: true,
|
|
149
|
+
showLineNumbers: true,
|
|
150
|
+
editorHeight: 600,
|
|
151
|
+
// editorWidthPercentage: 40,
|
|
152
|
+
}}
|
|
153
|
+
// style={{
|
|
154
|
+
// '--sp-font-size': '12px',
|
|
155
|
+
// '--sp-font-lineHeight': '17px',
|
|
156
|
+
// // '--sp-font-body': 'inherit',
|
|
157
|
+
// } as React.CSSProperties}
|
|
158
|
+
customSetup={{
|
|
159
|
+
dependencies: {
|
|
160
|
+
'react': '^19.0.0',
|
|
161
|
+
'react-dom': '^19.0.0',
|
|
162
|
+
'react-obj-view': '^1.0.4',
|
|
163
|
+
'react-state-custom': '^1.0.23',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<div style={{ marginTop: '2rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
170
|
+
<h3>How it works:</h3>
|
|
171
|
+
<ul style={{ marginLeft: '1.5rem' }}>
|
|
172
|
+
<li><code>createRootCtx</code> - Creates a context with a custom hook</li>
|
|
173
|
+
<li><code>createAutoCtx</code> - Automatically manages context lifecycle</li>
|
|
174
|
+
<li><code>useQuickSubscribe</code> - Subscribes to all context values via proxy</li>
|
|
175
|
+
<li>Multiple instances share the same context when parameters match</li>
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CartExample } from './view'
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
{/* <AutoRootCtx/> */}
|
|
7
|
+
<CartExample userId="alice" />
|
|
8
|
+
<CartExample userId="bob" />
|
|
9
|
+
<p style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
10
|
+
Shopping cart with product selection and quantity management.
|
|
11
|
+
Shows how to handle derived state (total, itemCount) and complex state updates.
|
|
12
|
+
</p>
|
|
13
|
+
{/* <DevToolContainer Component={DataView} /> */}
|
|
14
|
+
</>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createRootCtx, createAutoCtx } from '../../index'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface CartItem {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
price: number
|
|
8
|
+
quantity: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const PRODUCTS = [
|
|
12
|
+
{ id: '1', name: 'Apple', price: 1.5 },
|
|
13
|
+
{ id: '2', name: 'Banana', price: 0.8 },
|
|
14
|
+
{ id: '3', name: 'Orange', price: 1.2 },
|
|
15
|
+
{ id: '4', name: 'Mango', price: 2.5 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export const { useCtxState: useCartCtx } = createAutoCtx(
|
|
19
|
+
createRootCtx(
|
|
20
|
+
"cart",
|
|
21
|
+
({ userId }: { userId: string }) => {
|
|
22
|
+
const [items, setItems] = useState<CartItem[]>([])
|
|
23
|
+
|
|
24
|
+
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
25
|
+
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
26
|
+
|
|
27
|
+
const addItem = useCallback((product: typeof PRODUCTS[0]) => {
|
|
28
|
+
setItems(prev => {
|
|
29
|
+
const existing = prev.find(i => i.id === product.id)
|
|
30
|
+
if (existing) {
|
|
31
|
+
return prev.map(i =>
|
|
32
|
+
i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return [...prev, { ...product, quantity: 1 }]
|
|
36
|
+
})
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
const removeItem = useCallback((id: string) => {
|
|
40
|
+
setItems(prev => prev.filter(i => i.id !== id))
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
const updateQuantity = useCallback((id: string, quantity: number) => {
|
|
44
|
+
if (quantity <= 0) {
|
|
45
|
+
setItems(prev => prev.filter(i => i.id !== id))
|
|
46
|
+
} else {
|
|
47
|
+
setItems(prev => prev.map(i =>
|
|
48
|
+
i.id === id ? { ...i, quantity } : i
|
|
49
|
+
))
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const clear = useCallback(() => setItems([]), [])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
userId,
|
|
57
|
+
items,
|
|
58
|
+
total: total.toFixed(2),
|
|
59
|
+
itemCount,
|
|
60
|
+
addItem,
|
|
61
|
+
removeItem,
|
|
62
|
+
updateQuantity,
|
|
63
|
+
clear,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useQuickSubscribe } from '../../index'
|
|
2
|
+
import { useCartCtx, PRODUCTS } from './state'
|
|
3
|
+
|
|
4
|
+
export const CartExample = ({ userId = "user1" }: { userId?: string }) => {
|
|
5
|
+
const { items, total, itemCount, addItem, removeItem, updateQuantity, clear } =
|
|
6
|
+
useQuickSubscribe(useCartCtx({ userId }))
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ padding: '1rem', border: '1px solid #ccc', marginBottom: '1rem' }}>
|
|
10
|
+
<h3>Shopping Cart ({userId})</h3>
|
|
11
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
12
|
+
<h4>Products:</h4>
|
|
13
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0.5rem' }}>
|
|
14
|
+
{PRODUCTS.map(product => (
|
|
15
|
+
<button
|
|
16
|
+
key={product.id}
|
|
17
|
+
onClick={() => addItem?.(product)}
|
|
18
|
+
style={{ padding: '0.5rem' }}
|
|
19
|
+
>
|
|
20
|
+
{product.name} - ${product.price.toFixed(2)}
|
|
21
|
+
</button>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<h4>Cart ({itemCount ?? 0} items):</h4>
|
|
27
|
+
{(items?.length ?? 0) === 0 ? (
|
|
28
|
+
<p style={{ color: '#666' }}>Cart is empty</p>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
32
|
+
{items?.map(item => (
|
|
33
|
+
<li key={item.id} style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem', alignItems: 'center' }}>
|
|
34
|
+
<span style={{ flex: 1 }}>{item.name}</span>
|
|
35
|
+
<span>${item.price.toFixed(2)}</span>
|
|
36
|
+
<input
|
|
37
|
+
type="number"
|
|
38
|
+
value={item.quantity}
|
|
39
|
+
onChange={(e) => updateQuantity?.(item.id, parseInt(e.target.value) || 0)}
|
|
40
|
+
style={{ width: '3rem', padding: '0.25rem' }}
|
|
41
|
+
min="0"
|
|
42
|
+
/>
|
|
43
|
+
<span>${(item.price * item.quantity).toFixed(2)}</span>
|
|
44
|
+
<button onClick={() => removeItem?.(item.id)}>×</button>
|
|
45
|
+
</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
<div style={{ borderTop: '1px solid #ccc', paddingTop: '0.5rem', display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}>
|
|
49
|
+
<span>Total:</span>
|
|
50
|
+
<span>${total}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<button onClick={clear} style={{ marginTop: '0.5rem', width: '100%' }}>
|
|
53
|
+
Clear Cart
|
|
54
|
+
</button>
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default CartExample
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CounterExample } from './view'
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
{/* <AutoRootCtx/> */}
|
|
7
|
+
<CounterExample />
|
|
8
|
+
<p style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
9
|
+
A simple counter demonstrating basic state management with increment, decrement, and reset operations.
|
|
10
|
+
</p>
|
|
11
|
+
{/* <DevToolContainer Component={DataView} /> */}
|
|
12
|
+
</>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRootCtx, createAutoCtx } from '../../index'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export const { useCtxState: useCounterCtx } = createAutoCtx(
|
|
5
|
+
createRootCtx(
|
|
6
|
+
"counter",
|
|
7
|
+
() => {
|
|
8
|
+
const [count, setCount] = useState(0)
|
|
9
|
+
|
|
10
|
+
const increment = useCallback(() => setCount(c => c + 1), [])
|
|
11
|
+
const decrement = useCallback(() => setCount(c => c - 1), [])
|
|
12
|
+
const reset = useCallback(() => setCount(0), [])
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
count,
|
|
16
|
+
increment,
|
|
17
|
+
decrement,
|
|
18
|
+
reset,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useQuickSubscribe } from '../../index'
|
|
2
|
+
import { useCounterCtx } from './state'
|
|
3
|
+
|
|
4
|
+
export const CounterExample = () => {
|
|
5
|
+
const { count, increment, decrement, reset } = useQuickSubscribe(useCounterCtx({}))
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ padding: '1rem', border: '1px solid #ccc', marginBottom: '1rem' }}>
|
|
9
|
+
<h3>Basic Counter</h3>
|
|
10
|
+
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
|
11
|
+
<button onClick={decrement}>-</button>
|
|
12
|
+
<span style={{ minWidth: '3rem', textAlign: 'center' }}>{count}</span>
|
|
13
|
+
<button onClick={increment}>+</button>
|
|
14
|
+
<button onClick={reset}>Reset</button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default CounterExample
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FormExample } from './view'
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
{/* <AutoRootCtx/> */}
|
|
7
|
+
<FormExample formId="registration" />
|
|
8
|
+
<FormExample formId="profile" />
|
|
9
|
+
<p style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
10
|
+
Form validation example with multiple independent form instances.
|
|
11
|
+
Shows real-time validation and error handling.
|
|
12
|
+
</p>
|
|
13
|
+
{/* <DevToolContainer Component={DataView} /> */}
|
|
14
|
+
</>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createRootCtx, createAutoCtx } from '../../index'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface FormData {
|
|
5
|
+
name: string
|
|
6
|
+
email: string
|
|
7
|
+
age: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const { useCtxState: useFormCtx } = createAutoCtx(
|
|
11
|
+
createRootCtx(
|
|
12
|
+
"form",
|
|
13
|
+
({ formId }: { formId: string }) => {
|
|
14
|
+
const [data, setData] = useState<FormData>({ name: '', email: '', age: '' })
|
|
15
|
+
const [errors, setErrors] = useState<Partial<FormData>>({})
|
|
16
|
+
const [submitted, setSubmitted] = useState(false)
|
|
17
|
+
|
|
18
|
+
const validate = useCallback((data: FormData): Partial<FormData> => {
|
|
19
|
+
const errors: Partial<FormData> = {}
|
|
20
|
+
if (!data.name.trim()) errors.name = 'Name is required'
|
|
21
|
+
if (!data.email.includes('@')) errors.email = 'Invalid email'
|
|
22
|
+
if (data.age && isNaN(Number(data.age))) errors.age = 'Age must be a number'
|
|
23
|
+
return errors
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
const updateField = useCallback((field: keyof FormData, value: string) => {
|
|
27
|
+
setData(prev => ({ ...prev, [field]: value }))
|
|
28
|
+
setErrors(prev => ({ ...prev, [field]: undefined }))
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const submit = useCallback(() => {
|
|
32
|
+
const validationErrors = validate(data)
|
|
33
|
+
if (Object.keys(validationErrors).length === 0) {
|
|
34
|
+
setSubmitted(true)
|
|
35
|
+
setTimeout(() => setSubmitted(false), 2000)
|
|
36
|
+
} else {
|
|
37
|
+
setErrors(validationErrors)
|
|
38
|
+
}
|
|
39
|
+
}, [data, validate])
|
|
40
|
+
|
|
41
|
+
const reset = useCallback(() => {
|
|
42
|
+
setData({ name: '', email: '', age: '' })
|
|
43
|
+
setErrors({})
|
|
44
|
+
setSubmitted(false)
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
formId,
|
|
49
|
+
data,
|
|
50
|
+
errors,
|
|
51
|
+
submitted,
|
|
52
|
+
updateField,
|
|
53
|
+
submit,
|
|
54
|
+
reset,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useQuickSubscribe } from '../../index'
|
|
2
|
+
import { useFormCtx } from './state'
|
|
3
|
+
|
|
4
|
+
export const FormExample = ({ formId = "user-form" }: { formId?: string }) => {
|
|
5
|
+
const { data, errors, submitted, updateField, submit, reset } =
|
|
6
|
+
useQuickSubscribe(useFormCtx({ formId }))
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ padding: '1rem', border: '1px solid #ccc', marginBottom: '1rem' }}>
|
|
10
|
+
<h3>Form ({formId})</h3>
|
|
11
|
+
{submitted && (
|
|
12
|
+
<div style={{ padding: '0.5rem', background: '#d4edda', color: '#155724', marginBottom: '1rem' }}>
|
|
13
|
+
Form submitted successfully!
|
|
14
|
+
</div>
|
|
15
|
+
)}
|
|
16
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
17
|
+
<div>
|
|
18
|
+
<label style={{ display: 'block', marginBottom: '0.25rem' }}>Name:</label>
|
|
19
|
+
<input
|
|
20
|
+
value={data?.name ?? ''}
|
|
21
|
+
onChange={(e) => updateField?.('name', e.target.value)}
|
|
22
|
+
style={{ width: '100%', padding: '0.25rem' }}
|
|
23
|
+
/>
|
|
24
|
+
{errors?.name && <div style={{ color: 'red', fontSize: '0.875rem' }}>{errors.name}</div>}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<label style={{ display: 'block', marginBottom: '0.25rem' }}>Email:</label>
|
|
28
|
+
<input
|
|
29
|
+
value={data?.email ?? ''}
|
|
30
|
+
onChange={(e) => updateField?.('email', e.target.value)}
|
|
31
|
+
style={{ width: '100%', padding: '0.25rem' }}
|
|
32
|
+
/>
|
|
33
|
+
{errors?.email && <div style={{ color: 'red', fontSize: '0.875rem' }}>{errors.email}</div>}
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<label style={{ display: 'block', marginBottom: '0.25rem' }}>Age:</label>
|
|
37
|
+
<input
|
|
38
|
+
value={data?.age ?? ''}
|
|
39
|
+
onChange={(e) => updateField?.('age', e.target.value)}
|
|
40
|
+
style={{ width: '100%', padding: '0.25rem' }}
|
|
41
|
+
/>
|
|
42
|
+
{errors?.age && <div style={{ color: 'red', fontSize: '0.875rem' }}>{errors.age}</div>}
|
|
43
|
+
</div>
|
|
44
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
45
|
+
<button onClick={submit}>Submit</button>
|
|
46
|
+
<button onClick={reset}>Reset</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default FormExample
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TimerExample } from './view'
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return (
|
|
5
|
+
<>
|
|
6
|
+
{/* <AutoRootCtx/> */}
|
|
7
|
+
<TimerExample timerId="timer1" />
|
|
8
|
+
<TimerExample timerId="timer2" />
|
|
9
|
+
<p style={{ color: '#666', fontSize: '0.875rem' }}>
|
|
10
|
+
Multiple independent timers demonstrating side effects (setInterval) within context state.
|
|
11
|
+
Each timer can run independently.
|
|
12
|
+
</p>
|
|
13
|
+
{/* <DevToolContainer Component={DataView} /> */}
|
|
14
|
+
</>
|
|
15
|
+
)
|
|
16
|
+
}
|