How to invalidate routes/router in backend/webhook?

Hello, I'm making an app using T3 that uses Stripe for subscription and payment. I have made purchase and manage billing routes that all work as expected. Now, in this app there is going to be different subscription tiers and I want to allow users to upgrade/downgrade (update) their subscriptions. I've set up the webhook and tRPC routes to handle this and it's working--however, because the particular update does not send the user to an external page (like stripe portal) and back, the UI does not refresh to reflect the subscription update (once the webhook has completed its thing). Using onSuccess() in the useMutation() doesn't work, and I suspect that's because the mutation finishes well before the webhook receives the confirmation from stripe. I tried (foolishly) to use utils and api.useContext() to call the routes inside the webhook after the handler function had done its thing--but this, of course, only works in a react component. TL;DR: What's a good way to trigger a invalidate on some tRPC routes from the backend such that the frontend then refreshes? Appreciate any help with this. Cheers
4 Replies
cje
cje12mo ago
you can't talk to the frontend from the backend without using websockets or similar either open a socket or have some endpoint that you can ping every couple of seconds
Filip
Filip12mo ago
Alright! Thanks! I'll start looking into it and post my solution here once I have something that fits my use case. Ok, so I got an idea looking at one of the examples from Tanstack, and it involves using the refetchInterval option in useQuery. I'll post it here and add explanation of the following code in next post if it can be of help to others. So on my billing page I have:
const billingQueryInterval = useBillingQueryInterval(); //context provided state
const setBillingQueryInterval = useBillingQueryIntervalUpdate(); // context provided state setter
const {
data: userSubscriptionPlan,
isLoading: userSubscriptionPlanLoading,
isError: userSubscriptionPlanError
} = api.stripe.getUserSubscriptionPlan.useQuery(undefined, {
refetchInterval: billingQueryInterval,
onSuccess: (newData) => {
if (userSubPlanQueryRetries > 60) {
toastError();
setBillingQueryInterval(false);
setUserSubPlanQueryRetries(0);
return;
}
if (JSON.stringify(newData) !== JSON.stringify(userSubscriptionPlan)) {
if (isNumber(billingQueryInterval) && billingQueryInterval > 0) { // Condition to ensure toast runs only after user interaction.
toastSubUpdateSuccess();
}
setBillingQueryInterval(false);
setUserSubPlanQueryRetries(0);
return;
}
if (billingQueryInterval !== false) {
setUserSubPlanQueryRetries(prevCount => prevCount + 1)
}
},
});
const billingQueryInterval = useBillingQueryInterval(); //context provided state
const setBillingQueryInterval = useBillingQueryIntervalUpdate(); // context provided state setter
const {
data: userSubscriptionPlan,
isLoading: userSubscriptionPlanLoading,
isError: userSubscriptionPlanError
} = api.stripe.getUserSubscriptionPlan.useQuery(undefined, {
refetchInterval: billingQueryInterval,
onSuccess: (newData) => {
if (userSubPlanQueryRetries > 60) {
toastError();
setBillingQueryInterval(false);
setUserSubPlanQueryRetries(0);
return;
}
if (JSON.stringify(newData) !== JSON.stringify(userSubscriptionPlan)) {
if (isNumber(billingQueryInterval) && billingQueryInterval > 0) { // Condition to ensure toast runs only after user interaction.
toastSubUpdateSuccess();
}
setBillingQueryInterval(false);
setUserSubPlanQueryRetries(0);
return;
}
if (billingQueryInterval !== false) {
setUserSubPlanQueryRetries(prevCount => prevCount + 1)
}
},
});
In my updateSub component, I have:
const setBillingQueryInterval = useBillingQueryIntervalUpdate();
const errorToast = useErrorToast();
const { isLoading: updateIsLoading, mutateAsync: updateSubscriptionProcedure } = api.stripe.updateSubscription.useMutation({
onSuccess() {
setBillingQueryInterval(1000);
}
});

const btnDisabled = useBillingDisabled();
const setBillingQueryInterval = useBillingQueryIntervalUpdate();
const errorToast = useErrorToast();
const { isLoading: updateIsLoading, mutateAsync: updateSubscriptionProcedure } = api.stripe.updateSubscription.useMutation({
onSuccess() {
setBillingQueryInterval(1000);
}
});

const btnDisabled = useBillingDisabled();
Filip
Filip12mo ago
So the idea is that query will begin to refetch once interval value set to other than false. This will first be triggered if/when a user hits an update component. On first render of the billing page, a copy of the sub plan will be set to state. If/when an update component triggers refetch, on each succcesful query the incoming sub plan is checked against the existing in state. If webhook and db were successful, there should be a new sub plan, at which point it differs from the one in state and this re-renders the page and kicks off the code below: setting the interval to false (turning it off) and replacing the sub plan in the state with the new one, allowing the user to update sub plan again to another.
If the refetch has no new sub plan from the db, it continues based on the interval. It seems to be working. What I wanted was to only have the backend talk to the frontend if the user had clicked the update button, and then once the updated information had come through, shut the process down, minimizing excess calls. Does this seem like a good solution to build on? i have yet to take into account possible errors
Filip
Filip12mo ago
Update: the previous solution using a local react component state to store the past user data copy wasn't working as expected. I suspect that the way react batches things together was causing this. I found out that in tanstack that the onSuccess property of the query one can extract the incoming data before it is stored to the variable, and this allows me to make a comparison between the old and new to check for difference ( https://tanstack.com/query/v4/docs/react/reference/QueryCache ). This approach seems to work. See edit in posts above for the updated code. (Also moved the interval state to a context provider and added toast notifications).
QueryCache | TanStack Query Docs
The QueryCache is the storage mechanism for TanStack Query. It stores all the data, meta information and state of queries it contains. Normally, you will not interact with the QueryCache directly and instead use the QueryClient for a specific cache.