What is the best way to mutate remote data fetched with useQuery - reactjs

I am fairly new to graphQL and Apollo. I hope I can make myself clear:
I am fetching data using the apollo/react-hook useQuery. Afterwards I populate a form with the data so the client can change it. When he is done, the data gets send back to the server using useMutation.
Until now, I use onCompleted to store the fetched data in the component state. That looks like this:
import React, { useState } from 'react';
import { TextField } from '#material-ui/core';
const Index = () => {
const [state, setState] = useState(null)
const {data, loading, error} = useQuery<typeof queryType>(query, {
onCompleted: data => {
// modify data slightly
setState(data)
}
})
return (
<TextField value={state} onChange={() => setState(event.target.value)}/>
)
}
The form than uses the values stored in the component state and the form handlers use setState
to change it.
My question now is, if this is the best practice and if the storing of the fetched data in a local component state neccessary.

Because you don't want to just fetch and render the data -- you want to be able to mutate it when the form values change -- we can't just utilize the data as is. Unless you're using uncontrolled inputs and refs to manage your forms (which you probably shouldn't do), then you're going to need to use some component state.
I think your current approach is mostly fine. The biggest downside is that if the query takes a while (maybe the user has a spotty internet connection), there's a window for them to start filling out the form only to have their input overridden once the query completes. You'd have to explicitly disable the inputs or hide the form until loading completes in order to prevent this scenario.
An alternative approach is to split your component into two:
const Outer = () => {
const {data, loading, error} = useQuery(query)
if (!data) {
return null // or a loading indicator, etc.
}
return <Inner data={data}/>
}
const Inner = ({ data }) => {
const [value, setValue] = useState(data.someField)
<TextField value={value} onChange={() => setValue(event.target.value)}/>
}
By not rendering of the inner component until loading is complete, we ensure that the initial props we pass to it have the data from the query. We can then use those props to initialize our state.

Seems like useQuery only has a state for responseId: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useQuery.ts
But you get a refetch function from the useQuery as well.
const { loading, error, data, refetch } = useQuery(...);
Try calling refetch(), after you used useMutation() to update the data. It shouldn't trigger a rerender. You probably have to set your own state for that reason.
Maybe something like:
const handleUpdate = () =>{
setData(refetch());
}
Alternativly you get the data after using useMutation which is also in the state: https://github.com/trojanowski/react-apollo-hooks/blob/master/src/useMutation.ts
const [update, { data }] = useMutation(UPDATE_DATA);
data will always be the newest value so you could also do:
useEffect(()=>{
setData(data);
// OR
// setData(refetch());
}, [data])

Related

react table - useMemo vs useState

The react table documentation says that the data values should be memoized. However, in some of their examples on the site, they use useState instead of useMemo. Is useState already memoized? If not, what issues might I run into if the data is not memoized? (or how should it be done correctly when needing to change the data?) TIA!
documentation link with example of useState (link to code in case documentation link becomes 404 like a lot of old posts linking to old versions of react table documentation that I've come across)
I am not answering your question, but to show you the possible reason why they use useState against their Must be memoized statement.
Let's see react's documentation about useMemo & useState:
useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
useState Returns a stateful value, and a function to update it.
The first example is where the memoized dependencies rawData are passed down from its parent component and we want to customize the data by using expensiveMakeDataCalculation method. Here, the table data is renewed when changes of rawData are detected and we use useMemo for the sake of optimization, as the docs said.
function ExampleA({ rawData }) {
const data = useMemo(() => expensiveMakeDataCalculation(rawData), [rowData]);
...
}
But how if we want to fetch the data within the component? Yes, we can use useState to store our fetched data. As we know that rawData will always be the most recent state after applying updates and the memoized data will be renewed once rawData state changes.
function ExampleB({ initialData }) {
const [rawData, setRawData] = useState(initialData);
const data = useMemo(() => expensiveMakeDataCalculation(rawData), [rawData]);
// for example only
const onClick = (id) => {
try {
const res = await fetch(`url/${id}`);
const fetchedData = await res.json();
setRawData(fetchedData);
} catch (error) {
console.log(error);
}
}
...
}
Here, we can simplify the code as below. We do not use useMemo anymore, since we are confident that the expensiveMakeDataCalculation only runs when the onCLick method is called and the data state can be consumed immediately by your table. You can memoize the data by using useMemo, but I think it is not necessary.
function ExampleC({ initialData }) {
const [data, setData] = useState(initialData);
// for example only
const onClick = (id) => {
try {
const res = await fetch(`url/${id}`);
const rawData = await res.json();
const calculatedData = expensiveMakeDataCalculation(rawData);
setData(calculatedData);
} catch (error) {
console.log(error);
}
}
...
}
So, based on the above example, we can have new insight into how this example works and know the reason why the example uses useState over their Must be memoized statement. Here, the expensiveMakeDataCalculation is called inside setData setter and only runs when updateMyData method called.
const [data, setData] = React.useState(() => makeData(20))
const updateMyData = (rowIndex, columnId, value) => {
...
setData(old =>
// expensiveMakeDataCalculation. expensive? yes if you have a lot of rows data
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
}
}
return row
})
)
}

