Skip to content

Commit

Permalink
refactor: replace private Radix UI hook with public useControllableState
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Apr 9, 2024
1 parent 6446e49 commit 3b01ddd
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 21 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-use-controllable-state": "^1.0.1",
"@t3-oss/env-nextjs": "^0.9.2",
"@uploadthing/react": "^6.4.1",
"class-variance-authority": "^0.7.0",
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/file-uploader-primitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import * as React from "react"
import Image from "next/image"
import { Cross2Icon, UploadIcon } from "@radix-ui/react-icons"
import { useControllableState } from "@radix-ui/react-use-controllable-state"
import { cva, type VariantProps } from "class-variance-authority"
import {
useDropzone,
Expand All @@ -14,6 +13,7 @@ import {
import { toast } from "sonner"

import { cn, formatBytes } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Progress } from "@/components/ui/progress"
Expand Down
2 changes: 1 addition & 1 deletion src/components/file-uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import * as React from "react"
import Image from "next/image"
import { Cross2Icon, UploadIcon } from "@radix-ui/react-icons"
import { useControllableState } from "@radix-ui/react-use-controllable-state"
import Dropzone, {
type DropzoneProps,
type FileRejection,
} from "react-dropzone"
import { toast } from "sonner"

import { cn, formatBytes } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
import { ScrollArea } from "@/components/ui/scroll-area"
Expand Down
27 changes: 27 additions & 0 deletions src/hooks/use-callback-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react"

/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx
*/

/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef<T extends (...args: never[]) => unknown>(
callback: T | undefined
): T {
const callbackRef = React.useRef(callback)

React.useEffect(() => {
callbackRef.current = callback
})

// https://github.com/facebook/react/issues/19240
return React.useMemo(
() => ((...args) => callbackRef.current?.(...args)) as T,
[]
)
}

export { useCallbackRef }
67 changes: 67 additions & 0 deletions src/hooks/use-controllable-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from "react"

import { useCallbackRef } from "@/hooks/use-callback-ref"

/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
*/

type UseControllableStateParams<T> = {
prop?: T | undefined
defaultProp?: T | undefined
onChange?: (state: T) => void
}

type SetStateFn<T> = (prevState?: T) => T

function useControllableState<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableStateParams<T>) {
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
defaultProp,
onChange,
})
const isControlled = prop !== undefined
const value = isControlled ? prop : uncontrolledProp
const handleChange = useCallbackRef(onChange)

const setValue: React.Dispatch<React.SetStateAction<T | undefined>> =
React.useCallback(
(nextValue) => {
if (isControlled) {
const setter = nextValue as SetStateFn<T>
const value =
typeof nextValue === "function" ? setter(prop) : nextValue
if (value !== prop) handleChange(value as T)
} else {
setUncontrolledProp(nextValue)
}
},
[isControlled, prop, setUncontrolledProp, handleChange]
)

return [value, setValue] as const
}

function useUncontrolledState<T>({
defaultProp,
onChange,
}: Omit<UseControllableStateParams<T>, "prop">) {
const uncontrolledState = React.useState<T | undefined>(defaultProp)
const [value] = uncontrolledState
const prevValueRef = React.useRef(value)
const handleChange = useCallbackRef(onChange)

React.useEffect(() => {
if (prevValueRef.current !== value) {
handleChange(value as T)
prevValueRef.current = value
}
}, [value, prevValueRef, handleChange])

return uncontrolledState
}

export { useControllableState }
15 changes: 0 additions & 15 deletions src/hooks/use-debounce.ts

This file was deleted.

0 comments on commit 3b01ddd

Please sign in to comment.