@liveblocks/react-ui
Version:
A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.
1 lines • 7.12 kB
Source Map (JSON)
{"version":3,"file":"use-controllable-state.cjs","sources":["../../src/utils/use-controllable-state.ts"],"sourcesContent":["import { console } from \"@liveblocks/core\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useRerender } from \"./use-rerender\";\n\n/**\n * Hold a state in a \"controlled\" or \"uncontrolled\" way.\n */\nexport function useControllableState<T>(\n /**\n * The default uncontrolled value.\n */\n defaultValue: T,\n\n /**\n * The controlled value.\n *\n * If `undefined`, the returned value is uncontrolled.\n * If set, this controlled value is used and returned as is.\n */\n value: T | undefined,\n\n /**\n * The event handler called when the value changes.\n */\n onChange: ((value: T) => void) | undefined\n) {\n const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n const isControlled = value !== undefined;\n const wasControlled = useRef(isControlled);\n\n useEffect(() => {\n if (\n process.env.NODE_ENV !== \"production\" &&\n wasControlled.current !== isControlled\n ) {\n console.warn(\n `A component is changing from ${\n wasControlled.current ? \"controlled\" : \"uncontrolled\"\n } to ${isControlled ? \"controlled\" : \"uncontrolled\"}.`\n );\n }\n\n wasControlled.current = isControlled;\n }, [isControlled]);\n\n const currentValue = isControlled ? value : uncontrolledValue;\n\n const setValue = useCallback(\n (value: T) => {\n if (isControlled) {\n return onChange?.(value);\n } else {\n setUncontrolledValue(value);\n\n return onChange?.(value);\n }\n },\n [isControlled, onChange]\n );\n\n return [currentValue, setValue] as const;\n}\n\n/**\n * @experimental\n *\n * Hold a value in a \"semi-controlled\" way: a controlled value that can be\n * overridden by uncontrolled changes in a \"most recent wins\" way.\n *\n * @example\n *\n * A `Collapsible` component uses `useSemiControllableState` to control\n * its \"open\" state, it accepts two optional props: `open` and `onOpenChange`.\n *\n * Internally, it passes them to `useSemiControllableState`:\n *\n * ```tsx\n * const [isOpen, setIsOpen] = useSemiControllableState(\n * open ?? true, // Defaults to `true` if `open` is not provided\n * onOpenChange\n * );\n *\n * // ... `isOpen` and `setIsOpen` are used in the component's implementation ...\n * ```\n *\n * And finally here's how it could be used in a \"semi-controlled\" way:\n *\n * ```tsx\n * const status: `\"loading\" | \"success\" | \"error\"`;\n *\n * <Collapsible open={status === \"success\"} />\n * ```\n *\n * Like with a traditional controlled value, the collapsible will start closed\n * and will automatically open when `status` becomes `\"success\"`.\n *\n * But unlike with a traditional controlled value, the collapsible can still\n * open/close when it's clicked on by the user, overriding `open={status === \"success\"}`.\n *\n * It's possible to use it as a traditional uncontrolled value:\n *\n * ```tsx\n * const defaultOpen = false;\n *\n * <Collapsible open={defaultOpen} />\n * ```\n *\n * Or to sync the uncontrolled value like with a traditional uncontrolled value:\n *\n * ```tsx\n * const [isOpen, setIsOpen] = useState(false);\n *\n * // `isOpen` is synced with the uncontrolled value when the user clicks\n * <Collapsible open={isOpen} onOpenChange={setIsOpen} />\n * ```\n *\n * But with the caveat that it will still be possible to change the\n * uncontrolled value:\n *\n * ```tsx\n * const open = false;\n *\n * // Clicking on the collapsible will still open/close it, unlike with a\n * // traditional controlled value.\n * <Collapsible open={open} />\n * ```\n */\nexport function useSemiControllableState<T>(\n /**\n * The controlled value.\n *\n * When this value changes, it becomes the current value.\n * But unlike a traditional controlled value, it can be overridden by\n * uncontrolled changes.\n */\n value: T,\n\n /**\n * The event handler called when the uncontrolled value changes.\n */\n onChange: ((value: T) => void) | undefined\n) {\n const [uncontrolledValue, setUncontrolledValue] = useState(value);\n const lastChange = useRef<\"uncontrolled\" | \"controlled\">(\"controlled\");\n const lastValue = useRef(value);\n const [rerender] = useRerender();\n\n // Listen to `value` changes during the render phase to avoid\n // having to always sync `uncontrolledValue` on every change.\n if (!Object.is(lastValue.current, value)) {\n lastValue.current = value;\n lastChange.current = \"controlled\";\n }\n\n const setValue = useCallback(\n (value: T) => {\n lastChange.current = \"uncontrolled\";\n setUncontrolledValue(value);\n\n // If the new `uncontrolledValue` is the same as last time it was the \"last change\",\n // `setUncontrolledValue` won't trigger a re-render, but the fact that it's becoming\n // uncontrolled again is a change that needs a re-render.\n rerender();\n\n onChange?.(value);\n },\n [onChange, rerender]\n );\n\n const currentValue =\n lastChange.current === \"uncontrolled\" ? uncontrolledValue : value;\n\n return [currentValue, setValue] as const;\n}\n"],"names":["useState","useRef","useEffect","console","useCallback","value","useRerender"],"mappings":";;;;;;AAQgB,SAAA,oBAAA,CAId,YAQA,EAAA,KAAA,EAKA,QACA,EAAA;AACA,EAAA,MAAM,CAAC,iBAAA,EAAmB,oBAAoB,CAAA,GAAIA,eAAS,YAAY,CAAA,CAAA;AACvE,EAAA,MAAM,eAAe,KAAU,KAAA,KAAA,CAAA,CAAA;AAC/B,EAAM,MAAA,aAAA,GAAgBC,aAAO,YAAY,CAAA,CAAA;AAEzC,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IACE,QAAQ,GAAI,CAAA,QAAA,KAAa,YACzB,IAAA,aAAA,CAAc,YAAY,YAC1B,EAAA;AACA,MAAQC,YAAA,CAAA,IAAA;AAAA,QACN,gCACE,aAAc,CAAA,OAAA,GAAU,YAAe,GAAA,cAAA,CAAA,IAAA,EAClC,eAAe,YAAe,GAAA,cAAA,CAAA,CAAA,CAAA;AAAA,OACvC,CAAA;AAAA,KACF;AAEA,IAAA,aAAA,CAAc,OAAU,GAAA,YAAA,CAAA;AAAA,GAC1B,EAAG,CAAC,YAAY,CAAC,CAAA,CAAA;AAEjB,EAAM,MAAA,YAAA,GAAe,eAAe,KAAQ,GAAA,iBAAA,CAAA;AAE5C,EAAA,MAAM,QAAW,GAAAC,iBAAA;AAAA,IACf,CAACC,MAAa,KAAA;AACZ,MAAA,IAAI,YAAc,EAAA;AAChB,QAAA,OAAO,WAAWA,MAAK,CAAA,CAAA;AAAA,OAClB,MAAA;AACL,QAAA,oBAAA,CAAqBA,MAAK,CAAA,CAAA;AAE1B,QAAA,OAAO,WAAWA,MAAK,CAAA,CAAA;AAAA,OACzB;AAAA,KACF;AAAA,IACA,CAAC,cAAc,QAAQ,CAAA;AAAA,GACzB,CAAA;AAEA,EAAO,OAAA,CAAC,cAAc,QAAQ,CAAA,CAAA;AAChC,CAAA;AAkEgB,SAAA,wBAAA,CAQd,OAKA,QACA,EAAA;AACA,EAAA,MAAM,CAAC,iBAAA,EAAmB,oBAAoB,CAAA,GAAIL,eAAS,KAAK,CAAA,CAAA;AAChE,EAAM,MAAA,UAAA,GAAaC,aAAsC,YAAY,CAAA,CAAA;AACrE,EAAM,MAAA,SAAA,GAAYA,aAAO,KAAK,CAAA,CAAA;AAC9B,EAAM,MAAA,CAAC,QAAQ,CAAA,GAAIK,uBAAY,EAAA,CAAA;AAI/B,EAAA,IAAI,CAAC,MAAO,CAAA,EAAA,CAAG,SAAU,CAAA,OAAA,EAAS,KAAK,CAAG,EAAA;AACxC,IAAA,SAAA,CAAU,OAAU,GAAA,KAAA,CAAA;AACpB,IAAA,UAAA,CAAW,OAAU,GAAA,YAAA,CAAA;AAAA,GACvB;AAEA,EAAA,MAAM,QAAW,GAAAF,iBAAA;AAAA,IACf,CAACC,MAAa,KAAA;AACZ,MAAA,UAAA,CAAW,OAAU,GAAA,cAAA,CAAA;AACrB,MAAA,oBAAA,CAAqBA,MAAK,CAAA,CAAA;AAK1B,MAAS,QAAA,EAAA,CAAA;AAET,MAAA,QAAA,GAAWA,MAAK,CAAA,CAAA;AAAA,KAClB;AAAA,IACA,CAAC,UAAU,QAAQ,CAAA;AAAA,GACrB,CAAA;AAEA,EAAA,MAAM,YACJ,GAAA,UAAA,CAAW,OAAY,KAAA,cAAA,GAAiB,iBAAoB,GAAA,KAAA,CAAA;AAE9D,EAAO,OAAA,CAAC,cAAc,QAAQ,CAAA,CAAA;AAChC;;;;;"}