{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-controllable-state", "title": "useControllableState", "author": "tusharvarshney ", "files": [ { "path": "src/registry/hooks/use-controllable-state/use-controllable-state.tsx", "content": "// Thanks @radix-ui\n\nimport * as React from \"react\"\n\n// use-layout-effect.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}\n\n// use-controllable-state.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n (React as never)[\" useInsertionEffect \".trim().toString()] || useLayoutEffect\n\ntype ChangeHandler = (state: T) => void\ntype SetStateFn = React.Dispatch>\n\ninterface UseControllableStateParams {\n prop?: T | undefined\n defaultProp: T\n onChange?: ChangeHandler\n caller?: string\n}\n\nexport function useControllableState({\n prop,\n defaultProp,\n onChange = () => {},\n caller,\n}: UseControllableStateParams): [T, SetStateFn] {\n const [uncontrolledProp, setUncontrolledProp, onChangeRef] =\n useUncontrolledState({\n defaultProp,\n onChange,\n })\n const isControlled = prop !== undefined\n const value = isControlled ? prop : uncontrolledProp\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== \"production\") {\n const isControlledRef = React.useRef(prop !== undefined)\n React.useEffect(() => {\n const wasControlled = isControlledRef.current\n if (wasControlled !== isControlled) {\n const from = wasControlled ? \"controlled\" : \"uncontrolled\"\n const to = isControlled ? \"controlled\" : \"uncontrolled\"\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n )\n }\n isControlledRef.current = isControlled\n }, [isControlled, caller])\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n const setValue = React.useCallback>(\n (nextValue) => {\n if (isControlled) {\n const value = isFunction(nextValue) ? nextValue(prop) : nextValue\n if (value !== prop) {\n onChangeRef.current?.(value)\n }\n } else {\n setUncontrolledProp(nextValue)\n }\n },\n [isControlled, prop, setUncontrolledProp, onChangeRef]\n )\n\n return [value, setValue]\n}\n\nfunction useUncontrolledState({\n defaultProp,\n onChange,\n}: Omit, \"prop\">): [\n Value: T,\n setValue: React.Dispatch>,\n OnChangeRef: React.RefObject | undefined>,\n] {\n const [value, setValue] = React.useState(defaultProp)\n const prevValueRef = React.useRef(value)\n\n const onChangeRef = React.useRef(onChange)\n useInsertionEffect(() => {\n onChangeRef.current = onChange\n }, [onChange])\n\n React.useEffect(() => {\n if (prevValueRef.current !== value) {\n onChangeRef.current?.(value)\n prevValueRef.current = value\n }\n }, [value, prevValueRef])\n\n return [value, setValue, onChangeRef]\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n return typeof value === \"function\"\n}\n", "type": "registry:hook", "target": "@hooks/use-controllable-state.tsx" } ], "type": "registry:hook" }