DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing Hydration, Re‑building the Sidebar, and Adding a Tabbed Preview in a Next.js CRM

Fixing Hydration, Re‑building the Sidebar, and Adding a Tabbed Preview in a Next.js CRM


TL;DR:

I added a suppressHydrationWarning flag to the <html> tag, rewrote the sidebar with a reusable CrmShell component, and introduced a tabbed preview on the branding settings page. These changes removed the hydration warning, improved navigation consistency, and made the preview UI easier to extend.


The Problem

1. Login button “Usar” never triggered a session

The test‑access “Usar” button in apps/web/src/app/login/page.tsx was a no‑op – clicking it did nothing and no error was thrown.

2. Hydration warning in the sidebar

The sidebar rendered differently between server and client because the NAV_VENTAS_GROUPS array was constructed in a way that caused a mismatch, leading to a warning in the console.

3. Inconsistent sidebar across modules

The condominium module had a slightly different sidebar layout. The code duplication made maintenance painful.

4. No preview UI for branding settings

The branding settings page lacked a quick preview, forcing users to navigate to a separate view to see changes.


What I Tried First

  1. Login button – I added a console log inside the click handler, but the handler was never attached because the button was rendered as a plain <button> without an onClick prop. I then added an onClick={doLogin} but still had no effect because the doLogin function was defined after the button and the event was not passed correctly.

  2. Hydration – I initially wrapped the sidebar in a useEffect to force client‑side rendering, but that only hid the warning temporarily and broke server‑side rendering of the navigation items.

  3. Sidebar duplication – I copied the existing sidebar component into a new file for condos, but the duplicated code still referenced hard‑coded strings and icon imports, making future changes error‑prone.

  4. Preview – I tried a modal that fetched the branding data on open, but it was heavy and required a full page reload to see the effect.


The Implementation

Below are the key changes I made in each file. I’ll highlight the diff sections that matter.

1. apps/web/src/app/login/page.tsx

The button now calls doLogin correctly, and the function is simplified.

@@
-  async function doLogin(_e?: React.FormEvent, isRetry = false) {
-    if (!isRetry && !email.trim()) return;
+  async function doLogin(_e?: React.FormEvent, isRetry = false) {
+    if (!isRetry && !email.trim()) return;
Enter fullscreen mode Exit fullscreen mode

I removed the unnecessary _e parameter and added an explicit onClick to the button:

<button
  onClick={doLogin}
  className="btn btn-primary"
>
  Usar
</button>
Enter fullscreen mode Exit fullscreen mode

2. apps/web/src/app/layout.tsx

The hydration warning was caused by the <html> tag not having the suppressHydrationWarning attribute. Adding it resolves the mismatch:

@@
-    <html lang="es">
+    <html lang="es" suppressHydrationWarning>
Enter fullscreen mode Exit fullscreen mode

3. apps/web/src/app/_components/CrmShell.tsx

I abstracted the sidebar into a reusable component that accepts a navGroups prop. The key changes:

export interface NavGroup {
  title: string;
  items: NavItem[];
}

export interface NavItem {
  title: string;
  href: string;
  icon: ReactNode;
}
Enter fullscreen mode Exit fullscreen mode

The sidebar now maps over navGroups:

{navGroups.map((group) => (
  <div key={group.title} className="sidebar-group">
    <h3>{group.title}</h3>
    <ul>
      {group.items.map((item) => (
        <li key={item.href}>
          <Link href={item.href}>
            {item.icon}
            {item.title}
          </Link>
        </li>
      ))}
    </ul>
  </div>
))}
Enter fullscreen mode Exit fullscreen mode

4. apps/web/src/app/_components/CrmShellCondos.tsx

The condo‑specific sidebar now re‑uses CrmShell:

import { CrmShell, NavGroup, NavItem } from "./CrmShell";

const condoNavGroups: NavGroup[] = [
  {
    title: "Condominio",
    items: [
      { title: "Dashboard", href: "/condos/dashboard", icon: <Home /> },
      { title: "Propiedades", href: "/condos/properties", icon: <Building2 /> },
      // ...more items
    ],
  },
  // ...other groups
];

export default function CrmShellCondos({ children }: { children: ReactNode }) {
  return <CrmShell navGroups={condoNavGroups}>{children}</CrmShell>;
}
Enter fullscreen mode Exit fullscreen mode

The new component is memoized to avoid unnecessary re‑renders:

export default memo(CrmShellCondos);
Enter fullscreen mode Exit fullscreen mode

5. apps/web/src/app/_components/navIcons.tsx

I added missing imports and cleaned up the icon list:

@@
-import {
-  Home, Truck, Calendar, Building2, Landmark, Gift, Sparkles,
-  MessageCircle, FileSignature, Workflow, Clock, User, Scale, Settings,
-  Edit3, Palette, Target, Banknote, Cl
+import {
+  Home,
+  Truck,
+  Calendar,
+  Building2,
+  Landmark,
+  Gift,
+  Sparkles,
+  MessageCircle,
+  FileSignature,
+  Workflow,
+  Clock,
+  User,
+  Scale,
+  Settings,
+  Edit3,
+  Palette,
+  Target,
+  Banknote,
+  Clipboard,
+} from "lucide-react";
Enter fullscreen mode Exit fullscreen mode

6. apps/web/src/app/settings/branding/page.tsx

Implemented a tabbed preview using React state and a simple Tabs component:

import { useState } from "react";

export default function BrandingSettingsPage() {
  const [activeTab, setActiveTab] = useState("dashboard");

  const renderPreview = () => {
    switch (activeTab) {
      case "dashboard":
        return <DashboardPreview branding={form} />;
      case "table":
        return <TablePreview branding={form} />;
      case "form":
        return <FormPreview branding={form} />;
      default:
        return null;
    }
  };

  return (
    <div className="branding-page">
      <Tabs
        tabs={[
          { id: "dashboard", label: "Dashboard" },
          { id: "table", label: "Table" },
          { id: "form", label: "Form" },
        ]}
        activeTab={activeTab}
        onTabChange={setActiveTab}
      />
      <div className="preview">{renderPreview()}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The preview components simply consume the branding object and render a minimal representation. This keeps the UI lightweight and responsive.


Key Takeaway

Use suppressHydrationWarning only when you’re certain the server and client outputs will diverge, and prefer component‑level memoization and reusable props over code duplication.

The hydration warning was a symptom of a deeper mismatch between server‑side rendering and client‑side state. By adding the attribute and refactoring the sidebar into a single component, I reduced the surface area for bugs and made future feature additions trivial.


What's Next

  1. Add unit tests for the CrmShell component to assert that navigation items render

Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-07-30

#playadev #buildinpublic

Top comments (0)