Skip to content
Draft
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
54 changes: 34 additions & 20 deletions docs/guide/integration-with-svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ The HyperFormula API is identical in a Svelte app and in plain JavaScript. What

Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section.

The snippets below target **Svelte 5** (runes mode) — the current stable release. They assume a project created from the official `sv` / SvelteKit templates, which enable runes by default.

::: warning SvelteKit SSR
The primary snippet below assumes a browser environment. If you use SvelteKit with default SSR, skip to [Server-side rendering](#server-side-rendering-sveltekit) — `HyperFormula.buildFromArray` at `<script>` top level will crash on the server.
The primary snippet below assumes a browser environment. If you use SvelteKit with default SSR, skip to [Server-side rendering](#server-side-rendering-sveltekit) — initializing `HyperFormula.buildFromArray` at `<script>` top level would run on every server render, duplicating work and creating instances the server never releases.
:::

## Basic usage

Declare the engine at the top of `<script>` so it lives for the component's lifetime. Call `getCellValue` on demand and display results in the template. Release the engine with `onDestroy`.
Declare the engine at the top of `<script>` so it lives for the component's lifetime. Wrap values that drive the UI in the `$state` rune so the template updates when you reassign them. Release the engine with `onDestroy`.

```svelte
<script>
<script lang="ts">
import { onDestroy } from 'svelte';
import { HyperFormula } from 'hyperformula';
import { HyperFormula, type CellValue } from 'hyperformula';

const data = [
[1, 2, '=A1+B1'],
Expand All @@ -28,8 +30,7 @@ Declare the engine at the top of `<script>` so it lives for the component's life
});

const sheetId = 0;
/** @type {import('hyperformula').CellValue} */
let result = null;
let result = $state<CellValue>(null);

function calculate() {
result = hf.getCellValue({ sheet: sheetId, row: 0, col: 2 });
Expand All @@ -42,8 +43,8 @@ Declare the engine at the top of `<script>` so it lives for the component's life
onDestroy(() => hf.destroy());
</script>

<button on:click={calculate}>Run calculations</button>
<button on:click={reset}>Reset</button>
<button onclick={calculate}>Run calculations</button>
<button onclick={reset}>Reset</button>
{#if result !== null}
<p>Result: <strong>{result}</strong></p>
{/if}
Expand All @@ -52,7 +53,7 @@ Declare the engine at the top of `<script>` so it lives for the component's life
<tbody>
{#each data as row, r}
<tr>
{#each row as cell, c}
{#each row as _cell, c}
<td>
{#if hf.doesCellHaveFormula({ sheet: sheetId, row: r, col: c })}
{hf.getCellFormula({ sheet: sheetId, row: r, col: c })}
Expand All @@ -67,18 +68,26 @@ Declare the engine at the top of `<script>` so it lives for the component's life
</table>
```

A few things worth calling out:

- `hf` stays a plain `const` — it's a reference that never changes, so it doesn't need `$state`. Svelte 5 doesn't deeply proxy class instances passed to `$state`, so storing an engine there is safe if you do need re-assignability, but don't reach for it without a reason.
- The table cell expressions read no reactive state, so they run once at render time. If you later mutate the sheet via `hf.setCellContents(...)` and want the table to refresh, copy the rows you need into a `$state` (or a `$derived`) and update that value after each mutation.

If you prefer plain JavaScript, drop `lang="ts"` and the type annotations — the runtime behavior is unchanged.

## Server-side rendering (SvelteKit)

HyperFormula depends on browser-only APIs. In SvelteKit, initialize the engine inside `onMount` so the code never runs during SSR:
HyperFormula is a heavyweight engine that has no role in server-rendered markup. Build it inside `onMount`, which only runs in the browser, and release it in `onDestroy`.

`onMount` is allowed to be `async`, but its cleanup function must be returned **synchronously** — an async callback always returns a Promise, so any cleanup you `return` from it is silently ignored. Put the teardown in a separate `onDestroy` instead.

```svelte
<script>
// Svelte 4 + SvelteKit
import { onMount } from 'svelte';
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type { CellValue, HyperFormula } from 'hyperformula';

let hf;
/** @type {import('hyperformula').CellValue} */
let result = null;
let hf = $state<HyperFormula | undefined>();
let result = $state<CellValue>(null);

onMount(async () => {
const { HyperFormula } = await import('hyperformula');
Expand All @@ -89,10 +98,11 @@ HyperFormula depends on browser-only APIs. In SvelteKit, initialize the engine i
],
{ licenseKey: 'gpl-v3' }
);
// Return a cleanup function — this is the correct teardown pattern in onMount.
return () => hf?.destroy();
});

// Separate onDestroy: onMount cannot return a cleanup from an async callback.
onDestroy(() => hf?.destroy());

function calculate() {
if (!hf) return;
result = hf.getCellValue({ sheet: 0, row: 0, col: 2 });
Expand All @@ -103,13 +113,17 @@ HyperFormula depends on browser-only APIs. In SvelteKit, initialize the engine i
}
</script>

<button on:click={calculate}>Run calculations</button>
<button on:click={reset}>Reset</button>
<button onclick={calculate} disabled={!hf}>Run calculations</button>
<button onclick={reset}>Reset</button>
{#if result !== null}
<p>Result: <strong>{result}</strong></p>
{/if}
```

Two details that matter for correctness:

- **`$state` around `hf`** — the engine is assigned asynchronously inside `onMount`, so the variable needs to be reactive for `disabled={!hf}` to flip once the engine is ready. Svelte 5 does not deep-proxy class instances stored in `$state`, so the HyperFormula instance remains untouched; only the reassignment is tracked.
- **Dynamic `await import('hyperformula')` + type-only `import type`** — the runtime import keeps the module out of the server bundle, while `import type` is erased at compile time, so it adds nothing to the server payload while still giving you full type checking.

## Next steps

Expand Down
Loading