-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Next.js SSR example #1596
base: main
Are you sure you want to change the base?
Next.js SSR example #1596
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few comments.
Don't you want to use either https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props or https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props here? Right now I don't see that any data is being cached during server rendering.
EDIT: Oh I see... this is RSC stuff
64f3703
to
2d933e8
Compare
✅ Deploy Preview for electric-next ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
First, I think there is going to be two ways people want to do SSR with
I think we want to provide tooling for both of these cases. On 1 - snapshot of the shape and resumeWhile the pattern in this PR works, having to pass the shape snapshot to the component using I think this is doable if we create a We are somewhat restricted as we cant use a react context, but something like this POC is possible: https://github.com/samwillis/ssr-shape-poc (I've purposely not used any electric, just wanted to test the concept for injection) By the user adding This is also not tied to Next, and should work with all React SSR frameworks. On 2 - a minimal subset on the serverI think we should add a useShape({
url: `http://localhost:3000/v1/shape/foo`,
getServerSnapshot: () => {
// Do and return a Postgres query
},
}) |
Great stuff @samwillis :) On 2, I wonder if there's a way of supporting |
02ffd0d
to
dd64a75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samwillis inspired on your example and Nextjs docs, I gave another try. The idea here is to have a context around the component that takes the server-sent data from props and initializes the shapes on the client --- totally transparent on the client component!
Left some comments
@samwillis, @thruflo my initial idea was to call the database directly on the server. This is a powerful pattern to show, but because of the problems with ordering and paging, I thought it would be better to build just on Shapes. The benefit of Shapes is that requests go through the cache and doesn't put load on the server db. A naive way of rendering the subset of data on the server is to just drop the data that doesn't fit into the initial page rendering :) |
887f224
to
2d8330e
Compare
@samwillis , I did a DX refactoring addressing your concerns with it being simple to specify the shapes across server and client. Have a look at it or ping me to see together. I also want to replace the base nextjs example with this one (cc @KyleAMathews) |
0eccfa5
to
1b3880d
Compare
1b3880d
to
dc9da79
Compare
Examples |
@@ -62,7 +62,7 @@ jobs: | |||
|
|||
|
|||
- name: Deploy NextJs example | |||
working-directory: examples/nextjs-example | |||
working-directory: examples/nextjs-ssr-example |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a naming nit: there's no need to suffix with -example
, that's communicated by the parent folder name, so I would suggest examples/nextjs-ssr
.
@@ -0,0 +1,22 @@ | |||
# Next.js SSR example |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, the README for an example should ideally communicate a bit about what it's trying to show you, i.e.: what is the example, why is it interesting, what can you achieve with the pattern it demonstrates, etc. Plus perhaps some signposts into key parts of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will work on that when we decide to publish
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're purple nowadays/
} from '@electric-sql/client' | ||
import React from 'react' | ||
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector.js' | ||
|
||
type UnknownShape = Shape<Row<unknown>> | ||
type UnknownShapeStream = ShapeStream<Row<unknown>> | ||
export type SerializedShape = { | ||
offset: Offset | ||
shapeHandle: string | undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another very minor nit but seeings how this is in the react-hooks library not just the example, I've been trying to move us towards just using handle
rather than shapeHandle
.
Below when you getSerialisedShape you can also just read it from shape.handle
rather than shapeStream.shapeHandle
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, i started this a while back, things like the offset were not public at the time. Will cleanup.
const shape = getShape(shapeStream) | ||
return { | ||
shapeHandle: shapeStream.shapeHandle, | ||
offset: shapeStream.offset, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does offset
work? Is the property not lastOffset
?
@@ -245,6 +245,10 @@ export class ShapeStream<T extends Row<unknown> = Row> | |||
return this.#shapeHandle | |||
} | |||
|
|||
get offset() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is duplicated now -- we already expose lastOffset as a public getter.
I’ve read over the comments on the front page. Not dug into the files changed yet. I believe this approach should be using hydration and suspense. https://react.dev/reference/react-dom/client/hydrateRoot https://react.dev/reference/react/Suspense An excellent reference / role model would be Tanstack Query. Currently I use it and tRPC. My current solution using Tanstack Query and tRPC is; import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton";
import { HydrateClient, tRPCServerApi } from "@/trpc/server";
import { Suspense } from "react";
import { SiteDataTable } from "../components/site-table/site-data-table";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Company Sites",
description: "Company sites."
};
export default async function CompanySites(props: {
params: Promise<{ id: string }>;
}) {
const params = await props.params;
await tRPCServerApi.site.getAllCompanySites.prefetch({
companyId: params.id
});
return (
<HydrateClient>
<Suspense
fallback={
<div className="space-y-4 p-2 pt-8">
<DataTableSkeleton
columnCount={5}
searchableColumnCount={1}
filterableColumnCount={1}
cellWidths={["12rem", "14rem"]}
withPagination={true}
shrinkZero
/>
</div>
}
>
<SiteDataTable companyId={params.id} />
</Suspense>
</HydrateClient>
);
} Here you can see this page.tsx allows me to grab the data whilst the page it’s self is requested. The page is sent to the client immediately with the fullback. Once the data is obtained on the server it is streamed to the frontend and gets hydrated. I also use the new partial pre-rendering. Which works very well in this case. As the initial page and the fallback is stored in a CDN and the page loads instantly. Again, because the request for the page is handled by the server sending back the CDN result and later hydrating the client works seamlessly. Inside the SiteDataTable, which is a client component, we use the normal useQuery hook. But since we’re using it from within the HydrateClient boundary when the useQuery triggers on the client instead of performing a round trip to the server to fetch the data it instead caches the hydration cache and loads the data from there. Again because the data was requested at the same time as the page and streamed into the hydration cache the data is already in the client ready to be hydrated. const [data] = trpcClient.site.getAllCompanySites.useSuspenseQuery({
companyId
}); Then in the client the useQuery call is actually done using useSuspenseQuery. As this will look in the hydration cache. If the prefetch call wasn’t made it will just fetch the data as usual as well. At the moment I am considering ElectricSQL vs tRPC streaming both use server sent events (SSE) and currently given the above code I already use tRPC offers a nice fit with the existing codebase and a very nice API. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work 👍
@@ -0,0 +1,120 @@ | |||
"use client" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In nextJS the file naming conventions are lower kebab case.
@@ -0,0 +1,120 @@ | |||
"use client" | |||
|
|||
import { v4 as uuidv4 } from "uuid" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid external dependencies. You do not need one for a cryptographically secure uuid anymore.
https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
} | ||
|
||
export default function Home() { | ||
const { data: items } = useShape(getProxiedOptions(options)) as unknown as { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As unknown as any? Can generics not be used here?
const itemsMap = new Map() | ||
state | ||
.concat([{ id: newId, created_at: new Date().getTime() }]) | ||
.forEach((item) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear | ||
</button> | ||
</form> | ||
{optimisticItems |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using sort and map directly in the render response. Use a cost above with useMemo.
Also do not use index as the key.
https://react.dev/learn/rendering-lists#why-does-react-need-keys
description: `Example application with forms and Postgres.`, | ||
} | ||
|
||
export default function RootLayout({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React.PropsWithChildren
table: `items`, | ||
} | ||
|
||
const Page = async () => ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this async? Does the shape can use react hydrate?
children: React.JSX.Element | ||
options: SerializedShapeOptions[] | ||
}) { | ||
const clientShapes: SerializedShape[] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use useMemo
@@ -0,0 +1,44 @@ | |||
export async function GET(request: Request) { | |||
const url = new URL(request.url) | |||
const originUrl = new URL( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaner to just throw if the environment variable is missing or use an escape guard.
// ensure shape is not syncing on the server | ||
const serverOptions: Partial<ShapeStreamOptions> = {} | ||
if (typeof window === `undefined`) { | ||
const controller = new AbortController() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using fetch and AbortControllers adds a lot of bloat. Maybe use useQuery which has solved this problems already and provides a beautiful API.
Tanstack react query
Items example using SSR.
Load the entire shape on the server and send it down to the client, so that the client gets the initial state of the shape on first response and can resume replication from the tip of the shape stream.
First time with nextjs, I still don't know how some things work. I suppose this is good forn an example. I'm happy to receive feedback from nextjs experts :D