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-root
s) 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.astro
is responsible for aquiring the necesssary state data at build time. HeremakeState
is used to shape the data for the store, before the store is initialized with it.bootData.initialize
causes thebootData.dataExport
promise to resolve to the serialized data. - This causes the
PageBootData
astro component to render state data into the page. - Both the
DisplayCount
andCounter
are Preact components which each has it's ownastro-root
as they are both separated by theFiller
astro 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-store
state.
// 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
count
found 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
count
also 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
bootData
object is created withmakeBootData
.id
is initialized withdataId
to identify the script block that holds the relevant serialized state.dataExport
is a promise that resolves to the serialized state onceinitialize
has been invoked with the initial state. - browser side
hydrateFrom
is used to deserialize the state from the contents of the script block identified bydataId
- The returned object holds an
update
function and abootData
object (which is empty browser side).update
updates the state with the passed transform (after whichnotify
is invoked;initialized
is invoked instead ofnotify
when 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-store
exposes the store prepared byprimeStore
via auseCounter
hook for Preact components.page-boot-data
is the element ID used to render state server side and hydrate browser side.- subscribers are notified with the
count
property when the store'supdate
is invoked. - the
initialCount
is cached as a primitive value when the store initializes in order to have a "cheap" initialization value in theuseCounter
hook. makeState
shapes the primitivecount
value into an object to initialize the store.useCounter
will subscribe any component that uses it for updates.increment
anddecrement
"actions" are also made available (forCounter
component).