useControllableState

Back a value-bearing component with either a controlled value prop or internal state, decided by one call.
useControllableState replaces the hand-rolled isControlled + internalValue + sync-effect trio with one call. A component is controlled when its value prop is anything other than undefined — the parent owns the value and the hook never writes internal state. Otherwise it is uncontrolled, and the hook keeps the value in useState, seeded from defaultValue (falling back to finalValue).
The same StarPicker is rendered twice below: once with only defaultValue, once with value + onChange.

Signature

const

 

[

value

,

setValue

,

isControlled

]

 

=

 

useControllableState

(

{

  value

:

props

.

value

,

           

// controlled value (undefined ⇒ uncontrolled)

  defaultValue

:

props

.

defaultValue

,

  finalValue

:

 

0

,

                

// fallback when neither is provided

  onChange

:

props

.

onChange

,

}

)

;

Behavior

onChange fires synchronously from your event handler in both modes — not from an effect — so callbacks land before paint.

setValue accepts a value or an updater function, like useState. Several updater calls inside one handler compose against each other.

Extra arguments are forwarded to onChange after the value: setValue(next, { source: 'keyboard' }) calls onChange(next, { source: 'keyboard' }).

setValue is referentially stable for the component's lifetime — safe in dependency arrays and memoized context values.

Switching controlled → uncontrolled mid-life seeds internal state with the last controlled value so the UI holds its position. Any mode switch logs a warning in development.

See also

useDisclosure — the boolean-only case, with open / close / toggle

useDebouncedValue — debounce a value before acting on it

Loading demo…

import

 

{

useState

}

 

from

 

'react'

;

import

 

{

 

Badge

,

 

Block

,

 

Button

,

 

Rating

,

 

Row

,

 

Text

,

useControllableState

}

 

from

 

'@platform-blocks/ui'

;

 

interface

 

StarPickerProps

 

{

  

/** Controlled value. Passing this hands ownership to the parent. */

  value

?:

 

number

;

  

/** Initial value while uncontrolled. */

  defaultValue

?:

 

number

;

  onChange

?:

 

(

value

:

 

number

)

 

=>

 

void

;

}

 

/** One component that supports both modes — the hook decides which is active. */

function

 

StarPicker

(

{

value

,

defaultValue

,

onChange

}

:

 

StarPickerProps

)

 

{

  

const

 

[

rating

,

setRating

,

isControlled

]

 

=

 

useControllableState

(

{

    value

,

    defaultValue

,

    finalValue

:

 

0

,

    onChange

  

}

)

;

 

  

return

 

(

    

<Row

 

gap

=

"sm"

 

align

=

"center"

>

      

<Rating

 

value

=

{

rating

}

 

onChange

=

{

setRating

}

 

/>

      

<Badge

 

variant

=

"light"

 

color

=

{

isControlled

?

 

'primary'

 

:

 

'gray'

}

>

        

{

isControlled

?

 

'controlled'

 

:

 

'uncontrolled'

}

      

</Badge

>

    

</Row

>

  

)

;

}

 

export

 

default

 

function

 

Demo

(

)

 

{

  

const

 

[

rating

,

setRating

]

 

=

 

useState

(

3

)

;

 

  

return

 

(

    

<Block

 

gap

=

"lg"

>

      

<Block

 

gap

=

"xs"

>

        

<Text

 

size

=

"sm"

 

colorVariant

=

"muted"

>

No

value prop — the hook keeps the rating in internal state

.

</Text

>

        

<StarPicker

 

defaultValue

=

{

2

}

 

/>

      

</Block

>

 

      

<Block

 

gap

=

"xs"

>

        

<Text

 

size

=

"sm"

 

colorVariant

=

"muted"

>

A

value prop — the parent owns the rating

,

so it can drive it too

.

</Text

>

        

<StarPicker

 

value

=

{

rating

}

 

onChange

=

{

setRating

}

 

/>

        

<Row

 

gap

=

"sm"

 

wrap

=

"wrap"

>

          

<Button

 

size

=

"sm"

 

variant

=

"outline"

 

onPress

=

{

(

)

 

=>

 

setRating

(

5

)

}

>

Set

 

5

 

from

the parent

</Button

>

          

<Button

 

size

=

"sm"

 

variant

=

"ghost"

 

onPress

=

{

(

)

 

=>

 

setRating

(

0

)

}

>

Clear

</Button

>

        

</Row

>

      

</Block

>

    

</Block

>

  

)

;

}

Platform Blocks

A comprehensive React Native design system with components that work seamlessly across iOS, Android, and Web.