Next.js Discord

Discord Forum

How to use a shadcn <Button> inside shadcn <TooltipProvider> ?

Answered
European imported fire ant posted this in #help-forum
Open in Discord
Avatar
European imported fire antOP
Hello, I am trying to create a tooltip on a Button. Both are taken from shadcn. The way I have it set up is the following:
<Button
    variant='ghost'
    onClick={() => { router.push("/") }}
>
    <MessagesSquare />
</Button>

This is the button code in a component called "ClientChatLogButton". It displays like an icon (<MessagesSquare /> from Lucide)

This is the Tooltip.
<TooltipProvider>
    <Tooltip>
        <TooltipTrigger>
            <ClientChatLogButton />
        </TooltipTrigger>
        <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
    </Tooltip>
</TooltipProvider>

When the page loads, I get hydration errors but everything works. Below are the errors. From my understanding it doesnt like the fact that there is a button inside the TooltipProvider. When I comment out the <ClientChatLogButton /> component and replace it with plain text it wont complain. Any Ideas on how I can implement this functionality ?
Image
Answered by European imported fire ant
Yep, that was the issue, I changed my custom button like this and it worked
"use client"

import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <div
            className='
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </div>
    )
}

export default ClientChatLogButton
View full answer

48 Replies

Avatar
Have you tried adding the suppressHydrationWarning attribute to the tooltip

<TooltipProvider>
    <Tooltip suppressHydrationWarning > # See my change here
        <TooltipTrigger>
            <ClientChatLogButton />
        </TooltipTrigger>
        <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
    </Tooltip>
</TooltipProvider>
This is [solution #3 from hydration errors](https://nextjs.org/docs/messages/react-hydration-error)
Avatar
European imported fire antOP
Im not sure I understand why thats an error so I didnt want to just suppress it
Avatar
So when react renders things on the server. It does a check to make sure what your server sees, is the same thing the client sees.
Avatar
European imported fire antOP
And why wouldnt it in my case ?
the button is a client component btw
Avatar
I dont know the source code for the button, but sometimes when components are generated, the CSS classes have procedrually generated classes
So, if you use a math.random function for example
Your server would get 5 and your client would get 2
Avatar
European imported fire antOP
"use client"

import { Button } from "../ui/button"
import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <Button
            variant='ghost'
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </Button>
    )
}

export default ClientChatLogButton
this is the button
isnt that serious though ? Should I suppress it ?
Avatar
is the button file, client as well.
You can, if its just a client side button.
Avatar
European imported fire antOP
Button comes from shadcn
Avatar
in this file
import { Button } from "../ui/button"
Avatar
European imported fire antOP
Avatar
ctrl + click the link in your IDE
does the file in the root of that say 'use client;?
Avatar
European imported fire antOP
its not. I dont think shadcn gives you client components
Ah I see now
Avatar
you can make it client
Avatar
European imported fire antOP
its a server thing inside a client component
Avatar
So, answer me this. Does the button serve any server functionality at all?
Avatar
European imported fire antOP
import { Button } from "../ui/button"

This is used as a base to make my own buttons. Some of them are client, others are server side
Even though I made it client I still get the same error
Avatar
Could you open the inspect element in your browser
It tells you, what extactly is out of sync.
Avatar
European imported fire antOP
Image
Also
Image
Avatar
European imported fire antOP
I think the problem is that the icon from Lucide is a button, and its already in a button
Avatar
European imported fire antOP
Yep, that was the issue, I changed my custom button like this and it worked
"use client"

import { MessagesSquare } from "lucide-react"
import { useRouter } from "next/navigation"

function ClientChatLogButton() {
    const router = useRouter()

    return (
        <div
            className='
            onClick={() => { router.push("/chats") }}
        >
            <MessagesSquare />
        </div>
    )
}

export default ClientChatLogButton
Answer
Avatar
Large Münsterländer
bro just set the "asChild" prop on <TooltipTrigger> lmfaoo
Avatar
European imported fire antOP
what does this do ?
Avatar
Large Münsterländer
It IS what you are searching for
Avatar
European imported fire antOP
Yea im just asking what asChild does
Avatar
It merges the parent's components props down with it's child
both Tooltip and Button render a <button> tag, if you add aschild to the tooltip component it'll merge the Tooltip event listeners to the Button component and render both as a single tag
works in server components and it's a really nice composition practice although the radix team themselves said it has drawbacks
It requires that the component only has a single child though. e.g. this wont work
<TooltipTrigger asChild>
  <Button />
  <span />
</TooltipTrigger>
but this will if you install the @radix-ui/react-slot lib (which is the lib they use internally to create the asChild prop)
<TooltipTrigger asChild>
  <Slottable>
      <Button />
  </Slottable>
  <span />
</TooltipTrigger>
Avatar
Large Münsterländer
Take it or leave it
Avatar
European imported fire antOP
Not very helpful today are we
Avatar
European imported fire antOP
So would you advise I go with the asChild or is my solution ok too ?
Avatar
from what I can see you made a div interactive, that's not an ok solution not even in regular html
Do this
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild><ClientChatLogButton /></TooltipTrigger>
  </Tooltip>
  <TooltipContent>
            <p>View saved</p>
        </TooltipContent>
<TooltipProvider/>
make sure to use React.forwardRef on your ClientChatLogButton comp
Avatar
European imported fire antOP
So I use asChild on tooltiptrigger, then in my component I receive that as a forwardref and render it as the button I already have in my component right ?