MiHCM

Client and Server Components

MiHCM UI components are browser-capable React components. In a Next.js App Router app, treat the package entrypoints as client boundaries that can be rendered from Server Components.

That gives you the normal Next.js split:

  • Server Components fetch data, read the session, choose layout, and render static component trees.
  • Client Components own browser state, event handlers, controlled values, keyboard interaction, drag/drop, charts, and menu behavior.
  • Props that cross from a Server Component into a Client Component must be serializable.

Quick rule

Use a component directly in a Server Component when the props are static:

// app/settings/page.tsx -- Server Component
import { Button } from '@mihcm/ui/Button';
import { Card, CardContent, CardHeader, CardTitle } from '@mihcm/ui/Card';
 
export default async function SettingsPage() {
  const user = await getCurrentUser();
 
  return (
    <Card>
      <CardHeader>
        <CardTitle>{user.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <Button variant="secondary">View profile</Button>
      </CardContent>
    </Card>
  );
}

Create a client wrapper when the component needs handlers, local state, browser APIs, or non-serializable config:

// app/settings/DeleteUserButton.tsx -- Client Component
'use client';
 
import { useState } from 'react';
import { Button } from '@mihcm/ui/Button';
 
export function DeleteUserButton({ userId }: { userId: string }) {
  const [pending, setPending] = useState(false);
 
  return (
    <Button
      variant="destructive"
      loading={pending}
      onClick={async () => {
        setPending(true);
        await fetch(`/api/users/${userId}`, { method: 'DELETE' });
        setPending(false);
      }}
    >
      Delete user
    </Button>
  );
}
// app/settings/page.tsx -- Server Component
import { DeleteUserButton } from './DeleteUserButton';
 
export default async function SettingsPage() {
  const user = await getCurrentUser();
  return <DeleteUserButton userId={user.id} />;
}

Safe Server Component usage

These patterns are safe in Server Components:

import { Badge } from '@mihcm/ui/Badge';
import { Text } from '@mihcm/ui/Text';
 
export default async function Page() {
  const status = await getStatus();
 
  return (
    <section className="space-y-3">
      <Text size="2xl" weight="semibold">Payroll run</Text>
      <Badge tone={status === 'ready' ? 'success' : 'warning'}>
        {status}
      </Badge>
    </section>
  );
}

Use this for:

PatternServer-safe?Notes
Static visual primitives (Text, Card, Badge, Alert, Skeleton)YesProps are strings, booleans, variants, and JSX children.
Static form markup (Input, Label, Textarea)YesAdd a client wrapper once you need controlled state or validation events.
Overlay shell (Dialog, Popover, Sheet, Drawer)SometimesStatic composition can be declared on the server; open state and handlers belong in a wrapper.
Navigation shell (NavigationMenu, Sidebar, Tabs)SometimesStatic links are fine; dynamic active state, routing hooks, and menu data with icon constructors belong in a wrapper.
Data-heavy components (DataTable, Chart, Calendar, DatePicker)Usually wrapperColumn renderers, formatters, chart callbacks, dates, and interactive state are not clean RSC payloads.

What cannot cross the boundary

Do not pass these from a Server Component into a Client Component:

// Do not pass these across the RSC boundary.
onClick={() => save()}
formatter={(value) => value.toLocaleString()}
date={new Date()}
range={new Map()}
icon={WalletCards}
columns={[{ cell: ({ row }) => <Actions row={row} /> }]}

Use serializable values instead:

type PayrollSummary = {
  id: string;
  employeeCount: number;
  status: 'draft' | 'ready' | 'paid';
  payDate: string; // ISO string, not Date
};

Then rebuild handlers, icons, column renderers, and formatters inside a client wrapper.

Complex NavigationMenu pattern

For a large mega menu, keep the whole menu implementation in a client component. This avoids passing icon component constructors and nested interactive menu content across the server/client boundary.

// app/components/ProductNavigation.tsx
'use client';
 
import { WalletCards, ShieldCheck } from '@mihcm/icons';
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@mihcm/ui/NavigationMenu';
 
const productLinks = [
  {
    title: 'Payroll engine',
    description: 'Run compliant salary and statutory workflows.',
    href: '/products/payroll',
    icon: WalletCards,
  },
  {
    title: 'Benefits hub',
    description: 'Coordinate claims, eligibility, and provider flows.',
    href: '/products/benefits',
    icon: ShieldCheck,
  },
];
 
export function ProductNavigation() {
  return (
    <NavigationMenu className="w-full max-w-screen-lg justify-start">
      <NavigationMenuList className="justify-start">
        <NavigationMenuItem>
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-screen max-w-screen-lg gap-2 p-4 md:grid-cols-2">
              {productLinks.map((link) => {
                const Icon = link.icon;
 
                return (
                  <li key={link.href}>
                    <NavigationMenuLink asChild>
                      <a
                        href={link.href}
                        className="flex min-h-20 gap-3 rounded-md p-3 hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
                      >
                        <span className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-card text-primary">
                          <Icon className="h-4 w-4" aria-hidden />
                        </span>
                        <span>
                          <span className="block text-body-sm font-semibold text-foreground">
                            {link.title}
                          </span>
                          <span className="block text-body-sm text-muted-foreground">
                            {link.description}
                          </span>
                        </span>
                      </a>
                    </NavigationMenuLink>
                  </li>
                );
              })}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}
// app/layout.tsx -- Server Component
import { ProductNavigation } from './components/ProductNavigation';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ProductNavigation />
        {children}
      </body>
    </html>
  );
}

Dialog and form pattern

Use Server Components for the page and data, then put the controlled form inside a client file:

// app/people/EditPersonDialog.tsx
'use client';
 
import { useState } from 'react';
import { Button } from '@mihcm/ui/Button';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@mihcm/ui/Dialog';
import { Input } from '@mihcm/ui/Input';
 
export function EditPersonDialog({ name }: { name: string }) {
  const [value, setValue] = useState(name);
 
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Edit</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit person</DialogTitle>
        </DialogHeader>
        <Input value={value} onChange={(event) => setValue(event.target.value)} />
      </DialogContent>
    </Dialog>
  );
}

DataTable and Chart pattern

Keep server data serializable. Define column renderers, chart configs, formatters, and tooltip callbacks in a client component.

// app/analytics/AnalyticsPanel.tsx
'use client';
 
import { ChartContainer } from '@mihcm/ui/Chart';
 
interface Point {
  month: string;
  payroll: number;
}
 
export function AnalyticsPanel({ data }: { data: Point[] }) {
  const config = {
    payroll: {
      label: 'Payroll',
      color: 'var(--color-primary)',
    },
  };
 
  return (
    <ChartContainer config={config} className="h-80">
      {/* Recharts primitives live here. */}
    </ChartContainer>
  );
}

Checklist

  • Import by subpath: @mihcm/ui/Button, not the root barrel.
  • Server Component first for data and layout.
  • Client wrapper for useState, useEffect, browser APIs, event handlers, routing hooks, drag/drop, charts, tables, and menus with icon component maps.
  • Pass IDs, strings, numbers, booleans, arrays, plain objects, and ISO date strings across the boundary.
  • Recreate functions, renderers, icons, and class callback logic inside the client file.
  • Keep token styling semantic: bg-card, bg-muted, text-foreground, text-muted-foreground, text-primary, border-border, ring-ring.