Skip to content

Server-side rendering

In server-side rendering, Reactive Route acts as a matcher that executes the lifecycle, redirect chains, and validation of incoming data. For this, it is enough to call router.init and handle redirects.

To build a more complete picture, let us use a full server with Express.js.

Server

tsx
import { renderToString } from 'react-dom/server';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  );

  res.send(template.replace(`<!-- HTML -->`, html));
});
tsx
import { renderToString } from 'preact-render-to-string';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  );
  
  res.send(template.replace(`<!-- HTML -->`, html));
});
tsx
import { generateHydrationScript, renderToString } from 'solid-js/web';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
<!-- HYDRATION -->
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(() => (
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  ));
  
  res.send(template
    .replace(`<!-- HTML -->`, html)
    .replace(`<!-- HYDRATION -->`, generateHydrationScript())
  );
});
ts
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { getRouter, routerStoreKey } from './router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = await renderToString(
    createSSRApp(App, { router }).provide(routerStoreKey, { router })
  );

  res.send(template.replace(`<!-- HTML -->`, html));
});

In this example, links to js and css files are not inserted into the final HTML; this is usually done by the bundler. The full code with esbuild setup can be seen in Examples.

Client

On the client side, only skipLifecycle: true is added, because the lifecycle has already been called on the server, and the corresponding UI framework hydrate methods are used.

tsx
import { hydrateRoot } from 'react';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, { 
  skipLifecycle: true
});

hydrateRoot(
  document.getElementById('app')!,
  <RouterContext.Provider value={{ router }}>
    <App />
  </RouterContext.Provider>
);
tsx
import { hydrate } from 'preact';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

hydrate(
  <RouterContext.Provider value={{ router }}>
    <App />
  </RouterContext.Provider>,
  document.getElementById('app')!
);
tsx
import { hydrate } from 'solid-js/web';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

hydrate(
  () => (
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  ),
  document.getElementById('app')!
);
ts
import { createSSRApp } from 'vue';

import { App } from './App';
import { getRouter, routerStoreKey } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

createSSRApp(App, { router })
  .provide(routerStoreKey, { router })
  .mount('#app');

MPA

The Multi Page Application mode works out of the box if the Link component was created following the documentation example (that is, it uses the native href). When navigating to new pages, the server will return ready-made HTML, and the application remains functional even with JavaScript disabled in the browser.

Thus, the set of Config here acts as a route description with validation and, if necessary, an async lifecycle for loading data, and in some cases can replace traditionally used server routing libraries.

No AI participated in the development. MIT License