Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/2-guides/1-building-a-feedback-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,17 @@ jahiaComponent(
nodeType: "hydrogen:feedbackWidget",
componentType: "view",
},
({ question }: Props) => <RenderInBrowser child={Widget} props={{ question }} />,
({ question }: Props) => <Island clientOnly component={Widget} props={{ question }} />,
);
```

You'll need to restart `yarn dev` for Vite to collect your new client files, but once pushed, should see the exact same button as before, but now it will alert "Hello World!" when clicked.

The `RenderInBrowser` component is a wrapper that will ensure the code of `Widget` gets forwarded to the browser, enabling what is called Island Architecture: this component will be rendered client-side, but the rest of the page will remain server-rendered. This is a great way to improve performance, as it allows to only ship the JavaScript that is needed for the interactive parts of your page.
The `Island` component is a wrapper that will ensure the code of `Widget` gets forwarded to the browser, enabling what is called Island Architecture: this component will be rendered client-side, but the rest of the page will remain server-rendered. This is a great way to improve performance, as it allows to only ship the JavaScript that is needed for the interactive parts of your page.

The `props` prop of `RenderInBrowser` allows you to pass props to the component that will be rendered in the browser. Because they will be sent to the browser, they should be serializable.
The `props` prop of `Island` allows you to pass props to the component that will be rendered in the browser. Because they will be sent to the browser, they should be serializable.

If you nest children to `RenderInBrowser`, they will be displayed until `Widget` is loaded. This is where you can put a loading message for instance: `<RenderInBrowser child={Widget}>The widget is loading...</RenderInBrowser>`.
If you nest children to `Island` in `clientOnly` mode, they will be displayed until `Widget` is loaded. This is where you can put a loading message for instance: `<Island clientOnly component={Widget}>The widget is loading...</Island>`.

You now have all the tools needed to build any client-side component, but keep on reading to learn how to send data to jCustomer.

Expand Down
Empty file.
337 changes: 337 additions & 0 deletions docs/2-guides/2-island-architecture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
# Island Architecture

Jahia JavaScript Modules offers a first-class support for this architectural pattern, allowing interactivity without compromising on performance.

## What is Island Architecture?

We have written a [complete article on the topic,](https://www.jahia.com/blog/leveraging-the-island-architecture-in-jahia-cms) but here is a quick summary:

- Instead of shipping fully static or fully dynamic pages, Island Architecture is the middle ground where most of the page is static, but specific parts are made interactive on page load.

[A page mockup with two interactive islands: a navigation bar and a video player](./islands.svg)

In this example, the page is mostly static, with the exception of the `<Navigation />` and `<Video />` components, which are the islands of interactivity of the page. After the initial page load, JavaScript is used to make them interactive without affecting the rest of the page.

- Island Architecture offers the performance and SEO benefits of server-side rendering, but makes it easy to create highly interactive user experiences.

- The difference between islands and using server-side rendering with a bit of jQuery is that, when building islands, the exact same React components run on the server and the client. Having a single-language codebase is easier to maintain in the long run.

## The `<Island />` component

The `<Island />` component is the base of the Island Architecture in Jahia. It can be imported from the `@jahia/javascript-modules-library` and used in any React view or template:

```tsx
import { Island } from "@jahia/javascript-modules-library";
```

As with all imports from `@jahia/javascript-modules-library`, the `<Island />` component **can only be used on the server.**

Server files, files in `.server.tsx`, are used as entry points for your server code. They contain views and templates to be registered by Jahia.

Client files, files in `.client.tsx`, are used as entry points for your client code. All client files, as well as all their imports, will be made available for the browser to download. They should not contain any sensitive information, and cannot import server APIs (`@jahia/javascript-modules-library` and `.server.tsx` files).

In client files, **only the default export** can be used to create an island. For instance, here is a minimal interactive button:

```tsx
// Button.client.tsx
export default function Button() {
return (
<button
// Attach an event listener to the button
onClick={() => {
alert("Button clicked!");
}}
>
Click me!
</button>
);
}
```

If you use this button directly in a server file, it will not work as you might have expected:

```tsx
// default.server.tsx
import { jahiaComponent } from "@jahia/javascript-modules-library";
import Button from "./Button.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<h1>Hello World</h1>
<p>
{/* ❌ Do not do that, it does not work */}
<Button />
</p>
</article>
),
);
```

Your button will be sent properly to the client, but **not be made interactive.** This is because the default rendering mode of JavaScript Modules (and Jahia in general) is server-side rendering. **No JS is sent to the browser by default,** and therefore your button doesn't get its event listener attached.

The solution is the `<Island />` component:

```tsx
// default.server.tsx
import { Island, jahiaComponent } from "@jahia/javascript-modules-library";
import Button from "./Button.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<h1>Hello World</h1>
<p>
{/* ✅ This works*/}
<Island component={Button} />
</p>
</article>
),
);
```

The `<Island />` component always takes a `component` prop, which is the React component to be rendered as an island. It must be the default export from a `.client.tsx` file, otherwise it will not work.

It can also take other props, which are detailed in the following sections.

## `clientOnly`

By default, all islands are rendered on the server and made interactive on the client (the process is called [hydration](https://18.react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html)). This is great for the perceived performance of your application because even before being interactive, your page can be read by the user.

Sometimes, the server cannot render the content (for instance, because it needs browser APIs like `window`, `document` or `navigator`). For these cases, the `<Island />` component has a `clientOnly` mode, which will skip server-side rendering and only render your component on the client.

```tsx
// Language.client.tsx
export default function Language() {
// The `navigator.language` API is only available in the browser,
// and would error on the server as being undefined
return <p>According to your browser, you speak {navigator.language}.</p>;
}
```

```tsx
// default.server.tsx
import { Island, jahiaComponent } from "@jahia/javascript-modules-library";
import Language from "./Language.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<Island
clientOnly // <- Skip server-side rendering
component={Language}
/>
</article>
),
);
```

This ensures that `<Language />` only runs on the client (the browser).

## `props`

Your island component, as any React component, may take props. To do so, pass all the props of your component through the `props` prop of `<Island />`.

Because these props will be sent to the browser, two constraints apply:

- **Do not pass any sensitive information,** such as API keys.
- **The props must be serializable,** which means only a subset of all JS objects can be used. The serialization is performed by [devalue](https://www.npmjs.com/package/devalue), which offers a wider range of supported types than `JSON.stringify`, but still has limitations. For instance, you cannot send a JCR node through the props of a client component.

Here is an example of what you can do:

```tsx
// Pizza.client.tsx
export default function Pizza({
toppings,
selection,
}: {
/** All available pizza ingredients */
toppings: string[];
/** The user's current selection */
selection: Set<string>;
}) {
return (
// You can use React fragments, your island does not have to be a single element
<>
<h2>Available toppings:</h2>
<ul>
{toppings.map((topping) => (
<li key={topping}>
{topping} {selection.has(topping) ? "(selected)" : "(not selected)"})
</li>
))}
</ul>
</>
);
}
```

```tsx
// default.server.tsx
import { Island, jahiaComponent } from "@jahia/javascript-modules-library";
import Pizza from "./Pizza.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<Island
component={Pizza}
props={{
// The props must be serializable per devalue
toppings: ["cheese", "pepperoni", "mushrooms"],
selection: new Set(["cheese"]),
}}
/>
</article>
),
);
```

Our `<Pizza />` component receives its props during both server-side and client-side rendering.

## `children`

Last but not least, the `children` prop, which is the technical name for all children passed to a React component. (`<Parent children={<Child />} />` is the same as `<Parent><Child /></Parent>`.)

The `<Island />` component can take children, but its behavior depends on its `clientOnly` prop.

In default mode (without `clientOnly`), the children are rendered on the server and sent to the client, as children of your island component. The children will not be made interactive.

This behavior enables components like accordions, where the `<Island />` is not a leaf of the component tree.

[Schema of the accordion component](./accordion.svg)

Such a component can be implemented as follows:

```tsx
// Accordion.client.tsx
import type { ReactNode } from "react";

