Layouts & Shared Components
There's no special "layout" concept to learn — a layout is just a React component you import. This is one of the advantages of React-as-views: composition is ordinary React.
A shared layout
Create a components/Layout.tsx:
import type { ReactNode } from "react";
export default function Layout({ active, children }: { active?: string; children: ReactNode }) {
return (
<div>
<header>
<nav>
<a href="/">Home</a>
<a href="/Dashboard">Dashboard</a>
<a href="/Users">Users</a>
</nav>
</header>
<main>{children}</main>
</div>
);
}Use it from any view:
import Layout from "../components/Layout";
export default function Dashboard(props: DashboardProps) {
return (
<Layout active="dashboard">
<h1>Dashboard</h1>
{/* ... */}
</Layout>
);
}INFO
Only files in Views/ are registered as views. Shared components live elsewhere (e.g. components/) and are imported — they are not addressable as views themselves.
Partial views / shared widgets
Same idea — extract any reusable piece into a component and import it. There's no registry entry and no controller needed for shared components; they're bundled with whatever view imports them.
// components/StatCard.tsx
export function StatCard({ label, value }: { label: string; value: string }) {
return <div className="card"><span>{label}</span><strong>{value}</strong></div>;
}Styling with Tailwind CSS
Import your CSS from entry-client.tsx so Vite bundles it and the engine links it automatically:
// entry-client.tsx
import "./styles/globals.css";
import { createRegistry } from "@react-dotnetcore/runtime";
import { mount } from "@react-dotnetcore/runtime/client";
const registry = createRegistry(import.meta.glob("./Views/*.tsx", { eager: true }));
mount(registry);Set up Tailwind the standard way (tailwind.config.cjs, postcss.config.cjs, @tailwind directives in globals.css). Point the content globs at your views and components:
// tailwind.config.cjs
module.exports = {
content: ["./components/**/*.{ts,tsx}", "./Views/**/*.{ts,tsx}", "./entry-client.tsx"],
theme: { extend: {} },
plugins: [],
};In production the generated CSS is emitted as an asset and the engine injects a <link> into every page <head> (from the Vite manifest). In development Vite injects styles via HMR. Either way the class names are present in the server-rendered HTML, so there's no flash of unstyled content.
shadcn/ui
shadcn/ui components are copy-pasted React components on Tailwind — they work unchanged. Put them under components/ui/ and the cn() helper under lib/utils.ts, then import them from your views. The samples/ReactDotNetCore.Sample project includes a full dashboard built this way (Card, Button, Badge, lucide-react icons).
Important: SSR-safe components
Views render on the server first, so component bodies must not touch browser-only globals during render. Guard or defer browser APIs:
import { useEffect, useState } from "react";
export default function Widget() {
const [w, setW] = useState(0);
useEffect(() => setW(window.innerWidth), []); // runs only after hydration, in the browser
return <span>{w}</span>;
}Reading window/document/localStorage directly in the component body will throw during SSR — use useEffect (client-only) or check typeof window !== "undefined".