Using hooks results in infinite loop - reactjs

I have a modal component that has to only appear once, when user opens up the website. Here is my code:
import React, { useState } from 'react';
import { Modal, Button } from 'antd';
function WelcomeModal() {
const [visibility, setVisibility] = useState(true);
function handleVisibility() {
setVisibility(!visibility);
}
return (
<div>
<Modal
title="Vertically centered modal dialog"
centered
visible={visibility}
onOk={handleVisibility()}
onCancel={handleVisibility()}
></Modal>
</div>
)
}
export default WelcomeModal;
I created a state called visibility which is initially true, after the user clicks on Cancel or OK on the modal, I try to change the visibility to false, so the modal closes. The problem is that I get the following error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
How can I fix this ?

It looks like you mean to pass the handleVisibility function into the Modal component as the onOk and onCancel props but you are accidentally calling it with the parenthesis ().
This means that when the component renders for the first time, it calls the function, which changes the state, which triggers a rerender which then calls the function again and so on.

Related

Forceful Re-render with useEffect

import * as React from "react";
// import "./style.css";
export default function App() {
let [width, setWidth] = React.useState(window.innerWidth);
let [height, setHeight] = React.useState(window.innerHeight);
React.useEffect(() => {
console.log("useEffect is called");
window.addEventListener("resize", () => {
setHeight(window.innerHeight);
setWidth(window.innerWidth);
});
}, []);
return (
<div>
{/* <button onClick={handler}> Submit </button> */}
<h1>
{" "}
{height},{width}{" "}
</h1>
</div>
);
}
The above code causes re-render of height and width values on the UI (height =windows.innerHeight & width = windows.innerWidth) despite using useEffect with an empty dependency array.
I've deployed useState inside useEffect to update height and width. My understanding was that useEffect gets executed only once(after the initial render) if used with an empty dependency array but on resizing the screen size, height and width gets updated as well thereby causing re-render
you should use const to declare your states
window.addEventListener is declared only once but it will be triggered every resize - so your state will be updated every resize
every time your state change the component will rerender
The useEffect is called only once, but since you have added a listener on the window object using addEventListener. The code inside the handler (which sets the state) gets executed, whenever there window size is changed. This state update will cause your component to re-render and update the UI
I think that's because you added addEventListener for resizing window in useEffect, So whenever window in resized it's callback function will be called and your states will be changed so your component will be re-rendered
the window.addEventListener method runs just one time and then you have a listener for resize and its call back excuted. the window.addEventListener does'nt execute on each resize, it's callback does, ist it clear?

Why is my jest "onClick" test not firing the mocked function?

I am using jest to test whether a button is clickable or not. The button is a Material-UI component, but is itself working exactly as intended on the frontend; only the test is failing.
The code for the button:
import { Button as BaseButton } from '#mui/material';
export function Button({ children, isDisabled = false, onClick }) {
return (
<BaseButton disabled={isDisabled} onClick={onClick}>
{children}
</BaseButton>
);
}
And the code for the test:
import { Button } from './Button';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import userEvent from '#testing-library/user-event';
it('should be clickable', () => {
const text = 'Hello World!';
const testFunction = jest.fn();
render(<Button onClick={() => testFunction}>{text}</Button>);
userEvent.click(screen.getByText('Hello World!'));
expect(testFunction).toHaveBeenCalledTimes(1);
});
I have other tests in the same file which are passing, so I don't think it's some type of import issue. The button is definitely rendering properly on the test page, but can't be clicked. I have also tried updating the button locator to userEvent.click(screen.getByRole('button')); to no avail.
I am getting the following error from the test file, which I believe is caused by the click test:
(node:30870) UnhandledPromiseRejectionWarning: Error: Unable to perform pointer interaction as the element has or inherits pointer-events set to "none".
However, the button component does not even have a pointer-events property. The child span element, which forms the button label, does have pointer-events: none, but the parent element shouldn't be inheriting that from a child. In addition, I tested the jest.fn() on a normal, html element and got the exact same error.
You're not actually calling the function, just returning it from the onClick callback (which doesn't do anything)
onClick={() => testFunction}
Is the same as:
onClick={() => {
return testFunction;
}}
That is just returning testFunction. To call it you want to:
onClick={() => testFunction()}
Or, if you want to pass it directly so it get's called you can do:
onClick={testFunction}
Then, when the click fires it will call the passed function. But then it will receive the event arguments etc, which you might not want.

useeffect react cleanup not working with React Native modal

The effect for the modal looks like this:
useEffect(() => {
console.log('mounted')
return () => {
console.log('unounted')
}
}, [])
When I try to call the modal conditionally like this :
modal ? <Suspense fallback={<ActivityIndicator/>}><Modal /></Suspense> : null
the console shows mounted when modal===true but doesn't show unmounted when modal===false.What's going on here?Does functional component cleanups don't work in React Native?Or is there something else happening behind the curtain?
You should use the Modal's visible prop if you want to show the modal conditionally.
Add a state to the modal's father component like this:
const [isModalShown, setIsModalShown] = useState(false)
Then pass the state (isModalShown) to the prop and when you want to show the modal change the state to true (setIsModalShown(true))

Set react native component prop through passed reference

I need to show/hide a modal based on user interaction (ie - a button press) in a component which is neither a parent or child of the modal itself. I'm currently trying to do so by passing the modal as a prop to the modal controller, but the following errors are thrown depending on which method I call:
TypeError: modal.setNativeProps is not a function
TypeError: modal.setState is not a function
Is there a way to show the modal given how this is structured?
import Modal from 'react-native-modal'
const modalRef = React.createRef();
const modal = <Modal ref={modalRef} isVisible={false}>
<ModalController modal={modalRef} />
export const ModalController = ({modal}) => {
function onButtonPress(){
modal.setState({isVisible:true})
modal.setNativeProps({isVisible:true})
}
return (
<Button title='Show Modal' onPress={onButtonPress()} />
)
}
Ciao, in case there is no parent/child relation between components, the only way I found to pass/set data from one component to another is use a global state manager like react-redux. With redux you can easly define a global state and set it from component that fire modal open/close. Modal component reads this state and open/close itself.
So I think you've gotten a little confused.
Firstly, remember that everything inside ModalController is going to execute on every render. Your function onButtonPress will be created every render (this is fine), but you are actually calling that function when you pass it to onPress render . This means you're executing onButtonPress on every render, which is probably not what you want.
This is an easy fix - you just remove the () so it's just onPress={onButtonPress}. Now it'll only trigger when the button is pressed.
More fundamentally, the solution to your problem is much simpler than what you've done in your code. Generally 'refs' are only used in special cases where you really want to tell your components what to do (like telling a ScrollView to scroll to a particular position, or telling an input to focus so the keyboard shows). If you're using a ref it should be very intentional.
So a simple solution to have a component with a button that shows a modal could look like:
import React, {useState} from 'react';
import {View, Button, Text} from 'react-native';
import Modal from 'react-native-modal';
export const ModalController = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
function onButtonPress() {
setIsModalVisible(true);
}
return (
<View>
<Button title='Show Modal' onPress={onButtonPress} />
{isModalVisible && <MyModal />}
</View>
);
};
const MyModal = () => (
<Modal>
<Text>Hey I am a modal</Text>
</Modal>
);
Notice the use of useState. This is how you hold 'state' in a functional component like this (as opposed to a class component where you would use setState.
I hope this helps. Let me know if you have questions!

How to Unmount React Functional Component?

I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?
To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.
If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components

Resources