Subscription Getting Re-Subscribed on Component Refresh?

I have a subscription query in a React component and it looks like this:

function SubscribeToUserPresenceUpdates() {
    const {data, loading, error} = useSubscription(
        USER_PRESENCE_SUBSCRIPTION_QUERY,
        {
            variables: {
                "ids_to_watch": messageFolders.current.map(function (theFolder) {
                    return theFolder.id
                })
            },
            onSubscriptionData: ({client, subscriptionData, loading}) => {
                // do stuff with the subscription data
           }
        },
    )

    return null;
}

It seems like my subscription resolver is getting subscribed to again, every time there’s a component refresh. I can skip that by doing something like this:

const subscribed = useRef(false);

function SubscribeToUserPresenceUpdates() {
    const {data, loading, error} = useSubscription(
        USER_PRESENCE_SUBSCRIPTION_QUERY,
        {
            variables: {
                "ids_to_watch": messageFolders.current.map(function (theFolder) {
                    return theFolder.id
                })
            },
            skip: subscribed.current,
            onSubscriptionData: ({client, subscriptionData, loading}) => {
                subscribed.current = true;
                // do stuff with the subscription data
           }
        },
    )

    return null;
}

Is that the best practices approach?

Looks like you might be calling the useSubscription hook inside a function, beyond the component scope. Hooks may only be called at the top level (or inside other hooks), see https://reactjs.org/docs/hooks-overview.html#rules-of-hooks

This might be causing your hook to be re-initialized on each render.

1 Like

I moved it to the top level per your post, but it still re-subscribes on component refresh. I wonder what I’m missing?

Just a wild guess now, but since the ids_to_watch variable is getting recalculated on each render (map is creating a new array each time), maybe useSubscription is resubscribing with what it thinks is a new set of variables. You could try storing it in a variable and using the same reference on each render. Or see if it works without passing any variables.

1 Like

I am storing it in a useRef, but I guess that could still be it. I think useRefs aren’t recalculated on refresh but perhaps they are. I know they hold their values across refreshes and aren’t restored to their defaults.

I’ve tried a bunch of expressions for skip, but they either don’t solve the anomaly or else they stop the subscription from working.

Could this be an Apollo feature, rather than an anomaly in my code – is it possible that subscriptions are supposed to re-subscribe on component refresh?

Hmmmm… maybe I need to move the subscription to a parent of this component. I guess that’s the thing to try next.

I can assure you that subscriptions are not supposed to resubscribe on each render. It definitely doesn’t happen in my implementations. And refs only change when you actively change them. But are you storing messageFolders in a ref or messageFolders.current.map(function (theFolder) { return theFolder.id })? According to your code snippet, it seems to be the former - if it is, then you are generating a new array on each render, because that’s what map does. I’m still not sure that’s what the issue is, but it’s worth investigating.

1 Like

Oh I see what you’re saying. I’ll check that out!

Okay I got this solved!

I learned a few things while doing it. I’ll note them here in case they may be of interest to others.

For a while Apollo supported query components. So I had some subscriptions in subscription components. When those were deprecated in favor of useSubscription, I went to useSubscription, but left the subscriptions in their own stand-alone components since they looked nice and neat there.

Well… so when my subscription gets some onSubscriptionData data, it changes state in the parent component, and of course that re-renders the child subscription component, which re-runs the query.

I didn’t get tipped off to this because I didn’t get the infinite loop warning from React – I guess someplace either React or Apollo realized it was doing the same thing repeatedly and prevented the infinite loop.

Also I didn’t get tipped off because my useRefs weren’t retaining their values across parent component refreshes.

This was a surprise to me. Maybe it should not have been but it was. It looks like when a parent component refreshes, child components aren’t just refreshed, they are re-instantiated, i.e. their useRefs and useEffects are blown away and rebuilt from scratch. We may not notice this because when the dom gets reconciled by React it shows no trace of this. But debugging showed that my child component useRefs, which I was using to store info that I could use for a skip value telling my subscription not to run again, were getting blown away and re-built from scratch on parent component refresh.

So it seems like I’ve figured this out now… and solved the re-subscription anomaly in my app.

If I’m wrong or missing something about this, by all means please post about it here.

It looks like when a parent component refreshes, child components aren’t just refreshed, they are re-instantiated, i.e. their useRef s and useEffect s are blown away and rebuilt from scratch.

This should really only happen when the child component is unmounted and re-mounted. And this in turn should only happen if the child component has a key prop and that prop changes between renders, or if it is part of a list and has no key prop. Otherwise a child component will simply be re-rendered, just like the parent component. See this great blog post on the topic: https://kentcdodds.com/blog/understanding-reacts-key-prop

Does that apply to your child component? If not, I still don’t see why that re-subscription behaviour should be happening.

1 Like

The component that used to contain the subscription has been removed, and the useSubscription call has been moved to the parent component.

However, checking with previous commits, I see that it did not have a key prop!

<SubscribeToUserPresenceUpdates/>

Thanks very much for this great info. I will read the article.