1.0.0 • Published 7 months ago
@mosaicjs/react v1.0.0
@mosaicjs/react
React adapter for MosaicJS - Inertia.js-style SSR for Nanoservice-ts.
Overview
@mosaicjs/react provides React-specific server-side rendering capabilities for MosaicJS. It enables you to build modern "monolith" applications with React components while maintaining the benefits of SSR and SPA navigation.
Installation
npm install @mosaicjs/react react react-dom viteQuick Start
1. Create a Mosaic Node
// src/nodes/mosaic.ts
import { ReactMosaicNode } from '@mosaicjs/react';
export default class MosaicNode extends ReactMosaicNode {
constructor() {
super({
assetsVersion: process.env.ASSETS_VERSION || "1.0.0"
});
}
}2. Create React Components
// src/client/pages/Home.tsx
import React from 'react';
import { Link } from '@mosaicjs/react/client';
interface HomeProps {
auth: { user: any };
message: string;
}
const Home: React.FC<HomeProps> = ({ auth, message }) => {
return (
<div>
<h1>Welcome to MosaicJS!</h1>
<p>{message}</p>
{auth.user ? (
<p>Hello, {auth.user.name}!</p>
) : (
<Link href="/login">Login</Link>
)}
</div>
);
};
export default Home;3. Create Workflow
{
"name": "home-page",
"trigger": { "http": { "method": "GET", "path": "/" } },
"steps": [
{ "name": "fetch-data", "node": "fetch-home-data" },
{ "name": "render", "node": "mosaic" }
],
"nodes": {
"render": { "inputs": {} }
}
}Features
Server-Side Rendering
- Initial page loads render complete HTML on server
- React 18+ SSR with automatic hydration
- Fallback to client-side rendering if SSR fails
Client-Side Navigation
- SPA-style navigation with
Linkcomponent - No page reloads for internal navigation
- Automatic progress indication
- Browser history management
Layout System
// src/client/layouts/DefaultLayout.tsx
import React from 'react';
const DefaultLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div>
<nav>Navigation</nav>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
};
// Attach layout to component
const About: React.FC = () => <div>About page</div>;
About.layout = (page) => <DefaultLayout>{page}</DefaultLayout>;
export default About;Shared Props
Global data available to all pages:
// Automatic shared props
interface SharedProps {
auth: { user: any };
flash: { success?: string; error?: string };
}
// Custom shared props
const node = new ReactMosaicNode({
sharedPropsResolver: (ctx) => ({
auth: { user: ctx.request.user },
config: { appName: "My App" }
})
});Components
Link Component
import { Link } from '@mosaicjs/react/client';
// Basic navigation
<Link href="/about">About</Link>
// With options
<Link
href="/dashboard"
preserveScroll={true}
only={['stats']}
>
Dashboard
</Link>Progress Bar
import { ProgressBar } from '@mosaicjs/react/client';
// Add to your layout
const Layout = ({ children }) => (
<div>
<ProgressBar />
{children}
</div>
);Configuration
MosaicConfig Options
interface MosaicConfig {
assetsVersion?: string;
isDev?: boolean;
componentResolver?: (path: string) => string;
sharedPropsResolver?: (ctx: Context) => Record<string, any>;
titleResolver?: (component: string, props: any) => string;
}Custom Component Resolution
const node = new ReactMosaicNode({
componentResolver: (path) => {
// Custom URL to component mapping
if (path === '/') return 'HomePage';
if (path.startsWith('/admin/')) return 'AdminPage';
return 'NotFound';
}
});Development Setup
Entry Files
Create these files in your project:
// src/client/entry-client.tsx
import React from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app')!;
const pageData = JSON.parse(container.getAttribute('data-page') || '{}');
if (container.innerHTML) {
// Hydrate SSR content
hydrateRoot(container, <App {...pageData} />);
} else {
// Client-side only
const root = createRoot(container);
root.render(<App {...pageData} />);
}// src/client/entry-server.tsx
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
export async function render(url: string, pageData: any) {
const html = renderToString(<App {...pageData} />);
return { html };
}
export async function hasComponent(componentName: string) {
// Check if component exists
try {
await import(`./pages/${componentName}`);
return true;
} catch {
return false;
}
}// src/client/App.tsx
import React, { useState, useEffect } from 'react';
interface AppProps {
component: string;
props: any;
url: string;
version: string;
}
const App: React.FC<AppProps> = (initialPageData) => {
const [pageData, setPageData] = useState(initialPageData);
useEffect(() => {
const handleNavigation = (event: CustomEvent) => {
setPageData(event.detail);
};
document.addEventListener('mosaic:navigate', handleNavigation as EventListener);
return () => {
document.removeEventListener('mosaic:navigate', handleNavigation as EventListener);
};
}, []);
// Dynamic component loading
const Component = React.lazy(() => import(`./pages/${pageData.component}`));
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...pageData.props} />
</React.Suspense>
);
};
export default App;Build Configuration
Vite Config
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
client: 'src/client/entry-client.tsx',
server: 'src/client/entry-server.tsx'
},
output: {
dir: 'dist',
entryFileNames: (chunkInfo) => {
return chunkInfo.name === 'server'
? 'server/[name].js'
: 'client/[name]-[hash].js';
}
}
}
},
ssr: {
noExternal: ['@mosaicjs/react']
}
});Migration from NanoAdapter
If you're migrating from the original NanoAdapter:
Update imports:
// Before import { NanoAdapterNode } from 'nano-adapter'; // After import { ReactMosaicNode } from '@mosaicjs/react';Update headers:
// Before: X-Nano-Adapter // After: X-MosaicUpdate events:
// Before: nano-adapter:navigate // After: mosaic:navigate
TypeScript Support
Full TypeScript support with strict typing:
import type { MosaicConfig, MosaicNodeInputs } from '@mosaicjs/react';
interface PageProps {
auth: { user: any };
data: any;
}
const MyPage: React.FC<PageProps> = ({ auth, data }) => {
// Fully typed props
};License
MIT
1.0.0
7 months ago