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