From 946f1b7cccc1bf0bc33bb46122374c2ed2f4bc17 Mon Sep 17 00:00:00 2001 From: Valter Balegas Date: Fri, 25 Oct 2024 15:40:30 +0100 Subject: [PATCH] Initialise shapes with server data automatically --- examples/nextjs-ssr-example/app/Home.tsx | 42 +++++------------- examples/nextjs-ssr-example/app/page.tsx | 11 ++++- .../app/ssr-shapes-provider.ts | 43 +++++++++++++++++++ packages/react-hooks/src/react-hooks.tsx | 9 +++- 4 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 examples/nextjs-ssr-example/app/ssr-shapes-provider.ts diff --git a/examples/nextjs-ssr-example/app/Home.tsx b/examples/nextjs-ssr-example/app/Home.tsx index e6fe2af24f..a7586d2095 100644 --- a/examples/nextjs-ssr-example/app/Home.tsx +++ b/examples/nextjs-ssr-example/app/Home.tsx @@ -1,28 +1,19 @@ "use client" import { v4 as uuidv4 } from "uuid" -import { - useShape, - getShapeStream, - SerializedShapeData, -} from "@electric-sql/react" +import { useShape, getShapeStream } from "@electric-sql/react" import "./Example.css" import { matchStream } from "./match-stream" -import { Offset, ShapeStreamOptions } from "@electric-sql/client" +import { Row, ShapeStream, ShapeStreamOptions } from "@electric-sql/client" import { useOptimistic } from "react" -const ELECTRIC_URL = process.env.ELECTRIC_URL || "http://localhost:3000" +const ELECTRIC_URL = process.env.ELECTRIC_URL || `http://localhost:3000` const parser = { timestamptz: (date: string) => new Date(date).getTime(), } -const shapePosition: { shapeId?: string; offset?: Offset } = { - shapeId: undefined, - offset: `-1`, -} - -const shapeOptions: () => ShapeStreamOptions = () => { +const shapeOptions: () => ShapeStreamOptions = () => { if (typeof window !== `undefined`) { return { url: new URL(`/shape-proxy/items`, window?.location.origin).href, @@ -37,17 +28,12 @@ const shapeOptions: () => ShapeStreamOptions = () => { } } -const itemShape = () => ({ ...shapeOptions(), ...shapePosition }) - -const updateShapePosition = (offset: Offset, shapeId?: string) => { - shapePosition.offset = offset - shapePosition.shapeId = shapeId -} - type Item = { id: string; created_at: number } +const itemShape = (): ShapeStreamOptions => ({ ...shapeOptions() }) async function createItem(newId: string) { - const itemsStream = getShapeStream(itemShape()) + // FIX types later + const itemsStream = getShapeStream(itemShape()) as unknown as ShapeStream // Match the insert const findUpdatePromise = matchStream({ @@ -66,7 +52,9 @@ async function createItem(newId: string) { } async function clearItems() { - const itemsStream = getShapeStream(itemShape()) + // FIX types later + const itemsStream = getShapeStream(itemShape()) as unknown as ShapeStream + // Match the delete const findUpdatePromise = matchStream({ stream: itemsStream, @@ -82,17 +70,9 @@ async function clearItems() { return await Promise.all([findUpdatePromise, fetchPromise]) } -export default function Home({ - shapes, -}: { - shapes: { items: SerializedShapeData } -}) { - const { shapeId, offset, data } = shapes.items - updateShapePosition(offset, shapeId) - +export default function Home() { const { data: items } = useShape({ ...itemShape(), - shapeData: new Map(Object.entries(data ?? new Map())), parser, }) as unknown as { data: Item[] diff --git a/examples/nextjs-ssr-example/app/page.tsx b/examples/nextjs-ssr-example/app/page.tsx index 1b5b0a4c65..c95fe4e527 100644 --- a/examples/nextjs-ssr-example/app/page.tsx +++ b/examples/nextjs-ssr-example/app/page.tsx @@ -1,13 +1,20 @@ import React from "react" import { getSerializedShape } from "@electric-sql/react" import Home from "./Home" +import SSRShapesInitializer from "./ssr-shapes-provider" -const serverShapeOptions = { +const serverOptions = { url: new URL(`http://localhost:3000/v1/shape/items`).href, } const Page = async () => { - return + const data = getSerializedShape(serverOptions) + + return ( + + + + ) } export default Page diff --git a/examples/nextjs-ssr-example/app/ssr-shapes-provider.ts b/examples/nextjs-ssr-example/app/ssr-shapes-provider.ts new file mode 100644 index 0000000000..524fe0cad9 --- /dev/null +++ b/examples/nextjs-ssr-example/app/ssr-shapes-provider.ts @@ -0,0 +1,43 @@ +"use client" + +import { createContext } from "react" +import { SerializedShapeData, useShape } from "@electric-sql/react" +import { ShapeStreamOptions } from "@electric-sql/client/*" + +export const SSRShapesContext = createContext({}) + +type SerializedShape = { + serverOptions: ShapeStreamOptions + data: SerializedShapeData +} + +const getClientBaseUrl = () => window?.location.origin + +export default function SSRShapesInitializer({ + children, + serializedShapes, +}: { + children: React.ReactNode + serializedShapes: SerializedShape[] +}) { + if (typeof window === `undefined`) { + return children + } + + for (const { serverOptions, data } of serializedShapes) { + // FIX client url + const clientUrl = new URL(`/shape-proxy/items`, getClientBaseUrl()).href + const shapeOptions = { + ...serverOptions, + url: clientUrl, + offset: data.offset, + shapeId: data.shapeId, + } + + const shapeData = new Map(Object.entries(data.data ?? new Map())) + /* eslint-disable react-hooks/rules-of-hooks */ + useShape({ ...shapeOptions, shapeData }) + } + + return children +} diff --git a/packages/react-hooks/src/react-hooks.tsx b/packages/react-hooks/src/react-hooks.tsx index aee5dd90a3..116a20cacf 100644 --- a/packages/react-hooks/src/react-hooks.tsx +++ b/packages/react-hooks/src/react-hooks.tsx @@ -31,7 +31,14 @@ export async function preloadShape = Row>( } export function sortedOptionsHash(options: ShapeStreamOptions): string { - return JSON.stringify(options, Object.keys(options).sort()) + // Filter options that uniquely identify the shape. DISCUSS BEFORE MERGING + const uniqueShapeOptions = { + url: options.url, + where: options.where, + columns: options.columns, + headers: options.headers, + } + return JSON.stringify(uniqueShapeOptions, Object.keys(options).sort()) } export function getShapeStream>(