Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexible Hydration with Multi-Framework Islands on Vike #1

Closed
lourot opened this issue Dec 14, 2023 · 8 comments
Closed

Flexible Hydration with Multi-Framework Islands on Vike #1

lourot opened this issue Dec 14, 2023 · 8 comments

Comments

@lourot
Copy link
Contributor

lourot commented Dec 14, 2023

Thanks @luisfloat for exploring this. Interestingly I had a different understanding of what islands are, especially because one island could be in React, another one could be in Vue, etc. At least this is an idea that caught my eye in https://docs.astro.build/en/concepts/islands/ . So here is the direction I would have taken:

1. Replace +Page.tsx by one + file per island

I would create one new setting per island, say islandReact and islandVue, so for each page I would have files like +islandReact.tsx and +islandVue.vue instead of +Page.tsx.

See https://vike.dev/meta for creating your own settings. Page is just a setting.

2. Adapt onRenderHtml() and onRenderClient() for rendering both islands

It would look like

// https://vike.dev/onRenderHtml
export { onRenderHtml }

import React from 'react'
import { renderToString as renderReactToString } from 'react-dom/server'
import { renderToString as renderVueToString } from '@vue/server-renderer'
import { createVueApp } from './app'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'

async function onRenderHtml(pageContext) {
  const { islandReact } = pageContext
  const reactViewHtml = dangerouslySkipEscape(
    renderReactToString(<islandReact />)
  )

  const vueApp = createVueApp(pageContext)
  const vueViewHtml = await renderToVueString(app)

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="react-view">${reactViewHtml}</div>
        <div id="vue-view">${vueViewHtml}</div>
      </body>
    </html>`
}
// https://vike.dev/onRenderClient
export { onRenderClient }

import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { createVueApp } from './app'

async function onRenderClient(pageContext) {
  const { islandReact, islandVue } = pageContext
  hydrateRoot(
    document.getElementById('react-view'), <islandReact />
  )

  const vueApp = createVueApp(pageContext)
  vueApp.mount('#vue-view')
}

I basically merged these two examples:

What do you think? One drawback of this is that the amount of islands is hardcoded for all pages of the entire app, as is their layout. But I'm wondering if it is any different with Astro if you want to mix React and Vue? 👀 Anyway such an example of mixing React and Vue in the same page on Vike could be quite nice.

But if what you want is just to have certain areas of the app not being hydrated, you are basically reinventing Server Components? That's quite a complex topic and we have it on our radar: https://vike.dev/react#react-server-components

@luisfuturist
Copy link
Owner

It's been fascinating to explore this! Although there aren't many online resources about islands, I've found inspiration from Jason Miller's blog post (https://jasonformat.com/islands-architecture/), the documentation provided by Astro, and others.

To clarify my goals, I aim to avoid shipping JavaScript responsible for rebuilding the entire application/website state merely to hydrate a basic isolated widget without appealing to an imperative approach. Instead, I seek a declarative approach — one that enables selective and scheduled hydration, particularly focusing on strategies related to viewport visibility and media query matches — to significantly enhance TTI speed. Additionally, my preference leans toward creating islands (components) within another component using directives, providing greater control.

Initially, I had imagined something multi-view at the page level, like you showed. However, it might not offer ease of hydration management inside the components. Consider, for instance, hydrating a navbar menu solely on mobile devices... It's noteworthy that islands can be marked at the component level, not necessarily confined to the page level, as seen in Astro's usage of client directives on .

My current idea revolves around marking a Vue island within a JSX component, adjacent to a React island, inspired by your example but without replacing +Page. Take a look:

//+Page.tsx
import RClock from "~/components/Clock.tsx"; // React Component
import VCounter from "~/components/VCounter.vue"; // Vue Component
import { withIsland } from "island-framework";

const Clock = withIsland(RClock);
const Counter = withIsland(VCounter);

export default function Page() {
  return (
    <>
      <button>Static button not hydrated</button>
      
      {/* Hydrate React component on viewport visibility */}
      <Clock client:visible />
      
      {/* Hydrate Vue component only on mobile */}
      <Counter client:media="(max-width: 576px)" />
    </>
  );
}

In defining island architecture, I'd describe it as a set of techniques focused on selectively and strategically hydrating isolated widgets within fully server-rendered HTML, in contrast to full hydration. It offers framework-agnostic flexibility, although the use of multiple frameworks isn't mandatory. As the Astro docs state: "Most developers stick to one UI framework." Whether we label it an "island" or not, I think it'd be splitting hairs, the concept and its prerequisites align closely with my goals.

Once again, thanks for sharing your insights.

@lourot
Copy link
Contributor Author

lourot commented Dec 15, 2023

Super interesting, thanks for sharing.

My current idea revolves around marking a Vue island within a JSX component

What you show in your +Page.tsx would be very powerful and that sounds like an interesting endeavor. Concretely your +onRenderHtml() and +onRenderClient() would have to traverse the JSX tree and call the React and Vue renderers for each island. Ping me when you have your first prototype :)

To clarify my goals, I aim to avoid shipping JavaScript responsible for rebuilding the entire application/website state merely to hydrate a basic isolated widget without appealing to an imperative approach. Instead, I seek a declarative approach — one that enables selective and scheduled hydration, particularly focusing on strategies related to viewport visibility and media query matches

It sounds like you want to do way more than just Server Components. Server Components would be basically islands which are statically marked as never to be hydrated. And even this is quite a complex topic already. So if you manage to pull off something that works and has a nice DX, we (Vike) would be very interested in it (FYI @brillout)

@luisfuturist
Copy link
Owner

luisfuturist commented Dec 15, 2023

Concretely your +onRenderHtml() and +onRenderClient() would have to traverse the JSX tree and call the React and Vue renderers for each island.

That's exactly what I'm doing! Basically, HOC withIsland marks the component with the data-island="{framework}" attribute and provides it with a script containing data about hydration (especially props and strategy). Furthermore, I am modifying +onRenderHtml() to render the islands and +client (can also be +onRenderClient) to find the islands and hydrate them as demanded.

Finally, it seems to be functioning now, making significant progress in the Proof of Concept (PoC)! Feel free to explore the source code for further insights. Notably, we've achieved the ability to import Vue components within server-rendered React components, and vice versa. Moreover, we can control hydration using the directives.

<script setup async lang="ts">
//./pages/vue/+Page.vue
import { island } from "~/island/vue";
import RClock from "~/components/Clock.react";

const Clock = island(RClock);
</script>

<template>
  <Clock client:visible />
</template>
// pages/index/+Page.tsx
import VGreenCounter from "~/components/VGreenCounter.vue";
import { withIsland } from "~/island/react";

const GreenCounter = withIsland(VGreenCounter);

export default () => {
  return (
    <div>
      <h2>Vue Hydrated on Scroll</h2>
      <GreenCounter count={1} client:visible />
    </div>
  );
};

However, there's always room for enhancement. This setup can benefit from better type support, optimizations for performance, thorough testing, and potential refactoring for improved efficiency. What do you think?

@lourot
Copy link
Contributor Author

lourot commented Dec 15, 2023

Wow yes, just saw your latest pushes. This is becoming exciting :) I'll have a deeper look in the next couple of days and get back to you. Thank you so much for doing that!

@lourot
Copy link
Contributor Author

lourot commented Dec 17, 2023

@luisfloat This is very impressive!

  1. Would you like to rename this repo into luisfloat/vike-island-example or luisfloat/vike-island-demo?
  2. Would you like to create a pull-request for Vike's documentation to quickly describe islands (maybe linking to external resources like https://docs.astro.build/en/concepts/islands/ and https://jasonformat.com/islands-architecture/) and linking to this example? For example PR docs: add NextUI community example vikejs/vike#1352 added https://vike.dev/nextui with an example to the documentation
  3. Would you be interested in shipping these hooks in a vike-islands package? See https:/vikejs/vike-react for examples of such packages shipping hooks

Feel free to say no to any of these if you don't have time 🙏

@luisfuturist
Copy link
Owner

luisfuturist commented Dec 18, 2023

Thanks, @AurelienLourot!

I've just renamed the repo to "vike-island-example". I'm planning to refactor the code by the end of the week, enhancing integrations, refining types, and addressing dev-mode warnings.

After the refactor, I'll proceed with creating the PR you suggested, describing islands and linking to relevant resources alongside my example. Moreover, I anticipate gaining new insights to enhance the DX through this refactor. Creating a package for simplified implementation is something I'll strongly consider.

@luisfuturist
Copy link
Owner

Hey, @AurelienLourot.

I've completed the intended refactor along with incorporating some new significant features. Here's a breakdown of the updates:

  • Islands are now marked with HTML comments;
  • The withHydration(Component) can now reside inside the component file.
  • Implemented a framework-agnostic page context provider (powered by nanostores);
  • Eliminated the need for writing factories thanks to Vite glob import functionality;
  • Enhanced the withHydration HOC to override island components already wrapped in other frameworks.

Next, I'll start the Vike docs PR as per your suggestion.

I believe it might be a good time to close this issue (though anyone is welcome to create new issues). What do you think?

@luisfuturist luisfuturist changed the title I had a different understanding of islands Flexible Hydration with Multi-Framework Islands on Vike Dec 21, 2023
@lourot
Copy link
Contributor Author

lourot commented Dec 21, 2023

Really nice, sounds great, thanks a lot 🙏

@lourot lourot closed this as completed Dec 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants