Strengthening Subscriptions with Retry Mechanisms in React · Project

While building a fullstack course builder app powered by AI, I came across an interesting challenge that many real-time apps might face - unreliable subscriptions. In this post, I’ll share how I built a robust retry mechanism for GraphQL subscriptions using URQL in a Next.js frontend.


The solution is generic, reusable, and might save you hours if you’re trying to stream data reliably to your users.


The Context: A Generative AI Course Builder


An app I’m working on at Lyearn helps users create courses using AI. Users input some setup information, and the system uses AI to generate everything - title, description, outcomes, a structure of the course, lessons, and even assessments. There's a human in the loop during the content generation process to ensure quality and relevance.


Course builder UICourse builder UI (course structure tab)

When generating multiple lessons, we stream lesson content to the user using GraphQL subscriptions. This makes the experience fast and smooth. Users start seeing content as it's generated rather than waiting for everything to finish.


But... there was a catch.


The Problem: Subscriptions That Silently Die


Sometimes, the GraphQL subscription would just stop receiving data


This is a big problem - especially when you’re building user-facing features that depend on real-time updates.


I started wondering: How reliable is a single subscription? And if it’s not, how do I make my app tough enough to handle these hiccups without leaving users hanging? That’s when I decided to build a retry mechanism—something reusable across all the AI-driven subscriptions in my frontend.


Loading subscriptionLoading subscription

My Approach: A Four-Phase Safety Net


I came up with a step-by-step plan to catch failures and recover gracefully. Here’s how it works in simple terms:



Why This Matters


Building this retry logic adds robustness to my app. In simple words, this means it can handle errors without crashing or confusing the user. Plus, once I got this working, I could use it anywhere in my app where subscriptions might flake out. It’s a one-time effort with big payoffs.


The Code: Two Versions of the Solution


I wrote two versions of this retry mechanism as React hooks, each helpful in different use cases. I’ve tried to keep the code generic so it can fit anyone’s needs and is easy to understand. Let’s break them down.


Version 1: The Generic Hook


This one runs the retry logic automatically when you use it. It manages subscriptions with retry logic, timeouts, and state updates. It’s perfect for most cases where you just want the data to flow in without any extra trouble.


Here’s the code:



This is how you use it:



The hook tracks the task’s data, whether it’s loading, any errors, and the current status (like “subscribing” or “timed_out”). Inside, useEffect starts the subscription when there’s a task ID, and the executeSubscription and executeQuery functions handle the retry logic I described. The 30-second timer is managed with setTimeout, and I useuseRef to keep track of things like the active subscription so I can clean up properly.


Version 2: The Imperative Hook


Sometimes, React’s rules get in the way. You can’t call hooks conditionally, which can be a pain. So, I made an imperative version that gives you a function to call whenever you want.


Here’s that code:



Using it looks like this:



This version lets you control when the subscription starts by calling subscribe yourself. You pass in handlers to react to data, errors, status changes, or completion. It’s flexible and gets around React’s Rules of Hooks.


Wrapping Up


This retry mechanism has been a game-changer for my course builder. Lessons load reliably, and users don’t get stuck staring at a blank screen. What started as a quick fix ended up being a reusable pattern that I can now plug into any real-time feature powered by subscriptions.


Fully loaded course editorFully loaded course editor

If your app deals with generative content, streaming updates, or unreliable WebSocket connections, this approach might be exactly what you need.