Run Apollo mutation on first render with useEffect - reactjs

I have a form in a MUI dialogue. I want to create an order when the dialogue first opens. I thought useEffect with empty dependency would do that but I can't seem to figure out why it doesn't let the mutation resolve before the setState.
const [createOrder] = useMutation(CREATE_ORDER);
const [activeOrder, setActiveOrder] = useState({});
useEffect(() => {
const newOrder = async () => {
await createOrder({
variables: {
order: {
type,
table,
},
},
refetchQueries: [{ query: ORDERS_QUERY }],
});
};
setActiveOrder(newOrder);
}, []);
console.log(activeOrder); // gives me Promise{<fulfilled>: undefined}
Also, is the only way to get rid of the missing dependency warning with // eslint-disable-next-line? If I wrap my function in a useCallback to prevent render loop and add the dependencies requested, then useCallback gives me an error that the dependencies are unknown.

your problem is here setActiveOrder(newOrder);
setActiveOrder can accept function as an argument and call it;
newOrder is async function. async function always returns promise;
solution:
/*
maybe your don't need activeOrder, check data from result
https://www.apollographql.com/docs/react/data/mutations/#result
*/
const [createOrder, { data }] = useMutation(CREATE_ORDER, {
// https://www.apollographql.com/docs/react/data/mutations/#options
variables: { type, table },
refetchQueries: [{ query: ORDERS_QUERY }],
});
const [activeOrder, setActiveOrder] = useState({});
useEffect(() => {
createOrder().then((res) => {
setActiveOrder(res);
});
}, [createOrder]);
useEffect(() => console.log('data:\n', data), [data]);
useEffect(() => console.log('activeOrder:\n', activeOrder), [activeOrder]);

Related

How to stop useEffect from making so many requests? Empty Dependencies don't work

I have a component that updates a piece of state but I'm having issues with it
I have the state declared
const [data, setData] = useState([]);
Then in my useEffect I am
useEffect(() => {
const fetchData = async () => {
await axios
.get(
API_URL,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY
},
params:{
"titleId": id
}
}
)
.then((response) => {
setData(response.data.Item);
})
.catch((err) => {
console.error("API call error:", err.message);
});
}
fetchData();
}, [data, id])
If I declare "data" in my dependencies, I get an endless loop of requests which is obviously no good. But if I leave 'data' out from the dependencies it shows nothing, though I am successfully retrieving it in my network's tab and even when I {JSON.styringify(data)} in a div tag aI get the json content too. So the info is in the DOM, but it's not updating the components
How can I do this so I can make an initial request to load the data and not thousands of them?
I've tried the following:
a setTimeout on the callback function
the isCancelled way with a return (() => { callbackFunction.cancel(); })
And there is an Abort way of doing this too but I can't figure it out. Every example I've seen is for class components
Sorry for the vague code. I can't replicate this without lots of coding and an API. Thanks in advance
You want to set the state and then check if is different. I use a custom hook for this which uses the useRef hook:
export function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const prevData = usePrevious(data);
I don't know what your data looks like, but build a conditional from it. Inside of your useEffect you'll need something like:
if (data !== prevData) fetchData()
or
if (data.id !== prevData.id) fetchData()
You'll then add prevData to you dependencies:
[data, prevData, id]
So useEffects works with dependency.
With dependency - on changing dependency value useEffect will trigger
useEffect(() => {
// code
}, [dependency])
With empty brackets - will trigger on initial of component
useEffect(() => {
// code
}, [])
Without dependency and Brackets - will trigger on every state change
useEffect(() => {
// code
})
Do something like this, if that can help. I also used async/await so you can check that.
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(API_URL, {
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
},
params: {
titleId: id,
},
});
setData(response.data.Item);
} catch (err) {
console.error('API call error:', err.message);
}
};
fetchData();
}, [id]);
if (!data.length) return null;
return <p>Yes, I have data</p>;
};
obviously you will get an infinit loop !
you are updating the data inside your useEffect which means each time the data changes, triggers useEffect again and so on !
what you should do is change your dependencies depending on your case for example :
const [data, setData] = useState([])
const [fetchAgain, setFetchAgain] = useState(false)
useEffect(()=> {
fetchData();
}, [])
useEffect(() => {
if(fetchAgain) {
setFetchAgain(false)
fetchData();
}
}, [fetchAgain])
now each time you want to fetch data again you need to update the fetchAgain to true

How to control the execution timing of useEffect

