I pull data with react-query and need to store it in state due to some form editing that is happening later.
Before the form editing, it worked well:
import { useQuery } from '@apollo/client';
import { SINGLE_PARTICIPANT_QUERY } from 'queries/participantQueries';
import { ProfileGeneral } from './ProfileGeneral';
const ProfilePage = ({ id }) => {
const {data, loading, error} = useQuery(SINGLE_PARTICIPANT_QUERY, {
variables: {
id
}
});
if (loading) {
return <div>Loading</div>;
}
if (error) {
return (
<div>
{error.message} />
</div>
);
}
const { participant } =data;
return (
<div>
<ProfileGeneral participant={participant} />
</div>
But after trying to add it into state, I keep getting an error message, indicating that it renders without having the data ready.
import { useQuery } from '@apollo/client';
import { SINGLE_PARTICIPANT_QUERY } from 'queries/participantQueries';
import { ProfileGeneral } from './ProfileGeneral';
import { useEffect, useState } from 'react';
const ProfilePage = ({ id }) => {
const [participant, setParticipant] = useState(null);
const { data, loading, error } = useQuery(SINGLE_PARTICIPANT_QUERY, {
variables: {
id
}
});
useEffect(() => {
if (data && data.participant) {
setParticipant(data.participant);
}
}, [data, participant]);
if (loading) {
return <div>Loading</div>;
}
if (error) {
return (
<div>
{error.message} />
</div>
);
}
return (
<div>
<ProfileGeneral participant={participant} />
</div>
I get back:
Server Error
TypeError: Cannot read properties of null (reading 'firstName')
This error happened while generating the page. Any console logs will be displayed in the terminal window.
I know that I need to make it wait or re-render as soon as it has the data from the query, but I am not sure how to prevent it.
Thank you for taking a look!
CodePudding user response:
It because:
setParticipant
change state asynchronously,useEffect
invokes after render actually happend
so even if data.participant
is not empty, participant
is, until next render phase
You could change to this:
const ProfilePage = ({ id }) => {
//...
if (loading || !participant) {
return <div>Loading</div>;
}
//...
}
CodePudding user response:
An option is to split the component into two. One is responsible for fetching (useQuery
) and one for form editing (useState
) so that the parent component provides initial state. A downside to this approach is that until the query loads, you don't see the form (unless you make "a form placeholder").
Example:
function Fetcher() {
const {data, isError, isLoading} = useQuery(...)
if (isLoading) {
return <Loader />
}
if (isError) {
return <p>Oops, failed to load</p>
}
return <Form initialState={data} />
}
function Form({ initialState }) {
const [state, setState] = useState(initialState)
return <form>...</form>
}
Now you have a local copy of the data in Form's state and no longer have any async issue. Form simply doesn't render until you have data. If you want a better visual, you can replace Loader with anything!
In case you need to propagate changes (meaning that if the query re-runs and fetches fresh data), you can use a key
to reset the Form as well. Use this with caution because background sync would clear the form even if the user was just typing something, so you need to play with the options of such query.
const {data, isLoading, isError, dataUpdatedAt} = useQuery(...)
...
return <Form key={dataUpdatedAt} initialState={data} />