Sessions have a limited lifetime and will eventually expire. How you handle session expiration significantly impacts application performance and user experience. Below are the different approaches, organized from best to worst practice.
Recommended: Stateful with Proactive Validation
This is the best approach for production applications.
Store the session (in localStorage, cookies, or memory) and proactively validate its expiration before displaying the checkout form.
Implementation:
When creating a session on the server, store both the session token and its expiration time
Before displaying the checkout form, check if the stored session is still valid
If the session has expired, create a new session first
Only then display the checkout form with the valid session
Example:
Next.js (Client Component)
Vanilla TypeScript
'use client'
import { useEffect , useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'
interface StoredSession {
id : string
expiryDate : string
}
const CheckoutForm : React . FC = () => {
const [ clientToken , setClientToken ] = useState < string >( '' )
useEffect (() => {
let checkout : PayNextCheckout | undefined
const mountCheckout = async ( token : string ) => {
if ( ! token ) return
checkout = new PayNextCheckout ()
await checkout . mount ( 'paynext-checkout' , {
clientToken: token ,
environment: 'sandbox' ,
// ... other checkout config options
})
}
const validateAndMountCheckout = async () => {
// Get stored session from localStorage
const storedSessionData = localStorage . getItem ( 'paynext_session' )
let storedSession : StoredSession | null = null
if ( storedSessionData ) {
try {
storedSession = JSON . parse ( storedSessionData )
} catch ( e ) {
console . error ( 'Failed to parse stored session' , e )
}
}
// Validate session expiration
if ( storedSession && new Date ( storedSession . expiryDate ) > new Date ()) {
// Session is still valid - use it
console . info ( 'Using existing valid session' )
setClientToken ( storedSession . id )
} else {
// Session expired or doesn't exist - create new one
console . info ( 'Session expired or missing, creating new session' )
try {
const response = await fetch ( '/api/client-session' , { method: 'POST' , body: JSON . stringify ( payload ) })
const data = await response . json ()
// Store new session with expiry date
localStorage . setItem ( 'paynext_session' , JSON . stringify ({
id: data . id ,
expiryDate: data . expiry_date ,
}))
setClientToken ( data . id )
} catch ( error ) {
console . error ( 'Failed to create session' , error )
}
}
}
if ( ! clientToken ) {
validateAndMountCheckout ()
} else {
mountCheckout ( clientToken )
}
return () => {
checkout ?. unmount ()
}
}, [ clientToken ])
return < div id = 'paynext-checkout' />
}
export default CheckoutForm
Benefits:
Fast render time - form displays immediately with a valid session
Best user experience - no errors or delays
Optimal performance - reuses valid sessions, creates new ones only when needed
Optional: Reactive Error Handling
Use this only if you cannot implement proactive validation.
Do not validate session lifetime upfront. Instead, handle expiration errors reactively when they occur.
Implementation:
Display the checkout form with the existing session
When the session expires, the form will return an error
Catch this error, create a new session, and re-render the form
Example:
Next.js (Client Component)
Vanilla TypeScript
'use client'
import { useEffect , useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'
const CheckoutForm : React . FC = () => {
const [ clientToken , setClientToken ] = useState < string >( '' )
const [ sessionError , setSessionError ] = useState < boolean >( false )
useEffect (() => {
let checkout : PayNextCheckout | undefined
const mountCheckout = async ( token : string ) => {
if ( ! token ) return
checkout = new PayNextCheckout ()
await checkout . mount ( 'paynext-checkout' , {
clientToken: token ,
environment: 'sandbox' ,
onCheckoutLoaded : ( result ) => {
if ( ! result . success ) {
// Check if error is due to expired session
if ( result . error ?. status === 'SESSION_EXPIRED' ) {
console . warn ( 'Session expired, creating new one' )
setSessionError ( true )
return
}
console . error ( 'Checkout loading failed:' , result . error ?. status_reason ?. message )
return
}
console . info ( 'Checkout loaded successfully' )
setSessionError ( false )
},
onCheckoutFail : ( error ) => {
// Handle session expiration during payment
if ( error . status === 'SESSION_EXPIRED' ) {
console . warn ( 'Session expired during payment' )
setSessionError ( true )
return
}
console . error ( 'Payment failed:' , error . status , error . status_reason ?. message )
},
// ... other checkout config options
})
}
const createNewSession = async () => {
try {
const response = await fetch ( '/api/client-session' , { method: 'POST' , body: JSON . stringify ( payload ) })
const data = await response . json ()
// Optionally store session (though not validating expiry)
localStorage . setItem ( 'paynext_session' , JSON . stringify ({
id: data . id ,
expiryDate: data . expiry_date ,
}))
setClientToken ( data . id )
} catch ( error ) {
console . error ( 'Failed to create session' , error )
}
}
if ( sessionError ) {
// Session expired - create new one and remount
checkout ?. unmount ()
createNewSession ()
} else if ( ! clientToken ) {
// Initial load - get stored session or create new one
const storedSessionData = localStorage . getItem ( 'paynext_session' )
if ( storedSessionData ) {
try {
const storedSession = JSON . parse ( storedSessionData )
setClientToken ( storedSession . id )
} catch ( e ) {
createNewSession ()
}
} else {
createNewSession ()
}
} else {
mountCheckout ( clientToken )
}
return () => {
checkout ?. unmount ()
}
}, [ clientToken , sessionError ])
return < div id = 'paynext-checkout' />
}
export default CheckoutForm
Drawbacks:
User sees an error before the form reloads
Requires additional error handling logic
Suboptimal user experience (error → reload flow)
While this approach works, it creates unnecessary friction for users. Use proactive validation whenever possible.
Not Recommended: Stateless (No Session Storage)
Avoid this approach in production applications.
Never store the session anywhere. Always generate a new session every time you need to display the checkout form.
Implementation:
Next.js (Client Component)
Vanilla TypeScript
'use client'
import { useEffect , useState } from 'react'
import { PayNextCheckout } from '@paynext/sdk'
import '@paynext/sdk/styles'
const CheckoutForm : React . FC = () => {
const [ clientToken , setClientToken ] = useState < string >( '' )
useEffect (() => {
let checkout : PayNextCheckout | undefined
const mountCheckout = async ( token : string ) => {
if ( ! token ) return
checkout = new PayNextCheckout ()
await checkout . mount ( 'paynext-checkout' , {
clientToken: token ,
environment: 'sandbox' ,
// ... other checkout config options
})
}
// ❌ BAD: Always create new session - no storage, no reuse
const createAndMountCheckout = async () => {
try {
// Every component mount creates a new session
const response = await fetch ( '/api/client-session' , { method: 'POST' , body: JSON . stringify ( payload ) })
const data = await response . json ()
// Not storing the session anywhere - it's lost after unmount
setClientToken ( data . id )
} catch ( error ) {
console . error ( 'Failed to create session' , error )
}
}
if ( ! clientToken ) {
createAndMountCheckout () // Slow network request every time
} else {
mountCheckout ( clientToken )
}
return () => {
checkout ?. unmount ()
// Session is lost here - next mount will create another one
}
}, [ clientToken ])
return < div id = 'paynext-checkout' />
}
export default CheckoutForm
Why This is Bad:
Slow render time - must wait for server request before every form display
Increased server load - creates unnecessary sessions
Poor performance - network latency delays form appearance
Bad UX - users wait longer for the form to appear
Do not use this approach. It significantly degrades performance and user experience. Always store and reuse valid sessions.
Best Practice: Always implement stateful session management with proactive expiration validation for optimal performance and user experience.