Polling the `useQuery` causes un-necessary re-renders in React component - reactjs

I have a hook that looks like this
export const useThing = id => {
const { stopPolling, startPolling, ...result } = useQuery(GET_THING, {
fetchPolicy: 'cache-and-network',
variables: { id },
skip: !id
});
return {
startPolling,
stopPolling,
thing: result.data.thing,
loading: result.loading,
error: result.error
};
}
Then in my component I'm doing something like this;
const {loading, error, thing, startPolling, stopPolling} = useThing(id);
// this is passed as a prop to a childComponent
const onUpdateThing = ()=> {
setShowInfoMessage(true);
startPolling(5000);
}
const verficationStatus = thing?.things[0]?.verificationStatus || 'PENDING';
useEffect(()=> {
if(verficationStatus === 'FAILED' || verificationStatus === 'SUCCESS') {
stopPolling();
}
}, [verificationStatus])
I'm using the startPolling and startPolling function to dynamically poll the query. The problem with this approach is that whenever I start polling, let's say with an interval of 3 seconds, the component will re-render every 3 seconds. I want the component to render only when the server has sent the updated response.
Here are a few things I've tried to debug;
I've tried profiling the component using React Dev Tools and it displays hooks changed as the cause of re-render.
I'm using multiple useEffects inside my component so my next thought
was to check to see if any of the useEffect is causing the re-renders. I've tried adding and removing every single dependency in my useEffect calls and it doesn't seem to help.
I'm not storing any value returned in the state either.
Child component calls the mutation.
I've also tried using React.memo but it causes more problems than it solves.
Any ideas as to what might be causing the problem would be greatly appreciated.

Related

How to properly re-render functional component after API call

When the page loads, I am making an API call, displaying a table with appointments. After the API call, I set a state for hasData to true, and the data is inserted in another setState. The issue is when the API returns the data from the async call, the component does not show the data. Please see code below.
const [recentAppointmentData, setRecentAppointmentData] = useState([])
const [hasAppointmentData, setHasAppointmentData] = useState(false)
const getAppointments = useCallback(() => {
const getAppointmentDataService = new GetAppointmentsService();
getAppointmentDataService.getDataFromService("263749804").then((results) => {
console.log("APPOINTMENT DATA ", results);
results.recentAppointments.map((result) => {
var recentAppointments = {
appointmentObject: {
serviceCategory: [],
serviceId: "",
appointmentDate: "",
groomer: "",
resourceId: "",
visitId: "",
},
};
if (result["services"] !== undefined) {
console.log("SERVICESS", result["services"]);
result["services"].map((service) => {
recentAppointments.appointmentObject.serviceCategory.push(
service["serviceCategory"]
);
recentAppointments.appointmentObject.serviceId = service["serviceId"];
});
}
recentAppointments.appointmentObject.appointmentDate = moment(
result["appointmentDateTime"]
).format("MM/DD/YY");
recentAppointments.appointmentObject.groomer = result["groomer"];
recentAppointments.appointmentObject.resourceId = result["resourceId"];
recentAppointments.appointmentObject.visitId = result["visitId"];
appointments.push(recentAppointments.appointmentObject);
Here I am setting the has Appointment data to true after the async function has been completed.
if (!hasAppointmentData) {
setHasAppointmentData(true);
}
});
Here I am storing the data in another state.
if (!hasAppointmentData) {
console.log("APPOINTMEN", appointments);
setRecentAppointmentData(appointments);
}
});
}, [hasAppointmentData]);
I am calling the function in the useEffect.
useEffect(() => {
getAppointments();
renderTabs();
}, [getAppointments, renderTabs]);
Can someone guide me on what I am doing wrong? Thanks
The problem is that you're using the useEffect hook wrong.
useEffect runs every time one of its dependencies change, or runs just once when the component mounts if you don't pass in any dependency to it. The dependencies are usually state variables within the component that useEffect runs in.
You want your getAppointments() to run only once, since it calls an external API to get the data. And you want to call renderTabs() (which I assume is responsible for displaying the data in the UI) only when the data is available. So you need to put them into two separate useEffect hooks.
useEffect(() => {
getAppointments();
}, []); // Runs just once when the component is mounted
useEffect(() => {
if (hasAppointmentData) {
renderTabs();
}
}, [hasAppointmentData]); // Runs every time the value of hasAppointmentData changes
But you'll need to watch out for a problem here, when using hasAppointmentData as the dependency. You're calling setHasAppointmentData first, and then following it up with setRecentAppointmentData. The second useEffect hook would run right after you set the boolean to true. By the time renderTabs() tries to fetch the data from recentAppointmentData, the data may not have been updated.
To me, hasAppointmentData is pretty much useless here. Checking for recentAppointmentData.length would serve you just as well, and is guaranteed to work reliably every time. So my second hook would look like this:
useEffect(() => {
if (recentAppointmentData.length) {
renderTabs();
}
}, [recentAppointmentData.length]);

