Note: There is an official example with-nanostores that lets two islands communicate via nanostores.
Caution: At this point it is unclear whether this technique interferes with the islands autonomous hydration process. On the surface this seems to work but it's not clear what potential drawbacks there may be.
The experiment was performed on a "Starter Kit" installation with a Preact renderer.
The experiment has two separate islands (with separate astro-roots) running off of the same "store".
---
// file: src/pages/index.astro
import { makeState, bootData } from '../app/counter-store.js';
import Counter from '../components/Counter.jsx';
import DisplayCount from '../components/DisplayCount.jsx';
import BaseLayout from '../layouts/BaseLayout.astro'
import Filler from '../components/Filler.astro';
import PageBootData from '../components/PageBootData.astro';
// Assemble the initial state at build time
// by any means necessary (e.g. fetch)
//
const count = 10;
bootData.initialize(makeState(count));
// Ensure initial state is available
await bootData.dataExport;
---
<BaseLayout>
<main>
<DisplayCount client:load />
<Filler />
<Counter client:visible />
<PageBootData />
</main>
</BaseLayout>File index.astro: This outlines the overall concept. counter-store is the entity that both Counter and DisplayCount are (implicitly) connected to.
- The frontmatter of
index.astrois responsible for aquiring the necesssary state data at build time. HeremakeStateis used to shape the data for the store, before the store is initialized with it.bootData.initializecauses thebootData.dataExportpromise to resolve to the serialized data. - This causes the
PageBootDataastro component to render state data into the page. - Both the
DisplayCountandCounterare Preact components which each has it's ownastro-rootas they are both separated by theFillerastro component.
The rendered HTML looks something like
<main>
<astro-root uid="Z9PpVt">
<div class="counter">
<pre>10</pre>
</div>
</astro-root>
<div class="filler">
<img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
<h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
</div>
<astro-root uid="E3x84">
<div class="counter">
<button>-</button>
<pre>10</pre>
<button>+</button>
</div>
</astro-root>
<script id="page-boot-data" type="application/json">{"count":10}</script>
</main>---
// file: src/components/PageBootData.astro
import { bootData } from '../app/counter-store.js';
const data = await bootData.dataExport;
const scriptHtml = `<script id="${bootData.id}" type="application/json">${data}</script>`;
---
{ scriptHtml }- This component simply "awaits" for the serialized initial data to become available. Then it produces a script block to embed the serialized data in the HTML. In the browser this block will be read to initialize the
counter-storestate.
// file: src/components/DisplayCount.jsx
//
import { useCounter } from '../app/counter-store.js';
export default function DisplayCount() {
const count = useCounter();
return (
<div class="counter">
<pre>{count}</pre>
</div>
);
}- Simply displays the current
countfound inside ofcounter-store.
// file: src/components/Counter.jsx
//
import { increment, decrement, useCounter } from '../app/counter-store.js';
export default function Counter() {
const count = useCounter();
return (
<div class="counter">
<button onClick={decrement}>-</button>
<pre>{count}</pre>
<button onClick={increment}>+</button>
</div>
);
}- In addition to displaying the current
countalso exposes increment and decrement buttons.
// file: src/app/prime-store.js
function hydrateFrom(elementId) {
const element = document.getElementById(elementId);
return JSON.parse(element.text);
}
function makeBootData(elementId, setStore) {
let exportData;
return {
id: elementId,
dataExport: new Promise(resolve => exportData = resolve),
initialize(initialState) {
const serialized = JSON.stringify(initialState);
setStore(initialState);
if (exportData) exportData(serialized);
}
};
}
function primeStore(dataId, initialized, notify) {
let data;
const isBrowser = typeof window === 'object';
const initializeStore = state => {
data = state;
initialized(data);
};
const bootData = isBrowser ? {} : makeBootData(dataId, initializeStore);
if (isBrowser) initializeStore(hydrateFrom(dataId));
return {
update,
bootData
};
function update(transform) {
data = transform(data);
notify(data);
}
}
export {
primeStore
};primeStore- server side a
bootDataobject is created withmakeBootData.idis initialized withdataIdto identify the script block that holds the relevant serialized state.dataExportis a promise that resolves to the serialized state onceinitializehas been invoked with the initial state. - browser side
hydrateFromis used to deserialize the state from the contents of the script block identified bydataId - The returned object holds an
updatefunction and abootDataobject (which is empty browser side).updateupdates the state with the passed transform (after whichnotifyis invoked;initializedis invoked instead ofnotifywhen the store is initialized with its initial state).
- server side a
// file: src/app/counter-store.js
import { useEffect, useState } from 'preact/hooks';
import { primeStore } from './prime-store.js';
// store customizations
//
let initialCount = 0;
const subscribed = new Set();
const DATA_ID = 'page-boot-data';
function initialized(state){
initialCount = state.count;
}
function subscribe(cb) {
subscribed.add(cb);
const unsubscribe = () => subscribed.delete(cb);
return unsubscribe;
}
function notify(state) {
for (const cb of subscribed)
cb(state.count);
}
const store = primeStore(DATA_ID, initialized, notify);
const bootData = store.bootData;
function makeState(count) {
return {
count
};
}
// hook
//
function useCounter() {
const [count, setCount] = useState(initialCount);
useEffect(() => subscribe(
value => setCount(value)
), [setCount]);
return count;
}
const incCount = (state) => (++state.count, state);
const decCount = (state) => (--state.count, state);
const increment = () => store.update(incCount);
const decrement = () => store.update(decCount);
export {
makeState,
bootData,
useCounter,
increment,
decrement,
};counter-storeexposes the store prepared byprimeStorevia auseCounterhook for Preact components.page-boot-datais the element ID used to render state server side and hydrate browser side.- subscribers are notified with the
countproperty when the store'supdateis invoked. - the
initialCountis cached as a primitive value when the store initializes in order to have a "cheap" initialization value in theuseCounterhook. makeStateshapes the primitivecountvalue into an object to initialize the store.useCounterwill subscribe any component that uses it for updates.incrementanddecrement"actions" are also made available (forCountercomponent).