How to refresh graphql data on state change

import { useQuery, gql, useMutation } from "#apollo/client";
const Questions = () => {
const [modal, setModal] = useState(false)
const QUESTION_QUERIES = gql`
query getQuestions(
$subjectRef: ID
$gradeRef: ID
$chapterRef: ID
$status: String
) {
getQuestions(
subjectRef: $subjectRef
gradeRef: $gradeRef
chapterRef: $chapterRef
status: $status
) {
id
question_info
question_type
answer
level
published
subjectRef
gradeRef
chapterRef
levelRef
streamRef
curriculumRef
options
status
subject
grade
chapter
stream
curriculum
}
}
`;
const { loading, error, data } = useQuery(QUESTION_QUERIES);
return (
<div>
</div>
)
}
Here is my react graphql code.
I wants to fetch data when modal change using state if modal status change to true to false or false to
true it will make api call to fetch questions again
Please take a look how to solve the issue.
use useLazyQuery:
const [updateFn,{ loading, error, data }]= useLazyQuery(QUESTION_QUERIES);.
Then create useEffect with modal as dependency variable, and call updateFn inside useEffect
You want to fetch data after the modal state change, So you simply use useEffect and put modal in the dependency list of the useEffect and for useQuery there is also a function called refetch, the logic would be like this
const { loading, error, data, refetch } = useQuery(QUESTION_QUERIES);
useEffect(() => {
// the reason I put if condition here is that this useEffect will
// also run after the first rendering screen so you need to put a check
// to do not run refetch in that condition
if (data) refetch();
}, [modal]);

React: Query, delay useReducer

