@storve/react 1.0.2 → 1.0.4

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.
@@ -1,288 +0,0 @@
1
- import { render, screen, act } from '@testing-library/react'
2
- import { createStore } from '@storve/core'
3
- import { useStore } from '../src/useStore'
4
- import { expect, it, describe, vi } from 'vitest'
5
- import React, { useCallback } from 'react'
6
-
7
- describe('useStore with selector', () => {
8
- it('selector receives full state as argument', () => {
9
- const store = createStore({ a: 1, b: 2 })
10
- let receivedState: unknown
11
- function Test() {
12
- useStore(store, (s: unknown) => {
13
- receivedState = s
14
- return (s as { a: number; b: number }).a
15
- })
16
- return null
17
- }
18
- render(<Test />)
19
- expect(receivedState).toEqual({ a: 1, b: 2 })
20
- })
21
-
22
- it('selector return value is what component receives', () => {
23
- const store = createStore({ count: 10 })
24
- function Test() {
25
- const doubled = useStore(store, (s) => s.count * 2)
26
- return <div data-testid="val">{doubled}</div>
27
- }
28
- render(<Test />)
29
- expect(screen.getByTestId('val')).toHaveTextContent('20')
30
- })
31
-
32
- it('component re-renders when selector return value changes', () => {
33
- const store = createStore({ count: 0, unrelated: 0 })
34
- const renderSpy = vi.fn()
35
- function Test() {
36
- renderSpy()
37
- const count = useStore(store, (s) => s.count)
38
- return <div>{count}</div>
39
- }
40
- render(<Test />)
41
- expect(renderSpy).toHaveBeenCalledTimes(1)
42
- act(() => {
43
- store.setState({ count: 1 })
44
- })
45
- expect(renderSpy).toHaveBeenCalledTimes(2)
46
- })
47
-
48
- it('component does NOT re-render when selector return value unchanged', () => {
49
- const store = createStore({ count: 0, unrelated: 0 })
50
- const renderSpy = vi.fn()
51
- function Test() {
52
- renderSpy()
53
- const isPositive = useStore(store, (s) => s.count > 0)
54
- return <div>{String(isPositive)}</div>
55
- }
56
- render(<Test />)
57
- expect(renderSpy).toHaveBeenCalledTimes(1)
58
-
59
- // Unrelated state change
60
- act(() => {
61
- store.setState({ unrelated: 1 })
62
- })
63
- expect(renderSpy).toHaveBeenCalledTimes(1)
64
-
65
- // Related state change but result remains same (0 -> -1, both NOT > 0)
66
- act(() => {
67
- store.setState({ count: -1 })
68
- })
69
- expect(renderSpy).toHaveBeenCalledTimes(1)
70
- })
71
-
72
- it('selector returning primitive re-renders correctly', () => {
73
- const store = createStore({ val: 'a' })
74
- function Test() {
75
- const val = useStore(store, (s) => s.val)
76
- return <div data-testid="val">{val}</div>
77
- }
78
- render(<Test />)
79
- act(() => {
80
- store.setState({ val: 'b' })
81
- })
82
- expect(screen.getByTestId('val')).toHaveTextContent('b')
83
- })
84
-
85
- it('selector returning object re-renders correctly', () => {
86
- const store = createStore({ a: 1, b: 2 })
87
- const renderSpy = vi.fn()
88
- function Test() {
89
- renderSpy()
90
- const sub = useStore(store, (s) => ({ combined: s.a + s.b }))
91
- return <div>{sub.combined}</div>
92
- }
93
- render(<Test />)
94
- expect(renderSpy).toHaveBeenCalledTimes(1)
95
-
96
- // Even if values are same, returning a new object literal triggers re-render
97
- // because useSyncExternalStore uses Object.is comparison
98
- act(() => {
99
- store.setState({ a: 1 }) // No actual change, but store.subscribe might fire
100
- })
101
- // Storve only notifies if state actually changed.
102
- // If we change 'a' to '1' (same), Storve might not notify.
103
-
104
- act(() => {
105
- store.setState({ a: 10 })
106
- })
107
- expect(renderSpy).toHaveBeenCalledTimes(2)
108
- })
109
-
110
- it('selector returning array re-renders correctly', () => {
111
- const store = createStore({ items: [1, 2] })
112
- const renderSpy = vi.fn()
113
- function Test() {
114
- renderSpy()
115
- const first = useStore(store, (s) => [s.items[0]])
116
- return <div>{first[0]}</div>
117
- }
118
- render(<Test />)
119
- act(() => {
120
- store.setState({ items: [10, 2] })
121
- })
122
- expect(renderSpy).toHaveBeenCalledTimes(2)
123
- })
124
-
125
- it('selector returning derived number re-renders correctly', () => {
126
- const store = createStore({ a: 1, b: 2 })
127
- function Test() {
128
- const sum = useStore(store, (s) => s.a + s.b)
129
- return <div data-testid="sum">{sum}</div>
130
- }
131
- render(<Test />)
132
- act(() => {
133
- store.setState({ a: 5 })
134
- })
135
- expect(screen.getByTestId('sum')).toHaveTextContent('7')
136
- })
137
-
138
- it('selector returning derived boolean re-renders correctly', () => {
139
- const store = createStore({ count: 0 })
140
- function Test() {
141
- const isZero = useStore(store, (s) => s.count === 0)
142
- return <div data-testid="val">{String(isZero)}</div>
143
- }
144
- render(<Test />)
145
- act(() => {
146
- store.setState({ count: 1 })
147
- })
148
- expect(screen.getByTestId('val')).toHaveTextContent('false')
149
- })
150
-
151
- it('selector returning null does not crash', () => {
152
- const store = createStore<{ data: { name: string } | null }>({ data: { name: 'test' } })
153
- function Test() {
154
- const name = useStore(store, (s: unknown) => {
155
- const state = s as { data: { name: string } | null }
156
- return state.data ? state.data.name : null
157
- })
158
- return <div data-testid="val">{name === null ? 'null' : name}</div>
159
- }
160
- render(<Test />)
161
- act(() => {
162
- store.setState({ data: null })
163
- })
164
- expect(screen.getByTestId('val')).toHaveTextContent('null')
165
- })
166
-
167
- it('selector returning undefined does not crash', () => {
168
- const store = createStore({ val: 1 } as { val?: number })
169
- function Test() {
170
- const val = useStore(store, (s) => s.val)
171
- return <div data-testid="val">{val === undefined ? 'undefined' : val}</div>
172
- }
173
- render(<Test />)
174
- act(() => {
175
- store.setState({ val: undefined })
176
- })
177
- expect(screen.getByTestId('val')).toHaveTextContent('undefined')
178
- })
179
-
180
- it('selector with multiple state dependencies updates correctly', () => {
181
- const store = createStore({ a: 1, b: 2, c: 3 })
182
- function Test() {
183
- const total = useStore(store, (s) => s.a + s.b + s.c)
184
- return <div data-testid="total">{total}</div>
185
- }
186
- render(<Test />)
187
- act(() => {
188
- store.setState({ a: 10, b: 20 })
189
- })
190
- expect(screen.getByTestId('total')).toHaveTextContent('33')
191
- })
192
-
193
- it('inline selector function — stable behavior', () => {
194
- const store = createStore({ count: 0 })
195
- const renderSpy = vi.fn()
196
- function Test() {
197
- renderSpy()
198
- const count = useStore(store, (s) => s.count)
199
- return <div>{count}</div>
200
- }
201
- const { rerender } = render(<Test />)
202
- expect(renderSpy).toHaveBeenCalledTimes(1)
203
- rerender(<Test />)
204
- expect(renderSpy).toHaveBeenCalledTimes(2)
205
- })
206
-
207
- it('memoized selector via useCallback — no extra re-renders', () => {
208
- const store = createStore({ count: 0 })
209
- const renderSpy = vi.fn()
210
- function Test() {
211
- renderSpy()
212
- const selector = useCallback((s: unknown) => (s as { count: number }).count, [])
213
- const count = useStore(store, selector)
214
- return <div>{count}</div>
215
- }
216
- const { rerender } = render(<Test />)
217
- expect(renderSpy).toHaveBeenCalledTimes(1)
218
- rerender(<Test />)
219
- // Even if selector is memoized, Component itself re-renders because rerender() was called
220
- expect(renderSpy).toHaveBeenCalledTimes(2)
221
- })
222
-
223
- it('changing selector between renders — returns correct value', () => {
224
- const store = createStore({ a: 1, b: 2 })
225
- function Test({ mode }: { mode: 'a' | 'b' }) {
226
- const val = useStore(store, mode === 'a' ? (s) => s.a : (s) => s.b)
227
- return <div data-testid="val">{val}</div>
228
- }
229
- const { rerender } = render(<Test mode="a" />)
230
- expect(screen.getByTestId('val')).toHaveTextContent('1')
231
- rerender(<Test mode="b" />)
232
- expect(screen.getByTestId('val')).toHaveTextContent('2')
233
- })
234
-
235
- it('selector returning primitive then object — shallowEqual with type mismatch', () => {
236
- const store = createStore({ mode: 'num' as 'num' | 'obj' })
237
- const renderSpy = vi.fn()
238
- function Test() {
239
- renderSpy()
240
- const val = useStore(
241
- store,
242
- (s) => (s.mode === 'num' ? 42 : { value: 42 })
243
- )
244
- return (
245
- <div data-testid="val">
246
- {typeof val === 'object' ? val.value : val}
247
- </div>
248
- )
249
- }
250
- render(<Test />)
251
- expect(screen.getByTestId('val')).toHaveTextContent('42')
252
- act(() => store.setState({ mode: 'obj' }))
253
- expect(screen.getByTestId('val')).toHaveTextContent('42')
254
- })
255
-
256
- it('selector returning null then object — shallowEqual with null', () => {
257
- const store = createStore<{ data: { x: number } | null }>({ data: null })
258
- const renderSpy = vi.fn()
259
- function Test() {
260
- renderSpy()
261
- const data = useStore(store, (s) => s.data)
262
- return (
263
- <div data-testid="val">
264
- {data === null ? 'null' : data.x}
265
- </div>
266
- )
267
- }
268
- render(<Test />)
269
- expect(screen.getByTestId('val')).toHaveTextContent('null')
270
- act(() => store.setState({ data: { x: 1 } }))
271
- expect(screen.getByTestId('val')).toHaveTextContent('1')
272
- })
273
-
274
- it('selector returning same object reference — Object.is early return', () => {
275
- const nested = { x: 1 }
276
- const store = createStore({ nested, other: 0 })
277
- const renderSpy = vi.fn()
278
- function Test() {
279
- renderSpy()
280
- const sub = useStore(store, (s) => s.nested)
281
- return <div data-testid="val">{sub.x}</div>
282
- }
283
- render(<Test />)
284
- expect(screen.getByTestId('val')).toHaveTextContent('1')
285
- act(() => store.setState({ other: 1 }))
286
- expect(screen.getByTestId('val')).toHaveTextContent('1')
287
- })
288
- })
package/tests/setup.ts DELETED
@@ -1,7 +0,0 @@
1
- import '@testing-library/jest-dom'
2
- import { afterEach } from 'vitest'
3
- import { cleanup } from '@testing-library/react'
4
-
5
- afterEach(() => {
6
- cleanup()
7
- })
@@ -1,80 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render, screen, act } from '@testing-library/react';
3
- import React from 'react';
4
- import type { Store } from '../../storve/src/types';
5
- import { createStore } from '../../storve/src/store';
6
- import { withDevtools } from '../../storve/src/devtools/withDevtools';
7
- import { useDevtools } from '../src/useDevtools';
8
-
9
- /** @internal */
10
- type DevtoolsStore<S extends object> = Store<S> & {
11
- undo: () => void;
12
- redo: () => void;
13
- snapshot: (name: string) => void;
14
- }
15
-
16
-
17
- function TestComponent({ store }: { store: Store<object> }) {
18
- const { canUndo, canRedo, history, snapshots } = useDevtools(store);
19
-
20
- return (
21
- <div>
22
- <div data-testid="can-undo">{String(canUndo)}</div>
23
- <div data-testid="can-redo">{String(canRedo)}</div>
24
- <div data-testid="history-count">{history.length}</div>
25
- <div data-testid="snapshots">{snapshots.join(',')}</div>
26
- </div>
27
- );
28
- }
29
-
30
- describe('useDevtools hook', () => {
31
- it('canRedo becomes true after undo', () => {
32
- const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
33
-
34
- render(<TestComponent store={store} />);
35
- expect(screen.getByTestId('can-redo').textContent).toBe('false');
36
- expect(screen.getByTestId('history-count').textContent).toBe('0'); // empty until first state change
37
-
38
- act(() => {
39
- store.setState({ count: 1 });
40
- store.setState({ count: 2 });
41
- });
42
-
43
- act(() => {
44
- store.undo();
45
- });
46
-
47
- expect(screen.getByTestId('can-redo').textContent).toBe('true');
48
- });
49
-
50
- it('re-renders when undo is called', () => {
51
- const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
52
- act(() => {
53
- store.setState({ count: 1 });
54
- store.setState({ count: 2 });
55
- });
56
-
57
- render(<TestComponent store={store} />);
58
- expect(screen.getByTestId('history-count').textContent).toBe('2');
59
-
60
- act(() => {
61
- store.undo();
62
- });
63
-
64
- expect(screen.getByTestId('can-undo').textContent).toBe('false');
65
- expect(screen.getByTestId('can-redo').textContent).toBe('true');
66
- });
67
-
68
- it('snapshot list updates reactively after snapshot()', () => {
69
- const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
70
- render(<TestComponent store={store} />);
71
-
72
- expect(screen.getByTestId('snapshots').textContent).toBe('');
73
-
74
- act(() => {
75
- (store as unknown as DevtoolsStore<object>).snapshot('s1');
76
- });
77
-
78
- expect(screen.getByTestId('snapshots').textContent).toBe('s1');
79
- });
80
- });
@@ -1,233 +0,0 @@
1
- import { render, screen, act } from '@testing-library/react'
2
- import { createStore, type Store } from '@storve/core'
3
- import { useStore, shallowEqual } from '../src/useStore'
4
- import { expect, it, describe, vi } from 'vitest'
5
- import React from 'react'
6
-
7
- describe('useStore', () => {
8
- it('shallowEqual returns true for same reference (covers Object.is branch)', () => {
9
- const obj = { a: 1 }
10
- expect(shallowEqual(obj, obj)).toBe(true)
11
- })
12
-
13
- it('returns full state when no selector provided', () => {
14
- const store = createStore({ count: 0 })
15
- function Counter() {
16
- const state = useStore(store)
17
- return <div data-testid="count">{state.count}</div>
18
- }
19
- render(<Counter />)
20
- expect(screen.getByTestId('count')).toHaveTextContent('0')
21
- })
22
-
23
- it('returns correct initial state on first render', () => {
24
- type TestState = { name: string }
25
- const store = createStore<TestState>({ name: 'flux' })
26
- let renderedState: TestState | undefined
27
- function Profile() {
28
- renderedState = useStore(store)
29
- return null
30
- }
31
- render(<Profile />)
32
- expect(renderedState).toEqual({ name: 'flux' })
33
- })
34
-
35
- it('returns updated state after setState called', () => {
36
- const store = createStore({ count: 0 })
37
- function Counter() {
38
- const state = useStore(store)
39
- return <div data-testid="count">{state.count}</div>
40
- }
41
- render(<Counter />)
42
- act(() => {
43
- store.setState({ count: 1 })
44
- })
45
- expect(screen.getByTestId('count')).toHaveTextContent('1')
46
- })
47
-
48
- it('component re-renders when state changes', () => {
49
- const store = createStore({ count: 0 })
50
- const renderSpy = vi.fn()
51
- function Counter() {
52
- renderSpy()
53
- const state = useStore(store)
54
- return <div>{state.count}</div>
55
- }
56
- render(<Counter />)
57
- expect(renderSpy).toHaveBeenCalledTimes(1)
58
- act(() => {
59
- store.setState({ count: 1 })
60
- })
61
- expect(renderSpy).toHaveBeenCalledTimes(2)
62
- })
63
-
64
- it('component does NOT re-render when unrelated state changes', () => {
65
- const store = createStore({ a: 1, b: 2 })
66
- const renderSpy = vi.fn()
67
- function Component() {
68
- renderSpy()
69
- const state = useStore(store)
70
- // useSyncExternalStore with full state will re-render if any part of state changes
71
- // because the state object returned by storve (Proxy) is updated on any change
72
- return <div>{state.a}</div>
73
- }
74
- render(<Component />)
75
- expect(renderSpy).toHaveBeenCalledTimes(1)
76
-
77
- // Changing 'b' SHOULD trigger a re-render if we return the full state
78
- // because useSyncExternalStore checks reference equality of the return value
79
- act(() => {
80
- store.setState({ b: 3 })
81
- })
82
- expect(renderSpy).toHaveBeenCalledTimes(2)
83
- })
84
-
85
- it('multiple components using same store all update correctly', () => {
86
- const store = createStore({ count: 0 })
87
- function Counter() {
88
- const state = useStore(store)
89
- return <div data-testid="count">{state.count}</div>
90
- }
91
- render(
92
- <>
93
- <Counter />
94
- <Counter />
95
- </>
96
- )
97
- act(() => {
98
- store.setState({ count: 5 })
99
- })
100
- const elements = screen.getAllByTestId('count')
101
- expect(elements[0]).toHaveTextContent('5')
102
- expect(elements[1]).toHaveTextContent('5')
103
- })
104
-
105
- it('multiple components using different stores are independent', () => {
106
- const store1 = createStore({ a: 1 })
107
- const store2 = createStore({ b: 2 })
108
- function A() {
109
- const state = useStore(store1)
110
- return <div data-testid="a">{state.a}</div>
111
- }
112
- function B() {
113
- const state = useStore(store2)
114
- return <div data-testid="b">{state.b}</div>
115
- }
116
- render(
117
- <>
118
- <A />
119
- <B />
120
- </>
121
- )
122
- act(() => {
123
- store1.setState({ a: 10 })
124
- })
125
- expect(screen.getByTestId('a')).toHaveTextContent('10')
126
- expect(screen.getByTestId('b')).toHaveTextContent('2')
127
- })
128
-
129
- it('useStore works with flat state', () => {
130
- type TestState = { a: number; b: string }
131
- const store = createStore<TestState>({ a: 1, b: 'test' })
132
- const { result } = { result: { current: null as TestState | null } }
133
- function Test() {
134
- result.current = useStore(store)
135
- return null
136
- }
137
- render(<Test />)
138
- expect(result.current).toEqual({ a: 1, b: 'test' })
139
- })
140
-
141
- it('useStore works with nested state', () => {
142
- type TestState = { user: { id: number; profile: { name: string } } }
143
- const store = createStore<TestState>({ user: { id: 1, profile: { name: 'John' } } })
144
- const { result } = { result: { current: null as TestState | null } }
145
- function Test() {
146
- result.current = useStore(store)
147
- return null
148
- }
149
- render(<Test />)
150
- expect(result.current?.user.profile.name).toBe('John')
151
- })
152
-
153
- it('useStore works with array state', () => {
154
- type TestState = { list: number[] }
155
- const store = createStore<TestState>({ list: [1, 2, 3] })
156
- const { result } = { result: { current: null as TestState | null } }
157
- function Test() {
158
- result.current = useStore(store)
159
- return null
160
- }
161
- render(<Test />)
162
- expect(result.current?.list).toEqual([1, 2, 3])
163
- })
164
-
165
- it('useStore works with boolean state', () => {
166
- type TestState = { ok: boolean }
167
- const store = createStore<TestState>({ ok: true })
168
- const { result } = { result: { current: null as TestState | null } }
169
- function Test() {
170
- result.current = useStore(store)
171
- return null
172
- }
173
- render(<Test />)
174
- expect(result.current?.ok).toBe(true)
175
- })
176
-
177
- it('useStore works with null state', () => {
178
- // Note: createStore requires an object as initial state
179
- type TestState = { data: null }
180
- const store = createStore<TestState>({ data: null })
181
- const { result } = { result: { current: null as TestState | null } }
182
- function Test() {
183
- result.current = useStore(store)
184
- return null
185
- }
186
- render(<Test />)
187
- expect(result.current?.data).toBeNull()
188
- })
189
-
190
- it('useStore works with empty object state', () => {
191
- type TestState = Record<string, never>
192
- const store = createStore<TestState>({})
193
- const { result } = { result: { current: null as TestState | null } }
194
- function Test() {
195
- result.current = useStore(store)
196
- return null
197
- }
198
- render(<Test />)
199
- expect(result.current).toEqual({})
200
- })
201
-
202
- it('hook called with same store reference — stable behavior', () => {
203
- const store = createStore({ count: 0 })
204
- const renderSpy = vi.fn()
205
- function Test() {
206
- renderSpy()
207
- useStore(store)
208
- return null
209
- }
210
- const { rerender } = render(<Test />)
211
- expect(renderSpy).toHaveBeenCalledTimes(1)
212
- rerender(<Test />)
213
- expect(renderSpy).toHaveBeenCalledTimes(2)
214
- })
215
-
216
- it('hook called with different store reference — switches correctly', () => {
217
- const store1 = createStore({ val: 1 })
218
- const store2 = createStore({ val: 2 })
219
- function Test({ s }: { s: Store<{ val: number }> }) {
220
- const state = useStore(s)
221
- return <div data-testid="val">{state.val}</div>
222
- }
223
- const { rerender } = render(<Test s={store1} />)
224
- expect(screen.getByTestId('val')).toHaveTextContent('1')
225
- rerender(<Test s={store2} />)
226
- expect(screen.getByTestId('val')).toHaveTextContent('2')
227
-
228
- act(() => {
229
- store1.setState({ val: 10 })
230
- })
231
- expect(screen.getByTestId('val')).toHaveTextContent('2') // Should not update from store1 anymore
232
- })
233
- })
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "jsx": "react"
7
- },
8
- "include": [
9
- "src/**/*"
10
- ],
11
- "exclude": [
12
- "node_modules",
13
- "dist",
14
- "tests/**/*"
15
- ]
16
- }
package/vitest.config.mts DELETED
@@ -1,30 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: 'jsdom',
6
- globals: true,
7
- setupFiles: ['./tests/setup.ts'],
8
- include: ['tests/**/*.{test,spec}.{ts,tsx}'],
9
- pool: 'forks',
10
- poolOptions: {
11
- forks: {
12
- singleFork: true
13
- }
14
- },
15
- coverage: {
16
- provider: 'v8',
17
- reporter: ['text', 'json-summary'],
18
- thresholds: {
19
- statements: 85,
20
- branches: 85,
21
- functions: 85,
22
- lines: 85,
23
- },
24
- include: ['src/**/*.ts', 'src/**/*.tsx'],
25
- exclude: ['src/types.ts'],
26
- },
27
- },
28
- })
29
-
30
-