When user click search button, reset current page to 1, and set filter object, and then call fetch function.
The projectId is a global variable, when it's changed, need to reset page and filter, and reload data.
useEffect require dependence of fetch function, but when fetch function change, it will re-execute fetch function.
How can I deal with this logic and not to trigger any warning of eslint?
Sorry, my English is not good.
const fetch = useCallback(async () => {
setLoading(true);
// Call the request function passed in from the outside
const { data, total } = await request({ current: currPage }, filter);
setList(data);
setTotal(total);
setLoading(false);
}, [currPage, filter, request]);
useEffect(() => {
setCurrPage(1);
setFilter({});
fetch();
}, [fetch, projectId]);
I think there is no need to keep fetch in the dependency array of useEffect When the projectId changes you will change the currPage & filter which will automatically trigger the useEffect which has the fetch() function in it.
const fetch = useCallback(async () => {
setLoading(true);
// Call the request function passed in from the outside
const { data, total } = await request({ current: currPage }, filter);
setList(data);
setTotal(total);
setLoading(false);
}, [currPage, filter, request]);
useEffect(() => {
setCurrPage(1);
setFilter({});
fetch();
}, [projectId]);
I suggested this code serves the issue.

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

useState not updating an array correctly

I'm using React with Firebase to fetch data from firestore and display it to the user. Within a useEffect hook, I fetch the data and add it to an array of json objects. However, when adding a new element to the array, the previous one gets deleted. Below will be relevant portions of code.
const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(true);
const { currentUser } = useAuth();
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
}, []);
I've also tried updating the items list within the useEffect hook, but it produces the same bug.
Let me take a moment to explain what is happening here.
Your have to think in your react component as a "Snapshot", where a snapshot is the result of a render. Each snapshot points to a state created by useState and only to that one. What does this mean?
The first time your component renders (first snapshot) the items variable returned by useState is pointing to an empty array and as long as that snapshot is alive it would be pointing to that array.
So, lets take a look to the function called by useEffect
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
you are calling the set function for items once per elemtent in firebase query result and each call is seting a value that is the result of calling addItemToList
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
the point here is that you are always destructuring ...items which is always the items variable that the snapshot of the component is pointing to and it would not be updated until the component re rederds.
So, basically you all aways doing updatedList = [ ...[], { ... } ] becauses items has to wait to take update his value.
#Viet already responded with something good, which I think it's the best option
mapping the result of the query to an array and updating the array with the result of the map a parameter.
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const result = [];
itemSnapshot.forEach((doc) => {
result.push({
data: i.data(),
ref: i.ref
});
})
setItems(result);
})
setLoading(false);
}, []);
You need to add "addItemToList" to the dependencies of the useEffect hook.
Because you haven't done this, the effect references an old version of that function which references an old version of the items array.
You just use itemSnapshot.map like this:
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
setItems(
itemSnapshot.map((doc) => ({
data: i.data(),
ref: i.ref,
})),
);
})
Instead of running it for each value maybe you should try to map it as a list and update the state once
function addItemsToList(newItems) {
const updatedList = [
...items,
...newItems
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const docs = itemSnapshot.map((doc) => ({
data: doc.data(),
ref: doc.ref
});
setItems(addItemsToList(docs));
})
setLoading(false);
}, []);
You are using spread operator in a wrong way! This way will not update the array.
You can do that by using es6 spread to concat multiple arrays something like below:
const updatedList = [
...items,
...[{ data: i.data(), ref: i.ref }]
]
To know more: Using es6 spread to concat multiple arrays

Axios and looped promises

I have problem with loop on axis GET request, and I can't understood why.
const [ state, setState ] = useState<any[]>([]);
ids.forEach((id) => {
getData(id)
.then((smth: Map<string, any>[]) => getNeededData(smth, id));
});
console.log(JSON.stringify(state));
and getData (getNeededData is only choose parameters):
export const getData= async (id: string) => {
const response = await Axios.get(`/rest/${id}`)
.then((res: { data: any; }) => res.data);
return response;
};
I should have 2 response (it's 2 id in variable "ids"), but I have first, second, first, second, first, and this in a loop.
Why it's been working like this?
What I can change for fix this?
By putting that forEach at the top level of your component function, you're running it every time the function is called by React to render its contents, which React does when state changes. The code you've shown doesn't set state, but I'm assuming your real code does.
To do it only when the component first mounts, wrap it in a useEffect callback with an empty dependency array:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
ids.forEach((id) => {
getData(id)
.then(/*...*/);
});
}, []);
If all of the results are going in the state array, you probably want to use map and Promise.all to gether them all up and do a single state change with them, for instance:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
Promise.all(
ids.map((id) => {
return getData(id).then(/*...*/);
})
)
.then(allResults => {
// Use `allResults` to set state; it will be an array in the same order
// that the `id` array was in
})
.catch(error => {
// handle/report error
});
}, []);

Resources