reactjsreact-queryreact-state

React Query response data on useState "initialization value"


I want to put the "data" from the useQuery response as the initial value of my component.

    const {data, isLoading }  = useQuery({
            queryKey: ["name", nameId],
            queryFn: async () => {
                const consulta = await getFetch(NAME_URI + "/" + nameID);
                return name as NameType | undefined;
            },
    })
    
    const [name, setName] = useState(data.name)

return 

<div>
{ !isLoading ? <p>{JSON.stringify(data.name)}</p> : <p>Is loading...</p> 
<p>{name}</p>
</div>

In the moment the component mounts it shows:

"Is loading..." ""

After a while:

"Rob" ""

As the "View" is dynamic the result is right... the "name" state at begin is "" because the query on react query is not "immediate". I try putting a setName(data.name) inside a useEffect that runs only when "isLoading" changes, and it works... but I would think there is a better and more clean way to do it?

CONTEXT EDITED: Indeed I need to edit/change the "name" retrieved by the query... That's why I am lookin forward to:

get the name from API (done) > store the data fetched on a state variable > edit that data on the state variable > save the data edited (using mutateAsync function...)


Solution

  • I have a full blogpost covering this topic: https://tkdodo.eu/blog/react-query-and-forms

    In summary, there is two things you can do:

    1. Decouple the data fetching component from the form component

    The issue is that the useState initializer will only run on the first render. Since query data is asynchronous and takes some time to arrive, it will be undefined on the first render cycle. If that's where you mount your useState, it will not reflect the state. So we make sure that the form component is only rendered after data is ready:

    function App() {
      const { data, isPending, isError }  = useQuery({
        queryKey: ["name", nameId],
        queryFn: async () => {
          const consulta = await getFetch(NAME_URI + "/" + nameID);
           return name as NameType | undefined;
        },
      })
      
      if (isPending) return "loading ..."
      if (isError) return "error"
    
      // now we have data
      return <FormComponent initialData={data} />
    }
    
    function FormComponent({ initialData }) {
      const [name, setName] = useState(data.name);
    }
    

    That can work, but it means splitting up your component and background refetches will not be reflected. If your cache already has stale data when App mounts, you'll get that data in your state, and new data that comes in later will also not be reflected. So there's option 2:

    2. Keep server and client state separated and derive state

    It's not a must to initialize useState with any value. What if we just keep it undefined, meaning "the user hasn't made any changes yet". If it's undefined, we'll derive it from server state:

    function App() {
      const { data, isPending, isError }  = useQuery({
        queryKey: ["name", nameId],
        queryFn: async () => {
          const consulta = await getFetch(NAME_URI + "/" + nameID);
           return name as NameType | undefined;
        },
      })
      
      // initialize with nothing (undefined)
      const [localName, setName] = useState()
    
      // derive real name value
      const name = localName ?? data.name
    }
    

    This way, name will be whatever the user has given as input, but if they haven't done anything yet, the server state will be taken as fallback.


    Why the useEffect "solution" is bad

    React Query is a data synchronization tool, which means it will try to keep what you see on your screen up-to-date with what the source of truth (= the server) holds. It does so sometimes aggressively, with background refetches, e.g. on window focus.

    An effect that looks like this:

    useEffect(() => {
      if (data.name) {
        setName(data.name);
      }
    }, [data]);
    

    will always run when data changes, which means it might remove data the user has already changed without saving. On larger forms, this can be troublesome. For example:

    This obviously might not be a big issue in small forms, or when data can only be changed by one person, or when background refetches are turned off. But in larger forms, this can be critical to get right.

    So the effect adds nothing of value here, it's just unnecessarily complicated code that will also re-render your component a second time unnecessarily (also won't matter much).