skip to Main Content

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…)

2

Answers


  1. 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?

    No, if you have a need to edit a local copy then that’s pretty much how you’d handle it within the same component; use a useEffect hook to "synchronize" a React state to an external source.

    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);
    
    useEffect(() => {
      if (data.name) {
        setName(data.name);
      }
    }, [data]);
    
    return (
      <div>
        {!isLoading
          ? <p>{JSON.stringify(data.name)}</p>
          : <p>Is loading...</p> 
        }
        <p>{name}</p>
      </div>
    );
    

    An alternative would be to lift the query up higher in the ReactTree and conditionally render the child component with the fetched name as a prop.

    Parent:

    const Parent = () => {
      ...
    
      const { data, isLoading }  = useQuery({
        queryKey: ["name", nameId],
        queryFn: async () => {
          const consulta = await getFetch(NAME_URI + "/" + nameID);
          return name as NameType | undefined;
        },
      });
    
      ...
    
      return (
        ...
    
        isLoading
          ? <p>Is loading...</p>
          : <Child name={data.name} />
    
        ...
      );
    }
    

    Child:

    const Child = (props: ChildProps) => {
      const [name, setName] = useState(props.name);
    
      ... logic to handle updating the local name state ...
    
      return (
        ...
        <p>{name}</p>
        ...
      );
    }
    

    Just be aware that saving passed/fetched props/data like this is generally considered a React anti-pattern. If you have no need to edit/change the local name state then the solution is to just reference the returned query data directly and forego the useStateuseEffect coupling.

    const { data, isLoading }  = useQuery({
      queryKey: ["name", nameId],
      queryFn: async () => {
        const consulta = await getFetch(NAME_URI + "/" + nameID);
        return name as NameType | undefined;
      },
    });
    
    return (
      <div>
        {isLoading
          ? <p>Is loading...</p>
          : <p>{data.name}</p>
        }
      </div>
    );
    
    Login or Signup to reply.
  2. 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:

    • data comes in as { name: 'Alice' }, the effect runs and puts that into state.
    • User starts changing the name to Bob, but doesn’t save it yet.
    • User switches tabs to read stackoverflow or whatever.
    • User comes back, this triggers a background refetch.
    • In the meantime, name in the database was changed to Charlie by someone else.
    • The background refetch brings the new name to the app, and the effect will run and will remove the user’s changes (Bob) and overwrite them.

    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).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search