Next.js
Next.js is a full-stack React framework.
Electric and Next.js
Next.js is based on React. Electric works with React. You can integrate Electric into your Next.js application like any other npm / React library.
Examples
Next.js example
See the Nextjs example on GitHub. This demonstrates using Electric for read-path sync and a Next.js API for handling writes:
"use client"
import { v4 as uuidv4 } from "uuid"
import { useOptimistic } from "react"
import { useShape, getShapeStream } from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"
import { ShapeStreamOptions } from "@electric-sql/client/*"
const itemShape = (): ShapeStreamOptions => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy`, window?.location.origin).href,
params: {
table: `items`,
},
}
} else {
return {
url: new URL(`https://not-sure-how-this-works.com/shape-proxy`).href,
params: {
table: `items`,
},
}
}
}
type Item = { id: string }
async function createItem(newId: string) {
const itemsStream = getShapeStream<Item>(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<Item>(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() {
const { data: items } = useShape<Item>(itemShape())
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 }]).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.map((item: Item, index: number) => (
<p key={index} className="item">
<code>{item.id}</code>
</p>
))}
</div>
)
}
It also demonstrates using a shape-proxy endpoint for proxying access to the Electric sync service. This allows you to implement auth and routing in-front-of Electric (and other concerns like transforming or decrypting the stream) using your Next.js backend:
export async function GET(request: Request) {
const url = new URL(request.url)
const originUrl = new URL(
process.env.ELECTRIC_URL
? `${process.env.ELECTRIC_URL}/v1/shape`
: `http://localhost:3000/v1/shape`
)
url.searchParams.forEach((value, key) => {
originUrl.searchParams.set(key, value)
})
if (process.env.DATABASE_ID) {
originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID)
}
const headers = new Headers()
if (process.env.ELECTRIC_TOKEN) {
originUrl.searchParams.set(`token`, process.env.ELECTRIC_TOKEN)
}
const newRequest = new Request(originUrl.toString(), {
method: `GET`,
headers,
})
// When proxying long-polling requests, content-encoding & content-length are added
// erroneously (saying the body is gzipped when it's not) so we'll just remove
// them to avoid content decoding errors in the browser.
//
// Similar-ish problem to https://github.com/wintercg/fetch/issues/23
let resp = await fetch(newRequest)
if (resp.headers.get(`content-encoding`)) {
const headers = new Headers(resp.headers)
headers.delete(`content-encoding`)
headers.delete(`content-length`)
resp = new Response(resp.body, {
status: resp.status,
statusText: resp.statusText,
headers,
})
}
return resp
}
ElectroDrizzle
ElectroDrizzle is an example application by Leon Alvarez using Next.js, Drizzle, PGLite and Electric together.
See the Getting Started guide here.
SSR
Next.js supports SSR. We are currently experimenting with patterns to use Electric with SSR in a way that supports server rendering and client-side components seamlessly moving into realtime sync.
Help wanted Good first issue
We have a pull request open if you'd like to contribute to improving our Next.js documentation, patterns and framework integrations.
Please leave a comment or ask on Discord if you'd like any pointers or to discuss how best to approach this.