Core hook for managing WooCommerce session state with cart management, customer authentication, and loading state tracking.
The core hook for managing WooCommerce session state in headless applications. Provides cart management, customer authentication, and granular loading state tracking.
Note: If you're using
@woographql/next, session management is pre-configured for your project. The examples below are for reference and custom implementations.
The recommended way to use useSessionManager is to create application-specific SessionProvider and useSession exports using createSessionContext. This gives you full type safety with your own SessionData extension and codegen-generated types:
import { useContext, Context, PropsWithChildren } from 'react'; import { createSessionContext, useAsyncSessionManager, SessionData, SessionOperations, } from '@woographql/react-hooks'; import { AsyncTokenManagerInterface } from '@woographql/session-utils'; // 1. Extend SessionData with your app-specific fields export interface Data extends SessionData<AsyncTokenManagerInterface> { cartUrl: string; checkoutUrl: string; } // 2. Create a typed context and hook export const sessionContext = createSessionContext<Data, AsyncTokenManagerInterface>(); export const useSession = () => useContext(sessionContext); // 3. Create your provider — token manager and operations are internal export function SessionProvider({ children }: PropsWithChildren) { const tokenManager = useTokenManager(); // your app's token manager hook const operations = createSessionOperations(); // your app's operations factory const store = useAsyncSessionManager<Data>(tokenManager, operations, { startOnMount: true, initialState: { cartUrl: '/cart', checkoutUrl: '/checkout' }, }); const Provider = sessionContext.Provider as Context<Data>['Provider']; return <Provider value={store}>{children}</Provider>; }
Your components then import useSession from your own module, getting full type safety for all standard and custom fields without any typecasting.
useSessionManager (and useAsyncSessionManager for async token managers) creates a session store that:
isExecuting()type SessionManagerConfig<SD> = { initialState?: Partial<SD>; startOnMount?: boolean; /** Re-run `fetchSessionData` whenever this value changes — bridges * external auth-state changes (e.g. server actions, cookie writes) * into the in-memory store without unmounting the provider. */ revalidateKey?: unknown; }; // For synchronous token managers (localStorage) function useSessionManager<SD extends SessionData<TokenManagerInterface>>( tokenManager: TokenManagerInterface, operations: SessionOperations<SD>, config?: SessionManagerConfig<SD> ): SD; // For asynchronous token managers (cookies, server-side) function useAsyncSessionManager<SD extends SessionData<AsyncTokenManagerInterface>>( tokenManager: AsyncTokenManagerInterface, operations: SessionOperations<SD>, config?: SessionManagerConfig<SD> ): SD;
The hook moves through three phases on mount; each subsequent effect waits for the previous one to settle.
tokenManager.isReady() directly — it is not stored on session state and is not exposed to consumers. On first render the hook seeds a local isReady mirror from a synchronous read, then resolves the value asynchronously (handles both sync boolean and Promise<boolean> returns from the manager).startOnMount: true, the default) and the manager reports !isReady, the hook calls tokenManager.initializeSession() once, then re-reads isReady and flips the mirror when it resolves true.fetchSessionData() if either cart or customer is still null. revalidateKey re-runs the fetch on subsequent changes (see revalidateKey).Because readiness is read off the manager — not off context state — initialization stays self-correcting if consumers swap the manager instance, and there is no managerReady flag for callers to pass or maintain.
interface SessionData<TM extends TokenManagerBase> { // State isAuthenticated: boolean; cart: Cart | null; customer: Customer | null; started: boolean; // Methods setStarted(started: boolean): void; hasCredentials(): boolean | Promise<boolean>; isExecuting(operations?: string[]): boolean; // Operations fetchSessionData(): Promise<{ cart?, customer? } | null>; login(input: LoginMethod): Promise<Customer | null>; updateCart(cartAction: CartAction): Promise<Cart | null>; updateCustomer(input: CustomerAction): Promise<Customer | null>; logout(): void | Promise<void>; [key: string]: unknown; }
Operations are higher-order factories: (state, dispatch, tokenManager) => (...args) => result. The token manager is the same instance passed into the hook — use it inside an operation to read tokens, kick off a renewal, or hit the manager directly without threading a separate reference through your provider.
type SessionOperations<SD extends SessionData> = { fetchSessionData: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => () => Promise<{ cart?: Cart | null; customer?: Customer | null } | null>; login?: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => (input: LoginMethod) => Promise<Customer | null>; updateCart?: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => (cartAction: CartAction) => Promise<Cart | null>; updateCustomer?: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => (input: CustomerAction) => Promise<Customer | null>; logout?: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => (...args: unknown[]) => void | Promise<void>; [key: string]: (state: SD, dispatch: SessionDispatch<SD>, tokenManager: TM) => (...args: unknown[]) => unknown; };
The third parameter is optional in practice — TypeScript accepts a (state, dispatch) => fn factory wherever (state, dispatch, tokenManager) => fn is expected, so existing operations keep compiling.
Before invoking each operation, the hook checks tokenManager.isReady(). If the manager reports !isReady, the hook calls tokenManager.renewTokens(true) first, then runs the operation; a thrown renewal triggers session cleanup. This makes operations safe to call from UI without preflight checks.
logout is special-cased: the runtime forwards every argument from the caller to the consumer's logout factory output, so you can override logout(path: string) or pass a redirect target without losing it. Whatever the consumer returns is awaited, then the hook ends the session on the token manager and resets state.
fetchSessionData: (state, dispatch, tokenManager) => async () => { const { sessionToken, authToken } = await tokenManager.getTokens(); const headers: Record<string, string> = {}; if (sessionToken) headers['Cart-Token'] = sessionToken; if (authToken) headers['Authorization'] = `Bearer ${authToken}`; const res = await fetch('/api/session', { headers }); const { cart, customer } = await res.json(); dispatch({ type: 'UPDATE_STATE', payload: { cart, customer } }); return { cart, customer }; },
dispatch({ type: 'UPDATE_STATE', payload: { cart, customer } }); dispatch({ type: 'RESET_STATE' }); dispatch({ type: 'START_SESSION' });
Use intersection interfaces for shared fields:
query GetSession { cart { contents(first: 100) { itemCount nodes { key product { node { id databaseId name slug type ... on ProductWithPricing { price regularPrice salePrice } ... on InventoriedProduct { stockStatus soldIndividually } } } quantity total } } subtotal total(format: RAW) } customer { id firstName lastName email sessionToken } }
This example works with any GraphQL client:
// sessionOperations.ts import type { SessionOperations, SessionData } from '@woographql/react-hooks'; import type { AsyncTokenManagerInterface } from '@woographql/session-utils'; export interface AppSessionData extends SessionData<AsyncTokenManagerInterface> {} type GraphQLExecutor = <T>(query: string, variables?: Record<string, unknown>) => Promise<T>; export function createSessionOperations( execute: GraphQLExecutor ): SessionOperations<AppSessionData> { return { fetchSessionData: (state, dispatch) => async () => { const data = await execute<{ cart: Cart; customer: Customer }>(` query GetSession { cart { contents { itemCount nodes { key quantity } } total } customer { id firstName lastName email sessionToken } } `); dispatch({ type: 'UPDATE_STATE', payload: { cart: data.cart, customer: data.customer } }); return { cart: data.cart, customer: data.customer }; }, login: (state, dispatch) => async (input) => { const data = await execute<{ login: { customer: Customer } }>(` mutation Login($input: LoginInput!) { login(input: $input) { authToken refreshToken customer { id firstName lastName email } } } `, { input: input.input }); dispatch({ type: 'UPDATE_STATE', payload: { customer: data.login.customer } }); return data.login.customer; }, updateCart: (state, dispatch) => async (cartAction) => { const data = await execute<{ [key: string]: { cart: Cart } }>(` mutation AddToCart($input: AddToCartInput!) { addToCart(input: $input) { cart { contents { itemCount } total } } } `, { input: cartAction.input }); const cart = Object.values(data)[0].cart; dispatch({ type: 'UPDATE_STATE', payload: { cart } }); return cart; }, updateCustomer: (state, dispatch) => async (action) => { const data = await execute<{ updateCustomer: { customer: Customer } }>(` mutation UpdateCustomer($input: UpdateCustomerInput!) { updateCustomer(input: $input) { customer { id firstName lastName } } } `, { input: action.input }); dispatch({ type: 'UPDATE_STATE', payload: { customer: data.updateCustomer.customer } }); return data.updateCustomer.customer; }, logout: (state, dispatch) => async () => { // Clear tokens via your auth mechanism }, }; }
import { useSession } from './SessionProvider'; export function AccountPage() { const { customer, cart, isAuthenticated, isExecuting, logout } = useSession(); if (isExecuting()) return <div>Loading...</div>; if (!isAuthenticated) return <LoginForm />; return ( <div> <h1>Welcome, {customer?.firstName}</h1> <p>Items: {cart?.contents?.itemCount ?? 0}</p> <button onClick={() => logout()}>Sign Out</button> </div> ); }
export function CheckoutPage() { const { isExecuting } = useSession(); // Any operation running const isLoading = isExecuting(); // Specific operations const isUpdatingCart = isExecuting(['updateCart']); const isUpdatingCustomer = isExecuting(['updateCustomer']); return ( <button disabled={isLoading}> {isLoading ? 'Processing...' : 'Place Order'} </button> ); }
export function LoginForm() { const { login, isExecuting } = useSession(); const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); await login({ mutation: 'login', input: { username: formData.get('username') as string, password: formData.get('password') as string, }, }); }; return ( <form onSubmit={handleSubmit}> <input name="username" type="text" required /> <input name="password" type="password" required /> <button disabled={isExecuting(['login'])}>Sign In</button> </form> ); }
One approach is to use API routes to proxy GraphQL requests, keeping the endpoint hidden and managing session tokens server-side via HTTP cookies.
API Route Example:
// app/api/session/route.ts import { NextResponse, NextRequest } from 'next/server'; import { cookies } from 'next/headers'; import { GraphQLClient } from 'graphql-request'; const GRAPHQL_ENDPOINT = process.env.GRAPHQL_ENDPOINT!; export async function GET(request: NextRequest) { const cookieStore = await cookies(); const sessionToken = cookieStore.get('woo-session')?.value; const authToken = cookieStore.get('woo-auth')?.value; const client = new GraphQLClient(GRAPHQL_ENDPOINT); // Set headers for the GraphQL request if (authToken) { client.setHeader('Authorization', `Bearer ${authToken}`); } if (sessionToken) { client.setHeader('Cart-Token', sessionToken); } const { data, headers } = await client.rawRequest(` query GetSession { cart { contents { itemCount } total } customer { id firstName lastName email sessionToken } } `); // Save updated session token from response const newSessionToken = headers.get('Cart-Token'); if (newSessionToken && newSessionToken !== sessionToken) { cookieStore.set('woo-session', newSessionToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 14, // 14 days }); } return NextResponse.json({ cart: data.cart, customer: data.customer, }); }
// SessionProvider.tsx import { PropsWithChildren, useContext, Context } from 'react'; import { useAsyncSessionManager, createSessionContext, SessionData, SessionOperations, } from '@woographql/react-hooks'; import { AsyncTokenManager, AsyncTokenManagerInterface } from '@woographql/session-utils'; export interface AppSessionData extends SessionData<AsyncTokenManagerInterface> {} export const sessionContext = createSessionContext<AppSessionData, AsyncTokenManagerInterface>(); export const useSession = () => useContext(sessionContext); function createOperations(): SessionOperations<AppSessionData> { return { fetchSessionData: (state, dispatch) => async () => { const res = await fetch('/api/session'); const { cart, customer } = await res.json(); dispatch({ type: 'UPDATE_STATE', payload: { cart, customer } }); return { cart, customer }; }, login: (state, dispatch) => async (input) => { /* ... */ return null; }, updateCart: (state, dispatch) => async (action) => { /* ... */ return null; }, updateCustomer: (state, dispatch) => async (action) => { /* ... */ return null; }, logout: (state, dispatch) => async () => { /* ... */ }, }; } export function SessionProvider({ children, cookies, }: PropsWithChildren<{ cookies: { name: string; value: string }[] }>) { const tokenManager = new AsyncTokenManager({ ID: 'woo-session', cookies, startSession: async () => '', }); const operations = createOperations(); const store = useAsyncSessionManager(tokenManager, operations, { startOnMount: true }); const Provider = sessionContext.Provider as Context<AppSessionData>['Provider']; return <Provider value={store}>{children}</Provider>; }
// root.tsx import { useLoaderData } from '@remix-run/react'; import { SessionProvider } from './SessionProvider'; export const loader = async ({ request }) => { const cookieHeader = request.headers.get('Cookie') || ''; const cookies = cookieHeader.split(';').map(c => { const [name, value] = c.trim().split('='); return { name, value }; }).filter(c => c.name && c.value); return { cookies }; }; export default function App() { const { cookies } = useLoaderData<typeof loader>(); return ( <SessionProvider cookies={cookies}> <Outlet /> </SessionProvider> ); }
// useLocalStorageTokenManager.ts import { useState, useEffect } from 'react'; import { TokenManager, TokenManagerInterface } from '@woographql/session-utils'; export function useLocalStorageTokenManager(): TokenManagerInterface | null { const [manager, setManager] = useState<TokenManagerInterface | null>(null); useEffect(() => { if (typeof window === 'undefined') return; const tokenManager = new TokenManager({ ID: 'woo-session', behavior: ['AutoInitialize', 'AutoRenew'], startSession: async (tokens) => { const res = await fetch(import.meta.env.VITE_GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(tokens.sessionToken ? { 'Cart-Token': tokens.sessionToken } : {}), }, body: JSON.stringify({ query: `query { customer { sessionToken } }`, }), }); const { data } = await res.json(); return data?.customer?.sessionToken || ''; }, }); setManager(tokenManager); }, []); return manager; }
// SessionProvider.tsx import { PropsWithChildren, useContext, Context } from 'react'; import { useSessionManager, createSessionContext, SessionData, SessionOperations, } from '@woographql/react-hooks'; import { TokenManagerInterface } from '@woographql/session-utils'; export interface AppSessionData extends SessionData<TokenManagerInterface> {} export const sessionContext = createSessionContext<AppSessionData, TokenManagerInterface>(); export const useSession = () => useContext(sessionContext); function createOperations(tokenManager: TokenManagerInterface): SessionOperations<AppSessionData> { const endpoint = import.meta.env.VITE_GRAPHQL_ENDPOINT; const execute = async (query: string, variables?: Record<string, unknown>) => { const tokens = tokenManager.getTokens(); const headers: Record<string, string> = { 'Content-Type': 'application/json' }; if (tokens.sessionToken) headers['Cart-Token'] = tokens.sessionToken; if (tokens.authToken) headers['Authorization'] = `Bearer ${tokens.authToken}`; const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); const { data } = await res.json(); return data; }; return { fetchSessionData: (state, dispatch) => async () => { const data = await execute(` query { cart { contents { itemCount } total } customer { id firstName email } } `); dispatch({ type: 'UPDATE_STATE', payload: { cart: data.cart, customer: data.customer } }); return { cart: data.cart, customer: data.customer }; }, login: (state, dispatch) => async (input) => { /* ... */ return null; }, updateCart: (state, dispatch) => async (action) => { /* ... */ return null; }, updateCustomer: (state, dispatch) => async (action) => { /* ... */ return null; }, logout: (state, dispatch) => async () => { tokenManager.clearTokens(); }, }; } export function SessionProvider({ children, tokenManager, }: PropsWithChildren<{ tokenManager: TokenManagerInterface }>) { const operations = createOperations(tokenManager); const store = useSessionManager(tokenManager, operations, { startOnMount: true }); const Provider = sessionContext.Provider as Context<AppSessionData>['Provider']; return <Provider value={store}>{children}</Provider>; }
// App.tsx import { SessionProvider } from './SessionProvider'; import { useLocalStorageTokenManager } from './useLocalStorageTokenManager'; export function App({ children }) { const tokenManager = useLocalStorageTokenManager(); if (!tokenManager) { return <div>Initializing...</div>; } return ( <SessionProvider tokenManager={tokenManager}> {children} </SessionProvider> ); }
// Auto-start (default) useAsyncSessionManager(tokenManager, operations, { startOnMount: true }); // Manual start const store = useAsyncSessionManager(tokenManager, operations, { startOnMount: false }); store.setStarted(true); // Start later
useAsyncSessionManager(tokenManager, operations, { initialState: { cart: serverSideCart, customer: serverSideCustomer, }, });
A value of any shape that the hook compares between renders. When it
changes, the hook re-runs fetchSessionData (provided the store has
started and the token manager is ready). Use it to bridge external
auth-state changes into the in-memory session — e.g. a server action
flips a session cookie, a client-side watcher exposes the cookie value
as state, and that value is passed in here. Logged-in → logged-out,
logged-in → logged-in-as-different-user, and guest → logged-in all
trigger a re-sync as long as your key actually changes.
// Pair with a reactive cookie watcher const { tokenManager, sessionFlag } = useTokenManager(); useAsyncSessionManager(tokenManager, operations, { revalidateKey: sessionFlag, });
The first render is skipped so initial mount doesn't double-fetch; only subsequent changes (where the new value !== the previously-seen value) trigger the fetch.
The new key is marked "seen" after the fetch actually fires. A change that arrives before the token manager is ready or the store is started is held — once readiness flips, the held key still re-triggers the fetch instead of being silently swallowed.