Only trigger UseEffect() in Material UI Dialog when it is in opened - javascript

I have a parent component that contains a Material UI Dialog (child). Now this dialog's purpose is to fetch and display data from the REST API.
Currently this works by implementing UseEffect() in the Dialog component. However as soon as the parent component is mounted, the UseEffect() inside child component will kick in. The problem is I will be missing information that are supposed to be passed from the parent to the child itself (because both of this component are mounted at the same time).
Now what got me thinking is, I want that this dialog only be mounted when the button to show it is being clicked. Is that possible?
This is the snippet:
Parent.js
const Parent = React.forwardRef((props, ref) => {
const [openChildDialog, setOpenChildDialog] = useState(false)
useEffect(() => {
// function here. This also going to set value in a state that will be used for the child component
})
const handleOpenChildDialog = () => {
setOpenChildDialog(true)
}
const handleCloseChildDialog = () => {
setOpenChildDialog(false)
}
return (
<Page>
<PageTitle>
Parent Component Test
</PageTitle>
// Dialog will only be mounted when this button is clicked.
<Button onClick={handleOpenChildDialog}> Open the child dialog! </Button>
<ChildDialog
open={openChildDialog}
onClose={handleCloseChildDialog}
/>
</Page>
)
})
If what I am asking is not possible, then I am open to alternative as long as the UseEffect() inside the child dialog component is not immediately executed when the parent is mounted.

to only render the ChildDialog component when it's open, simply wrap it in a conditional:
{ openChildDialog && (
<ChildDialog
open={openChildDialog}
onClose={handleCloseChildDialog}
/>
)}
in terms of your useEffect - you can include an array as the 2nd parameter of a useEffect, and then the funciton will only run when anything in the array changes, for example:
useEffect(() => {
// this will run whenever any state or prop changes, as you haven't supplied a second parameter
})
useEffect(() => {
// this will now only run when openChildDialog changes
// you can easily put a check in here to see if openChildDialog is true to only run on open
}, [openChildDialog])
useEffect(() => {
// an empty array means this will only run when the component is first mounted
}, [])
so to answer your useEffect-inside-child running error, you could do something like:
useEffect(() => {
if (open) {
// do stuff only when the open prop changes to true here
}
}, [open])

UseEffect() behaves so that it is executed when the component is mounted, updated and unmounted. However the solution I see here it is using a conditional to render your child component when your openChildDialog change to true
{ openChildDialog &&
<ChildDialog
open={handleOpenChildDialog}
onClose={handleCloseChildDialog}
/>
}
I leave you this incredible guide so you can see in depth how to use this hook: https://overreacted.io/a-complete-guide-to-useeffect/

Related

React.js callback function as prop makes a parent component re-render three times when the child component changes

