React hook how to avoid executing child components when parent rerenders - javascript

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.

Related

useState element not rerendering when triggered by an event handler in a child component

I have a parent component with a useState that is being displayed. What I want to do is have a child component be able to update this state to change what the parent is displaying. I currently have the following:
function Parent() {
const [myWindow, setMyWindow] = useState(null)
useEffect(() => {
setMyWindow(<Child updateWindowFunc={() => setMyWindow(someNewWindow)} />)
}, []}
return (
<div>
{myWindow}
</div>
)
}
function Child({updateWindowFunc}) {
//Somehow calls updateWindowFunc
}
(Note that my actual code is set up somewhat differently, so don't mind the syntax as much as the general concept.)
I've found that I can get the value of myWindow to change, but the actual display doesn't show any change. I've tried forcing a re-render after the change, and adding a useRef to display useRef.current, but nothing seems to update which window is actually being rendered.
What am I doing wrong?
Edit:
I've found that it works if I switch to a different type of component, but if its just a different element of the same component then there is no re-render. I've been using React.createElement(), so I would think the 'objects' are distinct, but maybe I just misunderstand how this works.
Can you provide more details or working\not working code example that has more info?
Currently it's hard to get, cause that doesn't make any sense since you can just render it like below and that code will have same result as one that you are trying to run
function Parent() {
return (
<div>
<Child />
</div>
)
}
So for better understanding and fixing a problem \ finding a different approach, please, provide more details

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

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/

Why aren't my props available to use in my component when I need them?

I keep running into similar issues like this, so I must not fully understand the lifecycle of React. I've read a lot about it, but still, can't quite figure out the workflow in my own examples.
I am trying to use props in a child component, but when I reference them using this.props.item, I get an issue that the props are undefined. However, if the app loads and then I use the React Browser tools, I can see that my component did in fact get the props.
I've tried using componentDidMount and shouldComponentUpdate in order to receive the props, but I still can't seem to use props. It always just says undefined.
Is there something I'm missing in React that will allow me to better use props/state in child components? Here's my code to better illustrate the issue:
class Dashboard extends Component {
state = { reviews: [] }
componentDidMount () {
let url = 'example.com'
axios.get(url)
.then(res => {
this.setState({reviews: res.data })
})
}
render() {
return(
<div>
<TopReviews reviews={this.state.reviews} />
</div>
);
}
}
export default Dashboard;
And then my TopReviews component:
class TopReviews extends Component {
state = { sortedReviews: []}
componentDidMount = () => {
if (this.props.reviews.length > 0) {
this.sortArr(this.props.reviews)
} else {
return <Loader />
}
}
sortArr = (reviews) => {
let sortedReviews = reviews.sort(function(a,b){return b-a});
this.setState({sortedReviews})
}
render() {
return (
<div>
{console.log(this.state.sortedReviews)}
</div>
);
}
}
export default TopReviews;
I'm wanting my console.log to output the sortedReviews state, but it can never actually setState because props are undefined at that point in my code. However, props are there after everything loads.
Obviously I'm new to React, so any guidance is appreciated. Thanks!
React renders your component multiple times. So you probably see an error when it is rendered first and the props aren't filled yet. Then it re-renders once they are there.
The easy fix for this would be to conditionally render the content, like
<div>
{ this.props.something ? { this.props.something} : null }
</div>
I would also try and avoid tapping into the react lifecycle callbacks. You can always sort before render, like <div>{this.props.something ? sort(this.props.something) : null}</div>
componentDidMount is also very early, try componentDidUpdate. But even there, make your that your props are present.
For reference: see react's component documentation

Render Component inside a different location

This is my react app component structure
<App>
<Body>
<Top>
### //<- Position 1
</Top>
<Middle>
<MyComponent/>
</Middle>
</Body>
</App>
I want the <MyComponent/> to render at Position 1 anytime it is imported.
I have tried ReactDom.createPortal but i keep getting Target container is not a DOM element. If this is not possible how best can i achieve a similar setup.
What I have tried
//Top.js
<Top>
<div id="top-render"/>
</Top>
//MyCustomElement.js
class MyCustomElement extends Component{
render(){
return (<div>Demo Text</div>)
}
}
export default props => ReactDOM.createPortal(
<MyCustomElement />,
document.getElementById('top-render'),
);
//Middle.js
<Middle><MyCustomElement/></Middle>
You are getting the Target container is not a DOM element error, because by the time execution reaches your ReactDOM.createPortal() call, the DOM node that document.getElementById('top-render') is attempting to retrieve, hasn't yet been written to the DOM.
This is because by that time, React is yet to make a successful pass through your component tree. i.e. React is yet to successfully create a virtual representation of everything in the component tree, and as a result, nothing has been written to DOM yet. So although you were expecting the top-render div to be there in the DOM, it hadn't actually been written yet.
The solution is to create a DOM node owned by <MyComponent, that will contain it's children (let's call this node el), then pass that DOM node, as the target DOM element in your React.createPortal() call (aka, it's second argument). Then when <MyComponent> mounts, you simply retrieve the top-render div and append el to it. Like so:
const MyCustomComponent = () => {
return (<div>Demo Text: from "MyCustomComponent"</div>)
}
class MyComponent extends React.Component {
el = document.createElement("div");
componentDidMount() {
// saving this on the component so we can access it in `componentWillUnmount` later
this.targetEl = document.getElementById("top-render");
this.targetEl.appendChild(this.el);
}
componentWillUnmount() {
this.targetEl.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(<MyCustomComponent />, this.el);
}
}
I put together a working example.
This approach works, because React guarantees that componentDidMount() will get called immediately after the component is mounted (aka. written to the DOM). In other words, we know for sure that by the time componentDidMount() gets called, our component tree has been written to the DOM at least once, and consequently, the top-render div exists in the DOM.
What do you mean by "Anytime it is imported" ?
Anyway you can just use conditional rendering. It will do the job, but it seems your intention is something else!
render(){
const { showMyComp } = this.state
return(
<App>
<Body>
<Top>
{showMyComp && (<MyComponent />)}
</Top>
<Middle>
<MyComponent/>
</Middle>
</Body>
</App>
)
}
ReactDom.createPortal is suggested to be used on DOM nodes outside the scope of React App. Inside you should declare a component normally <Top><MyComponent/></Top>.
But for curiosity's sake, in case you want to go with this approach only, you should use the ref of the DOM node in Top element.
ReactDOM.createPortal(
<MyComponent/>,
/*Ref of Container element*/,
);

One time action in React Components

I have a question regarding "one time actions" in react components. Imagine for example I want to scroll some element to certain position, or reset the internal react state.
So far I've been doing this by using a combination of a boolean flag (e.g. doAction: true) and an update action (e.g. setDoActionBackToFalse), but this seems too complex. Does anyone have any nice solution to this?
Note: The action can actually happen multiple times during the lifetime of the component but each time it has to be specifically triggered and happen only once (not keep happening on every rerender). E.g. scroll to every newly added item in scrollpane.
I created small fiddle to make the problem more obvious:
https://jsfiddle.net/martinkadlec/et74rkLk/1/
This uses the boolean flag approach.
It has been some time since I asked this question and since then I found that as long as the "one time action" doesn't actually rerender the component, but instead just modifies some browser state (e.g. focus, scroll position, etc.) people generally tend to solve this by having a class method and calling it from the parent component using refs.
To illustrate on the focus example:
class Input extends React.Component {
inputElRef = React.createRef();
focus = () => this.inputElRef.current.focus();
render() {
return (
<input ref={this.inputElRef} />
);
}
}
class Parent extends React.Component {
inputRef = React.createRef();
render() {
return (
<div>
<button onClick={() => this.inputRef.current.focus()}>Focus input</button>
<Input ref={this.inputRef} />
</div>
);
}
}
I think that you can use componentDidMount lifecycle hook. This hook is invoked only once immediately after a component is mounted and the DOM can be accessed in it.
You can also call your 'one time action' in component constructor but it's called before component is mounted and before initial render so you can't access DOM there.
So you can initialize component state in a constructor (according to React docs: constructor is the right place to initialize state) but you can't scroll some element to certain position in constructor because you can't access component DOM elements in it.
Summing up: state initialization should be done in constructor while 'one time actions' manipulating DOM should be done in componentDidMount.
Wrap your action handlers inside a higher order function which invokes them only once. Lodash has once. Ramda has it too.
Updates for your scrolling scenario.... Scrolling is a side effect which must be initiated by the DOM API. You can write an HOC which wraps any component inside it -
function OnFocusExtender(Wrapped){
return class ExtendedFocus{
focus = _.once(elem => elem && elem.focus && elem.focus());
render(){
return <Wrapped ref={this.focus} {...this.props} />;
}
}
}
Then you can use it in your code like -
render(){
let FocusedComponent = FocusExtender(YourComponent);
return <FocusedComponent a={"blah"} b={blah} />
}
Updated for a generic side-effects approach:
The HOC:
function WelcomingParty(...party)=>(Wrapped)=>{
return class ExtendWelcome{
// Every host in the welcoming party greets
// the guest :)
welcome = (ref) => party.forEach(host => host(ref));
render(){
return <Wrapped ref={this.welcome} {...this.props} />;
}
}
}
Usage:
let hostFn = (fn)=>(ref)=> ref && (typeof ref[fn] == "function") && ref[fn](),
hosts = ["focus", "scrollIntoView"].map(hostFn);
render(){
let Component = WelcomingParty(...hosts)(YourComponent);
return <Component a={"blah"} b={blah} />
}

Categories

Resources