Skip to content

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:

tsx
"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"

const itemShape = () => {
  if (typeof window !== `undefined`) {
    return {
      url: new URL(`/shape-proxy`, window?.location.origin).href,
      table: `items`,
    }
  } else {
    return {
      url: new URL(`https://not-sure-how-this-works.com/shape-proxy`).href,
      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:

ts
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) {
    headers.set(`Authorization`, `Bearer ${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.