REACT - Hook make infinite loop

i have a problem with an hooks.
When i make
export default function Checkout() {
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
const handleReadRemoteFile = file => {
readRemoteFile(file, {
complete: (results) => {
setParsedCsvData(results.data);
},
});
};
handleReadRemoteFile(CSVsource);
}
I have an infinite loop but when i make
export default function Checkout() {
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
const handleReadRemoteFile = file => {
readRemoteFile(file, {
complete: (results) => {
console.log(results.data);
},
});
};
handleReadRemoteFile(CSVsource);
}
I have'nt the problem.
Have you an idea ?
Thank's Yann.
While the component code you posted is still incomplete (CSVsource is undefined, there's no return), I think the problem is visible now:
You're running handleReadRemoteFile(CSVsource) on every render (because you're calling it directly). When the file is read, you are setting parsedCsvData, which (because state updates and prop updates trigger a rerender) renders the component again, triggering handleReadRemoteFile once again...
So what's the solution?
Data fetching is a classical example of using the useEffect hook. The following code should do what you want
const { readRemoteFile } = usePapaParse();
const [parsedCsvData, setParsedCsvData] = useState([]);
useEffect(()=>{
readRemoteFile(CSVSource, {
complete: results => setParsedCsvData(results.data);
});
}, [CSVsource]); // rerun this whenever CSVSource changes.
Please read up on the useEffect hook in the React docs.
Note: there's one problem with the code I posted above. When the component is unmounted while the CSV is still loading, you will get an error "state update on unmounted component" as soon as the loading finished. To mitigate this, use the return function of useEffect in combination with a local variable.
So the final code looks like this:
useEffect(()=>{
let stillMounted = true;
readRemoteFile(CSVSource, {
complete: results => {
if(!stillMounted) return; // prevent update on unmounted component
setParsedCsvData(results.data);
}
});
return ()=>stillMounted = false; // this function is ran before the effect is ran again and on unmount
}, [CSVsource]);

setState isn't updating immediately

I am working with a react aplication and i'm tryna to update a state and it isnt updating immediately. I looked for it inm internet and found, but the problem is, all this people are using classes componets and i'm using functional componets. they are talking about callbacks in setState function, but i tried it in my code, and didn't work!!
Heres my code:
async function handleSelectImage(event: ChangeEvent <HTMLInputElement>) {
if (!event.target.files) {
return;
}
const selectedImages = Array.from(event.target.files);
selectedImages.map((image) => {
if (!(image.type === 'image/png' || image.type === 'image/jpg' || image.type === 'image/jpeg')) {
const imageIndex = selectedImages.indexOf(image);
selectedImages.splice(imageIndex, 1);
alert('Só são aceitos arquivos jpeg, jpg e png.');
}
});
try {
setImages(images.concat(selectedImages));
} catch (err) {
console.error(err);
}
console.log(images);
Hope you can help me!!!!
THX!!! :)
State can't be read in functional components in React because it's an async operation.
So, it's not that your state ISN'T updating, but that your console.log(images) function is called and read before your async function that updates state returns.
Okay... so what to do about that?
Two options:
1. Pass state into another component and read it there.
This one is preferred, imo, because you can separate your stateful components from your "dumb" components.
So in your component above, you can pass images as a prop to a child component:
Inside the ImageDisplay, get the images state from props.
2. Wait for the async function to update inside your component.
If you really want to read state inside your functional component, you have to wait for the async function to return. To do this, I think the easiest way is to set a "waiting" state, like so.
const [isLoading, setLoading] = useState(true);
/// rest of your call and your async function
///try block:
try {
setImages(images.concat(selectedImages));
setLoading(false);
} catch (err) {
console.error(err);
}
if(setLoading) {
console.log("The images haven't loaded yet");
} else {
console.log(images)
}
Basically, you're giving the component a conditional to wait for the image state to change. When that conditional is no longer true, the images will be ready to be displayed. (You can do this with rendering data on the frontend, too, not just the console!)
Good luck.

Too many re-renders. When setting state

