convex-helpers
Version:
A collection of useful code to complement the official convex package.
111 lines (110 loc) • 4.2 kB
JavaScript
"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();