tailwindlabs / tailwindui-issues

Bug fixes and feature request tracking for Tailwind UI.
233 stars 4 forks source link

FAQs - Centered accordion #1593

Closed mihany closed 3 months ago

mihany commented 3 months ago

What component (if applicable)

Describe the bug ⨯ Error: Functions are not valid as a child of Client Components. This may happen if you return children instead of from render. Or maybe you meant to call this function rather than return it. <... as="div" className="pt-6" children={function children}> ^^^^^^^^^^^^^^^^^^^

Expected behavior I just have copied the code and the error pops out.

Screenshots Screenshot 2024-06-04 at 9 24 58 AM

Browser/Device (if applicable)

reinink commented 3 months ago

Hey! My apologies for the delay.

So the issue here is that this Tailwind UI example uses a render function in the Headless UI Disclosure component. Unfortunately passing functions/callbacks like this is not allowed in React server components as the functions are not serializable.

To work around this you can do a couple different things:

1. Use a client component

The first option is to put your Tailwind UI/Headless UI component in a client component. You can mark a component as a client component by adding 'use client' at the top of the file.

So, for example, if you're using Next.js, you might have your page (server) component look something like this:

import { FAQs } from '@/components/faqs'

export default async function MyPage() {

  return (
    <>
      <h1>FAQs</h1>
      <FAQs />
    </>
  )
}

And then your <FAQs> component might look something like this (note the 'use client' at the top):

'use client'

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { MinusSmallIcon, PlusSmallIcon } from '@heroicons/react/24/outline'

export default function FAQs({ faqs }) {
  return (
    <div className="bg-white">
      <div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:px-8 lg:py-40">
        <div className="mx-auto max-w-4xl divide-y divide-gray-900/10">
          <h2 className="text-2xl font-bold leading-10 tracking-tight text-gray-900">Frequently asked questions</h2>
          <dl className="mt-10 space-y-6 divide-y divide-gray-900/10">
            {faqs.map((faq) => (
              <Disclosure as="div" key={faq.question} className="pt-6">
                {({ open }) => (
                  <>
                    {/* ... */}
                  </>
                )}
              </Disclosure>
            ))}
          </dl>
        </div>
      </div>
    </div>
  )
}

2. Use data attributes

Alternatively, if you are using the latest version of Headless UI (v2.x), then you can use the data attributes that we expose instead of a render function.

For example, the Disclosure component exposes a data-open attribute, which tells you if the disclosure is currently open. It renders something like this:

<!-- Rendered `Disclosure` -->
<button data-open>Do you offer technical support?</button>
<div data-open>No</div>

From here you can use CSS attribute selectors to conditionally apply styles based on the presence of these data attributes. If you're using Tailwind CSS, the data attribute modifier makes this easy:

Here's what the same FAQ example looks like using this approach:

{faqs.map((faq) => (
  <Disclosure as="div" key={faq.question} className="group pt-6">
    <dt>
      <DisclosureButton className="flex w-full items-start justify-between text-left text-gray-900">
        <span className="text-base font-semibold leading-7">{faq.question}</span>
        <span className="ml-6 flex h-7 items-center">
          <MinusSmallIcon className="hidden h-6 w-6 group-data-[open]:inline" aria-hidden="true" />
          <PlusSmallIcon className="h-6 w-6 group-data-[open]:hidden" aria-hidden="true" />
        </span>
      </DisclosureButton>
    </dt>
    <DisclosurePanel as="dd" className="mt-2 pr-12">
      <p className="text-base leading-7 text-gray-600">{faq.answer}</p>
    </DisclosurePanel>
  </Disclosure>
))}

A few things to note here:

  1. The {({ open }) => ()} render function is gone.
  2. I've added the group class to the Disclosure so that we can style child elements based on its data attributes.
  3. We're now always rendering the MinusSmallIcon and PlusSmallIcon svgs — no more open conditional.
  4. We're using the hidden and group-data-[open]:inline classes on the MinusSmallIcon to only show it when the disclosure is open.
  5. We're using group-data-[open]:hidden class on the PlusSmallIcon to hide it when the disclosure is open.

By using data attributes you get around the whole function/callback serialization problem, and honestly it makes the code read a lot nicer in my opinion.

We actually hope to eventually update all the components in Tailwind UI to use data attributes for conditional styles instead of render functions.

And who knows, maybe Next.js and React will one day figure out on how to serialize functions!

I hope that helps!