Feature Hub

Feature Hub

  • API
  • GitHub

›Guides

Getting Started

  • Introduction
  • Motivation
  • Demos

Guides

  • Integrating the Feature Hub
  • Writing a Feature App
  • Writing a Feature Service
  • Sharing the Browser History
  • Sharing npm Dependencies
  • Custom Logging
  • Server-Side Rendering
  • Feature App in Feature App
  • Reducing the Bundle Size

Help

  • FAQ
Edit

Server-Side Rendering

The features described in this guide are also demonstrated in the "Server-Side Rendering" demo.

State Serialization

When rendering on the server, there usually is the need to hydrate the server-rendered website on the client using the same state as on the server. To help with that, the @feature-hub/serialized-state-manager package provides the Serialized State Manager Feature Service that enables consumers, i.e. Feature Apps and Feature Services, to store their serialized state on the server, and retrieve it again on the client during hydration.

As a Consumer

Using the Serialized State Manager as a dependency, a consumer (in this case a Feature Service) could serialize its state like this:

const myFeatureServiceDefinition = {
  id: 'acme:my-feature-service',

  dependencies: {
    featureServices: {
      's2:serialized-state-manager': '^1.0.0',
    },
  },

  create(env) {
    let count = 0;

    const serializedStateManager =
      env.featureServices['s2:serialized-state-manager'];

    if (typeof window === 'undefined') {
      // on the server
      serializedStateManager.register(() => JSON.stringify({count}));
    } else {
      // on the client
      count = JSON.parse(serializedStateManager.getSerializedState()).count;
    }

    return {
      '1.0.0': () => ({
        featureService: {
          // We assume the setCount method is called by consumers while they are
          // rendered on the server.
          setCount(newCount) {
            count = newCount;
          },

          getCount() {
            return count;
          },
        },
      }),
    };
  },
};

On the server, Feature Apps and Feature Services register a callback that serializes their state with the register method. This callback is called after server-side rendering is completed. On the client, they retrieve their serialized state again with getSerializedState, and deserialize it.

In the example above, JSON.stringify is used for serialization, and JSON.parse is used for deserialization.

As the Integrator

The integrator has the responsibility to transfer all serialized consumer states from the server to the client.

On the server, the Serialized State Manager provides a serializeStates method, that serializes all consumer states. After server-side rendering is completed, the integrator must obtain the Feature Service and call this method:

const serializedStates = serializedStateManager.serializeStates();

The serializedStates string is encoded so that it can be safely injected into the HTML document, e.g. as text content of a custom script element.

On the client, before hydrating, this string must be extracted from the HTML document, e.g. from the text content of the custom script element, and passed unmodified into the defineSerializedStateManager function, where it will be decoded again:

defineSerializedStateManager(serializedStates);

Now the hydration can be started, and consumers will be able to retrieve their serialized state from the Serialized State Manager.

Preloading Feature Apps on the Client

Before hydrating server-rendered Feature Apps, their source code for the client must be preloaded, so that on the client the same UI is rendered as on the server.

On the server, the integrator must gather a list of all client module bundle URLs for the server-rendered Feature Apps, and transfer those URLs to the client, e.g. via the HTML document as text content of a custom script element.

On the client, before hydrating, the URLs must be extracted from the HTML document, e.g. from the text content of the custom script element, and then preloaded using the FeatureAppManager's preloadFeatureApp method:

const urlsForHydration = getUrlsForHydrationFromDom();

await Promise.all(
  urlsForHydration.map(async (url) => featureAppManager.preloadFeatureApp(url)),
);

Using React

On the server, a React integrator can use the FeatureHubContextProvider to provide a callback that is called by the FeatureAppLoader for server-rendered Feature Apps to populate a set of URLs for hydration on the client:

const urlsForHydration = new Set();
const addUrlForHydration = (url) => urlsForHydration.add(url);
<FeatureHubContextProvider value={{featureAppManager, addUrlForHydration}}>
  {/* render Feature Apps here */}
</FeatureHubContextProvider>

Handling Module Types

If, on the client, the integrator has provided a module loader that handles multiple module types, the module type of a Feature App's client module bundle must be used when preloading the Feature App.

On the server:

const hydrationSources = new Map();

const addUrlForHydration = (url, moduleType) =>
  hydrationSources.set(url + moduleType, {url, moduleType});

On the client:

const hydrationSources = getHydrationSourcesFromDom();

await Promise.all(
  hydrationSources.map(async ({url, moduleType}) =>
    featureAppManager.preloadFeatureApp(url, moduleType),
  ),
);

Adding Stylesheets to the Document

When a Feature App has been rendered on the server, and there are external stylesheets defined for this Feature App, those stylesheets should be added to the document head, before sending the HTML to the client. This allows the browser to render the Feature App HTML with the corresponding styles before all the scripts have been loaded and the server-rendered page has been hydrated.