In the beginning, I have a component called Search which has two child components SearchBar, and FilterBar. I created a function called onSearchSubmit, and onFilterSubmit which I pass them as callback prop functions to SearchBar and FilterBar respectively.
so it looks like that in the Search.js
const onFilterSubmit = (e) => {
e.preventDefault()
const filteredStacks = filterInput.map((stack) => { return stack.value }) //Extract the values field only
setCompanies([])
fetchData(fetchFilterData, filteredStacks) //API Call
setFilterInput([])
}
//use the API providing it the search input, and
//setCompanies hook to update list of companies
//I did this approach to avoid re-rendering
//whenever the user types in the input field
const onSearchSubmit = (e) => {
e.preventDefault()
setCompanies([])
fetchData(fetchSearchData, inputRef.current.value) //API Call
inputRef.current.value = ""; //useRef hook also passed as prop to the searchBar to access the input value in the input field.
}
and also the component is returned as follows
...
<SearchBar inputRef={inputRef} onSubmit={onSearchSubmit} />
<FilterBar filterInput={filterInput} onChange={setFilterInput} onSubmit={onFilterSubmit}/>
...
Now, inside the SearchBar I wrote a useEffect hook to re-render the component when the onSearchSubmit function changes or gets called (I guess?)
const SearchBar = ({inputRef,onSubmit}) => {
useEffect(()=>{
console.log('search now')
},[onSubmit])
return (
<Form
className="search-form"
onSubmit={onSubmit}
>
....
Then I tried to run it now, and found that search now is logged three times, which is weird actually I don't understand how.
In addition it will also log for 3 times when I search for a keyword. And logs one time if I applied a filter
I understand that a child component which is the SearchBar here will re-render because the component changed when onSubmit was called, but I don't understand why the parent component re-rendered since it I believe created the function once again three times causing the change in the SearchBar component.
Yet when I wrapped the function in useCallback hook, it only logged search now just once, and didn't log when I submitted a search nor when I applied a filter. Which is understandable since the callback prop function onSearchSubmit is now memoed.
So my question is why is the parent component is affected and re-creates the onSearchSubmit three times although the child component is one who is changed? Is there anything I am missing?

Calling useEffect in a functional component within a functional component causes this message: Rendered more hooks than during the previous render

first off - Happy Friday!
I just came on here to see if anyone had any input to an issue that I am seeing in my ReactJs application. So I have a functional component renderViews and in that functional component, there are multiple views to render. Then within the renderViews I have another functional component carDetailsView and I try to make a call to an api when that particular component appears(as a modal). requestCarsDetails() should only be called when that component appears so thats why I nested a useEffect hook in the carDetailsView. But that causes an issue:
Rendered more hooks than during the previous render
.Please see code below:
const renderViews = () = > {
useEffect(()=> {
requestCarInfos()
.then((res) => {
setCars(cars);
});
}, []);
const carDetailsView = () => {
useEffect(() => {
requestCarDetails()
.then((res) => {
setDetails(res.details);
});
}, []);
return (<div>carDetailsView</div>)
}
return (<div>{determineView()}</div>)
}
The useEffect that is being used at the top level works fine. The issue only appeared after I added the second useEffect which is in the carDetailsView. Any help or advice is appreciated. Thanks!
Its a rule of hooks.
https://reactjs.org/docs/hooks-rules.html
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
React relies on the order in which Hooks are called.
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them.
if we put a Hook call inside a condition we can skip the Hook during rendering, the order of the Hook calls becomes different:
React wouldn’t know what to return for the second useState Hook call. React expected that the second Hook call in this component corresponds to the persistForm effect, just like during the previous render, but it doesn’t anymore. From that point, every next Hook call after the one we skipped would also shift by one, leading to bugs.
This is why Hooks must be called on the top level of our components. If we want to run an effect conditionally, we can put that condition inside our Hook:
use the lint https://www.npmjs.com/package/eslint-plugin-react-hooks
this is a caveat of using functional components, on each render everything inside the functional component gets kind of executed. so react needs to maintain the list of all hooks which have been defined when the component was created. think of it as an array.
on each render, useState will return the value for you. if you understand this, you will understand what stale state also means. ( stale state can happen, when closures occur within these components )
Something like that?
const CarDetailsView = () => {
React.useEffect(() => {
console.log("Running CarDetailsView useEffect...") ;
},[]);
return(
<div>I amCarDetailsView</div>
);
};
const Views = () => {
const [showCarDetails,setShowCarDetails] = React.useState(false);
const toggleCarDetails = () => setShowCarDetails(!showCarDetails);
React.useEffect(() => {
console.log("Running Views useEffect...") ;
},[]);
return(
<div>
<div>I am Views</div>
<button onClick={toggleCarDetails}>Toggle car details</button>
{showCarDetails && <CarDetailsView/>}
</div>
);
};
const App = () => {
return(<Views/>);
};
ReactDOM.render(<App/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id="root"/>

React hook how to avoid executing child components when parent rerenders

Given the following are all written in react hooks:
export Parent = () => {
const [msgValue, setMsgValue] = useState();
....
return {
<>
...
<Child setMsgValue={setMsgValue}/>
...
</>
}
}
shouldSkipUpdate = (oldProps, newProps) => {
...
return true;
}
export Child = React.memo({setMsgValue} => {
return (
<>
<HeavyComponent1>
<HeavyComponent2>
<InputBox onChange={setMsgValue}>
</>
)
}, shouldSkipUpdate);
My problem is that the input box is not responsive, my investigation shows that every keydown, <Child> will get executed once, even shouldSkipUpdate returns true, which in turn causes <HeavyComponent1> and <HeavyComponent2> code get executed and causes lagging.
What have I done wrong and how do I actually prevent <HeavyComponent> gets executed?
I am also a bit confused about re-render vs the component code gets executed, would be great to get clarification on this as well.
Please check below codesanbox, i have a child component which does not get rendered even if parent component gets rendered. Hope tha gives you some clarity on what needs to be done.
https://codesandbox.io/s/friendly-cerf-qme94
If the shouldSkipUpdate returns true the component doesn't rerender. Is it possible that you have HeavyComponent1 and/or HeavyComponent2 somewhere else in the tree, and those component instances get executed and rendered?
I just realised that InputBox is being wrapped by formsy which somehow updates the parent component which causes the rerender of everything.

Using useRef to focus an element, migrating class to hooks question?

Does anyone knows hows the right approach to pass callback references using react hooks. I'm trying to convert a modal that is built in a class component, to a hook component, but I'm not sure what's the correct way to do it.
onOpen = () => {
this.setState({ isOpen: true }, () => {
// Ref for the button
this.closeButtonNode.focus();
});
this.toggleScrollLock();
};
And this is how I pass the the reference in the code
<ModalContent
buttonRef={(n) => {
this.closeButtonNode = n;
}}
{// More props...}
/>
And the modal content component has the buttonRef like this
<button
type="button"
className="close"
aria-labelledby="close-modal"
onClick={onClose}
ref={buttonRef}
>
<span aria-hidden="true">×</span>
</button>
So when the modal pops I was able to get focus in my button close, with hooks, the only way I managed to replicate the behavior is to add an useEffect hook that listen to the isOpen state like this:
useEffect(() => {
if (isOpen) closeButtonNode.current.focus();
}, [isOpen]);
const onOpen = () => {
setIsOpen(true);
toggleScrollLock();
};
And this is how I pass the prop
const closeButtonNode = useRef(null);
return (
<ModalContent
buttonRef={closeButtonNode}
{// More props...}
/>
)
And I just use it like a regular ref, without passing a callback function, this works but I wonder why it works that way and why I cannot set the focus on the onOpen function like the class based component.
This is the sandbox if you want to check the full code.
https://codesandbox.io/s/hooks-modal-vs-class-modal-bdjf0
Why I cannot set the focus on the onOpen function like the class based component
Because when onOpen function get called open toggle still false and it will get updated after the modal is already opened. Please note that useState doesn't have a second argument (callback) like setState in class based component as you have done to set the focus. That why you needed to use useEffect.
You can test that by setting a delay using setTimeout after you set open to true like so:
const onOpen = () => {
setIsOpen(true);
setTimeout(() => closeButtonNode.current.focus())
};
Although, your approach using useEffect would be maybe better option.
codeSandbox example.

Using refs with conditional rendering

I have a problem with ref and conditional rendering.
I would like to focus an input tag when I click on a button tag.
Basically, I have this simplified code.
class App extends React.Component {
textInput
constructor(props) {
super(props)
this.state = {isEditing: false}
this.textInput = React.createRef()
}
onClick = () => {
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
}
render () {
let edit = this.state.isEditing ?
(<input type="text" ref={this.textInput} />)
: ""
return (
<div>
<button onClick={this.onClick}>lorem </button>
{edit}
</div>
);
}
}
When I click on the button, the input tag is displayed but the ref textInput is still set to null. Thus I can't focus the input.
I found some workaround like:
set autoFocus property in the input tag
hide the input tag with css when isEditing == false
But actually it is a very basic pattern and I would like to know if there is a clean solution.
Thank you
TL;DR:
Change this:
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
to this:
this.setState(previousState => ({isEditing: !previousState.isEditing}), () => {
this.textInput.current.focus();
});
Update: Functional Components / Hooks
It's been asked in the comments how to do this with useState and functional components. Rafał Guźniczak's answer explains it, but I wanted to provide a bit more explanation and a runnable example.
You still don't want to read state immediately after setting it, but instead of using a second argument callback to setState, you need to run some code after the state is updated and the component has re-rendered. How do we do that?
The answer is useEffect. The purpose of effects are to synchronize external "things" (for example: imperative DOM things like focus) with React state:
const { useEffect, useRef, useState } = React;
const { render } = ReactDOM;
function App(props) {
const [isEditing, setIsEditing] = useState(false);
const textInputRef = useRef(null);
const toggleEditing = () => setIsEditing(val => !val);
// whenever isEditing gets set to true, focus the textbox
useEffect(() => {
if (isEditing && textInputRef.current) {
textInputRef.current.focus();
}
}, [isEditing, textInputRef]);
return (
<div>
<button onClick={toggleEditing}>lorem </button>
{isEditing && <input type="text" ref={textInputRef} />}
</div>
);
}
render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Details:
You're running into a common problem many people run into with React, which is the assumption that setting state is synchronous. It's not. When you call setState, you're requesting that React update the state. The actual state update happens later. This means that immediately after the setState call, the edit element hasn't been created or rendered yet, so the ref points to null.
From the docs:
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.
Thank a lot for your answer #rossipedia. I was wondering if I can do it with hooks.
And apparently you can't pass second parameter to useState setter as in setState. But you can use useEffect like this (note second parameter in useEffect):
const [isEditing, setIsEditing] = React.useState(false);
React.useEffect(() => {
if (isEditing) {
textInput.current.focus();
}
}, [isEditing]);
const handleClick = () => setIsEditing(isEditing);
And it worked! ;)
Source: https://www.robinwieruch.de/react-usestate-callback/

Categories

Resources