Remix
Example of an Electric app using Remix.
Remix example app
This is an example using Electric with Remix.
The entrypoint for the Electric-specific code is in ./app/routes/_index.tsx
:
tsx
import { useShape, preloadShape, getShapeStream } from "@electric-sql/react"
import { useFetchers, Form } from "@remix-run/react"
import { v4 as uuidv4 } from "uuid"
import type { ClientActionFunctionArgs } from "@remix-run/react"
import "../Example.css"
import { matchStream } from "../match-stream"
const itemShape = () => {
return {
url: new URL(`/shape-proxy`, window.location.origin).href,
params: {
table: `items`,
},
}
}
type Item = { id: string }
export const clientLoader = async () => {
return await preloadShape(itemShape())
}
export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
const body = await request.formData()
const itemsStream = getShapeStream<Item>(itemShape())
if (body.get(`intent`) === `add`) {
// Match the insert
const findUpdatePromise = matchStream({
stream: itemsStream,
operations: [`insert`],
matchFn: ({ message }) => message.value.id === body.get(`new-id`),
})
// Generate new UUID and post to backend
const fetchPromise = fetch(`/api/items`, {
method: `POST`,
body: JSON.stringify({ uuid: body.get(`new-id`) }),
})
return await Promise.all([findUpdatePromise, fetchPromise])
} else if (body.get(`intent`) === `clear`) {
// 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 Example() {
const { data: items } = useShape<Item>(itemShape())
const submissions = useFetchers()
.filter((fetcher) => fetcher.formData?.get(`intent`) === `add`)
.map((fetcher) => {
return { id: fetcher.formData?.get(`new-id`) } as Item
})
const isClearing = useFetchers().some(
(fetcher) => fetcher.formData?.get(`intent`) === `clear`
)
// 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()
items.concat(submissions).forEach((item) => {
itemsMap.set(item.id, { ...itemsMap.get(item.id), ...item })
})
return (
<div>
<Form navigate={false} method="POST" className="controls">
<input type="hidden" name="new-id" value={uuidv4()} />
<button className="button" name="intent" value="add">
Add
</button>
<button className="button" name="intent" value="clear">
Clear
</button>
</Form>
{isClearing
? ``
: [...itemsMap.values()].map((item: Item, index: number) => (
<p key={index} className="item">
<code>{item.id}</code>
</p>
))}
</div>
)
}
export function HydrateFallback() {
return ``
}