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