Adapters
One core runs every library. An adapter, @silgi/<library>, renders pages and layouts with its library and hands you the hooks. Routing, loaders, head, state and the server are the same everywhere. Pick the library in silgi.config.ts with framework, or install one @silgi/<library> and Silgi detects it. One app uses one library.
| Library | Package | Install next to it | Files | Server rendering |
|---|---|---|---|---|
| React 19 | @silgi/react | react, react-dom | .tsx | streams |
| Vue 3.5 | @silgi/vue | vue | .vue | string, no streaming |
| Preact 11 | @silgi/preact | preact, preact-render-to-string | .tsx | string, no streaming |
| Svelte 5 | @silgi/svelte | svelte | .svelte | string, no streaming |
| Solid 1.9 | @silgi/solid | solid-js, babel-preset-solid, @babel/core, @babel/preset-typescript | .tsx | streams |
| html | @silgi/html | nothing | .ts | string; no client library |
Silgi has no dependencies of its own. The library and its compiler are peers that your app installs.
The same page in each library
A page that reads loader data, sets the title and keeps a counter in shared state. Every library has useData, useHead and useState; the form of the value differs.
React:
import { useData, useHead, useState } from "@silgi/react";
export default function Post() {
const { title } = useData<{ title: string }>();
const [likes, setLikes] = useState("likes", 0);
useHead({ title });
return (
<article>
<h1>{title}</h1>
<button type="button" onClick={() => setLikes(likes + 1)}>{likes} likes</button>
</article>
);
}
Preact:
import { useData, useHead, useState } from "@silgi/preact";
export default function Post() {
const { title } = useData<{ title: string }>();
const [likes, setLikes] = useState("likes", 0);
useHead({ title });
return (
<article>
<h1>{title}</h1>
<button type="button" onClick={() => setLikes(likes + 1)}>{likes} likes</button>
</article>
);
}
Solid reads values through accessors:
import { useData, useHead, useState } from "@silgi/solid";
export default function Post() {
const data = useData<{ title: string }>();
const [likes, setLikes] = useState("likes", 0);
useHead(() => ({ title: data().title }));
return (
<article>
<h1>{data().title}</h1>
<button type="button" onClick={() => setLikes(likes() + 1)}>{likes()} likes</button>
</article>
);
}
Vue uses <script setup lang="ts"> and refs:
<script setup lang="ts">
import { useData, useHead, useState } from "@silgi/vue";
const data = useData<{ title: string }>();
const likes = useState("likes", 0);
useHead(() => ({ title: data.value.title }));
</script>
<template>
<article>
<h1>{{ data.title }}</h1>
<button type="button" @click="likes++">{{ likes }} likes</button>
</article>
</template>
Svelte 5 uses <script lang="ts"> and objects with value:
<script lang="ts">
import { useData, useHead, useState } from "@silgi/svelte";
const data = useData<{ title: string }>();
const likes = useState("likes", 0);
useHead(() => ({ title: data.value.title }));
</script>
<article>
<h1>{data.value.title}</h1>
<button type="button" onclick={() => likes.value++}>{likes.value} likes</button>
</article>
Svelte reserves the $ prefix, so import $fetch under another name: import { $fetch as api } from "silgi/app".
html returns a string. It does not escape what a template literal interpolates, so an html page must escape every value it interpolates: route parameters, loader data and state. Only the HTML that Link and Outlet return goes in as it is.
import { useData, useHead, useState } from "@silgi/html";
const escape = (value: unknown) => String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
export default function Post(): string {
const { title } = useData<{ title: string }>();
const likes = useState("likes", 0);
useHead({ title });
return `<article>
<h1>${escape(title)}</h1>
<p>${escape(likes.value)} likes</p>
</article>`;
}
What differs
| React, Preact | Vue | Svelte | Solid | html | |
|---|---|---|---|---|---|
| Layout content | children | default slot | children snippet | props.children | children, a string |
Outlet | component | component | component | component | function returning a string |
useRoute() | the route | a reactive object | an object of getters | an accessor | the route of this render |
useData() | the data | ComputedRef | { value } | accessor | the data |
useState() | [value, set] | Ref | { value } | [accessor, set] | { value }, not reactive |
useHead() takes | an object | object, ref or getter | object or getter | object or accessor | an object |
useRouter() | yes | yes | yes | yes | yes |
ClientOnly | yes | yes | yes | yes | no |
useCookie, useRuntimeConfig, useAppConfig, useApp, useError, useNavigation, Link, ErrorBoundary and DefaultErrorPage exist in every library. useNavigation() tells you whether a navigation is idle or loading, and whether refresh() is revalidating. useAction(), useForm() and useFormStatus() run a handler from a component and a form; they exist in every library too. useRuntimeConfig() returns { public, app }: the public runtime config and the base URL and assets directory.
Link takes to, replace, prefetch, prefetchOn and noPrefetch; other props go to the <a>.
Page meta
meta is a named export of the page module. In React, Preact, Solid and html it is export const meta in the page file. In Vue it goes in a plain <script lang="ts"> block next to <script setup>; in Svelte it goes in <script module>.
<script lang="ts">
import type { PageMeta } from "silgi/app";
export const meta = { layout: "admin", middleware: ["auth"] } satisfies PageMeta;
</script>
<script setup lang="ts">
import { useHead } from "@silgi/vue";
useHead({ title: "Admin" });
</script>
<template>
<h1>Admin</h1>
</template>
<script module lang="ts">
import type { PageMeta } from "silgi/app";
export const meta = { layout: "admin", middleware: ["auth"] } satisfies PageMeta;
</script>
<script lang="ts">
import { useHead } from "@silgi/svelte";
useHead({ title: "Admin" });
</script>
<h1>Admin</h1>
Data in components
useFetch and useAsyncData return { data, error, status, refresh } in every library, in the form of the library's values: plain values in React and Preact, ComputedRefs in Vue, accessors in Solid, getters in Svelte, and plain values with subscribe in html. watch is a list of values in React and Preact, watch sources in Vue, and accessors or getters in Solid and Svelte; html has no watch.
The server waits for the data in every library, as the table in Data says, so it is in the first HTML.
Error boundaries
ErrorBoundary catches render errors, in the browser and on the server, and gives its fallback { error, reset }. React, Preact and Solid take a fallback function. Vue takes a fallback function or a #fallback slot. Svelte takes a fallback snippet. html takes children and fallback functions that return strings.