Any way to subscribe for network status of queryRef?

Hello,
I’m building some search experience and would like to know if there is any way to subscribe for queryRef network status without using suspense?

So I have a search input and on change I update query params. When query params are updated my preloadQuery activates and refetches with new query string (I’m using react-router 7 loaders). Everything works fine but I’d like to display some spinner inside my search input. What’s the best way of doing this? Switching to useLazyQuery?

I assume this can be implemented with suspense APIs as well,e.g.

const MonitorNetworkStatus = () => {
    useReadQuery(queryRef);
}
...

<Suspense fallback={<Spinner/>}>
  <MonitorNetworkStatus queryRef={queryRef} />
</Suspense>

but this doesn’t feel very natural

Thanks in advance for any tips :person_bowing:

Hey @misha-erm :waving_hand:

Would you mind providing some more code and context on how query params and everything else plays into that queryRef? This may be tricky due to how Suspense works, but I’d like to see how you’re setting everything up before I can confidently provide an answer here :slightly_smiling_face:

Sure

So here is an example of UI

Here is the code for my Search Input:

export const StreamPostsSearch = () => {
  const { q } = useStreamPostsLoaderData();

  const [, setSearchParams] = useSearchParams();

  const debouncedSetSearchParams = useDebouncedCallback(setSearchParams, 200);

  const onValueChange = useCallback(
    (value?: string) => {
      const query = value || '';
      if (query.length === 0) {
        setSearchParams((prev) => deleteSearchParams(prev, ['q']), {
          preventScrollReset: true,
        });
        debouncedSetSearchParams.cancel();
      } else {
        debouncedSetSearchParams({ q: query }, { preventScrollReset: true });
      }
    },
    [debouncedSetSearchParams, setSearchParams],
  );

  return (
    <SearchInput
      placeholder="Filter posts..."
      onQueryChange={onValueChange}
      defaultValue={q}
    />
  );
};

and react-router loader looks like this:

const SearchStreamPostsQuery = gql(`
  query SearchStreamPostsQuery($query: String!, $streamId: ID!, $limit: Int!, $offset: Int!) {
    searchStreamPosts(input: {streamId: $streamId, text: $query, limit: $limit, offset: $offset}) {
      items {
        ... on StreamPost {
          ...StreamPostCardFragment
        }
      }
    }
  }
  `);

export const clientLoader = ({
  request,
  params,
}: Route.ClientLoaderArgs) => {
  const searchParams = new URL(request.url).searchParams;
  const q = (searchParams.get('q') || '').trim();

  return {
    q,
    searchStreamPostsQueryRef: q
      ? preloadQuery(SearchStreamPostsQuery, {
          variables: {
            streamId: params.id,
            query: q,
            limit: 20,
            offset: 0,
          },
          fetchPolicy: 'network-only',
        })
      : null,
  };
};

later in the code I do something like

const {searchStreamPostsQueryRef} = useLoaderData();

return searchStreamPostsQueryRef ? <SearchFeed/> :  

return (<Container after={<StreamPostsSearch/>}>
  <Suspense fallback={<Skeleton/>}>
    {searchStreamPostsQueryRef ? <SearchFeed queryRef={searchStreamPostsQueryRef} /> : <RegularFeed/>}
  </Suspense>
</Container>);

So I was thinking about the way of rendering spinner icon inside StreamPostsSearch in addition or instead of skeleton

Will this be enough or better to prepare the sandbox?

This helps. Thanks!

So this one is a bit tricky due to how Suspense works. Unfortunately networkStatus isn’t going to help much here because you’ll never have a render where you can observe networkStatus: NetworkStatus.loading since this state means you have a pending promise which will trigger your suspense boundary. The only time you’ll really only be able to observe networkStatus as something other than ready is if you’re using the cache-and-network fetchPolicy and a stale result is emitted while the fetch from the network is kicked off (we suspend until that cache result is returned, then update data/networkStatus once the network is finished).

Your best bet is to use transitions in order to prevent your Suspense fallback from showing up. If a state update is done in a transition, React will try and render your component and if that state update causes that render to suspend, it will show your old UI until the transition is complete (i.e. the promise resolves).

I would try adding a useTransition inside your StreamPostsSearch component and wrapping your setSearchParams in startTransition returned from that hook. You can use that isPending flag to determine if you’re in a transition or not and use this to show a loading indicator with your SearchInput. That transition should see that your search params update causes your component to suspend and show your old UI. Try this out and see if this works for you :slightly_smiling_face:

1 Like

awesome, thank you :person_bowing:

Completely missed the fact that useTransition can do the trick there

1 Like

No problem! Glad that helped!