@@ -4,13 +4,14 @@ import {
4
4
mapTemplate ,
5
5
onMount ,
6
6
atom ,
7
- map
7
+ map ,
8
+ MapStore
8
9
} from 'nanostores'
9
10
import React , { FC } from 'react'
10
11
import ReactTesting from '@testing-library/react'
11
12
import { delay } from 'nanodelay'
12
13
13
- import { useStore } from './index.js'
14
+ import { useStore , useStoreListener } from './index.js'
14
15
15
16
let { render, screen, act } = ReactTesting
16
17
let { createElement : h , useState } = React
@@ -177,16 +178,16 @@ it('does not reload store on component changes', async () => {
177
178
} )
178
179
179
180
it ( 'handles keys option' , async ( ) => {
180
- type MapStore = {
181
+ type StoreValue = {
181
182
a ?: string
182
183
b ?: string
183
184
}
184
185
let Wrapper : FC = ( { children } ) => h ( 'div' , { } , children )
185
- let mapStore = map < MapStore > ( )
186
+ let mapStore = map < StoreValue > ( )
186
187
let renderCount = 0
187
188
let MapTest = ( ) : React . ReactElement => {
188
189
renderCount ++
189
- let [ keys , setKeys ] = useState < ( keyof MapStore ) [ ] > ( [ 'a' ] )
190
+ let [ keys , setKeys ] = useState < ( keyof StoreValue ) [ ] > ( [ 'a' ] )
190
191
let { a, b } = useStore ( mapStore , { keys } )
191
192
return h (
192
193
'div' ,
@@ -245,3 +246,119 @@ it('handles keys option', async () => {
245
246
expect ( screen . getByTestId ( 'map-test' ) ) . toHaveTextContent ( 'map:a-b' )
246
247
expect ( renderCount ) . toBe ( 4 )
247
248
} )
249
+
250
+ describe ( 'useStoreListener hook' , ( ) => {
251
+ it ( 'throws on template instead of store' , ( ) => {
252
+ let Test = ( ) : void => { }
253
+ let [ errors , Catcher ] = getCatcher ( ( ) => {
254
+ // @ts -expect-error
255
+ useStoreListener ( Test , { listener : ( ) => { } } )
256
+ } )
257
+ render ( h ( Catcher ) )
258
+ expect ( errors ) . toEqual ( [
259
+ 'Use useStore(Template(id)) or useSync() ' +
260
+ 'from @logux/client/react for templates'
261
+ ] )
262
+ } )
263
+
264
+ function createTest ( opts = { } ) : {
265
+ Test : FC
266
+ stats : { renders : number ; calls : number }
267
+ store : MapStore
268
+ } {
269
+ let store = map ( { a : 0 } )
270
+ let stats = { renders : 0 , calls : 0 }
271
+ let Test = ( ) : React . ReactElement => {
272
+ stats . renders += 1
273
+ useStoreListener ( store , {
274
+ ...opts ,
275
+ listener : ( ) => {
276
+ stats . calls += 1
277
+ }
278
+ } )
279
+ return h ( 'span' )
280
+ }
281
+ return { Test, stats, store }
282
+ }
283
+
284
+ it ( 'invokes provided callback on store change' , async ( ) => {
285
+ let { Test, stats, store } = createTest ( )
286
+ render ( h ( Test ) )
287
+ await act ( async ( ) => {
288
+ store . set ( { a : 1 } )
289
+ await delay ( 1 )
290
+ } )
291
+ expect ( stats . calls ) . toBe ( 1 )
292
+ } )
293
+
294
+ it ( "doesn't trigger rerenders on store change, but invokes the callback" , async ( ) => {
295
+ let { Test, stats, store } = createTest ( )
296
+ render ( h ( Test ) )
297
+ await act ( async ( ) => {
298
+ store . set ( { a : 1 } )
299
+ await delay ( 1 )
300
+ store . set ( { a : 2 } )
301
+ await delay ( 1 )
302
+ } )
303
+ expect ( stats . calls ) . toBe ( 2 )
304
+ expect ( stats . renders ) . toBe ( 1 )
305
+ } )
306
+
307
+ it ( 'handles `leading` option' , ( ) => {
308
+ let { Test, stats } = createTest ( { leading : true } )
309
+ render ( h ( Test ) )
310
+ expect ( stats . calls ) . toBe ( 1 )
311
+ expect ( stats . renders ) . toBe ( 1 )
312
+ } )
313
+
314
+ it ( 'handles `keys` option' , async ( ) => {
315
+ let renders = 0
316
+ let calls = 0
317
+ type StoreValue = { a : number ; b : number }
318
+ let mapStore = map < StoreValue > ( { a : 0 , b : 0 } )
319
+ let MapTest = ( ) : React . ReactElement => {
320
+ renders += 1
321
+ let [ keys , setKeys ] = useState < ( keyof StoreValue ) [ ] > ( [ 'a' ] )
322
+ useStoreListener ( mapStore , {
323
+ keys,
324
+ listener : ( ) => {
325
+ calls += 1
326
+ }
327
+ } )
328
+ return h (
329
+ 'div' ,
330
+ { 'data-testid' : 'map-test' } ,
331
+ h ( 'button' , {
332
+ onClick : ( ) => {
333
+ setKeys ( [ 'a' , 'b' ] )
334
+ }
335
+ } ) ,
336
+ null
337
+ )
338
+ }
339
+ render ( h ( MapTest ) )
340
+ await act ( async ( ) => {
341
+ mapStore . setKey ( 'a' , 1 )
342
+ await delay ( 1 )
343
+ } )
344
+ expect ( calls ) . toBe ( 1 )
345
+ expect ( renders ) . toBe ( 1 )
346
+
347
+ // does not react to 'b' key change
348
+ await act ( async ( ) => {
349
+ mapStore . setKey ( 'b' , 1 )
350
+ await delay ( 1 )
351
+ } )
352
+ expect ( calls ) . toBe ( 1 )
353
+ expect ( renders ) . toBe ( 1 )
354
+
355
+ await act ( async ( ) => {
356
+ screen . getByRole ( 'button' ) . click ( ) // enable 'b' key
357
+ await delay ( 1 )
358
+ mapStore . setKey ( 'b' , 2 )
359
+ await delay ( 1 )
360
+ } )
361
+ expect ( calls ) . toBe ( 2 )
362
+ expect ( renders ) . toBe ( 2 ) // due to `keys` state change inside the component
363
+ } )
364
+ } )
0 commit comments