Skip to content

Instantly share code, notes, and snippets.

@markmals
Last active February 2, 2025 17:24
Show Gist options
  • Save markmals/f798e2089ed950ce90f4f6df7fd97d47 to your computer and use it in GitHub Desktop.
Save markmals/f798e2089ed950ce90f4f6df7fd97d47 to your computer and use it in GitHub Desktop.
Sketch of a server-first meta-framework for ivi
import { html, component, useState, getProps } from 'ivi';
import { cache, route, serverFunction, useActionState, use, Suspense } from 'ivi-router';
import { db, Messages } from 'db.server';
import { eq } from 'drizzle-orm';
// Block virtual DOM
// Compiler optimizations
// Small vendor & component bundle sizes
// Tagged template literals
// Server components
// Stateful hybrid routing
const Counter = component(c => {
const [count, setCount] = useState(c, 0);
return () => html`
<div>${count()}</div>
<button @click=${() => setCount(count() + 1)}>Increment</button>
`;
});
const message = serverFunction(async (id: number) => {
const message = await db.select().from(Messages).where(eq(Messages.id, id)).get();
return message ?? { localizedDescription: 'Message not found' };
});
const cachedMessage = cache(message);
const MessageClient = component<{ clientId: number }>(c => {
let { clientId } = getProps(c);
const resolvedMessage = use(c, () => cachedMessage(clientId));
return props => {
clientId = props.clientId;
return html`
${Counter()}
<span>${resolvedMessage().localizedDescription}</span>
`;
};
});
const MessageServer = async ({ clientId }: { clientId: number }) => {
const resolvedMessage = await cachedMessage(clientId);
return html`
<!-- This component will be hydrated -->
${Counter()}
<!-- This vDOM node will just be statically rendered in the JSON payload -->
<!-- the initial values from the initial server load. -->
<!-- It will never be hydrated because it's not a client component. -->
<span>${resolvedMessage.localizedDescription}</span>
`;
};
const echo = serverFunction(async (message: string) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return { message };
});
const Echo = component(c => {
const [state, dispatch, isPending] = useActionState(c, echo, {
message: 'Awaiting Message',
});
const handleChange = (event: Event & { currentTarget: HTMLInputElement }) => {
dispatch({ message: event.currentTarget.value });
};
return () => html`
<input type="text" @change=${handleChange} />
${!isPending() && state().message}
`;
});
const login = serverFunction(async (data: FormData) => {
await new Promise(resolve => setTimeout(resolve, 1000));
const username = data.get('username');
if (username === 'admin') {
return Response.redirect('/admin');
} else {
throw Response.redirect('/home', 301);
}
});
export default route({
params: ['id', 'brand?'],
head: {
links: () => [{ rel: 'stylesheet', href: 'styles.url' }],
meta: () => [{ title: 'My Route' }],
},
loader: async ({ params }) => {
const id = parseInt(params.id);
await cachedMessage(id);
return html`
<form action="${login}">
${MessageServer({ clientId: id })}
${Suspense(html`
<span>Message client component:</span>
${MessageClient({ clientId: id })}
`)}
<label for="username">Username:</label>
<input type="text" name="username" />
<input type="submit" value="submit" />
</form>
`;
},
error: async ({ error }) =>
html`<div>Received error: ${(error as any).localizedDescription}</div>`,
});
// TODO: useFormStatus example
// TODO: useOptimistic example
// TODO: Nested routes with Outlet() or with children props?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment