-
Notifications
You must be signed in to change notification settings - Fork 381
feat: composable primitive components #947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: future-uploadFiles
Are you sure you want to change the base?
feat: composable primitive components #947
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
I like how this is looking! I would probably add some basic styling to the examples, to at least identify the component, since right now, it kind of just looks like the page is broken, rather than there is another dropzone:
I think this is plenty in the examples, i'd potentially want to add an example
Tentatively I would say it doesn't need to be split for now, but cc @juliusmarminge -- thoughts? |
Kinda feels like it should be split? Should our default component use these primitives? A lot of code duplication if not? |
I guess not, perhaps a leading section with the level of theming possible with links to respective section could be sufficient |
WalkthroughThis update introduces a new set of unstyled "primitive" upload components for React, including Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Root as UT.Root
participant Dropzone as UT.Dropzone
participant Button as UT.Button
participant AllowedContent as UT.AllowedContent
User->>Dropzone: Drag and drop or select file
Dropzone->>Root: Notify files selected
Root->>Button: Enable upload action
User->>Button: Click to start upload
Button->>Root: Trigger upload process
Root->>Dropzone: Update progress/state
Root->>AllowedContent: Show allowed file types
Root->>User: Notify on upload completion
Possibly related PRs
Suggested reviewers
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
Scope: all 3 workspace projects This error happened while installing a direct dependency of /tmp/eslint/packages/uploadthing Packages found in the workspace: ✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
I'm currently refactoring the prebuilt components to use the primitives. For the dropzone, you can get the Exmaple: <Primitive.Dropzone
className={cn(
"data-[drag]:bg-blue-600/10",
className,
// isDragActive isn't defined here
styleFieldToClassName(appearance?.container, { isDragActive }),
)}
// or here
style={styleFieldToCssObject(appearance?.container, { isDragActive )}
>
{({ dropzone: { isDragActive } }) => /* we have it here */}
</Primitive.Dropzone> Should the <Primitive.Dropzone className={({ dropzone: { isDragActive }) => "your class"} /> This feels a bit hacky imo as it still means you can't use <Primitive.Dropzone
dropzone={dropzone}
onDropzoneChange={setDropzone}
className={`${dropzone.isDragActive ? "bg-blue-500/10" : ""`}
/> |
We hope to be able to get this in a minor release, meaning no breaking changes so Can we not just wrap in another div or something? <Primitive.Dropzone>
{({ dropzone: { isDragActive } }) => (
<div
className={cn(
"data-[drag]:bg-blue-600/10",
className,
// isDragActive isn't defined here
styleFieldToClassName(appearance?.container, { isDragActive }),
)}
// or here
style={styleFieldToCssObject(appearance?.container, { isDragActive )}
>
...
</div>
</Primitive.Dropzone> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (3)
packages/react/src/components/index.tsx (1)
63-81
: Add JSDoc documentation for the new function.Consider adding JSDoc documentation to explain the purpose, usage, and return value of
generateUploadPrimitives
. This would help developers understand how to use the composable primitive components effectively.Example documentation:
/** * Generates unstyled composable upload components that can be customized. * @param opts - Optional configuration options for the upload components * @returns An object containing primitive components: Root, Dropzone, Button, and AllowedContent * @example * const UT = generateUploadPrimitives(); * * function Upload() { * return ( * <UT.Root endpoint="imageUploader"> * <UT.Dropzone> * {({ isUploading }) => ( * <UT.Button>Upload a file</UT.Button> * )} * </UT.Dropzone> * </UT.Root> * ); * } */docs/src/app/(docs)/concepts/theming/page.mdx (2)
22-23
: Fix grammatical issues in the introduction.There are a couple of grammatical issues that should be addressed:
- "These comes" should be "These come" (subject-verb agreement)
- Add a comma before "but" as it connects two independent clauses
-These comes with behavior built-in but you have full control over what's rendered when and where. +These come with behavior built-in, but you have full control over what's rendered when and where.🧰 Tools
🪛 LanguageTool
[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...(SINGULAR_VERB_AFTER_THESE_OR_THOSE)
[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...(COMMA_COMPOUND_SENTENCE)
527-612
: Consider enhancing the Unstyled Primitive Components documentation.While the current documentation provides a good foundation, consider adding:
- A complete list of available state properties that can be accessed in children functions
- Examples of manual mode usage for both Button and Dropzone
- Examples of using the ClearButton component
- TypeScript examples showing proper type inference
Would you like me to help generate these additional documentation sections?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (2)
- docs/src/app/(docs)/concepts/theming/page.mdx (2 hunks)
- packages/react/src/components/index.tsx (2 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/src/app/(docs)/concepts/theming/page.mdx
[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...(SINGULAR_VERB_AFTER_THESE_OR_THOSE)
[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...(COMMA_COMPOUND_SENTENCE)
[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...(NOT_ONLY_ALSO)
🔇 Additional comments (3)
packages/react/src/components/index.tsx (2)
17-18
: LGTM! Clean import statements for new primitive components.The imports are well-structured and follow TypeScript best practices.
63-81
: Verify the primitive components implementation.Let's ensure all the primitive components mentioned in the PR objectives are properly implemented.
✅ Verification successful
All primitive components are properly implemented
The verification confirms that all required primitive components are properly implemented in the codebase:
Root
component (root.tsx
): Implements the core upload functionality, context provider, and state managementDropzone
component (dropzone.tsx
): Implements drag-and-drop functionality with proper file validationButton
component (button.tsx
): Implements file selection and upload trigger functionalityAllowedContent
component (allowed-content.tsx
): Implements content display for allowed file typesEach component follows best practices with proper TypeScript typing, client-side directive, and accessibility considerations. The implementation matches the PR objectives and maintains consistency across the component suite.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify the existence and implementation of primitive components # Test 1: Check if all primitive components exist echo "Checking for primitive component files..." fd --type f --glob "*.tsx" --glob "*.ts" . packages/react/src/components/primitive # Test 2: Verify the exports from primitive/index echo -e "\nChecking primitive exports..." rg --type ts "export.*from" packages/react/src/components/primitive/index.ts # Test 3: Look for component implementations echo -e "\nChecking component implementations..." for component in Root Dropzone Button AllowedContent; do echo -e "\nLooking for $component implementation..." ast-grep --pattern "export function $component" --pattern "export const $component" doneLength of output: 8324
Script:
#!/bin/bash # Let's verify each primitive component individually # Check Root component implementation echo "Checking Root component..." ast-grep --pattern 'export const Root = ' echo -e "\nChecking Dropzone component..." ast-grep --pattern 'export const Dropzone = ' echo -e "\nChecking Button component..." ast-grep --pattern 'export const Button = ' echo -e "\nChecking AllowedContent component..." ast-grep --pattern 'export const AllowedContent = ' # Check exports from primitive/index.ts echo -e "\nChecking primitive/index.ts contents..." cat packages/react/src/components/primitive/index.ts # Check individual component files echo -e "\nChecking individual component implementations..." for file in root.tsx dropzone.tsx button.tsx allowed-content.tsx; do echo -e "\nContents of packages/react/src/components/primitive/$file:" cat "packages/react/src/components/primitive/$file" doneLength of output: 24707
docs/src/app/(docs)/concepts/theming/page.mdx (1)
585-602
: Update Dropzone example for accuracy and completeness.The example needs several improvements:
- Remove
as
prop usage since it's not implemented yet (as mentioned in PR objectives)- Add error handling example
- Demonstrate more state properties
<UT.Root endpoint="mockRoute"> <UT.Dropzone> - {({ state }) => ( + {({ state, files, error }) => ( <div> <p>Drag and drop</p> + {error && <p style={{ color: 'red' }}>{error.message}</p>} + {files.length > 0 && ( + <p>Selected files: {files.map(f => f.name).join(', ')}</p> + )} - <UT.Button as="button"> + <UT.Button> {state === "uploading" ? "Uploading" : "Upload file"} </UT.Button> <UT.AllowedContent - as="p" style={{ fontSize: 12, width: "fit-content" }} /> </div> )} </UT.Dropzone> </UT.Root>
Just played with this a bit and I love this - found some issues with pasting files. I'll try and have a look at fixing that when i got some time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Outside diff range and nitpick comments (14)
playground/lib/uploadthing.ts (2)
9-11
: Add JSDoc comments to document the exported entitiesThe exports look well-structured and type-safe, but they would benefit from JSDoc comments explaining their purpose and usage patterns, especially since these are foundational primitives.
Here's a suggested improvement:
+/** + * Primitive components for building custom upload UI + * @example + * const MyUploader = () => ( + * <UT.Root> + * <UT.Dropzone> + * {({ isUploading }) => <div>{isUploading ? 'Uploading...' : 'Drop files'}</div>} + * </UT.Dropzone> + * </UT.Root> + * ); + */ export const UT = generateUploadPrimitives<UploadRouter>(); + +/** Pre-built button component for quick implementation */ export const UTButton = generateUploadButton<UploadRouter>(); + +/** Hook for custom upload implementations */ export const { useUploadThing } = generateReactHelpers<UploadRouter>();
9-9
: Consider a more explicit name for primitive componentsWhile
UT
is concise, a more explicit name likeUploadPrimitives
might better convey its purpose, especially for new developers encountering the codebase.playground/app/primitives/page.tsx (1)
26-38
: Consider extracting the dropzone UI into a separate componentThe inline JSX within the dropzone render prop is becoming complex. Consider extracting it into a separate component for better maintainability and reusability.
type DropzoneContentProps = { dropzone: { isDragActive: boolean }; state: string; uploadProgress: number; }; function DropzoneContent({ dropzone, state, uploadProgress }: DropzoneContentProps) { return ( <div data-dragactive={dropzone?.isDragActive} className="flex flex-col items-center rounded-md border-2 border-dashed border-zinc-200 p-4 data-[dragactive=true]:border-zinc-400" > <Subheading>Drag and drop</Subheading> <UT.Button as={Button} color="red" className="mt-2 px-4 py-2"> {state === "uploading" ? "Uploading" : "Upload file"} {!!uploadProgress && ` ${uploadProgress}%`} </UT.Button> <UT.AllowedContent as={Code} className="mt-2" /> </div> ); }playground/components/text.tsx (5)
1-6
: LGTM! Consider adding readonly modifier for better type safety.The imports and type definitions are well-structured. For additional type safety, consider making the level union type readonly:
type HeadingProps = { - level?: 1 | 2 | 3 | 4 | 5 | 6; + level?: readonly [1, 2, 3, 4, 5, 6][number]; }
8-20
: LGTM! Consider adding aria-level for accessibility.The component implementation is solid with proper type safety and responsive design.
<Element {...props} + aria-level={level} className={cx( "text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8", className, )} />
36-44
: LGTM! Document the purpose of data-slot attribute.The implementation is clean and follows best practices. Consider adding JSDoc to explain the purpose of the data-slot attribute.
+/** + * Text component that renders a paragraph with consistent styling. + * @param data-slot - Used for component targeting in the design system + */ export function Text({ className, ...props }: React.ComponentProps<"p">) {
46-59
: Consider using :hover pseudo-class for better accessibility.The component correctly wraps Next.js Link, but the data-hover approach might not work with all input devices.
<Link {...props} className={cx( - "text-zinc-950 underline decoration-zinc-950/50 data-[hover]:decoration-zinc-950", + "text-zinc-950 underline decoration-zinc-950/50 hover:decoration-zinc-950 focus-visible:decoration-zinc-950", className, )} />
73-83
: Consider adding semantic improvements for code elements.The styling provides good visual distinction, but could benefit from additional semantic attributes.
<code {...props} + translate="no" + role="code" className={cx( "rounded border border-zinc-950/10 bg-zinc-950/[2.5%] px-0.5 text-sm font-medium text-zinc-950 sm:text-[0.8125rem]", className, )} />The
translate="no"
attribute prevents translation tools from modifying code content, androle="code"
improves semantic meaning for screen readers.playground/components/uploader.tsx (4)
60-61
: Consider adding endpoint validationThe endpoint callback is using a generic
anything
type without validation. Consider adding type safety and validation for the endpoint configuration.-endpoint={(rr) => rr.anything} -input={{}} +endpoint={(rr) => { + if (!rr.anything) { + throw new Error("Endpoint configuration is invalid"); + } + return rr.anything; +}} +input={{ /* Add input validation schema here */ }}
73-76
: Review CSS specificity overridesThe appearance props use
!
to override styles. This might make it harder to maintain and customize styles in the future.Consider using a more maintainable approach:
-appearance={{ - button: "!text-sm/6", - allowedContent: "!h-0", -}} +appearance={{ + button: "text-sm leading-6", + allowedContent: "h-0", +}}
62-67
: Enhance error handling and success feedbackThe current implementation uses basic window.alert for errors and only refreshes the page on success. Consider implementing a more user-friendly feedback mechanism.
onUploadError={(error) => { - window.alert(error.message); + // Consider using a toast notification or error boundary + console.error('Upload failed:', error); + // Add user-friendly error display }} onClientUploadComplete={() => { + // Consider adding success feedback before refresh router.refresh(); }}
55-80
: Consider extracting upload configurationThe component mixes presentation with upload configuration. Consider extracting the upload configuration into a separate configuration object or custom hook for better maintainability.
Example approach:
// useUploadConfig.ts export const useUploadConfig = () => { const router = useRouter(); return { endpoint: (rr) => rr.anything, onError: (error) => { /* error handling */ }, onSuccess: () => router.refresh(), // ... other config }; }; // In component: const uploadConfig = useUploadConfig(); <UTButton {...uploadConfig} />playground/lib/actions.ts (1)
84-86
: Consider extracting the expiration time constant and documenting the duration.While adding URL expiration is a good security practice, the magic number calculation could be more readable and maintainable.
Consider applying this change:
- const { url } = await utapi.getSignedURL(key, { - expiresIn: 60 * 60 * 24 * 7, - }); + // URL expires in 7 days + const SIGNED_URL_EXPIRATION = 60 * 60 * 24 * 7; // seconds + const { url } = await utapi.getSignedURL(key, { + expiresIn: SIGNED_URL_EXPIRATION, + });playground/app/layout.tsx (1)
70-73
: Enhance navigation accessibility and user experience.Consider the following improvements:
- Add
aria-label
to the nav element for better screen reader support- Add visual indication for the current active route
- Include hover states for links
- <nav className="flex gap-4 bg-zinc-100 p-4"> + <nav + aria-label="Main navigation" + className="flex gap-4 bg-zinc-100 p-4" + > - <Link href="/">Home</Link> - <Link href="/primitives">Primitives</Link> + <Link + href="/" + className="hover:underline" + > + Home + </Link> + <Link + href="/primitives" + className="hover:underline" + > + Primitives + </Link> </nav>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (7)
package.json
(1 hunks)playground/app/layout.tsx
(2 hunks)playground/app/primitives/page.tsx
(1 hunks)playground/components/text.tsx
(1 hunks)playground/components/uploader.tsx
(2 hunks)playground/lib/actions.ts
(1 hunks)playground/lib/uploadthing.ts
(1 hunks)
🔇 Additional comments (13)
playground/lib/uploadthing.ts (2)
9-11
: LGTM! Type-safe implementation
The consistent use of the UploadRouter
type parameter ensures type safety across all exported entities. This will provide excellent TypeScript support for consumers of these primitives.
1-7
: Consider making the import path more resilient
The relative import path might be brittle if the file structure changes. Consider using path aliases (e.g., @/app/api/uploadthing/route
) to make imports more maintainable.
Let's check for potential circular dependencies and verify the route file location:
playground/app/primitives/page.tsx (3)
1-7
: LGTM: Appropriate setup for client-side component
The imports and client-side directive are correctly configured for an interactive file upload component.
44-50
: LGTM: Clean and focused page component
The Page component follows the single responsibility principle and provides appropriate layout spacing.
11-12
: Consider restricting the upload endpoint and configuring input parameters
The current endpoint configuration allows "anything" which might be too permissive. Consider restricting to specific file types or sizes based on your requirements.
playground/components/text.tsx (1)
61-71
: Verify text-foreground class in Tailwind configuration.
The implementation is clean, but let's ensure the text-foreground utility class is properly configured.
✅ Verification successful
The output shows that foreground
is properly defined in the Tailwind configuration as "hsl(var(--foreground))"
, which means the text-foreground
utility class is correctly configured and available for use in the Strong component.
The text-foreground utility class is properly configured in the Tailwind theme.
The class is defined using CSS variables and HSL color format, following a consistent pattern used across other color utilities in the theme configuration.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if text-foreground is defined in Tailwind config
fd -e js -e ts -e json -g "*tailwind*config*" | xargs cat | grep -A 5 "foreground"
Length of output: 1125
package.json (1)
29-29
: LGTM! Script addition follows established patterns.
The new dev:playground
script follows the existing pattern of development scripts and will help in testing the new primitive components.
Let's verify the playground setup:
✅ Verification successful
The playground setup is properly configured and integrated
The verification confirms:
- The playground directory exists and contains the expected Next.js files (layout.tsx and page.tsx)
- The new primitive components are properly imported and used in the playground, specifically in
playground/app/primitives/page.tsx
- The setup aligns with other examples in the repository and correctly implements the new UT components
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify the next-playground directory exists and contains necessary files
# Check if next-playground directory exists in examples
fd -t d "next-playground" examples/
# Check for typical Next.js files in the playground
fd -t f "^(page|layout)\.(tsx|js|jsx)$" -E "*.test.*" -E "*.spec.*"
# Check if the new primitive components are imported
rg -l "generateUploadPrimitives|UT\.(Root|Dropzone|Button|AllowedContent)"
Length of output: 1881
playground/components/uploader.tsx (2)
68-72
: LGTM: Composable content implementation
The content prop implementation using render props pattern aligns well with the PR objectives for composable components. This allows for flexible customization while maintaining type safety.
7-7
: Verify the local import path for UTButton
The import has been moved from @uploadthing/react
to a local path. Let's ensure this path is correct and the component exists.
✅ Verification successful
Import path for UTButton is correct and verified
The UTButton
component is correctly exported from playground/lib/uploadthing.ts
, and the relative import path "../lib/uploadthing"
in playground/components/uploader.tsx
is accurate.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if the UTButton export exists in the local path
rg -l "export.*UTButton" "lib/uploadthing"
Length of output: 98
Script:
#!/bin/bash
# Let's first find the correct path by searching for files containing "UTButton"
rg -l "UTButton"
# Also search for any file named "uploadthing" to verify the correct path
fd uploadthing
Length of output: 3341
Script:
#!/bin/bash
# Let's check the content of playground/lib/uploadthing.ts to verify UTButton export
rg "UTButton" "playground/lib/uploadthing.ts" -C 2
Length of output: 243
playground/lib/actions.ts (2)
84-86
: Verify the URL expiration time requirements.
The 7-day expiration time for signed URLs should be validated against:
- Application requirements for URL validity
- Security policies regarding temporary access
- User experience expectations
Consider making this duration configurable through environment variables if different environments need different expiration times.
Let's check if this duration is documented or configurable elsewhere:
✅ Verification successful
7-day expiration time is within the documented limits and follows library constraints
The expiration time of 7 days (60 * 60 * 24 * 7 seconds) is:
- Explicitly enforced by the library which throws an error if expiration exceeds 7 days
- Well documented in the API reference
- Configurable through the
expiresIn
parameter which accepts both numeric seconds or human-readable time strings - Matches the maximum allowed duration (604800 seconds) as per the library's validation
The implementation is using the maximum allowed duration, which is a reasonable default for temporary access URLs. If different durations are needed, they can be configured per request as documented.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for any documentation or configuration related to URL expiration
rg -i "url.*expir|signed.*url" --type md
rg "process\.env.*EXPIR|config.*EXPIR"
Length of output: 290
Script:
#!/bin/bash
# Let's check for any mentions of URL expiration or signed URLs in the codebase
rg -A 5 "getSignedURL|signedUrl|expiresIn"
# Also check for any configuration or constant definitions
ast-grep --pattern 'const $_ = $$$* 60 * 60 * 24'
Length of output: 19204
84-86
: Verify error handling for expired URLs.
Ensure that components consuming these signed URLs handle expiration gracefully. The application should:
- Detect expired URLs
- Provide appropriate user feedback
- Allow easy URL refresh when needed
Let's check how URL expiration errors are handled:
✅ Verification successful
URL expiration is properly handled in the application
The codebase already has appropriate error handling mechanisms in place:
- The uploader component has error handling that displays alerts to users when issues occur (
onUploadError
) - The actions have proper error states and return structured error responses
- The URL expiration time is set to a reasonable 7 days, which is within the allowed limits as verified by the test cases
- Components using these URLs have error states and user feedback mechanisms
The implementation follows best practices for handling URL-related errors, including expired URLs, through the existing error handling infrastructure.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for error handling related to URL expiration
ast-grep --pattern 'catch ($_) {
$$$
}'
# Search for error messages related to URL expiration
rg -i "url.*expired|link.*expired"
Length of output: 80
Script:
#!/bin/bash
# Let's search for any error handling around the getSignedURL usage
rg -A 5 "getSignedURL"
# Also check for any error boundaries or error handling in components that might use these URLs
rg -A 5 "catch|throw" --type typescript
# Search for any error states or loading states in components
ast-grep --pattern 'const [$_] = useState<$_>($$$)'
Length of output: 6974
Script:
#!/bin/bash
# Let's check for error handling in the specific file and its imports
rg -A 5 "catch|throw" playground/lib/actions.ts
# Check for any error states or loading states in components that might use this action
rg -A 5 "actions.ts" playground/
# Look for any error handling in components
rg -A 5 "error|Error" playground/
Length of output: 4241
playground/app/layout.tsx (2)
51-61
: Well-implemented loading state!
The Suspense fallback implementation follows best practices by:
- Maintaining layout stability
- Providing clear visual feedback
- Matching the structure of the loaded content
66-68
: Review security implications of displaying token information.
Exposing the appId
and region
information in the UI might not be necessary in a production environment. Consider:
- Moving this information to environment variables
- Only showing it in development mode
- Or removing it entirely if not needed for debugging
Thanks! In the meantime, should I fix the linting issues? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (7)
packages/react/src/components/primitive/clear-button.tsx (1)
30-35
: Enhance error handling and user experience.Consider adding error handling and user feedback when clearing files:
onClick={(e) => { if (state === "disabled") return; // eslint-disable-next-line @typescript-eslint/no-unsafe-call onClick?.(e); - setFiles([]); + try { + setFiles([]); + } catch (error) { + console.error("Failed to clear files:", error); + // Consider adding an onError prop to handle errors + } }}packages/react/src/index.ts (1)
7-7
: Add usage documentation forgenerateUploadPrimitives
.This new export looks good, aligning well with the composable approach outlined in the PR. However, it's unclear how consumers should utilize this function. Consider adding (or referencing) documentation to explain how it's intended to be used, along with any patterns or best practices you recommend.
Would you like help drafting minimal usage docs or code examples to guide users?
packages/react/src/components/primitive/allowed-content.tsx (1)
20-22
: Consider using a generic ref type to align with theas
propCurrently, the ref type is fixed to
HTMLDivElement
, but the component allows rendering different element types via theas
prop. To ensure accurate type inference, switch to a more generic type:- ref: Ref<HTMLDivElement>, + ref: Ref<React.ElementRef<TTag>>,packages/react/src/components/primitive/root.tsx (2)
230-237
: Simplify state determination logicThe inline function returning the
state
can be simplified while retaining clarity. Consider early returns or consolidated checks for improved readability:
239-258
: Refactor paste handling for better maintainabilityThe inline paste handling is beneficial, but extracting it into a dedicated custom hook (e.g.,
usePasteHandler
) can enhance clarity and potential reuse.packages/react/src/components/primitive/dropzone.tsx (2)
206-207
: Remove or reconsiderevent.persist()
usageFrom React v17 onwards, the synthetic event pool has been removed, making
event.persist()
largely unnecessary. Verify if you still need it, or remove for cleaner code.
371-375
: Evaluate continued support for IE/Edge checkIE11 and legacy Edge are deprecated. If your project no longer supports them, remove the
isIeOrEdge()
check and simplify the file dialog opening logic.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
packages/react/src/components/index.tsx
(2 hunks)packages/react/src/components/primitive/allowed-content.tsx
(1 hunks)packages/react/src/components/primitive/button.tsx
(1 hunks)packages/react/src/components/primitive/clear-button.tsx
(1 hunks)packages/react/src/components/primitive/dropzone.tsx
(1 hunks)packages/react/src/components/primitive/root.tsx
(1 hunks)packages/react/src/index.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/react/src/components/index.tsx
- packages/react/src/components/primitive/button.tsx
🧰 Additional context used
📓 Learnings (1)
packages/react/src/components/primitive/allowed-content.tsx (1)
Learnt from: veloii
PR: pingdotgg/uploadthing#947
File: packages/react/src/components/primitive/allowed-content.tsx:29-32
Timestamp: 2024-11-12T10:36:58.532Z
Learning: For slot components, sometimes it's preferred to manually specify `children` via props to avoid the appearance of nested elements when they aren't actually nested.
🪛 Biome (1.9.4)
packages/react/src/components/primitive/clear-button.tsx
[error] 38-38: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
packages/react/src/components/primitive/allowed-content.tsx
[error] 31-31: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
🔇 Additional comments (6)
packages/react/src/components/primitive/clear-button.tsx (3)
43-50
: LGTM! Well-structured type definitions.The type definitions and component export are well-implemented, providing good type safety and proper display name support.
38-38
: 🛠️ Refactor suggestionUse JSX children pattern instead of children prop.
The static analysis tool correctly flags that children should be passed using JSX syntax rather than as a prop.
-<PrimitiveSlot default="Clear" children={children} /> +<PrimitiveSlot default="Clear">{children}</PrimitiveSlot>Likely invalid or redundant comment.
🧰 Tools
🪛 Biome (1.9.4)
[error] 38-38: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
9-9
: 🛠️ Refactor suggestionConsider using 'button' as the default tag instead of 'label'.
The current implementation uses 'label' as the default tag for the clear button. While this works, it might be more semantically correct and accessible to use a 'button' element by default, as labels are typically used to describe form controls rather than trigger actions.
-const DEFAULT_CLEAR_BUTTON_TAG = "label"; +const DEFAULT_CLEAR_BUTTON_TAG = "button" as const;Likely invalid or redundant comment.
packages/react/src/index.ts (1)
15-15
: Verify consistency of path changes foruseDropzone
.You've updated the import path to
./components/primitive/dropzone
. Ensure that any references to the old path ("./components/dropzone"
) are removed or updated throughout the codebase.Below is a shell script you can run to find any outdated references:
✅ Verification successful
Let me verify if there are any other potential references to the dropzone component or its path.
Let me try a more comprehensive search without file type restrictions.
No outdated references to old dropzone path found
Based on the comprehensive search results, there are no references to the old path
"./components/dropzone"
in the codebase. The only import ofuseDropzone
found is in the documentation example which uses the package import@uploadthing/react
. The path change inpackages/react/src/index.ts
to"./components/primitive/dropzone"
appears to be consistent with the codebase structure.🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Search for all imports of useDropzone from old or new paths rg 'import\s*\{?\s*useDropzone\s*\}?\s*from\s*"[^"]+"'Length of output: 155
Script:
#!/bin/bash # Search for any mentions of dropzone path or component rg -i "dropzone" --type ts --type tsxLength of output: 65
Script:
#!/bin/bash # Search for any mentions of dropzone in the codebase rg -i "dropzone"Length of output: 45781
packages/react/src/components/primitive/allowed-content.tsx (1)
29-32
: Avoid passingchildren
via prop ()Passing
children
via thechildren
prop can be seen as an anti-pattern in React. While you have a valid reason (avoiding nested DOM nodes), consider the standard pattern of passing children as JSX elements for consistency.🧰 Tools
🪛 Biome (1.9.4)
[error] 31-31: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
packages/react/src/components/primitive/root.tsx (1)
189-189
: Ensure compatibility ofglobalThis.fetch
You are using
globalThis.fetch
to make upload requests. If the library is meant to be used in older browsers or non-browser environments, confirm the availability ofglobalThis.fetch
or provide a fallback.
onDrop: onDropCb as any, | ||
} | ||
: {}), | ||
}), | ||
[ | ||
disabled, | ||
onBlur, | ||
onClick, | ||
onDragEnter, | ||
onDragLeave, | ||
onDragOver, | ||
onDropCb, | ||
onFocus, | ||
onKeyDown, | ||
], | ||
); | ||
|
||
const getInputProps = useMemo( | ||
() => (): HTMLProps<HTMLInputElement> => ({ | ||
ref: inputRef, | ||
type: "file", | ||
style: { display: "none" }, | ||
accept: acceptAttr, | ||
multiple, | ||
tabIndex: -1, | ||
...(!disabled | ||
? { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
onChange: onDropCb as any, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eliminate as any
casts in event handlers
Casting to any
bypasses type safety. Instead, match the event signatures by adjusting the function’s argument type definition or refining TypeScript types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
packages/react/src/components/button.tsx (2)
168-170
: Remove redundantcase "disabled"
/"ready"
branches
default
already covers those states, so these clauses are dead code and
Biome correctly flags them.- case "disabled": - case "ready":Removing them shortens the switch and eliminates linter noise.
🧰 Tools
🪛 Biome (1.9.4)
[error] 168-168: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 169-169: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
100-103
: 🛠️ Refactor suggestionAvoid
as any
– preserve type safety
rootProps
is spread into<Primitive.Root>
with...(rootProps as any)
.
Casting toany
bypasses the benefit of the generics you already defined.-<Primitive.Root<TRouter, TEndpoint> - {...(rootProps as any)} +<Primitive.Root<TRouter, TEndpoint> + {...rootProps}If TypeScript complains, create a dedicated type that merges
UploadthingComponentProps<TRouter, TEndpoint>
with the internal flags instead
of silencing the checker.packages/react/src/components/dropzone.tsx (2)
150-151
: Delete useless switch cases
case "disabled"
andcase "ready"
are redundant – thedefault
branch
handles them. Please drop them to satisfy the linter and keep the code tidy.🧰 Tools
🪛 Biome (1.9.4)
[error] 150-150: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 151-151: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
110-110
: 🛠️ Refactor suggestionRetain type checking – remove
as any
when spreadingrootProps
Same issue as in
button.tsx
: spreading...(rootProps as any)
drops static
safety.-<Primitive.Root<TRouter, TEndpoint> {...(rootProps as any)}> +<Primitive.Root<TRouter, TEndpoint> {...rootProps}>Define/extend a proper type instead of falling back to
any
.
🧹 Nitpick comments (3)
docs/src/app/(docs)/concepts/theming/page.mdx (1)
14-28
: Fix grammar and flow in introductionMinor language issues hurt the professional tone:
-UploadThing ships with a default styled button and dropzone component that you -can mount in your app if you don't have special needs on design. +UploadThing ships with default-styled Button and Dropzone components that you +can mount in your app if you don't have special design requirements. -These default components are customizable, both in styling and content. +These default components are fully customizable in both styling and content. -These comes with behavior built-in but you have full control over what's rendered when and where. +These come with behaviour built-in, but you retain full control over what is rendered, when and where. -You can also build a fully custom flow you can opt for the -[`useUploadThing`](/api-reference/react#use-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. +If you need a completely custom flow, use the +[`useUploadThing`](/api-reference/react#use-upload-thing) hook, which gives you full control over both look and behaviour.This tightens wording, fixes “on design” → “in design”, corrects subject–verb agreement, and removes a “not only … but also” verbosity.
🧰 Tools
🪛 LanguageTool
[uncategorized] ~15-~15: The preposition “for” seems more likely in this position.
Context: ...our app if you don't have special needs on design. These default components are cu...(AI_EN_LECTOR_REPLACEMENT_PREPOSITION)
[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...(SINGULAR_VERB_AFTER_THESE_OR_THOSE)
[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...(COMMA_COMPOUND_SENTENCE)
[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...(NOT_ONLY_ALSO)
packages/react/src/components/button.tsx (1)
146-176
: Duplication withUploadDropzone
– extract shared “get button label” helperThe whole switch for deciding button content is copy-pasted in
dropzone.tsx
. Consider extracting to a pure helper:function getUploadLabel(state: State, opts: { files: File[]; ...}) { … }Benefits:
• single place to tweak wording
• smaller components → easier to testHappy to draft the refactor if useful.
🧰 Tools
🪛 Biome (1.9.4)
[error] 168-168: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 169-169: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
packages/react/src/components/dropzone.tsx (1)
125-158
: Factor out duplicated upload-button content logicThe logic in
getUploadButtonContents()
is 1-for-1 with the version in
button.tsx
. Extracting it to a shared utility (e.g.
shared-upload-label.ts
) avoids divergence and reduces bundle size.🧰 Tools
🪛 Biome (1.9.4)
[error] 150-150: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 151-151: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
docs/src/app/(docs)/concepts/theming/page.mdx
(2 hunks)packages/react/src/components/button.tsx
(2 hunks)packages/react/src/components/dropzone.tsx
(2 hunks)playground/app/layout.tsx
(2 hunks)playground/components/uploader.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- playground/components/uploader.tsx
- playground/app/layout.tsx
🧰 Additional context used
🧠 Learnings (1)
packages/react/src/components/dropzone.tsx (1)
Learnt from: juliusmarminge
PR: pingdotgg/uploadthing#977
File: packages/solid/src/components/dropzone.tsx:539-544
Timestamp: 2024-09-24T20:46:58.415Z
Learning: In `packages/solid/src/components/dropzone.tsx`, issues related to the `onClick` handler definition will be fixed in PR #980.
🧬 Code Graph Analysis (1)
packages/react/src/components/button.tsx (2)
packages/shared/src/component-utils.ts (3)
styleFieldToClassName
(168-180)styleFieldToCssObject
(182-194)contentFieldToContent
(196-207)packages/react/src/components/shared.tsx (2)
Spinner
(3-17)Cancel
(19-37)
🪛 LanguageTool
docs/src/app/(docs)/concepts/theming/page.mdx
[uncategorized] ~15-~15: The preposition “for” seems more likely in this position.
Context: ...our app if you don't have special needs on design. These default components are cu...
(AI_EN_LECTOR_REPLACEMENT_PREPOSITION)
[grammar] ~22-~22: The verb ‘comes’ is singular. Did you mean: “this comes” or “These come”?
Context: ...tives](#unstyled-primitive-components). These comes with behavior built-in but you have ful...
(SINGULAR_VERB_AFTER_THESE_OR_THOSE)
[uncategorized] ~23-~23: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...nts). These comes with behavior built-in but you have full control over what's rende...
(COMMA_COMPOUND_SENTENCE)
[style] ~27-~27: Opting for a less wordy alternative here can improve the clarity of your writing.
Context: ...e-upload-thing) hook that allows you to not only customize the look but also have full control of the behavior. ## Uploa...
(NOT_ONLY_ALSO)
🪛 Biome (1.9.4)
packages/react/src/components/button.tsx
[error] 168-168: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 169-169: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
packages/react/src/components/dropzone.tsx
[error] 150-150: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 151-151: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
# Unstyled Primitive Components | ||
|
||
These components allow you to bring your own styling solution while not having | ||
to implement any of the internals. They accept any normal HTML props and can be | ||
assigned specific HTML tags through the `as` prop. | ||
|
||
<Note>This is currently only implemented by `@uploadthing/react`.</Note> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs contradict current implementation of the as
prop
The paragraph and later examples advertise an as
prop on the primitives, but the PR description clearly states “there is no support for asChild
or as={YourComponent}
props, as the author is undecided”. Exposing the prop in the docs before it exists is misleading.
Either:
- Remove references & examples that use
as
, or - Land the actual
as
implementation in this PR.
Until the feature exists, visitors will copy-paste examples that fail at runtime.
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
fc8ae6d
to
36951d6
Compare
bc9db78
to
f8c7cd2
Compare
This PR adds unstyled composable components through
generateUploadPrimitives
which returns:<Root>
<Dropzone>
<Button>
<AllowedContent>
I've also added examples for
minimal-appdir
,minimal-pagedir
,with-clerk-appdir
,with-clerk-pagedir
. I can add it to all the rest of the react ones if needed.All these components accept parameters as children like
({ isUploading }) => <></>
.There is no
asChild
oras={YourComponent}
prop at the moment as I'm not sure which one to use.I haven't implemented the docs either as I'm not sure if the current styling page should be split up.
Summary by CodeRabbit
New Features
Documentation
Refactor
Chores