Using React

On the server, a React integrator can use the FeatureHubContextProvider to provide a callback that is called by the FeatureAppLoader for server-rendered Feature Apps to populate a collection of stylesheets that should be added to the document head:

const stylesheetsForSsr = new Map();

const addStylesheetsForSsr = (stylesheets) => {
  for (const stylesheet of stylesheets) {
    stylesheetsForSsr.set(stylesheet.href, stylesheet);
  }
};
<FeatureHubContextProvider value={{featureAppManager, addStylesheetsForSsr}}>
  {/* render Feature Apps here */}
</FeatureHubContextProvider>

Asynchronous Server-Side Rendering Using React

Since React does not yet support asynchronous rendering on the server, the @feature-hub/async-ssr-manager package provides the Async SSR Manager Feature Service that enables the integrator to render any given composition of React Feature Apps in multiple render passes until all Feature Apps and Feature Services have finished their asynchronous operations.

As a Feature App

A Feature App that, for example, needs to fetch data asynchronously when it is initially rendered, must define the Async SSR Manager as an optional dependency in its Feature App definition.

Note:
The dependency must be optional, since the integrator provides the Feature Service only on the server. The Feature App can determine from its presence whether it is currently rendered on the server or on the client.

On the server, the Feature App can use the scheduleRerender method to tell the Async SSR Manager that another render pass is required after the data has been loaded:

const myFeatureAppDefinition = {
  id: 'acme:my-feature-app',

  optionalDependencies: {
    featureServices: {
      's2:async-ssr-manager': '^1.0.0',
    },
  },

  create(env) {
    let data = 'Loading...';

    const fetchData = async () => {
      try {
        const response = await fetch('https://example.com/foo');
        data = await response.text();
      } catch (error) {
        data = error.message;
      }
    };

    const dataPromise = fetchData();
    const asyncSsrManager = env.featureServices['s2:async-ssr-manager'];

    if (asyncSsrManager) {
      asyncSsrManager.scheduleRerender(dataPromise);
    }

    return {
      render() {
        return <div>{data}</div>;
      },
    };
  },
};

Note:
The scheduleRerender method must be called synchronously during a render pass, or while already scheduled asynchronous operations are running. For more information see the API docs.

Note:
In more complex Feature Apps, it may be more difficult to determine the right point in time where all asynchronous operations have been completed. However, this is a problem that needs to be solved anyway when such web applications need to be rendered on the server. It is not a special requirement of the Feature Hub.

As a Feature Service

If a Feature Service consumer changes shared state of a Feature Service during a render pass on the server, the Feature Service should schedule a rerender to give its consumers a chance to update themselves based on the state change:

const myFeatureServiceDefinition = {
  id: 'acme:my-feature-service',

  optionalDependencies: {
    featureServices: {
      's2:async-ssr-manager': '^1.0.0',
    },
  },

  create(env) {
    let count = 0;

    const asyncSsrManager = env.featureServices['s2:async-ssr-manager'];

    return {
      '1.0.0': () => ({
        featureService: {
          // We assume the setCount method is called by consumers while they are
          // rendered on the server.
          setCount(newCount) {
            count = newCount;

            if (asyncSsrManager) {
              asyncSsrManager.scheduleRerender();
            }
          },

          getCount() {
            return count;
          },
        },
      }),
    };
  },
};

As the Integrator

The Async SSR Manager provides a renderUntilCompleted method, that resolves with an HTML string when all consumers have completed their asynchronous operations.

On the server, the integrator must first obtain the Feature Service. Together with the FeatureAppManager and the React FeatureAppLoader (or FeatureAppContainer), the integrator can then render React Feature Apps that depend on asynchronous operations to fully render their initial view:

const html = await asyncSsrManager.renderUntilCompleted(() =>
  ReactDOM.renderToString(
    <FeatureHubContextProvider value={{featureAppManager, asyncSsrManager}}>
      <FeatureAppLoader
        src="https://example.com/some-feature-app.js"
        serverSrc="https://example.com/some-feature-app-node.js"
      />
    </FeatureHubContextProvider>,
  ),
);

Note:
The client-side integrator code should not register the Async SSR Manager, so that consumers can determine from its presence whether they are currently rendered on the server or on the client.

Last updated on 1/27/2025 by Feature Hub CI
← Custom LoggingFeature App in Feature App →
  • State Serialization
    • As a Consumer
    • As the Integrator
  • Preloading Feature Apps on the Client
    • Using React
    • Handling Module Types
  • Adding Stylesheets to the Document
    • Using React
  • Asynchronous Server-Side Rendering Using React
    • As a Feature App
    • As a Feature Service
    • As the Integrator
Copyright (c) 2018-2025 Accenture Song Build Germany GmbH