1.0.0 • Published 7 months ago

@mosaicjs/react v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

@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 vite

Quick 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 Link component
  • 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:

  1. Update imports:

    // Before
    import { NanoAdapterNode } from 'nano-adapter';
    
    // After
    import { ReactMosaicNode } from '@mosaicjs/react';
  2. Update headers:

    // Before: X-Nano-Adapter
    // After: X-Mosaic
  3. Update 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