T
TanStack•4y ago
wee-brown

Syncing react query data with useState state

Hi all, I have a provider which is fetching a list of "KeyRecord" from the backend. This provider provides the state of a list of forms, each form allows to edit a single KeyRecord. When the provider is done fetching the list of KeyRecord from the backend, it needs to initialize the state of the forms. The problem is, because I do that with a useEffect, there is a small timeframe where isLoading is false, and keyRecordsFormState is an empty array [] and I would like to get rid of that because it causes an unnecessary render of the consumers with an empty list. Code:
type KeyRecordsProviderProps = {
children: ReactElement;
};

export const KeyRecordsProvider = (props: KeyRecordsProviderProps) => {
const { children } = props;
const { page } = usePagination();
const {
data: keyRecords = [],
isLoading,
isError,
} = useQuery(["keyRecords", page], () => getKeyRecords(page));
// State of the form list of key records (editable list of forms in the UI, one form per key record)
const [keyRecordsFormState, setKeyRecordsFormState] = useState<KeyRecord[]>(
[]
);

useEffect(() => {
if (!isLoading) {
// When the key records have been fetched from the backend, assign the value to the form state
setKeyRecordsFormState(keyRecords);
}
}, [isLoading]);

return (
<KeyRecordsContext.Provider
value={{
isError,
isLoading,
keyRecordsFormState,
}}
>
{children}
</KeyRecordsContext.Provider>
);
};
type KeyRecordsProviderProps = {
children: ReactElement;
};

export const KeyRecordsProvider = (props: KeyRecordsProviderProps) => {
const { children } = props;
const { page } = usePagination();
const {
data: keyRecords = [],
isLoading,
isError,
} = useQuery(["keyRecords", page], () => getKeyRecords(page));
// State of the form list of key records (editable list of forms in the UI, one form per key record)
const [keyRecordsFormState, setKeyRecordsFormState] = useState<KeyRecord[]>(
[]
);

useEffect(() => {
if (!isLoading) {
// When the key records have been fetched from the backend, assign the value to the form state
setKeyRecordsFormState(keyRecords);
}
}, [isLoading]);

return (
<KeyRecordsContext.Provider
value={{
isError,
isLoading,
keyRecordsFormState,
}}
>
{children}
</KeyRecordsContext.Provider>
);
};
6 Replies
correct-apricot
correct-apricot•4y ago
You'd want to split things up into two components. I'm going into details here: https://tkdodo.eu/blog/react-query-and-forms
React Query and Forms
Forms tend to blur the line between server and client state, so let's see how that plays together with React Query.
wee-brown
wee-brownOP•4y ago
Thank you for the quick reply! I will read that article 🙂 If I understand right a solution would be something like this: if (data) { return <PersonForm person={data} onSubmit={mutate} /> } return 'loading...' But I think I would have the same issue, at one point keyRecordsFormState is [] while isLoading is false Which ideally should never happen, that's what I'm trying to solve
correct-apricot
correct-apricot•4y ago
why would the keyRecordsFormState be an empty array when loading is false? Loading false means you now have access to the data returned from the queryFn
wee-brown
wee-brownOP•4y ago
Yes I do have access to the server state returned by queryFn, but I duplicate this state for my form so that I can mutate it later… it’s then the state of the form (in keyRecordsFormState) That’s the source of my problem I guess By the time I duplicate it isLoading is false and we’re not done with the duplication in the useEffect
correct-apricot
correct-apricot•4y ago
with what I've described in the article, there is no useEffect ...
const { data } = useQuery(...)

if (data) {
return <Form initialData={data} />
}
return 'loading...'

function Form({ initialData }) {
const [state, setState] = useState<KeyRecord[]>(initialData)
}
const { data } = useQuery(...)

if (data) {
return <Form initialData={data} />
}
return 'loading...'

function Form({ initialData }) {
const [state, setState] = useState<KeyRecord[]>(initialData)
}
wee-brown
wee-brownOP•4y ago
I just realized while simplifying my example I forgot to add that setKeyRecordsFormState is also exposed in the context Right that would be a solution, but then it would display a loading spinner for the whole children tree, at the moment each child handles its own loading state with skeletons. But maybe it would work to have a huge single skeleton component instead… So <FormSkeleton /> instead of Loading in your example I’ll do some refactoring with this approach in mind thanks a lot 😚

Did you find this page helpful?