How to use dynamic calculated tailwind classes in Shadcn UI with react?

clock icon

asked 2 months ago

message

2 Answers

eye

9 Views

I am using NextJS with Shadcn. I am trying to display a command component that contains a dynamic number of items from a database.

This is a snippet of the line that is not working. Hard coding numbers works fine, but using variables has no effect on the layout.

 <CommandList className={`min-h-[calc(${favProjects.length * 50}px+${recentProjects.length * 50}px)]`}>

Here is the full code for context.

import {
    CalendarIcon,
    EnvelopeClosedIcon,
    FaceIcon,
    GearIcon,
    PersonIcon,
    RocketIcon,
} from "@radix-ui/react-icons"

import {
    Command,
    CommandEmpty,
    CommandGroup,
    CommandInput,
    CommandItem,
    CommandList,
    CommandSeparator,
    CommandShortcut,
} from "@/components/ui/command"

export function TopNavMenuProjects() {
    const favProjects = [
        {
            id: 1,
            name: "Project 1",
        },
        {
            id: 2,
            name: "Project 2",
        },
        {
            id: 3,
            name: "Project 3",
        },
    ]

    const recentProjects = [
        {
            id: 4,
            name: "Project 4",
        },
        {
            id: 5,
            name: "Project 5",
        },
        {
            id: 6,
            name: "Project 6",
        },
    ]
    return (
        <Command className="rounded-lg border shadow-md h-full">
            <CommandInput placeholder="Search ..." />
            <CommandList className={`min-h-[calc(${favProjects.length * 50}px+${recentProjects.length * 50}px)]`}>
                <CommandEmpty>No results found.</CommandEmpty>
                <CommandGroup heading={`Favourites (${favProjects.length})`}>
                    <CommandItem>
                        {/* <CalendarIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 1</span>
                    </CommandItem>
                    <CommandItem>
                        {/* <FaceIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 2</span>
                    </CommandItem>
                    <CommandItem>
                        {/* <RocketIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 3</span>
                    </CommandItem>
                </CommandGroup>
                <CommandSeparator />
                <CommandGroup heading="Recent">
                    <CommandItem>
                        {/* <PersonIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 4</span>
                        {/* <CommandShortcut>⌘P</CommandShortcut> */}
                    </CommandItem>
                    <CommandItem>
                        {/* <EnvelopeClosedIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 5</span>
                        {/* <CommandShortcut>⌘B</CommandShortcut> */}
                    </CommandItem>
                    <CommandItem>
                        {/* <GearIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project 6</span>
                        {/* <CommandShortcut>⌘S</CommandShortcut> */}
                    </CommandItem>
                </CommandGroup>
                <CommandGroup heading="More Projects">
                    <CommandItem>
                        {/* <PersonIcon className="mr-2 h-4 w-4" /> */}
                        <span>Project Dashboard</span>
                        {/* <CommandShortcut>⌘P</CommandShortcut> */}
                    </CommandItem>
                </CommandGroup>
            </CommandList>
        </Command>
    )
}


I doubt the issue is specific to shadcn, but I am new to react, so don't want to rule anything out.

2 Answers

The reason it doesn't work is because tailwind only includes the classes it recognized when scanning your code, so dynamically generated classes are not included.

From the tailwind docs:

Don’t construct class names dynamically:

// BAD
<div class="text-{{ error ? 'red' : 'green' }}-600"></div>

Instead, make sure any class names you’re using exist in full

// GOOD
<div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>

The cn utility function

If you've installed shadcnui following the installation guide on the site, your project will have a /lib/utils.ts file. Look here. It exports a cn() function, that may help applying whole tailwind styles based on a variable.

For example:

<Link
    className={cn(
        "text-sm hover:text-primary",
        someVariableClass
        route.active ? "text-black" : "text-muted-foreground",)
    }
>
    Some label here.
</Link>

Default className would be this string "text-sm hover:text-primary". If the route.active is true, then adds "text-black" else "text-muted-foreground".

Note that I'm using complete styles. If you're able to use the cn() utility function to achieve your results, go for it.

The style prop

If you truly want dynamic styles on runtime, then you can use the style prop to dynamically update the inline props passed to your component. Unlike tailwind which creates a stylesheet, the style prop passes inline styles which take immediate effect on runtime.

 

Retrieving session data

The tutorial you provided utilizes 3 party libraries for authentication, more specifically next-auth. So if you want to access session data, all you need is just to use useSession hook. It provides status field, which may be one of following values: "loading" | "authenticated" | "unauthenticated".

Passing data from middleware to pages

However, you are also asking how to pass data from middleware to pages, which is a bit more complicated. As for the current version (14.2.4) Next.js uses Edge runtime for middlewares and api routes, while using Node for rendering by default. So I doubt it's even possible to do so. Well, you may opt-in to edge runtime, but Next.js still does not provide any direct way to access req object, if you use App router. See discussions here.

As for possible solutions for passing data from middleware, I may suggest a few solutions:

  1. Move middleware logic to template.tsx or layout.tsx component. There you can access you have access to headers and cookies and retrieve necessary data from them. Then you may put it in state management solution you're comfortable with (Redux, React Context, etc). Something like this:

    export const Layout = ({ children }: LayoutProps) => {
      const userData = getUserData();
      return (
        <StateProvider userData={userData}>
          {children}
        </StateProvider>
      )
    }
    

This way it would be more convenient to pass more user data if needed - user info, role, etc. Also, that's a good place to make a network request, if data retrieved from it is relevant for the whole application.

  1. If that's not an option for you due to some constraints, you can make a pretty hacky solution, but working nonetheless. You can create a custom header, set it in middleware and retrieve it from your page.

     // middleware.ts
     const myMiddleware = (req: Request, res: Response, next: NextFunction) => {
       const userIsAuthenticated = checkAuthentication(req);
       req.headers['x-user-authenticated'] = userIsAuthenticated;
       return next();
     }; 
    
    
     // page.ts
     const MyPage = () => {
       const userIsAuthenticated = headers().get('x-user-authenticated');
       // rest of the logic
     }; 
    

However, I would abstain from this solution, because usually in the context of authentication, you need more than a single field. Well, passing each individual field in a separate header is cumbersome.

Summary

  1. If you use next-auth library (or something similar), it provides an api to retrieve session data within pages - useSession hook for instance
  2. There's no straightforward way to pass data from next middleware. You can move logic to layout or template component for app pages or use custom headers, if that's not an option.
 

Top Questions