I've been scratching my head around this one for quite some time now, but I'm still not sure what to do.
Basically, I'm pulling data from a database via useQuery, which all works well and dandy, but I'm also trying to use useReducer (which I'm still not 100% familiar with) to save the initial data as the state so as to detect if any changes have been made.
The problem:
While the useQuery is busy fetching the data, the initial data is undefined; and that's what's being saved as the state. This causes all sorts of problems with regards to validation amd saving, etc.
Here's my main form function:
function UserAccountDataForm({userId}) {
const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
const rows = React.useMemo(() => {
if (userData) { /* Process userData here into arrays */ }
return [];
}, [isLoadingUserData, userData]); // Watches for changes in these values
const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
const { submit, /* updateFunctions here */ } = methods;
if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues
return (
<>
Render stuff here
*isDirty* is used to detect if changes have been made, and enables "Update Button"
</>
)
}
Here's useUserData (responsible for pulling data from the DB):
export function useUserData(user_id, column = "data") {
const query = useQuery({
queryKey: ["user_data", user_id],
queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
staleTime: Infinity,
});
}
return { query }
And here's the reducer:
function userDataFormReducer(state, action) {
switch(action.type) {
case "currency":
return {... state, currency: action.currency}
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
default:
return;
}
}
function useUserDataForm(handleSubmit, userData) {
const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.
const updateFunction = (type, value) => { // sample only
dispatch({type: type, value: value});
}
}
export { useUserDataForm };
Compounding the issue is that it doesn't always happen. The main form resides in a <Tab>; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
The quickest "fix" I can think of is to NOT set the initial data (by not calling the reducer) while useQuery is running. Unfortunately, I'm not sure this is possible. Is there anything else I can try to fix this?
Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
This is likely to be expected because useQuery will give you data back from the cache if it has it. So if you come back to your tab, useQuery will already have data and only do a background refetch. Since the useReducer is initiated when the component mounts, it can get the server data in these scenarios.
There are two ways to fix this:
Split the component that does the query and the one that has the local state (useReducer). Then, you can decide to only mount the component that has useReducer once the query has data already. Note that if you do that, you basically opt out of all the cool background-update features of react-query: Any additional fetches that might yield new data will just not be "copied" over. That is why I suggest that IF you do that, you turn off the query to avoid needless fetches. Simple example:
const App = () => {
const { data } = useQuery(key, fn)
if (!data) return 'Loading...'
return <Form data={data} />
}
const Form = ({ data }) => {
const [state, dispatch] = useReducer(userDataFormReducer, data)
}
since the reducer is only mounted when data is available, you won't have that problem.
Do not copy server state anywhere :) I like this approach a lot better because it keeps server and client state separate and also works very well with useReducer. Here is an example from my blog on how to achieve that:
const reducer = (amount) => (state, action) => {
switch (action) {
case 'increment':
return state + amount
case 'decrement':
return state - amount
}
}
const useCounterState = () => {
const { data } = useQuery(['amount'], fetchAmount)
return React.useReducer(reducer(data ?? 1), 0)
}
function App() {
const [count, dispatch] = useCounterState()
return (
<div>
Count: {count}
<button onClick={() => dispatch('increment')}>Increment</button>
<button onClick={() => dispatch('decrement')}>Decrement</button>
</div>
)
}
If that works is totally dependent on what your reducer is trying to achieve, but it could look like this:
const App = () => {
const { data } = useQuery(key, fn)
const [state, dispatch] = useReducer(userDataFormReducer)
const currency = state.currency ?? data.currency
}
By keeping server state and client state separately, you'll only store what the user has chosen. The "default values" like currency stay out of the state, as it would essentially be state duplication. If the currency is undefined, you can still choose to display the server state thanks to the ?? operator.
Another advantage is that the dirty check is relatively easy (is my client state undefined?) and resets to the initial state also just mean to set the client state back to undefined.
So the actual state is essentially a computed state from what you have from the server and what the user has input, giving precedence to the user input of course.

React hook useCallback with get API call inside useEffect not behaving correctly

I want to achieve the following.
Action from redux is imported in this component. It fetches data from firebase, which I use as a database. Data is displayed inside this component.
My problem is that data is fetched every time component is mounted, which is not an ideal scenario.
I would like to prevent that behavior.
Ideal scenario is that data should be fetched only first time component is rendered and no more afterwards. Of course it should also fetch when data is changed otherwise no. I used useCallback, but certainly there is a mistake in the code below. Token and Id are passed when user is authenticated.
import { fetchFavs } from "../../store/actions";
import { useSelector, useDispatch } from "react-redux";
const token = useSelector((state) => state.auth.token);
const id = useSelector((state) => state.auth.userId);
const fetchedFavs = useSelector((state) => state.saveFavs.fetchedFavs);
const dispatch = useDispatch();
const fetchFavsOptimized = useCallback(() => {
dispatch(fetchFavs(token, id));
console.log("rendered");
}, [token, id, dispatch]);
useEffect(() => {
if (token) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token]);
There are two ways to handle this, one explicit and one implicit.
The explicit way would be to add a new boolean item in your redux store and check against that every time you load your component to see if you should load the data from the db or not.
So your redux store may have a new key/value: dataLoaded: false;
In your component now you need to load this from your redux store and check against it in your use effect:
const dataLoaded = useSelector((state) => state.dataLoaded);
useEffect(() => {
if (token && !dataLoaded) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, dataLoaded]);
Don't forget in your fetchFavsOptimized function to update the dataLoaded to true after the loading of the data from the db is complete.
This has the upside that every time you want to load the db from the db again you simply need to set dataLoaded to false.
The implicit way is to check the data length and if that is 0 then execute the load. Assuming that you store data in your redux store in this form: data: [] and your data is an array (you could do the same thing by initiating data as null and check if data !== null) your component would look like this:
const data = useSelector((state) => state.data);
useEffect(() => {
if (token && data.length === 0) {
fetchFavsOptimized();
}
}, [fetchFavsOptimized, token, data]);
Keep in mind that I wouldn't do it like that because if you don't have any data you may end up with an infinite loop, but it's a good technique to use in other cases.
For fetching only once on component mounting: (mind [])
React.useEffect(() => {
fetchFavsOptimized()
}, []) // <- this empty square brackets allow for one execution during mounting
I'm not sure, but the issue of continous calling of useEffect might be [fetchFavsOptimized, token]. Placing fetchFavsOptimized function in [] might not be the best way.
You want to fetch, when data changes, but what data? You mean token ? Or something else?
React.useEffect() hook alows for fetching when data changes with placing this changable data in [here_data] of useEffect. Example:
const [A, setA] = React.useState();
React.useEffect(() => {
// this block of code will be executed when state 'A' is changed
},[A])
The best practise of using useEffect is to separate the conditions in runs with.
If You need to fetch on some data change AND on component mount, write TWO useEffect fuctions. Separate for data change and component mount.