export default function Accordion({ children }: { children: ReactNode }) {
// The accordion can be opened and closed
const [isOpen, setIsOpen] = useState(false);

return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>{isOpen ? "Close" : "Open"} accordion</button>
<div style={{ display: isOpen ? "block" : "none" }}>
{/* Children will be inserted here: */}
{children}
</div>
</div>
);
}
```

And on the server:

```tsx
// default.server.tsx
import { Island, jahiaComponent } from "@jahia/javascript-modules-library";
import Accordion from "./Accordion.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<Island component={Accordion}>
<p>I'm rendered on the server!</p>
</Island>
</article>
),
);
```

A few things to note:

- The `{children}` insertion point must always be there. If you want to hide the children of your component, use CSS instead of a JS condition. Otherwise, they will not be sent to the client and your component will appear to have no children.
- Children will be wrapped in a `jsm-children` element. This should not affect your code most of the time, but don't use the `>` CSS selector to target children of your component.

In `clientOnly` mode, the children of an island will not be used as children of your island component. Instead, they will be rendered on the server and used as a placeholder until the client component is loaded.

```tsx
// default.server.tsx
import { Island, jahiaComponent } from "@jahia/javascript-modules-library";
import Map from "./Map.client.tsx";

jahiaComponent(
{
componentType: "view",
nodeType: "hydrogen:example",
},
() => (
<article>
<Island component={Map}>
{/* Placeholder until <Map /> has loaded */}
<p>The map is loading...</p>
</Island>
</article>
),
);
```

This is a good UX practice to tell users that your site is currently loading instead of leaving an empty space. It can also prevent [layout shifts](https://web.dev/articles/cls) when the component finally loads.

## Implementation details

It is not necessary to know any of this to create a successful Jahia integration, but it might come in handy if you need to debug your application:

- The `<Island />` component will be sent to the client as a `<jsm-island />` custom element. In client only mode, it will have a `data-client-only` attribute.

Do not target `jsm-island` nor `jsm-children` in your CSS as they are implementation details and may change in non-major versions.

- Client-side libraries used by your islands will be imported on the server, by the chain of top-level imports:

```ts
// Map.client.tsx
import { foo, bar } from "map-provider";

