Alex Ironside
Alex Ironside

Reputation: 5049

Make code execute only on the first react-query success

I have a weird case, where I need to allow for standard useQuery behavior, but at the same time need to run a piece of code only on the first onSuccess.

I know I can achieve this with useRef, but is there a way around it?

I wasn't able to reproduce the issue 1:1, but you can see it after the first increment counter gets re-set to 100.

  const [counter, setCounter] = useState(0);
  const { data, isLoading } = useQuery(
    ["key"],
    () => {
      return axios.get("https://picsum.photos/200");
    },
    {
      onSuccess() { // this runs twice
        setCounter(100);
        console.log("fetch");
      }
    }
  );

  return (
    <div className="App">
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        counter {counter}
      </button>
      {data?.data && "data is present"}
    </div>
  );

https://codesandbox.io/s/react-query-once-66lows?file=/src/App.tsx

Upvotes: 1

Views: 4755

Answers (5)

Alex Ironside
Alex Ironside

Reputation: 5049

After a while, we found a better solution. We can simply set staleTime to Infinity.

https://react-query-v3.tanstack.com/reference/useQuery#_top

enter image description here

This allows us to still invalidate the data and refetch if needed, but until it's invalidated, the query will not refetch

https://react-query-v3.tanstack.com/guides/query-invalidation#query-matching-with-invalidatequeries

enter image description here

We might also need to disable refetching on window focus

Upvotes: 1

Rodrigo Garcia
Rodrigo Garcia

Reputation: 81

I run into a similar issue a few days ago and found this GH thread that suggest this solution that did the trick for me:

const [counter, setCounter] = useState(0);

const { data, isLoading } = useQuery(
    ["key"],
    () => axios.get("https://picsum.photos/200").then(res => {
       // This will only run once
       setCounter(100);
       return res;
    })
  );

  return (
    <div className="App">
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        counter {counter}
      </button>
      {data?.data && "data is present"}
    </div>
  );

Upvotes: 2

codemzy
codemzy

Reputation: 89

I had a similar need (query gets updated by user in WYSIWYG editor) and needed to get the initial data once only.

Instead of using the onSuccess callback, I did something like:

const [initialData, setInitialData] = useState(false);
const { data, isLoading } = useQuery(...);
// sets initialData if not set yet and data has been fetched
if (!initialData && !isLoading && data) {
  setInitialData(data);
}

Upvotes: -1

Aleksandr Kositsyn
Aleksandr Kositsyn

Reputation: 39

Use lodash's once Like this

  const [counter, setCounter] = useState(0);
  const onSuccess = _.once(() => {
        setCounter(100);
        console.log("fetch"));
      }
  const { data, isLoading } = useQuery(
    ["key"],
    () => {
      return axios.get("https://picsum.photos/200");
    },
    {
      onSuccess
    }
  );

  return (
    <div className="App">
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        counter {counter}
      </button>
      {data?.data && "data is present"}
    </div>
  );

Upvotes: 2

Biller Builder
Biller Builder

Reputation: 323

The easy hack is to add another boolean state like const [isRan, switchIsRan] = useState(false) and then check its value in onSuccess() before running any logic and also switch it to true at the end of that callback.

Upvotes: 2

Related Questions