Prevent infinite renders when updating state variable inside useEffect hook with data fetched using useQuery of graphql

Graphql provides useQuery hook to fetch data. It will get called whenever the component re-renders.
//mocking useQuery hook of graphql, which updates the data variable
const data = useQuery(false);
I am using useEffect hook to control how many times should "useQuery" be called.
What I want to do is whenever I receive the data from useQuery, I want to perform some operation on the data and set it to another state variable "stateOfValue" which is a nested object data. So this has to be done inside the useEffect hook.
Hence I need to add my stateOfValue and "data" (this has my API data) variable as a dependencies to the useEffect hook.
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = (currentState) => {
return {
...currentState,
options: [1, 2, 3]
};
}
useEffect(() => {
if (data) {
let newValue = someOperation(stateOfValue);
setStateOfValue(newValue);
}
}, [data, stateOfValue]);
Basically I am adding all the variables which are being used inside my useEffect as a dependency because that is the right way to do according to Dan Abramov.
Now, according to react, state updates must be done without mutations to I am creating a new object every time I need to update the state. But with setting a new state variable object, my component gets re-rendered, causing an infinite renders.
How to go about implementing it in such a manner that I pass in all the variables to my dependency array of useEffect, and having it execute useEffect only once.
Please note: it works if I don't add stateOfValue variable to dependencies, but that would be lying to react.
Here is the reproduced link.
I think you misunderstood
what you want to be in dependencies array is [data, setStateOfValue] not [data, stateOfValue]. because you use setStateOfValue not stateOfValue inside useEffect
The proper one is:
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = useCallback((prevValue) => {
return {
...prevValue,
options: [1, 2, 3]
};
},[])
useEffect(() => {
if (data) {
setStateOfValue(prevValue => {
let newValue = someOperation(prevValue);
return newValue
});
}
}, [data, setStateOfValue,someOperation]);
If you want to set state in an effect you can do the following:
const data = useQuery(query);
const [stateOfValue, setStateOfValue] = useState({});
const someOperation = useCallback(
() =>
setStateOfValue((current) => ({ ...current, data })),
[data]
);
useEffect(() => someOperation(), [someOperation]);
Every time data changes the function SomeOperation is re created and causes the effect to run. At some point data is loaded or there is an error and data is not re created again causing someOperation not to be created again and the effect not to run again.
First I'd question if you need to store stateOfValue as state. If not (eg it won't be edited by anything else) you could potentially use the useMemo hook instead
const myComputedValue = useMemo(() => someOperation(data), [data]);
Now myComputedValue will be the result of someOperation, but it will only re-run when data changes
If it's necessary to store it as state you might be able to use the onCompleted option in useQuery
const data = useQuery(query, {
onCompleted: response => {
let newValue = someOperation();
setStateOfValue(newValue);
}
)

Resources