@@ -8,6 +8,7 @@ import useMergedState from '../src/hooks/useMergedState';
8
8
import useMobile from '../src/hooks/useMobile' ;
9
9
import useState from '../src/hooks/useState' ;
10
10
import useSyncState from '../src/hooks/useSyncState' ;
11
+ import useControlledState from '../src/hooks/useControlledState' ;
11
12
12
13
global . disableUseId = false ;
13
14
@@ -317,6 +318,163 @@ describe('hooks', () => {
317
318
} ) ;
318
319
} ) ;
319
320
321
+ describe ( 'useControlledState' , ( ) => {
322
+ const FC : React . FC < {
323
+ value ?: string ;
324
+ defaultValue ?: string | ( ( ) => string ) ;
325
+ } > = props => {
326
+ const { value, defaultValue } = props ;
327
+ const [ val , setVal ] = useControlledState < string > (
328
+ defaultValue ?? null ,
329
+ value ,
330
+ ) ;
331
+ return (
332
+ < >
333
+ < input
334
+ value = { val }
335
+ onChange = { e => {
336
+ setVal ( e . target . value ) ;
337
+ } }
338
+ />
339
+ < span className = "txt" > { val } </ span >
340
+ </ >
341
+ ) ;
342
+ } ;
343
+
344
+ it ( 'still control of to undefined' , ( ) => {
345
+ const { container, rerender } = render ( < FC value = "test" /> ) ;
346
+
347
+ expect ( container . querySelector ( 'input' ) . value ) . toEqual ( 'test' ) ;
348
+ expect ( container . querySelector ( '.txt' ) . textContent ) . toEqual ( 'test' ) ;
349
+
350
+ rerender ( < FC value = { undefined } /> ) ;
351
+ expect ( container . querySelector ( 'input' ) . value ) . toEqual ( 'test' ) ;
352
+ expect ( container . querySelector ( '.txt' ) . textContent ) . toEqual ( '' ) ;
353
+ } ) ;
354
+
355
+ describe ( 'correct defaultValue' , ( ) => {
356
+ it ( 'raw' , ( ) => {
357
+ const { container } = render ( < FC defaultValue = "test" /> ) ;
358
+
359
+ expect ( container . querySelector ( 'input' ) . value ) . toEqual ( 'test' ) ;
360
+ } ) ;
361
+
362
+ it ( 'func' , ( ) => {
363
+ const { container } = render ( < FC defaultValue = { ( ) => 'bamboo' } /> ) ;
364
+
365
+ expect ( container . querySelector ( 'input' ) . value ) . toEqual ( 'bamboo' ) ;
366
+ } ) ;
367
+ } ) ;
368
+
369
+ it ( 'not rerender when setState as deps' , ( ) => {
370
+ let renderTimes = 0 ;
371
+
372
+ const Test = ( ) => {
373
+ const [ val , setVal ] = useControlledState ( 0 ) ;
374
+
375
+ React . useEffect ( ( ) => {
376
+ renderTimes += 1 ;
377
+ expect ( renderTimes < 10 ) . toBeTruthy ( ) ;
378
+
379
+ setVal ( 1 ) ;
380
+ } , [ setVal ] ) ;
381
+
382
+ return < div > { val } </ div > ;
383
+ } ;
384
+
385
+ const { container } = render ( < Test /> ) ;
386
+ expect ( container . firstChild . textContent ) . toEqual ( '1' ) ;
387
+ } ) ;
388
+
389
+ it ( 'React 18 should not reset to undefined' , ( ) => {
390
+ const Demo = ( ) => {
391
+ const [ val ] = useControlledState ( 33 , undefined ) ;
392
+
393
+ return < div > { val } </ div > ;
394
+ } ;
395
+
396
+ const { container } = render (
397
+ < React . StrictMode >
398
+ < Demo />
399
+ </ React . StrictMode > ,
400
+ ) ;
401
+
402
+ expect ( container . querySelector ( 'div' ) . textContent ) . toEqual ( '33' ) ;
403
+ } ) ;
404
+
405
+ it ( 'uncontrolled to controlled' , ( ) => {
406
+ const Demo : React . FC < Readonly < { value ?: number } > > = ( { value } ) => {
407
+ const [ mergedValue , setMergedValue ] = useControlledState < number > (
408
+ ( ) => 233 ,
409
+ value ,
410
+ ) ;
411
+
412
+ return (
413
+ < span
414
+ onClick = { ( ) => {
415
+ setMergedValue ( v => v + 1 ) ;
416
+ setMergedValue ( v => v + 1 ) ;
417
+ } }
418
+ onMouseEnter = { ( ) => {
419
+ setMergedValue ( 1 ) ;
420
+ } }
421
+ >
422
+ { mergedValue }
423
+ </ span >
424
+ ) ;
425
+ } ;
426
+
427
+ const { container, rerender } = render ( < Demo /> ) ;
428
+ expect ( container . textContent ) . toEqual ( '233' ) ;
429
+
430
+ // Update value
431
+ rerender ( < Demo value = { 1 } /> ) ;
432
+ expect ( container . textContent ) . toEqual ( '1' ) ;
433
+
434
+ // Click update
435
+ rerender ( < Demo value = { undefined } /> ) ;
436
+ fireEvent . mouseEnter ( container . querySelector ( 'span' ) ) ;
437
+ fireEvent . click ( container . querySelector ( 'span' ) ) ;
438
+ expect ( container . textContent ) . toEqual ( '3' ) ;
439
+ } ) ;
440
+
441
+ it ( 'should alway use option value' , ( ) => {
442
+ const Test : React . FC < Readonly < { value ?: number } > > = ( { value } ) => {
443
+ const [ mergedValue , setMergedValue ] = useControlledState < number > (
444
+ undefined ,
445
+ value ,
446
+ ) ;
447
+ return (
448
+ < span
449
+ onClick = { ( ) => {
450
+ setMergedValue ( 12 ) ;
451
+ } }
452
+ >
453
+ { mergedValue }
454
+ </ span >
455
+ ) ;
456
+ } ;
457
+
458
+ const { container } = render ( < Test value = { 1 } /> ) ;
459
+ fireEvent . click ( container . querySelector ( 'span' ) ) ;
460
+
461
+ expect ( container . textContent ) . toBe ( '1' ) ;
462
+ } ) ;
463
+
464
+ it ( 'render once' , ( ) => {
465
+ let count = 0 ;
466
+
467
+ const Demo : React . FC = ( ) => {
468
+ const [ ] = useControlledState ( undefined ) ;
469
+ count += 1 ;
470
+ return null ;
471
+ } ;
472
+
473
+ render ( < Demo /> ) ;
474
+ expect ( count ) . toBe ( 1 ) ;
475
+ } ) ;
476
+ } ) ;
477
+
320
478
describe ( 'useLayoutEffect' , ( ) => {
321
479
const FC : React . FC < Readonly < { defaultValue ?: string } > > = props => {
322
480
const { defaultValue } = props ;
0 commit comments