Skip to content
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

feat: composable primitive components #947

Open
wants to merge 47 commits into
base: main
Choose a base branch
from

Conversation

veloii
Copy link

@veloii veloii commented Sep 14, 2024

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 or as={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

Release Notes: UploadThing v6.0.0

  • New Features

    • Introduced unstyled primitive components for advanced file upload customization
    • Added generateUploadPrimitives() for creating flexible upload interfaces
    • Enhanced drag-and-drop and file upload functionality with more granular state management
  • Improvements

    • Refactored upload components to use a more modular, context-based architecture
    • Improved state handling and component composition
    • Added more comprehensive type safety and error handling
  • Breaking Changes

    • Significant restructuring of upload component internals
    • Updated import paths and component usage patterns

Copy link

changeset-bot bot commented Sep 14, 2024

⚠️ No Changeset found

Latest commit: d88e8a3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Sep 14, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs-uploadthing ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 4, 2025 2:44am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
legacy-docs-uploadthing ⬜️ Ignored (Inspect) Visit Preview Jan 4, 2025 2:44am

@markflorkowski
Copy link
Collaborator

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:
image

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.

I think this is plenty in the examples, i'd potentially want to add an example with-composable-components that shows off an example of what one could do with them, but I wouldn't block on this.

I haven't implemented the docs either as I'm not sure if the current styling page should be split up.

Tentatively I would say it doesn't need to be split for now, but cc @juliusmarminge -- thoughts?

@juliusmarminge
Copy link
Member

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?

@markflorkowski
Copy link
Collaborator

Kinda feels like it should be split?

How would you want it split? I felt like the sub sections in theming wasn't bad
image

Should our default component use these primitives? A lot of code duplication if not?

This is a good point, and also makes sure that they stay in sync. I don't see why we couldn't.

@juliusmarminge
Copy link
Member

How would you want it split? I felt like the sub sections in theming wasn't bad

I guess not, perhaps a leading section with the level of theming possible with links to respective section could be sufficient

Copy link
Contributor

coderabbitai bot commented Sep 20, 2024

Walkthrough

This pull request introduces a comprehensive refactoring of the UploadThing React components, focusing on creating a more modular and flexible file upload system. The changes include developing primitive components for upload functionality, introducing new utility hooks, and updating documentation to reflect the new architecture. The refactoring aims to provide developers with more granular control over upload interfaces while maintaining a simple and intuitive API.

Changes

File Change Summary
packages/react/src/components/primitive/* Introduced new primitive components for upload functionality, including Button, Dropzone, Root, ClearButton, and AllowedContent
packages/react/src/components/button.tsx, packages/react/src/components/dropzone.tsx Refactored existing components to use new primitive architecture
packages/react/src/utils/useControllableState.ts Added new utility hook for managing controllable state
examples/*/src/pages/index.tsx, examples/*/src/app/page.tsx Updated example applications to use new upload primitives
docs/src/app/(docs)/concepts/theming/page.mdx Updated documentation to explain new theming and primitive components
playground/* Added new components and utilities for demonstrating upload functionality

Sequence Diagram

sequenceDiagram
    participant User
    participant UploadRoot as UT.Root
    participant UploadDropzone as UT.Dropzone
    participant UploadButton as UT.Button
    participant Backend

    User->>UploadRoot: Initiate upload
    UploadRoot->>UploadDropzone: Configure upload endpoint
    User->>UploadDropzone: Drag and drop files
    UploadDropzone->>UploadButton: Update upload state
    UploadButton->>Backend: Upload files
    Backend-->>UploadButton: Upload progress
    UploadButton-->>User: Show upload status
Loading

Possibly related PRs

Suggested Labels

sdk

Suggested Reviewers

  • markflorkowski

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@veloii
Copy link
Author

veloii commented Sep 20, 2024

I'm currently refactoring the prebuilt components to use the primitives. For the dropzone, you can get the isDragActive property through the children as a parameter or if you want to change a className based on it you use the data-drag attribute (ex: data-[drag]:bg-blue-500/30). In the current prebuilt button style API the isDragActive property is passed a parameter for the class but we can't access it.

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 className and style property accept a parameter like?

<Primitive.Dropzone className={({ dropzone: { isDragActive }) => "your class"} />

This feels a bit hacky imo as it still means you can't use isDragActive to compute any other property (unless we did an Object.entries on the props and made a ts helper to convert all the normal attributes to accept functions too). Another way is with a controlled parameter? Imo this feels better.

<Primitive.Dropzone
  dropzone={dropzone}
  onDropzoneChange={setDropzone}
  className={`${dropzone.isDragActive ? "bg-blue-500/10" : ""`}
/>

@veloii
Copy link
Author

veloii commented Sep 21, 2024

cc @juliusmarminge

@juliusmarminge
Copy link
Member

juliusmarminge commented Sep 25, 2024

We hope to be able to get this in a minor release, meaning no breaking changes so
styleFieldToClassName($props.appearance?.container, styleFieldArg) should if possible work as before.

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>

@veloii
Copy link
Author

veloii commented Oct 23, 2024

Sorry this took so long. I've fixed all the merge conflicts, but I'm not very experienced with documentation and lack the time at the moment so I've added some extremely basic examples for the docs. Except from that the PR is ready for review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (7)
examples/with-clerk-pagesdir/src/pages/index.tsx (1)

Line range hint 13-76: Remove duplicate upload implementations.

The example currently shows both old and new upload implementations. This could confuse users about which approach to follow. Consider:

  1. Removing the older UploadButton and UploadDropzone components
  2. Adding comments explaining the composable nature of the new implementation
  3. Adding links to documentation for the new components
examples/minimal-appdir/src/app/page.tsx (1)

84-93: Consider extracting styles to a design system.

While the inline styles work for a minimal example, consider extracting them to a design system or CSS module for better maintainability in a real application.

+// styles/dropzone.module.css
+.dropzone {
+  margin-top: 24px;
+}
+
+.container {
+  border-width: 2px;
+  border-style: dashed;
+  padding: 16px;
+}
+
+.container[data-active="true"] {
+  border-color: #2563f5;
+}
+
+.container[data-active="false"] {
+  border-color: #11182725;
+}
packages/react/src/components/primitive/root.tsx (2)

31-71: Enhance type safety and documentation for PrimitiveContextValues

Consider these improvements to the type definitions:

  1. Define state as a constant union type for better type safety:
-  state: "readying" | "ready" | "uploading" | "disabled";
+  type UploadState = "readying" | "ready" | "uploading" | "disabled";
+  state: UploadState;
  1. Add JSDoc for the dropzone property:
   /**
    * @remarks This will be only defined when nested in a <Dropzone>
+   * @property isDragActive - Indicates whether a file is being dragged over the dropzone
    */
   dropzone?: {
     isDragActive: boolean;
   };

155-163: Add comprehensive documentation for RootPrimitiveComponentProps

The TODO comment indicates missing documentation. Consider adding the following JSDoc:

+/**
+ * Props for the Root primitive component
+ * @template TRouter - The file router type
+ * @template TEndpoint - The endpoint key type
+ * @property {PrimitiveComponentChildren} children - Render function or React nodes
+ * @property {File[]} files - Controlled files state
+ * @property {(_: File[]) => void} onFilesChange - Callback for files state changes
+ */
 export type RootPrimitiveComponentProps<
   TRouter extends FileRouter,
   TEndpoint extends keyof TRouter,
 > = UploadthingComponentProps<TRouter, TEndpoint> & {
-  // TODO: add @see comment for docs
   children?: PrimitiveComponentChildren;
   files?: File[];
   onFilesChange?: (_: File[]) => void;
 };
docs/src/app/(docs)/concepts/theming/page.mdx (3)

518-526: Enhance setup instructions for better clarity.

Consider the following improvements to make the setup instructions more robust:

  1. Add explicit type imports
  2. Use absolute paths instead of the ~ alias which might not work in all setups
  3. Add context about file placement
 ```ts {{ title: 'src/utils/uploadthing.ts' }}
+import type { FileRouter } from "uploadthing/next";
 import { generateUploadPrimitives } from "@uploadthing/react";
 
-import type { OurFileRouter } from "~/server/uploadthing";
+import type { OurFileRouter } from "@/server/uploadthing";
 
 export const UT = generateUploadPrimitives<OurFileRouter>();

Add a note explaining where this file should be placed in the project structure and how it relates to the server-side configuration.

---

`528-531`: **Enhance usage documentation with complete API reference.**

The current overview is too brief. Consider adding:
1. A comprehensive list of all available state properties
2. TypeScript interfaces for the child function parameters

Add a code block showing the TypeScript interfaces:

```ts
type UploadState = "ready" | "uploading" | "error";

interface ChildFunctionProps {
  files: File[];
  state: UploadState;
  dropzone?: {
    isDragActive: boolean;
    // ... other dropzone properties
  };
  // ... document other available properties
}

555-563: Enhance Button example with more features.

The current example is too basic. Consider enhancing it to show:

  1. Error handling
  2. Upload progress
  3. File selection feedback
 <UT.Root endpoint="mockRoute">
   <UT.Button>
-    {({ state }) => state === "uploading" ? "Uploading" : "Upload file"}
+    {({ state, files, error, uploadProgress }) => {
+      if (error) return `Error: ${error.message}`;
+      if (state === "uploading") {
+        return `Uploading... ${uploadProgress}%`;
+      }
+      return files.length > 0
+        ? `Upload ${files.length} files`
+        : "Select files";
+    }}
   </UT.Button>
+  <UT.AllowedContent />
 </UT.Root>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between f49d5c6 and aac8916.

📒 Files selected for processing (8)
  • docs/src/app/(docs)/concepts/theming/page.mdx (1 hunks)
  • examples/minimal-appdir/src/app/page.tsx (2 hunks)
  • examples/minimal-pagedir/src/pages/index.tsx (2 hunks)
  • examples/with-clerk-appdir/src/app/page.tsx (2 hunks)
  • examples/with-clerk-pagesdir/src/pages/index.tsx (2 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/root.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/minimal-pagedir/src/pages/index.tsx
  • examples/with-clerk-appdir/src/app/page.tsx
  • packages/react/src/components/primitive/button.tsx
🧰 Additional context used
🪛 Biome
packages/react/src/components/primitive/clear-button.tsx

[error] 37-37: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🔇 Additional comments (7)
packages/react/src/components/primitive/clear-button.tsx (1)

42-50: LGTM! Well-structured type definitions and exports.

The type definitions provide good type safety for the polymorphic component while maintaining proper ref forwarding.

examples/with-clerk-pagesdir/src/pages/index.tsx (1)

4-4: Consider removing older component imports.

Since you're introducing the new composable UT components, consider removing the older UploadButton and UploadDropzone imports to avoid confusion and maintain a single pattern.

examples/minimal-appdir/src/app/page.tsx (4)

7-7: LGTM: Import of primitive components.

The import of UT is correctly added alongside existing imports from the same module.


74-83: LGTM: Root component configuration.

The UT.Root component is properly configured with the endpoint and necessary callbacks, maintaining consistency with the existing implementation above.


104-107: Utilize render props for better content customization.

Based on the previous discussion, AllowedContent supports render props for content customization. Consider utilizing this capability for better flexibility.

 <UT.AllowedContent
   as="p"
   style={{ fontSize: 12, width: "fit-content" }}
+  children={({ fileTypes }) => (
+    `Allowed types: ${fileTypes.join(", ")}`
+  )}
 />

74-111: Verify coexistence with existing upload components.

The new primitive components are added alongside existing UploadButton and UploadDropzone. Ensure they don't interfere with each other's functionality.

✅ Verification successful

Based on the search results, I can now provide a final response since I have enough information to verify the coexistence of components:

No conflicts between primitive and existing upload components

The primitive components (UT.Root, UT.Dropzone, etc.) and the existing components (UploadButton, UploadDropzone) are designed to work independently and do not share any state or event handlers that could cause interference. Each component:

  • Maintains its own local state for files and upload progress
  • Has its own instance of useUploadThing/createUploadThing hooks
  • Handles its own lifecycle events (onClientUploadComplete, onUploadBegin, etc.)

The components can be safely used together on the same page as demonstrated in multiple example implementations (e.g., minimal-appdir, minimal-pagedir).

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential conflicts between old and new components

# Test: Search for any shared state or conflicting event handlers
rg -A 5 'onClientUploadComplete|onUploadBegin|startUpload'

# Test: Look for any shared DOM elements that might conflict
ast-grep --pattern $'<(UploadButton|UploadDropzone|UT.Root)$$$'

Length of output: 109022

docs/src/app/(docs)/concepts/theming/page.mdx (1)

514-517: ⚠️ Potential issue

Remove or update the as prop reference.

According to the PR objectives, the as prop hasn't been implemented yet. This documentation should either remove the reference or be updated once the implementation is finalized.

✅ Verification successful

Let me try a different search strategy to verify the implementation of the as prop.


Based on the search results, I can now generate a final response. The results show that the as prop is actually implemented in the primitive components:

  1. Found in packages/react/src/components/primitive/button.tsx: { children, onClick, as, ...props }: PrimitiveButtonProps<TTag>
  2. Found in packages/react/src/components/primitive/clear-button.tsx: { children, onClick, as, ...props }: PrimitiveClearButtonProps<TTag>
  3. Found in packages/react/src/components/primitive/allowed-content.tsx: { children, as, ...props }: PrimitiveAllowedContentProps<TTag>

Documentation about the as prop is accurate

The documentation correctly states that unstyled primitive components "can be assigned specific HTML tags through the as prop". This is implemented in the codebase's primitive components, making the documentation accurate and consistent with the implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify if the `as` prop is implemented in the codebase
rg -A 5 "as: (string|React\.ElementType|ComponentType)"

Length of output: 57


Script:

#!/bin/bash
# Search for type definitions or interfaces that might include the `as` prop
rg -g "*.ts*" -A 3 "interface.*Props|type.*Props" 

# Search for any usage or implementation of `as` prop in components
ast-grep --pattern 'const $_ = $$$as$$$'

# Search for any direct references to `as` prop in the codebase
rg "\bas\b.*prop"

Length of output: 21761

}}
ref={ref}
>
<PrimitiveSlot default="Clear" children={children} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use JSX children pattern instead of children prop.

