In my component im trying to sync the received props with the current state in order to make it visible from outside (I know this is an anti-pattern, but I haven't figured out another solution to this yet. Im very open to suggestions!).
Anyways, this is what I've got:
export class PopupContainer extends React.Component {
state = {
show: false,
};
shouldComponentUpdate(nextProps, nextState) {
if (this.props.show === nextProps.show) return true;
return true;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// if popup shown, fade it out in 500ms
if (this.props.show !== prevProps.show)
this.setState({ show: this.props.show });
if (this.state.show) {
setTimeout(() => this.setState({ show: false }), 2000);
}
}
render() {
return <Popup {...{ ...this.props, show: this.state.show }} />;
}
}
And in my external component I'm rendering the container :
<PopupContainer
show={this.state.popup.show}
message={this.state.popup.message}
level={this.state.popup.level}
/>
Now when I initially set this.state.show to true it works, but every successive assignment which is also true without any false assignment inbetween doesn't work. How do I force componentdidUpdate() to fire anyways even if the props are the same value? shouldComponentUpdate() didn't seem to solve the problem.
Thank you!
Edit: I noticed that the render() method is only called in the parent element. It seems like as there is no change in properties for the child, react doesn't even bother rerendering the childern which somehow makes sense. But how can I force them to rerender anyways?
This is kind of a hack, but it works for me.
In the child class
Add a property to state in constructor - let's call it myChildTrigger, and set it to an empty string:
this.state = {
...
myChildTrigger: ''
}
then add this to componentDidUpdate:
componentDidUpdate(prevProps) {
if(this.state.myChildTrigger !== this.props.myParentTrigger) {
// Do what you want here
this.setState({myChildTrigger: this.props.myParentTrigger});
}
}
In the parent class
Add a myParentTrigger to state in constructor:
this.state = {
...
myParentTrigger: ''
}
In the render method, add it as a prop, like this:
<ChildClass ... myParentTrigger={this.state.myParentTrigger} />
Now you can trigger a call to componentDidUpdate to execute whatever is inside the if-statement, just by setting myParentTrigger to a new value, like:
this.setState({ myParentTrigger: this.state.myParentTrigger + 'a' });
Related
I want to send events down to my React child.
I feel like this is kind of an easy thing to do, so maybe i just have a mental block, and there is something obvious that is staring me in the face.
Anyway, I have a little Test app which illustrates the problem:
export class Test extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let {buttonClicked, textFieldChanged} = this.state
return (
<div>
<button onClick={()=>this.handleClick()}>
Click
</button>
<input type={"text"} onChange={()=>this.handleTextChange()}/>
<Inner buttonClicked={buttonClicked} textFieldChanged={textFieldChanged}/>
</div>
);
}
handleClick(e) {
this.setState({ buttonClicked: true })
}
handleTextChange(e) {
this.setState({textFieldChanged:true})
}
}
class Inner extends React.Component {
render() {
let {buttonClicked, textFieldChanged} = this.props;
return (
<React.Fragment>
<div>Clicked : {buttonClicked ? "CLICKED!" : " "}</div>
<div>Text input : {textFieldChanged ? "TYPED!" : " "}</div>
</React.Fragment>
);
}
}
A button and a textfield live in the parent. Both these widgets can fire off events and change the child component.
This is simply achieved by passing a state value as a property down to the child. Very easy stuff.
However I would like an either/or situation. When I click the button this removes the text event, and vice versa. Ie. I do not want to see a situation like this :
Now there is a very obvious way to fix this by changing the state value to "false" of the other value.
handleClick(e) {
this.setState({ buttonClicked: true, textFieldChanged: false })
}
handleTextChange(e) {
this.setState({textFieldChanged:true, buttonClicked: false})
}
Is there any OTHER way of doing this?
The problem is that I have LOTS and LOTS of even handlers in my component and I don't want to negate the other state properties of the other values.
if i understood you correctly just one function will help - pass the attribute name into it
handleClick(propName) {
this.setState({
...this.state,
[propName]: !this.state[propName]
})
}
Create property lastEventType in parent component state , whenever you click or type - update it. And pass only this property to Inner component
Background
I am trying to make an element disappear after the animation ends (I am using animate.css to create the animations).
The above 'copied' text uses animated fadeOut upon clicking the 'Copy to Journal Link'. Additionally, the above demo shows that it takes two clicks on the link to toggle the span containing the text 'copied' from displayed to not displayed.
According to the animate.css docs, one can also detect when an animation ends using:
const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')
element.addEventListener('animationend', function() { doSomething() })
My Problem
However, within the componentDidMount() tooltip is null when attempting to integrate what animate.css docs suggest.
What am I doing wrong? Is there a better way to handle this behavior?
ClipboardBtn.js
import React, { Component } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
class ClipboardBtn extends Component {
constructor(props) {
super(props)
this.state = {
copied: false,
isShown: true,
}
}
componentDidMount() {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
handleAnimationEnd() {
this.setState({
isShown: false,
})
}
render() {
const { isShown, copied } = this.state
const { title, value } = this.props
return (
<span>
<CopyToClipboard onCopy={() => this.setState({ copied: !copied })} text={value}>
<span className="clipboard-btn">{title}</span>
</CopyToClipboard>
{this.state.copied ? (
<span
id="clipboard-tooltip"
className="animated fadeOut"
style={{
display: isShown ? 'inline' : 'none',
marginLeft: 15,
color: '#e0dbda',
}}
>
Copied!
</span>
) : null}
</span>
)
}
}
export default ClipboardBtn
Using query selectors in React is a big NO. You should NEVER do it. (not that that's the problem in this case)
But even though it's not the problem, it will fix your problem:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
https://reactjs.org/docs/refs-and-the-dom.html
componentDidMount gets called only once during the inital mount. I can see that in the inital component state, copied is false, hence #clipboard-tooltip never gets rendered. That is why tooltip is null.
Instead try this :
componentDidUpdate(prevProps, prevState) {
if(this.state.copied === true && prevState.copied === false) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
if(this.state.copied === false && prevState.copied === true) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.removeEventListener('animationend', this.handleAnimationEnd)
}
}
componentDidUpdate gets called for every prop/state change and hence as soon as copied is set to true, the event handler is set inside componentDidUpdate. I have added a condition based on your requirement, so that it doesn't get executed everytime. Feel free to tweak it as needed.
I've recently seen this type of react pattern where the state is being set in a render by using this.state:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
};
}
render() {
if (this.props.show) {
this.state.showButton = true; //setting state in render!!
}
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
This seems like an anti-pattern. Can this cause bugs? It seems to work properly though.
I would just use a component lifecycle to set the state:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
};
}
componentWillReceiveProps(nextProps) {
if(nextProps.show) {
this.setState({
showButton: true,
})
}
}
render() {
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
What is the recommended way?
render should always be pure without any side effects, so it's certainly a bad practice.
from the React docs :
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead. Keeping render() pure makes components easier to think about.
Take a look also here and here.
It is an anti-pattern. If showButton state is not always equal to show props (which is the case in the example), I would use this:
class ShowMe extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: this.props.show,
};
}
componentDidUpdate(prevProps, prevState) {
prevProps.show !== this.props.show && this.setState({showButton: this.props.show})
}
render() {
return (
<div>
<div> Show or hide button </div>
{this.state.showButton && <Button content='Btn'/>}
</div>
)
}
}
Edit: As of React 16.3 one should use getDerivedStateFromProps in this case.
Note that componentWillReceiveProps will be deprecated.
From the docs: getDerivedStateFromProps is invoked after a component is instantiated as well as when it receives new props. It should return an object to update state, or null to indicate that the new props do not require any state updates.
https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
It is incorrect setting state in render method. You can set state in lifecyles method. But other thing is that your component can receive same props many times, so your component will be set state many times, and renderd. To solve this problem you need to compare your new with your current props for example compare json objects:
componentWillReceiveProps(nextProps) {
if(JSON.stringify(this.props) !== JSON.stringify(nextProps) && nextProps.show) {
this.setState({
showButton: true,
})
}
}
or use PureComponent. And that garentee you that your component will not rerendered constantly.
And it will be better if you do not rerender component if state.showButton currently seted to true.
componentWillReceiveProps(nextProps) {
if(JSON.stringify(this.props) !== JSON.stringify(nextProps) && nextProps.show) {
if(!this.state.showButton) {
this.setState({
showButton: true,
})
}
}
}
I have a react class in which I need to use shouldComponentUpdate(), to prevent an infinite loop between the component and its parent.
I simply check whether a deep clone of nextProps is equal to this.props, and I only update the component if they're not.
So far, so good. (?)
class Child extends Component {
onComponentUpdate = (e) => {
this.props.update(e)
}
shouldComponentUpdate(nextProps, nextState) {
return JSON.stringify(nextProps) !== JSON.stringify(this.props)
}
render() {
return (
<div>
// some code that might trigger onComponentUpdate()
</div>
);
}
}
Now, in my parent component, something happens that makes me want to re-render the child, without specific props changing. What I did now, is changing a counter in state and passing it to the child as a prop. I never do anything with the counter itself, it is merely an indication for the child that props actually changed so that the child should update.
class Parent extends Component {
constructor() {
super()
this.state = { counter: 0 }
}
otherChildChanged = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<Child
counter={this.state.counter}
update={"some function"}
other={"props"}
>
</Child>
<OtherChild onChange={this.otherChildChanged}>
</OtherChild>
// some code that might trigger onComponentUpdate()
</div>
);
}
}
Is there a better way to do this?
You should pass down the size of your resizable div as a prop to the Child Component. This way, when it changes, JSON.stringify(nextProps) !== JSON.stringify(this.props) will be true and a re-render will occur.
If a component has to behave in a certain way (re-render, for example) depending on something that happens on his Parent, it should be passed to it as a prop.
I have the following function:
update() {
this.currentItem = [];
//...Populate currentItem
this.setState({
currentItem
});
}
Which renders on the page like this;
render() {
const { currentItem } = this.state;
return {currentItem}
}
I then pass this function into a child component, like this:
<Component
update={this.update.bind(this)}
/>
and then call it like this in my child component:
let { update } = this.props;
if (typeof update === "function")
update();
The problem is that the update function does not re render the content I am updating on the parent page. As far as I understand this, whenever setState is called, render also gets called. render() does seem to be getting called, but it does not seem to display the updated value - why is this, and how can I resolve it?
I guess it could be to do with the fact that it is coming from a child component?
I have tried forceUpdate, but it does not re render the component either - what am I missing?
Try avoiding this.forceUpdate() because this will not fire shouldComponentUpdate() which is a performance hook for your Component in React. As, I saw that you are passing your state to child component and trying to update the parents state object from there, which against the law. You should create a method in parent and pass that method as a prop to the child component. It should look like this
constructor(props) {
super(props);
this.state = { loading: false };
this.update = this.update.bind(this);
}
update(newState) {
this.setState({loading: newState })
}
render() {
return <ChildComponent update={this.update} />
}
I am just guessing here but i think you set the initial value for the child component in the constructor and the value you want it to reflect points to its own state instead of the parents state
I needed to set the state to loading first, then when I set it to loading = false, it would re render that page
this.setState({
loading:true
});
//...Do the work
this.setState({
loading:false
});