Skip to content

On the client side we essentially run through the following steps:

1. Provide the user session

We are not going to discuss authentication here, so this all assumes the user has been authenticated on the server.

tsx
"use client";

import { useEffect } from "react";
import type { Info as SessionInfo } from "~/actions/session";
import { isSessionInfo, session$ } from "~/stores/session";

type Props = React.PropsWithChildren<Partial<SessionInfo>>;  

export default function SessionProvider({ children, ...session }: Props) {
	useEffect(() => {
		if (isSessionInfo(session)) session$.provide(session);
	}, [session]);

	return children;

}

We provide the session observable the server side session data. And we persist this to localStorage. We do not persist the accessToken or refreshToken, it exists only in memory.

2. Create databases and connections

We have another provider to help create the environment for a certain user in a certain workspace.

This involves waiting for the observables to be ready, and for the syncing of those to be ready. You can see here that we await Workspace.ready.

tsx
"use client";

import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { session$, syncSessionData } from "~/stores/session";
import { Workspace } from "~/stores/workspace";

export function SessionProvider({
	children,
	workspace,
}: {
	children: React.ReactNode;
	workspace: string;
}) {
	const [isLoading, setIsLoading] = useState(true);
	const router = useRouter();

	useEffect(() => {
		const initSession = async () => {
			try {
				await syncSessionData();

				session$.set((prev) => ({
					...prev,
					currentWorkspace: prev.workspaces?.find((x) => x.url === workspace),
				}));

				await Workspace.ready;

				const unsubscribe = session$.state.onChange(({ value }) => {
					if (value === "authenticated" || value === "unauthenticated") {
						setIsLoading(false);
					}
				});

				const currentState = session$.state.get();

				if (
					currentState === "authenticated" ||
					currentState === "unauthenticated"
				) {
					setIsLoading(false);
				}

				if (currentState === "unauthenticated") {
					router.push("/login");
				}

				return unsubscribe;
			} catch (error) {
				console.error("Session sync error:", error);
				setIsLoading(false);
			}
		};

		initSession();
	}, [workspace]);

	if (isLoading) {
		return (
			<motion.div
				initial={{ opacity: 0 }}
				animate={{ opacity: 1 }}
				className="flex items-center justify-center min-h-screen"
			>
				<div className="text-center">
					<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
					<p className="text-gray-600">Loading session...</p>
				</div>
			</motion.div>
		);
	}

	return (
		<motion.div
			initial={{ opacity: 0 }}
			animate={{ opacity: 1 }}
			exit={{ opacity: 0 }}
			transition={{ duration: 0.25 }}
		>
			{children}
		</motion.div>
	);
}

3. Creating a data store

To create your own datastore there are several tools that are designed to help you do this. We are not going to discuss the backend, just the client.

tsx
const store = Store.create<Workspace, WorkspaceRow>({
	persist: { name: "workspace" },
	shape: shapeConfig("workspace", {
		parser: {
			timestamptz: (date: string) => new Date(date),
		},
	}),
	userID: session$.userID,
	workspaceID: session$.currentWorkspace.id,
	transform: (row) => {
		return {
			...row,
			createdAt: row.time_created,
			updatedAt: row.time_updated,
			deletedAt: row.time_deleted,
		};
	},
});

The datastore accepts 2 type arguments.

  1. The type that the documents will be.
  2. The type of the document being streamed in from electric.

You can then provide some arguments:

  1. persist - the name and other information for IndexDB. [link]
  2. shape - Use the shapeConfig helper, that will handle most situations for a given table.
  3. userID - An observable representing the session's userID
  4. workspaceID - An observable representing the session's workspaceID
  5. transform - A function that turns the streamed data into the final data structure. Others that are not in this example:
  6. create
  7. update
  8. delete