export default function Map() {}

// default.server.tsx
import Map from "./Map.client.tsx"; // Will indirectly import "map-provider"
```

This is not an issue for modern, well-built libraries, but can be troublesome for libraries with top-level side effects. If you have error messages on the server like `window/document is not defined`, it is likely that one of your dependencies is not compatible with server-side rendering (SSR).

To work around this problem, you can use the `import()` function in an effect instead of a top-level import:

```tsx
// Map.client.tsx
import { useEffect } from "react";

export default function Map() {
// useEffect only runs on the client, it is skipped during SSR
useEffect(() => {
import("map-provider").then(({ foo, bar }) => {
// Use foo and bar here
});
}, []);
}

// default.server.tsx
import Map from "./Map.client.tsx"; // "map-provider" no longer imported here
```

You can also report this issue (_the library is not compatible with server-side rendering_) to the library maintainers.

- We have written a complete article on the implement details of the `<Island />` component. You can read it on our blog: [Under the Hood: Hydrating React Components in Java](https://www.jahia.com/blog/under-the-hood-hydrating-react-components-in-java).
1 change: 1 addition & 0 deletions docs/2-guides/2-island-architecture/accordion.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/2-guides/2-island-architecture/islands.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
10 changes: 1 addition & 9 deletions docs/2-guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
This section contains independent, self-contained guides that help you build specific features or solve specific problems. Each guide is a step-by-step tutorial that you can follow to achieve a specific goal.

- [Building a Feedback Form](./1-building-a-feedback-form/)
- [Accessibility and Performance](./2-accessibility-and-performance/)
- [Building a Menu and Sitemap](./3-building-a-menu-and-sitemap/)
- [Adding Icons](./4-adding-icons/)
- [Adding Tailwind](./5-adding-tailwind/)
- [Building for Production](./6-building-for-production/)
- [Rendering Markdown](./7-rendering-markdown/)
- [RSS Feed](./8-rss-feed/)
- [Debugging](./9-debugging/)
- [Using Web Components](./A-using-web-components/)
- [Island Architecture](./2-island-architecture/)

More guides are coming soon, and contributions are welcome!

Expand Down
Empty file.
Empty file removed docs/3-reference/3-jcr/README.md
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Start here: [Setting up your dev environment](./1-getting-started/1-dev-environm
This section contains independent, self-contained guides that help you build specific features or solve specific problems. Each guide is a step-by-step tutorial that you can follow to achieve a specific goal.

- [Building a Feedback Form](./2-guides/1-building-a-feedback-form/)
- [Island Architecture](./2-guides/2-island-architecture/)

<!-- Hidden until available:
- [Accessibility and Performance](./2-guides/2-accessibility-and-performance/)
Expand Down
Loading
Loading