Shadcn Hooks

useControllableValue

A hook to manage a controllable value

Loading...

Installation

npx shadcn@latest add @hooks/use-controllable-value
pnpm dlx shadcn@latest add @hooks/use-controllable-value
yarn dlx shadcn@latest add @hooks/use-controllable-value
bun x shadcn@latest add @hooks/use-controllable-value

Copy and paste the following code into your project.

use-memoized-fn.ts
import { useMemo, useRef } from 'react'

type noop = (this: any, ...args: any[]) => any

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>

export function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn)

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo<T>(() => fn, [fn])

  const memoizedFn = useRef<PickFunction<T>>(undefined)

  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args)
    }
  }

  return memoizedFn.current
}

use-update.ts
import { useCallback, useState } from 'react'

export function useUpdate() {
  const [, setState] = useState({})

  return useCallback(() => setState({}), [])
}

use-controllable-value.ts
import { isFunction } from 'es-toolkit/predicate'
import { useMemo, useRef } from 'react'
import { useMemoizedFn } from '@/registry/hooks/use-memoized-fn'
import { useUpdate } from '@/registry/hooks/use-update'
import type { SetStateAction } from 'react'

export interface Options<T> {
  defaultValue?: T
  defaultValuePropName?: string
  valuePropName?: string
  trigger?: string
}

export type Props = Record<string, any>

export interface StandardProps<T> {
  value: T
  defaultValue?: T
  onChange: (val: T) => void
}

function useControllableValue<T = any>(
  props: StandardProps<T>,
): [T, (v: SetStateAction<T>) => void]
function useControllableValue<T = any>(
  props?: Props,
  options?: Options<T>,
): [T, (v: SetStateAction<T>, ...args: any[]) => void]
function useControllableValue<T = any>(
  defaultProps?: Props,
  options: Options<T> = {},
) {
  const props = defaultProps ?? {}

  const {
    defaultValue,
    defaultValuePropName = 'defaultValue',
    valuePropName = 'value',
    trigger = 'onChange',
  } = options

  const value = props[valuePropName] as T
  const isControlled = Object.prototype.hasOwnProperty.call(
    props,
    valuePropName,
  )

  const initialValue = useMemo(() => {
    if (isControlled) {
      return value
    }
    if (Object.prototype.hasOwnProperty.call(props, defaultValuePropName)) {
      return props[defaultValuePropName]
    }
    return defaultValue
  }, [])

  const stateRef = useRef(initialValue)
  if (isControlled) {
    stateRef.current = value
  }

  const update = useUpdate()

  const setState = (v: SetStateAction<T>, ...args: any[]) => {
    const r = isFunction(v) ? v(stateRef.current) : v

    if (!isControlled) {
      stateRef.current = r
      update()
    }
    if (props[trigger]) {
      props[trigger](r, ...args)
    }
  }

  return [stateRef.current, useMemoizedFn(setState)] as const
}

export { useControllableValue }

API

export interface Options<T> {
  defaultValue?: T
  defaultValuePropName?: string
  valuePropName?: string
  trigger?: string
}

export type Props = Record<string, any>

export interface StandardProps<T> {
  value: T
  defaultValue?: T
  onChange: (val: T) => void
}

/**
 * A hook to manage a controllable value
 * @param props - The props object containing value, defaultValue, and onChange
 */
function useControllableValue<T = any>(
  props: StandardProps<T>,
): [T, (v: SetStateAction<T>) => void]
function useControllableValue<T = any>(
  props?: Props,
  options?: Options<T>,
): [T, (v: SetStateAction<T>, ...args: any[]) => void]

Credits

Last updated on