query icon indicating copy to clipboard operation
query copied to clipboard

broadcastQueryClient doesn't keep cache between tabs

Open DoneDeal0 opened this issue 3 years ago • 7 comments

Describe the bug I'm trying broadcastQueryClient feature to make the cache persist across tabs. When opening a new tab, the server endpoint is called, while react-query should prevent this and return the cache data instead.

My usecase: When the app mounts, I make an api call to retrieve the user's info and store them in a global state. There are sensitive informations, so local-storage is not an option. I have a search result page. Users open a new tab each time they click on a product. Each new tab remounts the whole app, and the /get-user-infos route is called repeatedly. I need to cache the result across tabs.

To Reproduce index.js

import { QueryClient, QueryClientProvider } from "react-query";
import { broadcastQueryClient } from "react-query/broadcastQueryClient-experimental";

const queryClient = new QueryClient();
broadcastQueryClient({
  queryClient,
  broadcastChannel: "myapp",
});

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  document.getElementById("root")
);

App.js

export default function App() {
  const { user, loading } = getUserInfos();
  return (
    <BrowserRouter>
      {loading ? "loading..." : <div>hello {user.name}! </div>}
      <Switch>
        <Route to="/" render={Home} />
        <Route to="/user" render={User} />
      </Switch>
    </BrowserRouter>
  );
}

service.js (where the api cal is made)

/* eslint-disable react-hooks/rules-of-hooks */
import { useQuery } from "react-query";
import { api } from "./config";

const _getUserInfos = async () => {
  try {
    const res = api.get("/get-user-infos");
    return res;
  } catch (err) {
    return err;
  }
};

export const getUserInfos = () => {
  const { data, isLoading } = useQuery("contact", () => _getUserInfos(), {
    staleTime: 1000 * 60 * 60 * 24, // 24 hours
    cacheTime: 1000 * 60 * 60 * 24, // 24 hours
  });
  return { user: data && data.data.user, loading: isLoading };
};

The new tab is open like this on <Home/>

  const onOpen = () => {
    const tab = window.open("/user");
    tab.focus();
  };

The cache works fine as long as I use react-router-dom on the same page.

The server:

const app = require("express")();
const cors = require("cors");

app.use(cors({ credentials: true, origin: ["http://localhost:3000"] }));

app.listen(8000, () => console.log(`server is listening on port 8000!`));

app.get("/get-user-infos", (req, res) => {
  console.log("login route called!");
  res.status(200).json({
    user: {
      name: "joe",
      _id: "1234",
    },
  });
});

Expected behavior React-query returns the cache for /get-user-infos when opening a new tab.

Desktop (please complete the following information):

  • OS: osx
  • Browser: chrome

DoneDeal0 avatar Apr 17 '21 18:04 DoneDeal0

What was the problem please?

TkDodo avatar Apr 18 '21 09:04 TkDodo

Hi, it was a stupid mistake, I actually instantiated react-query with const client = new QueryClient() in a project, and added it to broadcastQueryClient as such, whereas I had to write: queryClient. So false alert.

However, this feature doesn't prevent the app to repeat the same api calls when opening a new tab from the main page (usecase: search results page, the user opens each result in new tab. The app fetches the user information on every tab as if the app was launched for the first time instead of relying on a cached result.). Maybe I'm doing something wrong.

DoneDeal0 avatar Apr 18 '21 14:04 DoneDeal0

does it not depend on the staleTime you have set on your queries? Maybe you can create a runnable example - I've never used it :)

TkDodo avatar Apr 18 '21 14:04 TkDodo

I've set a 24hours cacheTime on this request. I'll make a public repo with a simple node server and a basic react-query app tonight or tomorrow evening and post it here.

DoneDeal0 avatar Apr 18 '21 14:04 DoneDeal0

@TkDodo I've updated the main question so as not to open a new thread. The exemple code is very short and to the point. Since a public repo may not be available forever, I think it will be more useful for futur users to have access to the whole code directly in the thread. It's just 4 file, ready to copy-paste.

I've clearly explained the issue. Please let me know if you have a solution or questions :).

DoneDeal0 avatar Apr 19 '21 15:04 DoneDeal0

There are sensitive informations, so local-storage is not an option.

Just an FYI, if I read this correctly, broadcast-channel might use localStorage under the hood to communicate, e.g. if the native version is not supported:

https://github.com/pubkey/broadcast-channel#set-options-when-creating-a-channel-optional


If I'm reading the implementation of the plugin correctly, it:

  • opens the channel
  • creates a new subscription to the query cache
  • posts a message to the channel whenever something changes in the query cache

Now I don't see that when you open a new tab and the channels opens that it somehow "requests" data from other open tabs. Even if that were possible, it would probably happen asynchronously, so we'd somehow need to stop the queries from executing normally until that data is received.

If you have ideas how to do that, or how to synchronously send data to a new tab when it opens, maybe you can PR it?

TkDodo avatar Jun 12 '21 21:06 TkDodo

I may have found a possible workaround to this problem. You can use PersistQueryClientProvider to hydrate the cache across windows:

import { QueryClient } from '@tanstack/react-query'
import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      /**
       * `cacheTime` must be at least 24 hours in order for cache persistence
       * to work properly
       * @see {@link https://tanstack.com/query/v4/docs/react/plugins/persistQueryClient#how-it-works}
       */
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours,

      /**
       * Make `staleTime` a fairly large number or Infinity so that we don't always 
       * trigger a network request on mount
       * 
       * @see {@link https://tanstack.com/query/v4/docs/react/guides/initial-query-data#staletime-and-initialdataupdatedat}
       */
      staleTime: Number.POSITIVE_INFINITY
    }
  }
})

const localStoragePersister = createSyncStoragePersister({
  storage: window.localStorage
})

/**
 * Use `broadcastQueryClient` so that state changes are synced across open
 * browser windows
 */
broadcastQueryClient({ queryClient, broadcastChannel: 'foobar' })

function App() {
  return (
    <PersistQueryClientProvider client={queryClient} persistOptions={{ persister: localStoragePersister }}>
       {...}
    </PersistQueryClientProvider>
  )
}

export default App

ronaldcurtis avatar Feb 08 '23 11:02 ronaldcurtis