Search code examples
reactjsnext.jstailwind-css

React, Tailwind, NextJS: Button's color is not updating when clicked until after I change the specified color


I'm using tailwind to conditionally highlight a button based on the URL in a client component in NextJS. However, the buttons color does not display properly. While the html in the page does update when I check the dev tools (in both Safari and Chrome), what is displayed does not. This issue goes away when I change the color in the tailwind className, but returns if I reset the color to its original state. I presume this has something to do with how and when React is updating the browser, but I'm struggling to overcome that.

I've made many, many attempts at fixing the problem, and I'll try telegraphing a few that I've remembered:

Attempt 1: Using state, checking current page with an enum

Component 1:

'use client'

import { useState } from "react";
import NavButton from "./navbutton";

enum Page {
    Dashboard=0,
    Shelves
};

export default function Navbar()
{
    const [currentPage, setCurrentPage] = useState(Page.Dashboard);

    const navButtons = [
        { page: "/dashboard", image: "home.svg", pageID: Page.Dashboard},
        { page: "/dashboard/shelves", image: "grid.svg", pageID: Page.Shelves}
    ];

    return(
        <div className="sticky top-0 h-dvh w-[3rem] flex flex-col justify-center items-center p-0 bg-white text-black drop-shadow-xl">
            {navButtons.map((navButton) =>
                <NavButton 
                    key={navButton.pageID}
                    page={navButton.page}
                    image={navButton.image}
                    clickHandler={(e: React.MouseEvent) => (setCurrentPage(navButton.pageID))}
                    styles={currentPage==navButton.pageID ? "bg-blue-100" : ""}
                />
            )}
        </div>
    );
}

Component 2:

import Link from "next/link";
import Image from "next/image";

export default function NavButton({ page, image, clickHandler, styles="" } : 
    { page: string, image: string, clickHandler: (e: React.MouseEvent) => void, styles?: string })
{
    return(
        <Link href={page} className="w-[100%]">
            <button onClick={clickHandler} className={`h-[3rem] w-[100%] flex justify-center items-center bg-white text-black ${styles}`}>
                <Image 
                    src={`../../${image}`}
                    width={30}
                    height={30}
                    alt="Image not found"
                />
            </button>
        </Link>
    );
}

This does not work on the initial render. When I change "bg-blue-100" to a different color (e.g. "bg-blue-200") it does start to work. But if I go back, it does not.

Attempt 2: Using the usePathname hook in NextJS

Component 1:

'use client'

import { useState } from "react";
import NavButton from "./navbutton";

export default function Navbar()
{
    const navButtons = [
        { page: "/dashboard", image: "home.svg"},
        { page: "/dashboard/shelves", image: "grid.svg"}
    ];

    return(
        <div className="sticky top-0 h-dvh w-[3rem] flex flex-col justify-center items-center p-0 bg-white text-black drop-shadow-xl">
            {navButtons.map((navButton) =>
                <NavButton 
                    key={navButton.page}
                    page={navButton.page}
                    image={navButton.image}
                />
            )}
        </div>
    );
}

Component 2:

import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";

export default function NavButton({ page, image } : 
    { page: string, image: string, })
{
    const pathname = usePathname();

    return(
        <Link href={page} className="w-[100%]">
            <button className={`h-[3rem] w-[100%] flex justify-center items-center bg-white text-black ${pathname === page ?  "bg-blue-200" : ""}`}>
                <Image 
                    src={`../../${image}`}
                    width={30}
                    height={30}
                    alt="Image not found"
                />
            </button>
        </Link>
    );
}

This exhibits the exact same behavior as attempt 1. It doesn't work until I change the color, and if I go back, it doesn't work. Also noting that in both cases the html always successfully changes when I check my browser's dev tools.

Attempt 3: Mostly does the same as attempt 2, but instead I inline the button and don't use a separate component/file. Same exact behavior.

Attempt 4: Creating a currentPage state, and passing it down along with an event handler.

Component 1:

'use client'

import { useState } from "react";
import NavButton from "./navbutton";
import { usePathname } from "next/navigation";

export default function Navbar()
{
    const navButtons = [
        { page: "/dashboard", image: "home.svg"},
        { page: "/dashboard/shelves", image: "grid.svg"}
    ];

    const pathname = usePathname();
    const [currentPage, setCurrentPage] = useState(pathname);

    return(
        <div className="sticky top-0 h-dvh w-[3rem] flex flex-col justify-center items-center p-0 bg-white text-black drop-shadow-xl">
            {navButtons.map((navButton) =>
                <NavButton 
                    key={navButton.page}
                    page={navButton.page}
                    image={navButton.image}
                    currentPage={currentPage}
                    onClick={() => {setCurrentPage(navButton.page)}}
                />
            )}
        </div>
    );
}

Component 2:

import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";

export default function NavButton({ page, image, currentPage, onClick } : 
    { page: string, image: string, currentPage: string, onClick: () => void})
{

    return(
        <Link href={page} className="w-[100%]">
            <button onClick={onClick} className={`h-[3rem] w-[100%] flex justify-center items-center bg-white text-black ${currentPage === page ?  "bg-blue-200" : ""}`}>
                <Image 
                    src={`../../${image}`}
                    width={30}
                    height={30}
                    alt="Image not found"
                />
            </button>
        </Link>
    );
}

Again, this exhibits the exact same behavior as the first 3 attempts I made.

My other attempts were mostly permutations of these attempts.

In the examples using state, I think it has to do with the state updating first, refreshing the browser, then updating the tailwind, which might be why it displays in the browser but not on the screen? But then I can't explain why it works when I force an update by changing the color.

I am also not sure why it works when I force an update in the non-stateful versions.


Solution

  • This is happening due to duplicate classNames. styles are applied based on the order class is defined in the CSS files rather than the order of the HTML.

    You can use tailwind-merge which removes duplicate classes.

    here's what your component 2 would look like.

    import Link from "next/link";
    import Image from "next/image";
    import { twMerge } from "tailwind-merge";
    
    export default function NavButton({ page, image, clickHandler, styles="" } : 
        { page: string, image: string, clickHandler: (e: React.MouseEvent) => void, styles?: string })
    {
        return(
            <Link href={page} className="w-[100%]">
                <button onClick={clickHandler} className={twMerge('h-[3rem] w-[100%] flex justify-center items-center bg-white text-black', styles)}>
                    <Image 
                        src={`../../${image}`}
                        width={30}
                        height={30}
                        alt="Image not found"
                    />
                </button>
            </Link>
        );
    }