dev-react-microstore 4.0.0 → 5.0.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.
@@ -0,0 +1,63 @@
1
+ import { useState } from 'react'
2
+ import { useStoreSelector } from '../../../src/index'
3
+ import store from '../store'
4
+
5
+ export default function TodoList() {
6
+ const { todos } = useStoreSelector(store, ['todos'])
7
+ const [todoText, setTodoText] = useState('')
8
+
9
+ const handleAddTodo = () => {
10
+ if (todoText.trim()) {
11
+ const newTodo = {
12
+ id: Date.now(),
13
+ text: todoText.trim(),
14
+ completed: false
15
+ }
16
+ store.set({ todos: [...todos, newTodo] })
17
+ setTodoText('')
18
+ }
19
+ }
20
+
21
+ const handleToggleTodo = (id: number) => {
22
+ const updatedTodos = todos.map(todo =>
23
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
24
+ )
25
+ store.set({ todos: updatedTodos })
26
+ }
27
+
28
+ const handleDeleteTodo = (id: number) => {
29
+ const updatedTodos = todos.filter(todo => todo.id !== id)
30
+ store.set({ todos: updatedTodos })
31
+ }
32
+
33
+ return (
34
+ <section className="section">
35
+ <h3>📝 Todos</h3>
36
+ <div className="todo-form">
37
+ <input
38
+ type="text"
39
+ placeholder="Add a todo..."
40
+ value={todoText}
41
+ onChange={(e) => setTodoText(e.target.value)}
42
+ onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
43
+ />
44
+ <button onClick={handleAddTodo}>Add</button>
45
+ </div>
46
+ <div className="todo-list">
47
+ {todos.map(todo => (
48
+ <div key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
49
+ <input
50
+ type="checkbox"
51
+ checked={todo.completed}
52
+ onChange={() => handleToggleTodo(todo.id)}
53
+ />
54
+ <span className="todo-text">{todo.text}</span>
55
+ <button onClick={() => handleDeleteTodo(todo.id)} className="delete">
56
+ ×
57
+ </button>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ </section>
62
+ )
63
+ }
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useStoreSelector } from '../../../src/index'
3
+ import store from '../store'
4
+
5
+ export default function UserManager() {
6
+ const { userName: currentUserName, userEmail: currentUserEmail, isLoggedIn, userError } = useStoreSelector(store, ['userName', 'userEmail', 'isLoggedIn', 'userError'])
7
+ const [userName, setUserName] = useState('')
8
+ const [userEmail, setUserEmail] = useState('')
9
+
10
+ // Clear form on successful login
11
+ useEffect(() => {
12
+ if (isLoggedIn && !userError) {
13
+ setUserName('')
14
+ setUserEmail('')
15
+ }
16
+ }, [isLoggedIn, userError])
17
+
18
+ const handleUserUpdate = () => {
19
+ store.set({
20
+ userName,
21
+ userEmail,
22
+ isLoggedIn: true
23
+ })
24
+ }
25
+
26
+ const handleUserLogout = () => {
27
+ store.set({
28
+ userName: '',
29
+ userEmail: '',
30
+ isLoggedIn: false
31
+ })
32
+ }
33
+
34
+ return (
35
+ <section className="section">
36
+ <h3>👤 User Management (with persistence & transformation)</h3>
37
+ <div className="user-info">
38
+ {isLoggedIn ? (
39
+ <div>
40
+ <p><strong>Name:</strong> {currentUserName}</p>
41
+ <p><strong>Email:</strong> {currentUserEmail}</p>
42
+ <button onClick={handleUserLogout}>Logout</button>
43
+ </div>
44
+ ) : (
45
+ <div className="user-form">
46
+ <input
47
+ type="text"
48
+ placeholder="Name"
49
+ value={userName}
50
+ onChange={(e) => setUserName(e.target.value)}
51
+ />
52
+ <input
53
+ type="email"
54
+ placeholder="Email"
55
+ value={userEmail}
56
+ onChange={(e) => setUserEmail(e.target.value)}
57
+ />
58
+ <button onClick={handleUserUpdate}>Login</button>
59
+ {userError && <div className="error-message">{userError}</div>}
60
+ <p className="help-text">
61
+ Names are normalized and emails are validated *
62
+ </p>
63
+ </div>
64
+ )}
65
+ </div>
66
+ </section>
67
+ )
68
+ }
@@ -0,0 +1,68 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,223 @@
1
+ import { createStoreState, createPersistenceMiddleware, loadPersistedState } from '../../src/index';
2
+
3
+ // Define our app state interface
4
+ interface AppState {
5
+ count: number;
6
+ userName: string;
7
+ userEmail: string;
8
+ isLoggedIn: boolean;
9
+ userError: string;
10
+ theme: 'light' | 'dark';
11
+ todos: Array<{ id: number; text: string; completed: boolean }>;
12
+ searchResults: string[];
13
+ isSearching: boolean;
14
+ logs: Array<{ id: number; message: string; type: string; timestamp: string }>;
15
+ // Complex object for custom compare demo
16
+ gameState: {
17
+ player: {
18
+ name: string;
19
+ health: number;
20
+ mana: number;
21
+ level: number;
22
+ experience: number;
23
+ };
24
+ enemy: {
25
+ name: string;
26
+ health: number;
27
+ damage: number;
28
+ isAlive: boolean;
29
+ };
30
+ ui: {
31
+ showInventory: boolean;
32
+ showMap: boolean;
33
+ notifications: string[];
34
+ };
35
+ turn: 'player' | 'enemy';
36
+ isProcessingTurn: boolean;
37
+ };
38
+ }
39
+
40
+ // Load persisted state
41
+ const persistedState = loadPersistedState<AppState>(
42
+ localStorage,
43
+ 'microstore-example',
44
+ ['userName', 'userEmail', 'isLoggedIn', 'theme']
45
+ );
46
+
47
+ // Create store with initial state merged with persisted state
48
+ const store = createStoreState<AppState>({
49
+ count: 0,
50
+ userName: '',
51
+ userEmail: '',
52
+ isLoggedIn: false,
53
+ userError: '',
54
+ theme: 'light',
55
+ ...persistedState, // Apply persisted state
56
+ todos: [],
57
+ searchResults: [],
58
+ isSearching: false,
59
+ logs: [],
60
+ // Complex object for custom compare demo
61
+ gameState: {
62
+ player: {
63
+ name: 'Player 1',
64
+ health: 100,
65
+ mana: 50,
66
+ level: 1,
67
+ experience: 0
68
+ },
69
+ enemy: {
70
+ name: 'Goblin',
71
+ health: 30,
72
+ damage: 5,
73
+ isAlive: true
74
+ },
75
+ ui: {
76
+ showInventory: false,
77
+ showMap: false,
78
+ notifications: []
79
+ },
80
+ turn: 'player',
81
+ isProcessingTurn: false
82
+ }
83
+ });
84
+
85
+ // Persistence
86
+ store.addMiddleware(createPersistenceMiddleware(localStorage, 'microstore-example', ['userName', 'userEmail', 'isLoggedIn', 'theme']));
87
+
88
+ // Block negative counts
89
+ store.addMiddleware((state, update, next) => {
90
+ if (update.count !== undefined && update.count < 0) {
91
+ addLog(`Blocked: ${formatValue(state.count)} → ${formatValue(update.count)}`, 'error');
92
+ return;
93
+ }
94
+ next();
95
+ }, ['count']);
96
+
97
+ // Email validation
98
+ store.addMiddleware((state, update, next) => {
99
+ if (update.userEmail && !update.userEmail.includes('@')) {
100
+ addLog(`Blocked: ${formatValue(state.userEmail)} → ${formatValue(update.userEmail)}`, 'error');
101
+ next({ userError: 'Invalid email format' });
102
+ return;
103
+ }
104
+ if (update.userEmail !== undefined) {
105
+ next({ ...update, userError: '' });
106
+ } else {
107
+ next();
108
+ }
109
+ }, ['userEmail']);
110
+
111
+ // Auto-normalize user data
112
+ store.addMiddleware((_, update, next) => {
113
+ const mod = { ...update };
114
+ let changed = false;
115
+
116
+ if (update.userName) {
117
+ const norm = update.userName.trim().toUpperCase();
118
+ if (norm !== update.userName) { mod.userName = norm; changed = true; }
119
+ }
120
+
121
+ if (update.userEmail) {
122
+ const norm = update.userEmail.trim().toLowerCase();
123
+ if (norm !== update.userEmail) { mod.userEmail = norm; changed = true; }
124
+ }
125
+
126
+ if (changed) addLog('Transform: name/email normalized', 'info');
127
+ next(mod);
128
+ }, ['userName', 'userEmail']);
129
+
130
+ // Log all changes
131
+ store.addMiddleware((state, update, next) => {
132
+ Object.keys(update).filter(k => k !== 'logs').forEach(key => {
133
+ const before = state[key as keyof AppState];
134
+ const after = update[key as keyof AppState];
135
+ if (JSON.stringify(before) !== JSON.stringify(after)) {
136
+ addLog(`${key}: ${formatValue(before)} → ${formatValue(after)}`, 'info');
137
+ }
138
+ });
139
+ next();
140
+ });
141
+
142
+ // Helper function to format values for display
143
+ function formatValue(value: unknown): string {
144
+ if (value === null) return 'null';
145
+ if (value === undefined) return 'undefined';
146
+ if (typeof value === 'string') return `"${value}"`;
147
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
148
+ if (Array.isArray(value)) return `[${value.length} items]`;
149
+ if (typeof value === 'object') {
150
+ try {
151
+ const str = JSON.stringify(value, null, 2);
152
+ const lines = str.split('\n');
153
+ if (lines.length > 5) {
154
+ return lines.slice(0, 4).join('\n') + '\n ...\n}';
155
+ }
156
+ return str;
157
+ } catch {
158
+ return '{object}';
159
+ }
160
+ }
161
+ return String(value);
162
+ }
163
+
164
+ // Mock search function
165
+ function performSearch(query: string): string[] {
166
+ const mockData = [
167
+ 'JavaScript', 'TypeScript', 'React', 'Vue', 'Angular', 'Node.js',
168
+ 'Express', 'MongoDB', 'PostgreSQL', 'Redis', 'Docker', 'Kubernetes'
169
+ ];
170
+
171
+ if (!query.trim()) return [];
172
+
173
+ return mockData.filter(item =>
174
+ item.toLowerCase().includes(query.toLowerCase())
175
+ );
176
+ }
177
+
178
+ // Search functionality with debouncing
179
+ export function updateSearchQuery(query: string) {
180
+ // Check current state
181
+ const {isSearching} = store.select(['isSearching']);
182
+ const results = performSearch(query);
183
+
184
+ if(!query){
185
+ store.set({
186
+ searchResults: [],
187
+ isSearching: false
188
+ });
189
+ return;
190
+ }
191
+
192
+ if(!isSearching) {
193
+ store.set({
194
+ isSearching: query.length > 0
195
+ });
196
+ }
197
+
198
+ // Debounced search execution (300ms delay)
199
+ store.set({
200
+ searchResults: results,
201
+ isSearching: false
202
+ }, 300);
203
+ }
204
+
205
+ // Helper function to add logs to store
206
+ let logCounter = 0;
207
+ function addLog(message: string, type: string = 'info') {
208
+ const currentState = store.get();
209
+ const newLog = {
210
+ id: Date.now() + (++logCounter), // Ensure unique IDs
211
+ message,
212
+ type,
213
+ timestamp: new Date().toLocaleTimeString()
214
+ };
215
+
216
+ // Keep only last 50 logs
217
+ const newLogs = [...currentState.logs, newLog].slice(-50);
218
+
219
+ // Update logs without triggering middleware (direct state update)
220
+ store.set({ logs: newLogs });
221
+ }
222
+
223
+ export default store;
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "dev-react-microstore",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "A minimal global state manager for React with fine-grained subscriptions.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/OhadBaehr/react-microstore.git"
10
+ },
11
+ "homepage": "https://github.com/OhadBaehr/react-microstore",
12
+ "bugs": {
13
+ "url": "https://github.com/OhadBaehr/react-microstore/issues"
14
+ },
7
15
  "scripts": {
8
16
  "build": "tsup src/index.ts --format esm,cjs --dts --minify",
9
17
  "prepare": "npm run build"
@@ -21,8 +29,7 @@
21
29
  "author": "Ohad Baehr",
22
30
  "license": "MIT",
23
31
  "peerDependencies": {
24
- "react": ">=17.0.0",
25
- "react-dom": ">=17.0.0"
32
+ "react": ">=17.0.0"
26
33
  },
27
34
  "devDependencies": {
28
35
  "@types/react": "^19.1.2",