Skip to content

Commit

Permalink
feat(transfer-state): new library for managing state in an isolated w…
Browse files Browse the repository at this point in the history
…ay with server to client transfer (#4)

* feat(transfer-state): new library for managing state in a n isolated way

* feat(transfer-state): add dev toolbar integration to view the transferred state

* ci: add validation workflow

* ci: fix name of validate action
  • Loading branch information
wishrd authored Dec 14, 2024
1 parent bfbb9d8 commit 8eaa0de
Show file tree
Hide file tree
Showing 27 changed files with 604 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/popular-wasps-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astro-tools/transfer-state": minor
---

Initial version of transfer-state package: a state management integration for having request isolated state transferred from server to client
37 changes: 37 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Validate

on:
pull_request:

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
validate:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4

- name: Setup PNPM
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: 'pnpm'

- name: Cache turbo build setup
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Install dependencies
run: pnpm install

- name: Build packages
run: pnpm exec turbo build --filter="./packages/*"
8 changes: 7 additions & 1 deletion apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import svelte from '@astrojs/svelte';
import { transferState } from '@astro-tools/transfer-state';
import { onClientDirective } from '@astro-tools/client-directives/on';
import { eventClientDirective } from '@astro-tools/client-directives/event';
import { clickClientDirective } from '@astro-tools/client-directives/click';
Expand All @@ -19,13 +20,18 @@ export default defineConfig({
{
label: 'Client directives',
autogenerate: { directory: 'client-directives' },
}
},
{
label: 'State management',
autogenerate: { directory: 'state-management' },
}
],
customCss: [
'./src/styles/theme.scss',
],
}),
svelte(),
transferState(),
onClientDirective({
directives: [
{ name: 'event', entrypoint: '@astro-tools/client-directives/event/directive' },
Expand Down
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@astrojs/svelte": "^6.0.2",
"@astro-tools/client-directives": "workspace:*",
"@astro-tools/docs-utils": "workspace:*",
"@astro-tools/transfer-state": "workspace:*",
"svelte": "^5.8.0",
"sass-embedded": "^1.82.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import { setState } from '@astro-tools:transfer-state';
import ExampleComponent from './Example.svelte';
interface Props {
id: string;
}
const { id } = Astro.props;
setState('uuid-after-hydration', 'bed6fb83-0dd9-4566-a675-55052529f18e');
---
<button id="after-hydration-trigger">Click me to hydrate!</button>
<hr />
<ExampleComponent {id} client:on="click #after-hydration-trigger" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { onMount } from 'svelte';
import { notifyHydration } from '@astro-tools/docs-utils/hydration';
import { Output } from '@astro-tools/docs-utils/components';
import { getState } from '@astro-tools:transfer-state';
export let id: string;
let hydrated = false;
let text = 'Waiting for hydration...';
onMount(() => {
text = getState('uuid-after-hydration');
hydrated = true;
notifyHydration(id);
});
</script>

<Output hydrated={hydrated} text={text} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import { setState } from '@astro-tools:transfer-state';
import ExampleComponent from './Example.svelte';
interface Props {
id: string;
}
const { id } = Astro.props;
setState('uuid', 'fc379108-c24e-47f5-b119-45db86e0e94a');
---
<button id="trigger">Click me to hydrate!</button>
<hr />
<ExampleComponent {id} client:on="click #trigger" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { onMount } from 'svelte';
import { notifyHydration } from '@astro-tools/docs-utils/hydration';
import { Output } from '@astro-tools/docs-utils/components';
import { getState } from '@astro-tools:transfer-state';
export let id: string;
let hydrated = false;
onMount(() => {
hydrated = true;
notifyHydration(id);
});
</script>

<Output hydrated={hydrated} text={getState('uuid')} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';

import { transferState } from '@astro-tools/transfer-state';

export default defineConfig({
integrations: [
transferState(),
],
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions apps/docs/src/content/docs/state-management/transfer-state.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Transfer state
description: State integration isolated by request that is transferred from server to client
sidebar:
badge:
text: Featured
variant: tip
---
import { Code, Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components';

import { Example, ExamplePreview } from '@astro-tools/docs-utils/example';

import TransferStateExample from './_examples/transfer-state/before-hydration/Example.astro';
import TransferStateAfterHydrationExample from './_examples/transfer-state/after-hydration/Example.astro';

import setupExampleRaw from './_examples/transfer-state/setup.ts?raw';
import transferStateExampleRaw from './_examples/transfer-state/before-hydration/Example.astro?raw';
import transferStateExampleChildRaw from './_examples/transfer-state/before-hydration/Example.svelte?raw';
import transferStateAfterHydrationExampleRaw from './_examples/transfer-state/after-hydration/Example.astro?raw';
import transferStateAfterHydrationExampleChildRaw from './_examples/transfer-state/after-hydration/Example.svelte?raw';
import outputCodeExample from '@astro-tools/docs-utils/components/Output.svelte?raw';

The Transfer state integration allows you to manage the state of your application isolated by request and transferred from the server to the client.

With this, instead of having to use `Astro.locals` for isolation and pass every value to the UI frameworks through properties or custom scripts, you can use the `setState` and `getState` functions to share the state between Astro and any UI framework component (Svelte, SolidJS, React...).

<Aside type="caution">Adding this integration will disable stream rendering because the state must be available before executing any client script.</Aside>

## Setup

For setting up the state management with request isolation and transfer state, include the integration in your Astro project.

<Steps>
1. Install the library using your preferred package manager:
```
npm i -D @astro-tools/transfer-state
```
2. Add the integration to your project configuration:
<Code title="astro.config.mjs" code={setupExampleRaw} lang="typescript" />
</Steps>

## Use

For using the state management, just use `setState` and `getState` methods:

### setState
Use `setState` with a key to save the value (JSON-like) into the state. Keys can be removed using `null` as value.

```typescript
import type { MyData } from './my-data';

const myData: MyData = { name: 'example' };
setState('my-data', myData);
```

### getState
Use `getState` to get the current value of a key. When there is no value, `null` will be returned.

```typescript
import type { MyData } from './my-data';

const myData = getState<MyData>('my-data');
```

<Aside type="note">The `setState` and `getState` methods will work in client-side without issues. The state will be loaded from the server-side and then kept in memory.</Aside>

## Debugging

This integration adds a new option into the Astro Dev Toolbar which allows to easily check the transferred state from the server to the client. Find the icon highlighed in the image below and click it to toggle the state viewer:
![Transfer State Astro Dev Toolbar integration](./_examples/transfer-state/transfer-state-toolbar.png)

<Aside type="tip">If you don't want to use the Astro Dev Toolbar, the transferred state from the server to the client can be checked in the DOM tree looking for a `script` tag with id `astro-tools-transfer-state`.</Aside>

## Examples

The state can be used to render server-side UI framework components, like the one in this example.

The `Example.svelte` is rendered in server-side using the `uuid` state and transferred to the client. The hydration process keeps the value is it is.

<Example>
<Tabs>
<TabItem label="Example.astro">
<Code code={transferStateExampleRaw} lang="astro" />
</TabItem>
<TabItem label="Example.svelte">
<Code code={transferStateExampleChildRaw} lang="astro" />
</TabItem>
<TabItem label="Output.svelte">
<Code code={outputCodeExample} lang="svelte" />
</TabItem>
</Tabs>
<ExamplePreview hydration="example">
<TransferStateExample id="example" />
</ExamplePreview>
</Example>

Also, the state value can be recovered at any moment, for example, after hydration or any logic you want.

<Example>
<Tabs>
<TabItem label="Example.astro">
<Code code={transferStateAfterHydrationExampleRaw} lang="astro" />
</TabItem>
<TabItem label="Example.svelte">
<Code code={transferStateAfterHydrationExampleChildRaw} lang="astro" />
</TabItem>
<TabItem label="Output.svelte">
<Code code={outputCodeExample} lang="svelte" />
</TabItem>
</Tabs>
<ExamplePreview hydration="example-after-hydration">
<TransferStateAfterHydrationExample id="example-after-hydration" />
</ExamplePreview>
</Example>

8 changes: 8 additions & 0 deletions packages/docs-utils/src/components/Output.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.output {
color: var(--at-color-hydration-off);
transition: color 200ms ease;
}

.output--hydrated {
color: var(--at-color-hydration-on);
}
10 changes: 10 additions & 0 deletions packages/docs-utils/src/components/Output.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
export let text: string;
export let hydrated = false;
</script>

<div class="output" class:output--hydrated={hydrated}>{text}</div>

<style lang="scss">
@import '@astro-tools/docs-utils/components/Output.scss';
</style>
2 changes: 2 additions & 0 deletions packages/docs-utils/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Square from './Square.svelte';
import Output from './Output.svelte';

export {
Square,
Output,
}
51 changes: 51 additions & 0 deletions packages/transfer-state/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@astro-tools/transfer-state",
"version": "0.0.0",
"description": "A state management integration for having request isolated state transferred from server to client",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/wishrd/astro-tools",
"directory": "packages/transfer-state"
},
"homepage": "https://astro-tools.pages.dev",
"keywords": [
"astro",
"withastro",
"transfer",
"state"
],
"author": {
"name": "wishrd"
},
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
"type": "module",
"peerDependencies": {
"astro": "^4.16.17",
"vite": "^5.4.11"
},
"dependencies": {
"astro-integration-kit": "^0.17.0",
"@alenaksu/json-viewer": "^2.1.2"
},
"devDependencies": {
"tsup": "^8.3.5",
"vite": "^5.4.11"
}
}
Loading

0 comments on commit 8eaa0de

Please sign in to comment.