Client-side JavaScript

May 25, 2024

There are different ways to use client-side JavaScript in JavaScript modules. You can either use:

  • Custom client-side JavaScript by simply loading it as static resources (from the javascript directory)
  • React Hydration from React Server-Side Rendering components

Using React Hydration

Jahia’s JavaScript modules have native support for React Hydration, which means that it is possible to bootstrap a component on the server side using React SSR and then initialize its client-side state using the server-side rendered component. This is called React Hydration, and it is a very powerful way of combining the advantages of server-side rendering while still offering all the nice functionalities of client-side JavaScript execution.

Server-Side and Client-Side Component Separation

To facilitate the separation between server-side and client-side components within a React project, a new React component has been introduced: <HydrateInBrowser ... />. This component lets developers maintain static HTML rendering for server-side components while seamlessly integrating dynamic client-side hydration for specific components.

Component differences

  • Server-Only Components: Components declared without <HydrateInBrowser /> wrapper are designated as server-only components. They cannot be hydrated and have access to backend features.
  • Server-Rendered + Client-Side Hydrate Components: Components declared with <HydrateInBrowser /> wrapper can be used in views and will be automatically hydrated on the client side. However, they do not have access to backend features.

 

  Folder Declaration Is rendered on the server Is rendered in the client
Views /src/server/* jahiaComponent metadata, directly called from Jahia render chain Yes no
Templates /src/server/* jahiaComponent metadata, directly called from Jahia render chain Yes no
Server only components /src/server/* Called from views, templates or other server only components Yes no
Hydrated components /src/client/* Called with HydrateInBrowser from templates, views other other components Yes (prerender) Yes (rerender)
Client-side components /src/client/* Called with RenderInBrowser from templates, views other other components no Yes

 

Usage

To leverage this feature, developers can encapsulate client-side components within the <HydrateInBrowser /> component. For example:

<HydrateInBrowser child={SampleHydrateInBrowserReact} props={{ initialValue: 9 }} />    

Here, SampleHydrateInBrowserReact is the client-side React component to be rendered and hydrated. The props object contains initial values for the component, which are used for server-side rendering and serialized/deserialized for client-side hydration.

Please note that the child property cannot work as the React children property, it is NOT possible to do something like this:
<HydrateInBrowser>
<SampleHydrateInBrowserReact initialValue=9 />
</HydrateInBrowser>    

Code rendered one time on the server, and then re-rerendered (hydrated) on the client: 

‚Äč‚Äčimport React, { useState } from 'react';

const SampleHydrateInBrowserReact = ({initialValue}) => {
    const [count, setCount] = useState(initialValue);

    const handleClick = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <h2>This React component is hydrated client side:</h2>
            <p data-testid="count">Count: {count}</p>
            <button data-testid="count-button" onClick={handleClick}>Increment</button>
        </div>
    );
}

export default SampleHydrateInBrowserReact;
    

Server-side code

import React from 'react';
import {HydrateInBrowser,useServerContext} from '@jahia/js-server-core';
import SampleHydrateInBrowserReact from "../../../../client/SampleHydrateInBrowserReact";

export const TestReactClientSide = () => {
    const {currentResource} = useServerContext();

    return (
        <>
            <h2>Just a normal view, that is using a client side react component: </h2>
            <HydrateInBrowser child={SampleHydrateInBrowserReact} props={{initialValue: 9}}/>
        </>
    )
}

TestReactClientSide.jahiaComponent = {
    id: 'test_react_react_client_side',
    nodeType: 'npmExample:testReactClientSide',
    componentType: 'view'
}
    

WebPack Configuration

The NPM module must provide a WebPack configuration to integrate client-side React components seamlessly. Our NPX starter tool already provides this configuration by default. Key components of this configuration include:

  • Compiling output in the /javascript/client directory.
  • Specifying module federation to expose client-side React components for reuse.
new ModuleFederationPlugin({
    // Other configurations...
    exposes: {
        SampleHydrateInBrowserReact: './src/client/SampleHydrateInBrowserReact',
        // Other client-side components...
    }
})
    

RenderInBrowser Component

An additional React component, <RenderInBrowser />, also exists. Like <HydrateInBrowser />, it accepts the same parameters but renders nothing server-side and performs full render/hydration of the component client-side. This effectively disables server-side rendering for the specified component.

The <HydrateInBrowser /> and <RenderInBrowser /> components enhance the flexibility and efficiency of React applications by enabling seamless integration of server-side and client-side rendering while maintaining clear component separation.

Here is an example of using RenderInBrowser.

Client-side code:

import React, {useEffect, useState} from 'react';

const SampleRenderInBrowserReact = ({path}) => {
    const [currentDate, setCurrentDate] = useState(new Date());
    const [counter, setCounter] = useState(3);

    const updateDate = () => {
        setCurrentDate(new Date());
    };

    useEffect(() => {
        counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
        const timerID = setInterval(updateDate, 2000);
        return () => clearInterval(timerID);
    }, [currentDate, counter]);

    return (
        <div>
            <h2>This React component is fully rendered client side:</h2>
            <p>Able to display current node path: <span data-testid="path">{path}</span></p>
            <p>And refreshing date every 2 sec: <span data-testid="date">{currentDate.toLocaleString()}</span></p>
            <p>Countdown: <span data-testid="counter">{counter}</span></p>
        </div>
    );
}

export default SampleRenderInBrowserReact;
    

Server-side code:

import React from 'react';
import {RenderInBrowser, useServerContext} from '@jahia/js-server-core';
import SampleRenderInBrowserReact from "../../../../client/SampleRenderInBrowserReact";

export const TestReactRenderClientSide = () => {
    const {currentResource} = useServerContext();

    return (
        <>
            <h2>Just a normal view, that is using a client side react component: </h2>
            <RenderInBrowser child={SampleRenderInBrowserReact} props={{path: currentResource.getNode().getPath()}}/>
        </>
    )
}

TestReactRenderClientSide.jahiaComponent = {
    id: 'test_react_render_client_side',
    nodeType: 'npmExample:testReactClientSide',
    componentType: 'view'
}
    

Full SSR vs. SSR + Hydration vs. CSR/RenderInBrowser vs. Single-Page-Application

We present here a table that compares the different types of implementation strategies that might be used and their strengths and weaknesses:

Feature/Functionality Server-Side Rendering (SSR) Server-Side Rendering with Client-Side Hydration (SSR+CS) Client-Side Rendering (CSR) Single-Page Application (SPA)
SEO Improved SEO as search engines receive fully-rendered HTML Combines benefits of SSR for SEO and initial load time with the interactivity of CSR SEO challenges as search engines may not index dynamically rendered content SEO challenges as search engines may not index dynamically rendered content
Initial Page Load Time Faster perceived performance for users due to pre-rendered HTML Faster time-to-interactivity as initial HTML is pre-rendered on the server Longer initial load times for the first-page visit due to fetching and rendering JavaScript assets Longer initial load times for the first-page visit due to fetching and rendering JavaScript assets
Real-time Updates Improved scalability as server load is reduced by offloading rendering to the client Complexity in managing client-side state, routing, and data fetching Improved scalability as server load is reduced by offloading rendering to the client Improved scalability as server load is reduced by offloading rendering to the client
User Interaction Better support for older browsers or devices that may not fully handle client-side rendering Increased complexity in development and debugging Faster subsequent page transitions and interactions as most rendering is handled on the client side Faster subsequent page transitions and interactions as most rendering is handled on the client side
Security Improved security by controlling data rendering on the server Complexity in managing client-side state and ensuring synchronization between server-rendered and client-rendered content Can be tricky if accessing server-side APIs; auth needs to be handled properly Can be tricky if accessing server-side APIs; auth needs to be handled properly
Offline Access & Performance Better support for offline access and low network conditions Complexity in managing client-side state and ensuring synchronization between server-rendered and client-rendered content Better support for offline access and low network conditions Better support for offline access and low network conditions

 

Using other client-side JavaScript frameworks

Of course, other client-side JavaScript frameworks can be used. In this section, we will examine a few examples.

Light Frameworks: Vanilla JS / HTMX / Alpine

These lightweight client-side JavaScript frameworks offer simplicity and efficiency for enhancing web applications. Integrating them with React SSR requires careful consideration to avoid conflicts and ensure smooth operation.

Vanilla JS

As a framework-less approach, Vanilla JS can be seamlessly integrated into React SSR projects. Developers should ensure that any direct DOM manipulation or event handling performed with Vanilla JS does not conflict with React JSX syntax. For example, it might be necessary to externalize as much of the client-side JavaScript code outside of the JSX; otherwise, conflicts might occur. Here is another way to perform this:

import React from 'react'
import { useServerContext, getNodeProps } from '@jahia/js-server-core'

export const HelloDefault = () => {
    const { currentNode } = useServerContext();
    const props = getNodeProps(currentNode, ['textHello']);
    const clientSideScript = `
        document.addEventListener('DOMContentLoaded', function() {
            var count = 0;
            var btn = document.getElementById('incrementBtn');
            var display = document.getElementById('countDisplay');

            btn.addEventListener('click', function() {
                count++;
                display.textContent = 'Count: ' + count;
            });
        });
    `;

    return (
        <div>
            <h2>{props.textHello}</h2>
            <p id="countDisplay">Count: 0</p>
            <button id="incrementBtn">Increment</button>
            <script dangerouslySetInnerHTML={{ __html: clientSideScript }}></script>
        </div>
    );
}

HelloDefault.jahiaComponent = { // this object is used to register the view in Jahia
    nodeType: 'vanillajsClient:hello', // The content node type the template applies to
    displayName: 'Hello (default)', // The display name of the view
    componentType: 'view' // the component type is set to view (as opposed to template component types)
}
    
HTMX

Integrating HTMX with React SSR involves annotating HTML elements with HTMX attributes to define dynamic behavior, such as fetching data from the server. Developers might want to use GraphQL extensions or custom Jahia actions to interact with the HTMX getter and post functionality.

Inside page template:

<script src="https://unpkg.com/htmx.org@1.9.12"></script>    

In your view:

import React from 'react'
import { useServerContext, getNodeProps, buildUrl } from '@jahia/js-server-core';

export const HelloDefault = () => {
    const { currentNode, renderContext, currentResource } = useServerContext();
    const props = getNodeProps(currentNode, ['textHello']);
    // This link will load the same node again but with the .ajax extension
    const ajaxLink = buildUrl({path: currentNode.getPath()}, renderContext, currentResource) + '.ajax';
    
    const termLinkStyle = {
        display: 'flex',
        textTransform: 'capitalize' 
    };

    return (
        <div>
             <a href='#' hx-get={ajaxLink}  
                hx-target={'#ajax-' + currentNode.getIdentifier()}
                hx-swap='innerHTML'>
                <div style={termLinkStyle}>
                    {props.textHello}  Click me to load content 
                </div>
            </a>
            
            <div id={'ajax-'+ currentNode.getIdentifier()}>
                Content will load here
            </div>
        </div>
    );
}

HelloDefault.jahiaComponent = { // this object is used to register the view in Jahia
    nodeType: 'htmxClient:hello', // The content node type the template applies to
    displayName: 'Hello (default)', // The display name of the view
    componentType: 'view' // the component type is set to view (as opposed to template component types)
}
    
Alpine

Similar to Vanilla JS, Alpine can be integrated directly into React SSR projects by annotating HTML elements with Alpine directives. However, developers should be cautious of potential conflicts between Alpine's directives and React's rendering, ensuring that both frameworks work harmoniously together. Here is an example:

In the page template, include the AlpineJS library:

<script src="//unpkg.com/alpinejs" defer></script>    

Then you can use it from a React component as such:

import React from 'react'
import { useServerContext, getNodeProps } from '@jahia/js-server-core'

export const HelloDefault = () => {
    const { currentNode } = useServerContext();
    const props = getNodeProps(currentNode, ['textHello']);
    // A basic template for Alpine.js initialization to manage tasks
    const alpineInit = `
        {
            newTask: '',
            tasks: [],
            filter: 'all',
            addTask() {
                if (this.newTask.trim() === '') return;
                this.tasks.push({ title: this.newTask, completed: false });
                this.newTask = '';
            },
            toggleTask(index) {
                this.tasks[index].completed = !this.tasks[index].completed;
            },
            filteredTasks() {
                if (this.filter === 'active') {
                    return this.tasks.filter(task => !task.completed);
                } else if (this.filter === 'completed') {
                    return this.tasks.filter(task => task.completed);
                }
                return this.tasks;
            }
        }
    `;

  const alpineMarkup = `
      <input x-model="newTask" type="text" placeholder="Add a new task..." />
      <button x-on:click="addTask">Add Task</button>

      <template x-for="(task, index) in filteredTasks()" :key="index">
        <div>
          <input type="checkbox" x-model="task.completed" x-on:click="toggleTask(index)" />
          <span x-text="task.title"></span>
        </div>
      </template>

      <button x-on:click="filter = 'all'">All</button>
      <button x-on:click="filter = 'active'">Active</button>
      <button x-on:click="filter = 'completed'">Completed</button>
    `;


    return (
        <>
        <div>
            <h2>{props.textHello}</h2>
        </div>
        <div x-data={ alpineInit } dangerouslySetInnerHTML={{ __html: alpineMarkup }}>
        {/* Alpine.js content is injected here as HTML, React does not manage this part */}
      </div>
        </>
    );
}

HelloDefault.jahiaComponent = { // this object is used to register the view in Jahia
    nodeType: 'alpinejsClient:hello', // The content node type the template applies to
    displayName: 'Hello (default)', // The display name of the view
    componentType: 'view' // the component type is set to view (as opposed to template component types)
}
    

Other standard JavaScript Frameworks: VueJS & Angular

For more comprehensive client-side JavaScript frameworks like VueJS and Angular, integration is best achieved through Jahia's headless GraphQL API.

  • VueJS: By leveraging Jahia's headless GraphQL API, VueJS components can efficiently fetch data from the CMS and render it on the client side. Developers can use Vue's reactive data binding to enhance interactivity and dynamic content.
  • Angular: Similarly, Angular components can interact with Jahia's headless GraphQL API to fetch data and render it on the client side. Developers can utilize Angular's powerful features, such as dependency injection and two-way data binding, to create rich and interactive user interfaces integrated with Jahia CMS.

It should be noted that, as with any headless application integration, integration with Jahia’s content editing features will require additional work to provide a smooth content editor experience.