reactjsreact-querytanstackreact-query

React Query v3 to v4


We are trying to migrate from react. query v3 to react query v4.

Following same code is working differently in v3 and v4.

Basically,

We have a custom hook which uses useMutation to do update in backend. It returns some data, which is then supposed to be set in local state variable.

export const useUpdate = (): any => {
  const result = useMutation(async () => {
    const updateResponse = await axios.get(
      "https://jsonplaceholder.typicode.com/posts"
    );
    return updateResponse;
  });
  return result;
};

and we have two variables like below

const { mutateAsync: update, isSuccess } = useUpdate();
const [data, setData] = React.useState<any>(null);

and then this mutateAsync function is called

const data = await update();
setData(data);

There is useEffect which is invoked when data is set to do some other manipulation but when useEffect is called data is always null even though setData is called when response is received from useMutation.

React.useEffect(() => {
    if (isSuccess && data) {
      console.log("in if");
    } else {
      console.log("in else - Resetting data ");
      setData(null);
    }
  }, [data, update, isSuccess]);   

in v4 console output is like below

refresh called
MutateDemo.tsx:24 {data: Array(100), status: 200, statusText: '', headers: AxiosHeaders, config: {…}, …}
MutateDemo.tsx:25 setting data in refresh
MutateDemo.tsx:9 useEffect called
MutateDemo.tsx:10 isSuccess true
MutateDemo.tsx:11 data null
MutateDemo.tsx:15 in else - Resetting data

and in v3 console output is like below.

refresh called
MutateDemo.tsx:24 {data: Array(100), status: 200, statusText: '', headers: AxiosHeaders, config: {…}, …}
MutateDemo.tsx:25 setting data in refresh
MutateDemo.tsx:9 useEffect called
MutateDemo.tsx:10 isSuccess true
MutateDemo.tsx:11 data [object Object]
MutateDemo.tsx:13 in if

In v4, data is not getting set because of which in v4 flow does not go to if block. It always goes to else block.

If you see last two lines of output console. you will see the difference.

Basically, when isSuccess is true data is null. its not getting set by setData(data) call even after success.

Why such behavior in v4?

import { useMutation } from "@tanstack/react-query";
import axios from "axios";

export const useUpdate = (): any => {
  const result = useMutation(async () => {
    const updateResponse = await axios.get(
      "https://jsonplaceholder.typicode.com/posts"
    );
    return updateResponse;
  });
  return result;
};

import React from "react";
import { useUpdate } from "./use-update";

export default function MutateDemo() {
  const { mutateAsync: update, isSuccess } = useUpdate();
  const [data, setData] = React.useState<any>(null);

  React.useEffect(() => {
    console.log("useEffect called");
    console.log("isSuccess " + isSuccess);
    console.log("data " + data);
    if (isSuccess && data) {
      console.log("in if");
    } else {
      console.log("in else - Resetting data ");
      setData(null);
    }
  }, [data, update, isSuccess]);

  const refresh = async () => {
    console.log("refresh called ");
    try {
      const data = await update();
      console.log(data);
      console.log("setting data in refresh");
      setData(data);
    } catch (e) {
      throw new Error(`${e}`);
    }
  };

  return <button onClick={() => refresh()}>Click me</button>;
}

https://codesandbox.io/p/sandbox/frosty-dan-wnfssc

One thing I noticed, if I use following code then it breaks. This is basically React 18 way to render using createRoot.

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

But if I use older way before react 18 like below, then it works

render(<App />, document.getElementById("root"));

So, may be something to do with React 18 being used.

EDIT:-

If I wrap setData(data) in flushSync like below then also it works.

const refresh = async () => {
    console.log("refresh called ");
    try {
      const data = await update();
      console.log(data);
      console.log("setting data in refresh");
      flushSync(() => {
        setData(data);
      });
    } catch (e) {
      throw new Error(`${e}`);
    }
  };

Solution

  • I think that it is correct to use the data returned from the mutation (and possibly deploy it to the state) and not directly control the state from the outside. If I use the data from useUpdate as the control variable, it does work as it should... or am I missing something?

    I would use data as _data from mutation:

    const { mutateAsync: update, isSuccess, data: _data } = useUpdate();
    

    changed the use effect dependency on _data (and use it in its body instead of data)

    ...
       setData(_data);
    }, [_data, update, isSuccess]);
    ...
    

    and finally i would just call update without doing anything else then catching error states because _data are being controlled from mutation.

    const refresh = async () => {
      try {
        await update();
      } catch (e) {
        throw new Error(`${e}`);
      }
    };
    

    Sendbox preview