primitives icon indicating copy to clipboard operation
primitives copied to clipboard

Components used with "asChild" should only accept a single child

Open equal-matt opened this issue 2 years ago • 11 comments

Feature request

Overview

Current state of affairs, using a Dialog as an example:

<Dialog.Root>
    <Dialog.Trigger />
    <Dialog.Portal>
      <Dialog.Overlay />
      <Dialog.Content asChild>
        {/* runtime error here */}
        <Dialog.Title />
        <Dialog.Description />
        <Dialog.Close />
      </Dialog.Content>
    </Dialog.Portal>
  </Dialog.Root>

When setting asChild on a component, an error is thrown at runtime because there must be a single child.

The type system should be stricter on this and raise before the runtime error is experienced.

Who does this impact? Who is this for?

This impacts all users. The current error feedback looks something like this (excuse the next-related error stack, I haven't made a minimal reproduction):

The above error occurred in the <SlotClone> component:

    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:47:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:21:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-primitive/dist/index.module.js:44:26)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-dismissable-layer/dist/index.module.js:45:42)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:47:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:21:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-primitive/dist/index.module.js:44:26)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-focus-scope/dist/index.module.js:33:19)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-dialog/dist/index.module.js:269:28)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-dialog/dist/index.module.js:207:102)
    at $921a889cee6df7e8$export$99c2b779aa4e8b8b (webpack-internal:///./node_modules/@radix-ui/react-presence/dist/index.module.js:30:22)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-dialog/dist/index.module.js:192:108)
    at DialogContent (webpack-internal:///./components/Dialog.tsx:54:27)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:47:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-slot/dist/index.module.js:21:23)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-primitive/dist/index.module.js:44:26)
    at eval (webpack-internal:///./node_modules/@radix-ui/react-portal/dist/index.module.js:26:24)
    at $921a889cee6df7e8$export$99c2b779aa4e8b8b (webpack-internal:///./node_modules/@radix-ui/react-presence/dist/index.module.js:30:22)
    at Provider (webpack-internal:///./node_modules/@radix-ui/react-context/dist/index.module.js:48:28)
    at $5d3850c4d0b4e6c7$export$dad7c95542bacce0 (webpack-internal:///./node_modules/@radix-ui/react-dialog/dist/index.module.js:134:28)
    at Provider (webpack-internal:///./node_modules/@radix-ui/react-context/dist/index.module.js:48:28)
    at $5d3850c4d0b4e6c7$export$3ddf2d174ce01153 (webpack-internal:///./node_modules/@radix-ui/react-dialog/dist/index.module.js:78:28)
    at CreateEventPopup
    at article
    at div
    at BookingCalendar (webpack-internal:///./components/BookingCalendar.tsx:164:24)
    at LoadableImpl (webpack-internal:///./node_modules/next/dist/shared/lib/loadable.js:65:9)
    at main
    at div
    at Bookings (webpack-internal:///./pages/bookings/index.tsx:32:78)
    at QueryParamProviderInner (webpack-internal:///./node_modules/use-query-params/dist/QueryParamProvider.js:28:3)
    at NextAdapter (webpack-internal:///./node_modules/next-query-params/dist/next-query-params.esm.js:15:23)
    at QueryParamProvider (webpack-internal:///./node_modules/use-query-params/dist/QueryParamProvider.js:47:3)
    at ErrorBoundary (webpack-internal:///./components/ErrorBoundary.tsx:22:90)
    at SessionProvider (webpack-internal:///./node_modules/next-auth/react/index.js:454:24)
    at Hydrate (webpack-internal:///./node_modules/@tanstack/react-query/build/lib/Hydrate.mjs:30:3)
    at QueryClientProvider (webpack-internal:///./node_modules/@tanstack/react-query/build/lib/QueryClientProvider.mjs:47:3)
    at App (webpack-internal:///./pages/_app.tsx:34:28)
    at ErrorBoundary (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:8:20742)
    at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:8:23635)
    at Container (webpack-internal:///./node_modules/next/dist/client/index.js:70:9)
    at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:216:26)
    at Root (webpack-internal:///./node_modules/next/dist/client/index.js:407:27)

React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
window.console.error @ next-dev.js?3515:20
next-dev.js?3515:20 Error: React.Children.only expected to receive a single React element child.
    at Object.onlyChild [as only] (react.development.js?1b7e:1229:1)
    at eval (index.module.js?ad9d:42:50)
    at renderWithHooks (react-dom.development.js?ac89:16305:1)
    at updateForwardRef (react-dom.development.js?ac89:19226:1)
    at beginWork (react-dom.development.js?ac89:21636:1)
    at beginWork$1 (react-dom.development.js?ac89:27426:1)
    at performUnitOfWork (react-dom.development.js?ac89:26557:1)
    at workLoopSync (react-dom.development.js?ac89:26466:1)
    at renderRootSync (react-dom.development.js?ac89:26434:1)
    at recoverFromConcurrentError (react-dom.development.js?ac89:25850:1)
    at performSyncWorkOnRoot (react-dom.development.js?ac89:26096:1)
    at flushSyncCallbacks (react-dom.development.js?ac89:12042:1)
    at eval (react-dom.development.js?ac89:25651:1) Object
window.console.error @ next-dev.js?3515:20

I think it's easy to agree that this error information isn't very clear, especially for newcomers. I personally experienced this during a refactoring exercise and it wasn't obvious what the problem was from the actions I had taken.

As a first pass, better error messages might bridge the gap more quickly than the types re-work.

equal-matt avatar Feb 22 '23 13:02 equal-matt