Skip to content
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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions examples/nextjs-ssr-example/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build/**
44 changes: 44 additions & 0 deletions examples/nextjs-ssr-example/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
`eslint:recommended`,
`plugin:@typescript-eslint/recommended`,
`plugin:prettier/recommended`,
`eslint-config-next`,
],
parserOptions: {
ecmaVersion: 2022,
requireConfigFile: false,
sourceType: `module`,
ecmaFeatures: {
jsx: true,
},
},
parser: `@typescript-eslint/parser`,
plugins: [`prettier`],
rules: {
quotes: [`error`, `backtick`],
"no-unused-vars": `off`,
"@typescript-eslint/no-unused-vars": [
`error`,
{
argsIgnorePattern: `^_`,
varsIgnorePattern: `^_`,
caughtErrorsIgnorePattern: `^_`,
},
],
"@next/next/no-img-element": `off`,

},
ignorePatterns: [
`**/node_modules/**`,
`**/dist/**`,
`tsup.config.ts`,
`vitest.config.ts`,
`.eslintrc.js`,
],
};
10 changes: 10 additions & 0 deletions examples/nextjs-ssr-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
dist
.env.local

# Turborepo
.turbo

# next.js
/.next/
/out/
next-env.d.ts
5 changes: 5 additions & 0 deletions examples/nextjs-ssr-example/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"semi": false,
"tabWidth": 2
}
22 changes: 22 additions & 0 deletions examples/nextjs-ssr-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Next.js SSR example

## Setup

1. Make sure you've installed all dependencies for the monorepo and built packages

From the root directory:

- `pnpm i`
- `pnpm run -r build`

2. Start the docker containers

`pnpm run backend:up`

3. Start the dev server

`pnpm run dev`

4. When done, tear down the backend containers so you can run other examples

`pnpm run backend:down`
25 changes: 25 additions & 0 deletions examples/nextjs-ssr-example/app/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.App {
text-align: center;
}

.App-logo {
height: min(160px, 30vmin);
pointer-events: none;
margin-top: min(30px, 5vmin);
margin-bottom: min(30px, 5vmin);
}

.App-header {
background-color: #1c1e20;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: top;
justify-content: top;
font-size: calc(10px + 2vmin);
color: white;
}

.App-link {
color: #61dafb;
}
41 changes: 41 additions & 0 deletions examples/nextjs-ssr-example/app/Example.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.controls {
margin-bottom: 1.5rem;
}

.button {
display: inline-block;
line-height: 1.3;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
user-select: none;
width: calc(15vw + 100px);
margin-right: 0.5rem !important;
margin-left: 0.5rem !important;
border-radius: 32px;
text-shadow: 2px 6px 20px rgba(0, 0, 0, 0.4);
box-shadow: rgba(0, 0, 0, 0.5) 1px 2px 8px 0px;
background: #1e2123;
border: 2px solid #229089;
color: #f9fdff;
font-size: 16px;
font-weight: 500;
padding: 10px 18px;
}

.item {
display: block;
line-height: 1.3;
text-align: center;
vertical-align: middle;
width: calc(30vw - 1.5rem + 200px);
margin-right: auto;
margin-left: auto;
border-radius: 32px;
border: 1.5px solid #bbb;
box-shadow: rgba(0, 0, 0, 0.3) 1px 2px 8px 0px;
color: #f9fdff;
font-size: 13px;
padding: 10px 18px;
}
156 changes: 156 additions & 0 deletions examples/nextjs-ssr-example/app/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"use client"

import { v4 as uuidv4 } from "uuid"
import {
useShape,
getShapeStream,
SerializedShapeData,
} from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"
import { Offset, ShapeStreamOptions } from "@electric-sql/client"
import { useOptimistic } from "react"

const parser = {
timestamptz: (date: string) => new Date(date).getTime(),
}

const shapePosition: { shapeId?: string; offset?: Offset } = {
shapeId: undefined,
offset: `-1`,
}

const baseItemShape: () => ShapeStreamOptions = () => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy/items`, window?.location.origin).href,
}
} else {
const controller = new AbortController()
controller.abort()
return {
url: new URL(`https://not-sure-how-this-works.com/shape-proxy/items`)
.href,
signal: controller.signal,
subscribe: false,
}
}
}
const itemShape = () => ({ ...baseItemShape(), ...shapePosition })

const updateShapePosition = (offset: Offset, shapeId?: string) => {
shapePosition.offset = offset
shapePosition.shapeId = shapeId
}

type Item = { id: string; created_at: number }

async function createItem(newId: string) {
const itemsStream = getShapeStream(itemShape())

// Match the insert
const findUpdatePromise = matchStream({
stream: itemsStream,
operations: [`insert`],
matchFn: ({ message }) => message.value.id === newId,
})

// Generate new UUID and post to backend
const fetchPromise = fetch(`/api/items`, {
method: `POST`,
body: JSON.stringify({ uuid: newId }),
})

return await Promise.all([findUpdatePromise, fetchPromise])
}

async function clearItems() {
const itemsStream = getShapeStream(itemShape())
// Match the delete
const findUpdatePromise = matchStream({
stream: itemsStream,
operations: [`delete`],
// First delete will match
matchFn: () => true,
})
// Post to backend to delete everything
const fetchPromise = fetch(`/api/items`, {
method: `DELETE`,
})

return await Promise.all([findUpdatePromise, fetchPromise])
}

export default function Home({
shapes,
}: {
shapes: { items: SerializedShapeData }
}) {
const { shapeId, offset, data } = shapes.items
updateShapePosition(offset, shapeId)

const { data: items } = useShape({
...itemShape(),
shapeData: new Map(Object.entries(data ?? new Map())),
parser,
}) as unknown as {
data: Item[]
}

const [optimisticItems, updateOptimisticItems] = useOptimistic<
Item[],
{ newId?: string; isClear?: boolean }
>(items, (state, { newId, isClear }) => {
if (isClear) {
return []
}

if (newId) {
// Merge data from shape & optimistic data from fetchers. This removes
// possible duplicates as there's a potential race condition where
// useShape updates from the stream slightly before the action has finished.
const itemsMap = new Map()
state
.concat([{ id: newId, created_at: new Date().getTime() }])
.forEach((item) => {
itemsMap.set(item.id, { ...itemsMap.get(item.id), ...item })
})
return Array.from(itemsMap.values())
}

return []
})

return (
<div>
<form
action={async (formData: FormData) => {
const intent = formData.get(`intent`)
const newId = formData.get(`new-id`) as string
if (intent === `add`) {
updateOptimisticItems({ newId })
await createItem(newId)
} else if (intent === `clear`) {
updateOptimisticItems({ isClear: true })
await clearItems()
}
}}
>
<input type="hidden" name="new-id" value={uuidv4()} />
<button type="submit" className="button" name="intent" value="add">
Add
</button>
<button type="submit" className="button" name="intent" value="clear">
Clear
</button>
</form>
{optimisticItems
.sort((a, b) => a.created_at - b.created_at)
.map((item: Item, index: number) => (
<p key={index} className="item">
<code>{item.id}</code>
</p>
))}
</div>
)
}
17 changes: 17 additions & 0 deletions examples/nextjs-ssr-example/app/api/items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { db } from "../../db"
import { NextResponse } from "next/server"

export async function POST(request: Request) {
const body = await request.json()
const result = await db.query(
`INSERT INTO items (id)
VALUES ($1) RETURNING id;`,
[body.uuid]
)
return NextResponse.json({ id: result.rows[0].id })
}

export async function DELETE() {
await db.query(`DELETE FROM items;`)
return NextResponse.json(`ok`)
}
14 changes: 14 additions & 0 deletions examples/nextjs-ssr-example/app/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pgPkg from "pg"
const { Client } = pgPkg

const db = new Client({
host: `localhost`,
port: 54321,
password: `password`,
user: `postgres`,
database: `electric`,
})

db.connect()

export { db }
26 changes: 26 additions & 0 deletions examples/nextjs-ssr-example/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import "./style.css"
import "./App.css"

export const metadata = {
title: `Next.js Forms Example`,
description: `Example application with forms and Postgres.`,
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<div className="App">
<header className="App-header">
<img src="/logo.svg" className="App-logo" alt="logo" />
{children}
</header>
</div>
</body>
</html>
)
}
Loading
Loading