Too many re-renders. When setting state - reactjs

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.

Related

How exactly data in useEffect work on page load

I am fetching data from Firebase when the page loads with useEffect. I want to use socialLinks state in the application like socialLinks.instagram but I am getting this error " Cannot read properties of undefined (reading 'instagram') " .When I assign socialLinks one by one and assign them in the useEffect and use in the page that doesn't give an error. Can anyone explain why I need to assign them one by one instead read from socialLinks.instagram .You can also find my firebase data image below.
const [socialLinks, setSocialLinks] = useState();
const [instagram, setInstagram] = useState();
const [tiktok, setTiktok] = useState();
const [youtube, setYoutube] = useState();
useEffect(() => {
const getSocialLinks = async () => {
const docRef = doc(socialLinksCol, "24STOxvBEmCezY16LD");
const fireStoreData = await getDoc(docRef);
setSocialLinks(fireStoreData.data()); // I can't use this data in <a>
setYoutube(fireStoreData.data().youtube); // I can use this data in <a>
setInstagram(fireStoreData.data().instagram);
setTiktok(fireStoreData.data().tiktok);
};
getSocialLinks();
}, []);
console.log(socialLinks)
//output : {instagram: 'https://www.instagram.com/', youtube: 'https://www.youtube.com/', tiktok: 'https://www.tiktok.com/en/'}
//This is where I use the socialLinks in
<a href={socialLinks.instagram}>Instagram</a> // This give error
<a href={tiktok}>Tiktok</a> // This is fine
<a href={youtube}>Youtube</a>
The first time your component renders you set up some empty state.
For example:
const [socialLinks, setSocialLinks] = useState();
results in a variable called socialLinks being declared. And since you passed no argument to useState, socialLinks will be undefined.
Now useEffect will run. It will kick off an async action in the background (not blocking the component from rendering).
While the data is fetching your component will try render the first time. You reference socialLinks.instagram and socialLinks is undefined so you get an error.
You probably want to not render anything until the data has been loaded.
There are multiple ways to do that. If you want the code to work with the html you have right now, just declare the initial state as empty for social links like this:
const [socialLinks, setSocialLinks] = useState({
instagram: '', tiktok: '', youtube: ''
});
If you just want to check if the data is there yet then you can leave the state as undefined and then check if its available yet when rendering:
if (socialLinks && socialLinks.instagram) {
return <a href={socialLinks.instagram}>Instagram</a>;
} else {
return null;
}
Remember that calling setSocialLinks causes a state update so it will re-render the component when the data is there!

Rendering order for React apps - why does a "Too many re-renders" error occur?

I'm new to React and modifying an existing starter app.
I'm implementing a check to see if the user is logged in, based on the existence of a token in localStorage. I modified my App.js file from this:
function App() {
let [ isLoggedIn, setLoggedIn ] = useState(false);
return (
<><Index isLoggedIn={isLoggedIn} setLoggedIn={setLoggedIn} /></>
);
to this:
function App() {
let [ isLoggedIn, setLoggedIn ] = useState(false);
const storageToken = localStorage.getItem('token');
if (storageToken) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
return [same as above]
This change results in a Uncaught Error: Too many re-renders. error. Putting the check in Index.js works fine.
Looking into this some more (eg from this other question or this blog post), I understand that it's because you can't modify a state in the same function where you declare it with useState because that state change causes the overall function to run again. But my question is: why? I suspect this is background info about how React works / the order in which React calls components, so any background reading on that topic would be appreciated!
I think you should read this for understading how Reactjs actually works in these cases
App is a functional component, which has its code run on every render. Changing state triggers a new render. By calling setLoggedIn() in the main body of the function, you’re creating an infinite loop.
The solution is to only read from localStorage when the component mounts/unmounts. This can be done with an effect.
useEffect(() => {
// code to run on component mount
const storageToken = localStorage.getItem('token');
if (storageToken) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
}, []);

How is my React hook not respecting default?

I have a component like this
export default function Panel() {
const rpcClient = RpcClient.getInstance(PLUGIN_ID);
setupOnce(rpcClient);
const [currentXMPID, setCurrentXMPID] = useState<string | null>(null);
function isAttachStorageOn(): boolean {
if (!currentXMPID) {
return false;
}
// real code looks at a cookie
return true;
}
// This panel is frequently re-rendered and we need to use saved setting if XMP ID was retrieved already
const renderAttachState = isAttachStorageOn();
// this is outputting true
console.log('set attach checked default to', renderAttachState);
const [isAttachChecked, setAttachChecked] = useState(renderAttachState);
// this is outputting false
console.log('after using state', isAttachChecked);
// This will only fire once
useEffect(() => {
(async () => {
const xmpID = await rpcClient.getXMPID();
setCurrentXMPID(xmpID);
setAttachChecked(isAttachStorageOn());
})();
}, []);
return <div>Just a test</div>;
}
I am bewildered as it logs true and then false. This seems to make zero sense. I am running this component in an app I have limited control over. I did find that this component is being re-rendered a lot and am wondering if there could be issue around that... but even if it re-renders 1000 times it should do so in order such that the console.logs match up.
Update
It seems as though the hook is staying as whatever value is first given in the useEffect which is only run once. If I set setAttachChecked to bananas in that method, no subsequent calls are updating it.
Well, I am not sure whether or not this is the solution you are looking for.
useState is used to set default value. This line here
console.log('after using state', isAttachChecked);
is for displaying isAttachChecked value. And it might be equal to the defaultValue for the first time, but might not be after that even though you called it after useState hook.
I should just be doing const [isAttachChecked, setAttachChecked] = useState(false);
No changes in render should be expected to actually update isAttachChecked. Since I care about return of isAttachStorageOn, which changes value based upon currentXMPID, all I needed to do was make useEffect do:
}, [currentXMPID]);

React Router how to update the route twice in one render cycle

I think it is an obvious limitation in React & React Router, but I'm asking maybe someone will have another idea, or someone else that will come here and will understand it impossible.
I have two custom hooks to do some logic related to the navigation. In the code below I tried to remove all the irrelevant code, but you can see the original code here.
Custom Hook to control the page:
const usePageNavigate = () => {
const history = useHistory()
const navigate = useCallback((newPage) => history.push(newPage), [history])
return navigate
}
Custom Hook to constol the QueryString:
const useMoreInfo = () => {
const { pathname } = useLocation()
const history = useHistory()
const setQuery = useCallback((value) => history.replace(pathname + `?myData=${value}`), [history, pathname])
return setQuery
}
Now you can see the *QueryString hook is used the location to concatenate the query string to the current path.
The problem is that running these two in the same render cycle (for example, in useEffect or in act in the test) causes that only the latest one will be the end result of the router.
const NavigateTo = ({ page, user }) => {
const toPage = usePageNavigate()
const withDate = useMoreInfo()
useEffect(() => {
toPage(page)
withDate(user)
}, [])
return <div></div>
}
What can I do (except creating another hook to do both changes as one)?
Should/Can I implement a kind of "events bus" to collect all the history changes and commit them at the end of the loop?
Any other ideas?

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

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.

Resources