Fintech applications often rely on real-time quotes that expire within minutes, creating a unique challenge for frontend developers. A well-designed payout confirmation flow must synchronize user input, quote expiration, and form validation without introducing performance bottlenecks or memory leaks. The solution lies in separating concerns, using the right state management tools, and implementing clean, reusable components.
The Core Challenge: Synchronizing Multiple Flows
A payout confirmation flow typically involves three interdependent tasks that must remain in sync:
- Displaying a dynamic form based on user-selected options, such as route or channel preferences.
- Fetching a quote from the backend and tracking its expiration time.
- Preventing expired quotes from being submitted at any stage of the process.
Each of these tasks introduces complexity. Forms must adapt to user choices, timers must update in real time, and validation must occur at multiple checkpoints. Failing to address these properly can lead to poor user experience, financial discrepancies, or even failed transactions.
Dividing State: Leveraging TanStack Query and Redux
State management is critical in this scenario. Server state (e.g., quotes, fees, route configurations) behaves differently from client-side state (e.g., countdown timers, derived values). Mixing them can cause unnecessary re-renders and performance issues.
I opted for a hybrid approach:
- TanStack Query manages server state, including the quote object, fees, and route configurations. This provides built-in caching, loading states, and error handling, reducing redundant API calls.
- Redux handles client-derived state, such as the quote expiration timestamp and countdown duration. This ensures a single source of truth for the clock that starts when the quote is generated.
The key insight was recognizing that quoteExpiresAt should not be derived from query responses on every render. Instead, it should be dispatched to Redux once at quote creation, providing a stable anchor for the countdown. This approach allows multiple components—such as the quote summary, submit button, and route guard—to check expiration independently without re-subscribing to the entire query object.
const quoteSlice = createSlice({
name: 'quote',
initialState: {
quoteExpiresAt: null, // epoch timestamp, the source of truth
quoteExpiryCountdownTime: null // seconds remaining, for UI display
},
reducers: {
setQuoteExpiry(state, action) {
state.quoteExpiresAt = action.payload.expiresAt;
state.quoteExpiryCountdownTime = action.payload.durationSeconds;
},
clearQuoteExpiry(state) {
state.quoteExpiresAt = null;
state.quoteExpiryCountdownTime = null;
},
},
});By storing quoteExpiresAt as an absolute timestamp, the system can validate expiration by comparing it to Date.now()—a simple, efficient check that avoids recalculating values repeatedly.
Implementing a Memory-Safe Countdown Timer
Countdown timers are notorious for causing memory leaks in React applications. A common mistake is setting an interval that persists beyond a component’s lifecycle or using stale closures that hold outdated values.
To address this, I built a custom hook that ensures cleanup and recalculates the remaining time from the absolute expiration timestamp on every tick:
function useQuoteCountdown(expiresAt) {
const [secondsLeft, setSecondsLeft] = useState(null);
useEffect(() => {
if (!expiresAt) {
setSecondsLeft(null);
return;
}
const tick = () => {
const remaining = Math.max(0, Math.round((expiresAt - Date.now()) / 1000));
setSecondsLeft(remaining);
};
tick();
const intervalId = setInterval(tick, 1000);
return () => clearInterval(intervalId); // Critical cleanup
}, [expiresAt]);
return secondsLeft;
}This implementation avoids drift caused by tab throttling and ensures the interval resets when a new quote is created. The cleanup function (clearInterval) prevents memory leaks by removing the interval when the component unmounts or a new quote is fetched.
Validating Expiration at Strategic Points
Tracking expiration is only half the battle. The other half is deciding how the application should respond when a quote expires. Reacting too early—such as interrupting users mid-form—can frustrate them, while reacting too late risks financial errors.
The solution is to validate expiration at key decision points, not continuously:
- During form review or confirmation: If the quote has expired, request a fresh one before proceeding. This ensures users review up-to-date information without encountering unexpected errors.
- Before final submission: Perform a final check to prevent stale quotes from reaching the backend, especially after identity verification or other delays.
This approach aligns validation with user context. Instead of treating every expiration the same way, the system adapts to the user’s current step, reducing friction while maintaining accuracy.
Generating Dynamic Forms from Metadata
Another challenge was rendering the correct form based on user selections without duplicating components for every possible combination. The solution was to treat form structure as data rather than hardcoded JSX.
The backend can provide field metadata—such as input names, types, labels, placeholders, and select options—allowing the frontend to generate forms dynamically. Two helper functions bridge the gap between metadata and form components:
function buildFormFields(routeConfig) {
return routeConfig.map((field) => ({
name: field.key,
label: field.label,
placeholder: field.placeholder,
type: field.type,
options: field.options,
validation: field.validation,
}));
}
function buildFormSchema(fields) {
return fields.reduce((acc, field) => {
acc[field.name] = {
required: field.validation?.required || false,
pattern: field.validation?.pattern,
};
return acc;
}, {});
}This architecture reduces redundancy, simplifies maintenance, and allows the backend to control form behavior without frontend changes. It also makes it easier to adapt to new routes or channels without refactoring entire components.
Designing for the Future
Building a payout confirmation flow with expiring quotes is as much about engineering as it is about user experience. The key takeaways are to separate server and client state, implement memory-safe timers, validate at strategic points, and generate forms dynamically from metadata.
As fintech applications evolve, developers must prioritize scalability and robustness. The patterns outlined here—state separation, clean timers, and data-driven forms—can be adapted to new challenges, ensuring systems remain performant and reliable even as complexity grows.
AI summary
Learn how to manage expiring quotes in a fintech payout flow with Redux, TanStack Query, and dynamic forms for better UX and fewer errors.