I'm trying to make an e-commerce app using expo with typescript and I'm having issues with loading my categories page.
There's two ways which one can access the category page
Through bottom nav - this one works fine
Through buttons on the home page
Trying to setState based off the params that's coming in through the homepage but it's giving me the Too many re-renders error.
Below is the code that I currently have for it.
The default needs to be there because of the first method of entering the category page, I've tried putting a conditional as the default category, but category wont change the second time if being accessed through the home page again.
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
if (params) {
setCategory({categories: params.categories})
}
}
You can also use useEffect with useRef:
export default function CategoryList(props: Props) {
let { params } = useRoute<StackRouteProp<'Home'>>()
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const canSet = useRef(true)
useEffect(() => {
if (params && canSet.current) {
setCategory({ categories: params.categories })
canSet.current = false
} else {
canSet.current = true
}
}, [params])
Everytime you call setCategory, it will re-render the component. Then, every time you render the component, if params exists, it will call setCategory again, which will cause another render, which calls setCategory again, and so on and on until it hits React's render limit.
One possible way to work around this would be to add a boolean to set after the first time which will prevent it from going inside the if block the second time:
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const [hasParamsCategory, setParamsCategory] = useState(false);
if (params && !hasParamsCategory) {
setCategory({categories: params.categories});
setParamsCategory(true);
}
This still isn't perfect because in general it's not a good practice to directly call state-changing functions during render, but this will serve as a quick and dirty workaround.

Why can't useEffect access my state variable in a return statement?

I don't understand why my useEffect() React function can't access my Component's state variable. I'm trying to create a log when a user abandons creating a listing in our app and navigates to another page. I'm using the useEffect() return method of replicating the componentWillUnmount() lifecycle method. Can you help?
Code Sample
let[progress, setProgress] = React.useState(0)
... user starts building their listing, causing progress to increment ...
console.log(`progress outside useEffect: ${progress}`)
useEffect(() => {
return () => logAbandonListing()
}, [])
const logAbandonListing = () => {
console.log(`progress inside: ${progress}`)
if (progress > 0) {
addToLog(userId)
}
}
Expected Behavior
The code would reach addToLog(), causing this behavior to be logged.
Observed Behavior
This is what happens when a user types something into their listing, causing progress to increment, and then leaves the page.
The useEffect() method works perfectly, and fires the logAbandonListing() function
The first console.log() (above useEffect) logs something greater than 0 for the progress state
The second console.log() logs 0 for the progress state, disabling the code to return true for the if statement and reach the addToLog() function.
Environment
Local dev environment of an app built with Next.js running in Firefox 76.0.1
nextjs v 8.1.0
react v 16.8.6
I'd really appreciate some help understanding what's going on here. Thanks.
I think it is a typical stale closure problem. And it is hard to understand at first.
With the empty dependency array the useEffect will be run only once. And it will access the state from that one run. So it will have a reference from the logAbandonListing function from this moment. This function will access the state from this moment also. You can resolve the problem more than one way.
One of them is to add the state variable to your dependency.
useEffect(() => {
return () => logAbandonListing()
}, [progress])
Another solution is that you set the state value to a ref. And the reference of the ref is not changing, so you will always see the freshest value.
let[progress, setProgress] = React.useState(0);
const progressRef = React.createRef();
progressRef.current = progress;
...
const logAbandonListing = () => {
console.log(`progress inside: ${progressRef.current}`)
if (progressRef.current > 0) {
addToLog(userId)
}
}
If userId is changing too, then you should add it to the dependency or a reference.
To do something in the state's current value in the useEffect's return function where the useEffects dependencies are am empty array [], you could use useReducer. This way you can avoid the stale closure issue and update the state from the useReducer's dispatch function.
Example would be:
import React, { useEffect, useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "set":
return action.payload;
case "unMount":
console.log("This note has been closed: " + state); // This note has been closed: 201
break;
default:
throw new Error();
}
}
function NoteEditor({ initialNoteId }) {
const [noteId, dispatch] = useReducer(reducer, initialNoteId);
useEffect(function logBeforeUnMount() {
return () => dispatch({ type: "unMount" });
}, []);
return <div>{noteId}</div>;
}
export default NoteEditor;
More info on this answer
When you return a function from useEffect, it behaves like componentWillUnmount so I think it only runs while cleaning up. You'd need to actually call logAbandonListing like:
useEffect(() => {
logAbandonListing();
}, []);
So it runs everytime a component re-renders. You can read more about useEffect on https://reactjs.org/docs/hooks-effect.html
It's written excellently.
I tried using this sandbox to explain my answer.
Basically you are returning a function from your useEffect Callback. But that returned function is never really invoked so it does no actually execute and thus log the abandon action. If you look at the Code in the sandbox I have added a wrapper Parens and () afterwards to actually cause the method to be invoked leading to console.log executing.

Resources