The children prop should be passed using JSX syntax rather than as a prop to follow React conventions.

-<PrimitiveSlot default="Clear" children={children} />
+<PrimitiveSlot default="Clear">{children}</PrimitiveSlot>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PrimitiveSlot default="Clear" children={children} />
<PrimitiveSlot default="Clear">{children}</PrimitiveSlot>
🧰 Tools
🪛 Biome

[error] 37-37: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

Comment on lines 30 to 34
onClick={(e) => {
if (state === "disabled") return;
onClick?.(e);
setFiles([]);
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add basic error handling for setFiles.

Consider adding basic error handling when clearing files to gracefully handle potential failures.

 onClick={(e) => {
   if (state === "disabled") return;
   onClick?.(e);
-  setFiles([]);
+  try {
+    setFiles([]);
+  } catch (error) {
+    console.error("Failed to clear files:", error);
+  }
 }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={(e) => {
if (state === "disabled") return;
onClick?.(e);
setFiles([]);
}}
onClick={(e) => {
if (state === "disabled") return;
onClick?.(e);
try {
setFiles([]);
} catch (error) {
console.error("Failed to clear files:", error);
}
}}

Comment on lines +101 to +103
<UT.Button as="button">
{state === "uploading" ? "Uploading" : "Upload file"}
</UT.Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance button loading state feedback.

The button's state indication could be more informative. Consider adding a loading spinner and disabled state during upload.

 <UT.Button as="button">
-  {state === "uploading" ? "Uploading" : "Upload file"}
+  {state === "uploading" ? (
+    <>
+      <LoadingSpinner className="mr-2" />
+      <span>Uploading...</span>
+    </>
+  ) : (
+    "Upload file"
+  )}
 </UT.Button>

Committable suggestion was skipped due to low confidence.

Comment on lines 230 to 241
const state = (() => {
if (props.__internal_state) return props.__internal_state;

const ready =
props.__internal_ready ??
(props.__internal_state === "ready" || fileTypes.length > 0);

if (fileTypes.length === 0 || !!props.disabled) return "disabled";
if (!ready) return "readying";
if (ready && !isUploading) return "ready";
return "uploading";
})();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify state computation logic

The state computation can be simplified using early returns:

 const state = (() => {
   if (props.__internal_state) return props.__internal_state;
+  if (fileTypes.length === 0 || props.disabled) return "disabled";

   const ready =
     props.__internal_ready ??
     (props.__internal_state === "ready" || fileTypes.length > 0);

-  if (fileTypes.length === 0 || !!props.disabled) return "disabled";
   if (!ready) return "readying";
-  if (ready && !isUploading) return "ready";
-  return "uploading";
+  return isUploading ? "uploading" : "ready";
 })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const state = (() => {
if (props.__internal_state) return props.__internal_state;
const ready =
props.__internal_ready ??
(props.__internal_state === "ready" || fileTypes.length > 0);
if (fileTypes.length === 0 || !!props.disabled) return "disabled";
if (!ready) return "readying";
if (ready && !isUploading) return "ready";
return "uploading";
})();
const state = (() => {
if (props.__internal_state) return props.__internal_state;
if (fileTypes.length === 0 || props.disabled) return "disabled";
const ready =
props.__internal_ready ??
(props.__internal_state === "ready" || fileTypes.length > 0);
if (!ready) return "readying";
return isUploading ? "uploading" : "ready";
})();

Comment on lines +215 to +221
startUpload(files, fileRouteInput).catch((e) => {
if (e instanceof UploadAbortedError) {
void onUploadAborted?.();
} else {
throw e;
}
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling in uploadFiles

Consider adding specific error handling for common upload scenarios:

 startUpload(files, fileRouteInput).catch((e) => {
   if (e instanceof UploadAbortedError) {
     void onUploadAborted?.();
+  } else if (e instanceof TypeError) {
+    // Handle network errors
+    throw new Error(`Upload failed due to network error: ${e.message}`);
+  } else if (e.name === "QuotaExceededError") {
+    // Handle storage quota errors
+    throw new Error(`Upload failed due to storage quota: ${e.message}`);
   } else {
     throw e;
   }
 });

Committable suggestion was skipped due to low confidence.

Comment on lines 104 to 117
export function PrimitiveSlot({
children,
componentName,
default: defaultChildren,
}: {
children: PrimitiveComponentChildren;
componentName?: string;
default?: React.ReactNode;
}) {
if (!children) return defaultChildren;
return typeof children === "function"
? children?.(usePrimitiveValues(componentName))
: children;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize PrimitiveSlot component with memoization

The component re-renders on every parent update. Consider using useMemo for the function result:

 export function PrimitiveSlot({
   children,
   componentName,
   default: defaultChildren,
 }: {
   children: PrimitiveComponentChildren;
   componentName?: string;
   default?: React.ReactNode;
 }) {
   if (!children) return defaultChildren;
-  return typeof children === "function"
+  return useMemo(() => (typeof children === "function"
     ? children?.(usePrimitiveValues(componentName))
-    : children;
+    : children), [children, componentName]);
 }

Committable suggestion was skipped due to low confidence.

@juliusmarminge juliusmarminge self-requested a review October 24, 2024 10:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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:

  1. "These comes" should be "These come" (subject-verb agreement)
  2. 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:

  1. A complete list of available state properties that can be accessed in children functions
  2. Examples of manual mode usage for both Button and Dropzone
  3. Examples of using the ClearButton component
  4. 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

📥 Commits

Files that changed from the base of the PR and between aac8916 and 11f77eb.

📒 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:

  1. Root component (root.tsx): Implements the core upload functionality, context provider, and state management
  2. Dropzone component (dropzone.tsx): Implements drag-and-drop functionality with proper file validation
  3. Button component (button.tsx): Implements file selection and upload trigger functionality
  4. AllowedContent component (allowed-content.tsx): Implements content display for allowed file types

Each 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"
done

Length 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"
done

Length 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:

  1. Remove as prop usage since it's not implemented yet (as mentioned in PR objectives)
  2. Add error handling example
  3. 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>

@juliusmarminge
Copy link
Member

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 entities

The 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 components

While UT is concise, a more explicit name like UploadPrimitives 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 component

The 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, and role="code" improves semantic meaning for screen readers.

playground/components/uploader.tsx (4)

60-61: Consider adding endpoint validation

The 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 overrides

The 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 feedback

The 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 configuration

The 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:

  1. Add aria-label to the nav element for better screen reader support
  2. Add visual indication for the current active route
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 11f77eb and d1291ff.

📒 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:

  1. Application requirements for URL validity
  2. Security policies regarding temporary access
  3. 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:

  1. Detect expired URLs
  2. Provide appropriate user feedback
  3. 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:

  1. Moving this information to environment variables
  2. Only showing it in development mode
  3. Or removing it entirely if not needed for debugging

@veloii
Copy link
Author

veloii commented Nov 27, 2024

Thanks! In the meantime, should I fix the linting issues?

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 for generateUploadPrimitives.

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 the as prop

Currently, the ref type is fixed to HTMLDivElement, but the component allows rendering different element types via the as 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 logic

The 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 maintainability

The 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 reconsider event.persist() usage

From 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 check

IE11 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

📥 Commits

Reviewing files that changed from the base of the PR and between d1291ff and d88e8a3.

📒 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 suggestion

Use 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 suggestion

Consider 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 for useDropzone.

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 of useDropzone found is in the documentation example which uses the package import @uploadthing/react. The path change in packages/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 tsx

Length 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 passing children via prop ()

Passing children via the children 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 of globalThis.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 of globalThis.fetch or provide a fallback.

Comment on lines +392 to +420
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants