UNPKG

convex-helpers

Version:

A collection of useful code to complement the official convex package.

111 lines (110 loc) 4.2 kB
"use client"; /** * React helpers for adding session data to Convex functions. * * !Important!: To use these functions, you must wrap your code with * ```tsx * <ConvexProvider client={convex}> * <SessionProvider> * <App /> * </SessionProvider> * </ConvexProvider> * ``` * * With the `SessionProvider` inside the `ConvexProvider` but outside your app. * * See the associated [Stack post](https://stack.convex.dev/track-sessions-without-cookies) * for more information. */ import React, { useCallback, useContext, useMemo, useState } from "react"; import { useQuery, useMutation, useAction } from "convex/react"; import { assert } from ".."; const SessionContext = React.createContext(null); /** * Context for a Convex session, creating a server session and providing the id. * * @param useStorage - Where you want your session ID to be persisted. Roughly: * - sessionStorage is saved per-tab * - localStorage is shared between tabs, but not browser profiles. * @param storageKey - Key under which to store the session ID in the store * @param idGenerator - Function to return a new, unique session ID string. Defaults to crypto.randomUUID * @returns A provider to wrap your React nodes which provides the session ID. * To be used with useSessionQuery and useSessionMutation. */ export const SessionProvider = ({ useStorage, storageKey, idGenerator, children }) => { const storeKey = storageKey ?? "convex-session-id"; const idGen = idGenerator ?? crypto.randomUUID.bind(crypto); const initialId = useMemo(() => idGen(), []); // Get or set the ID from our desired storage location. const useStorageOrDefault = useStorage ?? useSessionStorage; const [sessionId, setSessionId] = useStorageOrDefault(storeKey, initialId); const refreshSessionId = useCallback(async (beforeUpdate) => { const newSessionId = idGen(); if (beforeUpdate) { await beforeUpdate(newSessionId); } setSessionId(newSessionId); return newSessionId; }, [setSessionId]); return React.createElement(SessionContext.Provider, { value: { sessionId, refreshSessionId } }, children); }; // Like useQuery, but for a Query that takes a session ID. export function useSessionQuery(query, ...args) { const skip = args[0] === "skip"; const [sessionId] = useSessionId(); const originalArgs = args[0] === "skip" ? {} : args[0] ?? {}; const newArgs = skip ? "skip" : { ...originalArgs, sessionId }; return useQuery(query, ...[newArgs]); } // Like useMutation, but for a Mutation that takes a session ID. export function useSessionMutation(name) { const [sessionId] = useSessionId(); const originalMutation = useMutation(name); return useCallback((...args) => { const newArgs = { ...(args[0] ?? {}), sessionId, }; return originalMutation(...[newArgs]); }, [sessionId, originalMutation]); } // Like useAction, but for a Action that takes a session ID. export function useSessionAction(name) { const [sessionId] = useSessionId(); const originalAction = useAction(name); return useCallback((...args) => { const newArgs = { ...(args[0] ?? {}), sessionId, }; return originalAction(...[newArgs]); }, [sessionId, originalAction]); } export function useSessionId() { const ctx = useContext(SessionContext); if (ctx === null) { throw new Error("Missing a <SessionProvider> wrapping this code."); } return [ctx.sessionId, ctx.refreshSessionId]; } export function useSessionStorage(key, initialValue) { const [value, setValueInternal] = useState(() => { if (typeof sessionStorage !== "undefined") { const existing = sessionStorage.getItem(key); if (existing) { return existing; } sessionStorage.setItem(key, initialValue); } return initialValue; }); const setValue = useCallback((value) => { sessionStorage.setItem(key, value); setValueInternal(value); }, [key]); return [value, setValue]; } assert(); assert(); assert(); assert();