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
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 anonClickprop. I then added anonClick={doLogin}but still had no effect because thedoLoginfunction was defined after the button and the event was not passed correctly.Hydration – I initially wrapped the sidebar in a
useEffectto force client‑side rendering, but that only hid the warning temporarily and broke server‑side rendering of the navigation items.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.
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;
I removed the unnecessary _e parameter and added an explicit onClick to the button:
<button
onClick={doLogin}
className="btn btn-primary"
>
Usar
</button>
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>
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;
}
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>
))}
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>;
}
The new component is memoized to avoid unnecessary re‑renders:
export default memo(CrmShellCondos);
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";
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>
);
}
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
-
Add unit tests for the
CrmShellcomponent 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)