I have a parent component and a child component. In the parent component I defined the state which is passed as props to my child component. I'm also passing the function "onUpdateQuestion" in the props of the child component. This function will be executed every time the value of the input field of the child changes (so a re-render will occur).
export default class Parent extends React.PureComponent<{},{question:Question}> {
public state = {
question: new Question()
};
public onUpdateQuestion = (): void => {
let _question = this.state.question;
this.setState({ question: _question });
};
public render(): React.ReactNode {
return (
<QuestionEditorChild
question={this.state.question}
onUpdateQuestion={this.onUpdateQuestion}/>);
}
}
class QuestionEditorChild extends React.Component<{question: Question; onUpdateQuestion: Function;}> {
render() {
console.log(this.props.question.description);
return (
<div>
<h2>Question</h2>
<input
value={this.props.question.description}
onChange={e => {
this.props.question.setDescription(e.target.value);
this.props.onUpdateQuestion();
}}
/>
</div>
);
}
}
class Question implements IQuestion {
public description: string;
constructor(question?: IQuestion) {}
public setDescription(value: string): void {
this.description = value;
}
}
interface IQuestion {
description: string;
}
My child component never gets re-rendered again. Does this have something to do with the function setDescription() I'm executing in the child component?
What are the best practices for re-rendering the child correctly while working with models/interfaces?
public onUpdateQuestion = (): void => {
let _question = this.state.question;
this.setState({ question: _question });
};
The code above isn't actually using any new data from your form.
It should be...
onUpdateQuestion = (incomingData) => {
this.setState({ question: incomingData })
}
So technically your code, never updating, is correct because it's always using new Question() which is created once, then never changes.
Related
I have a parent component A which will have a child component B. The parent component can have multiple similar child components. I want to process the data of this child component in the parent component hence I pass a functions as props to the child component. Now whenever a change is made in the child component I want to re-render all the components from the parent. I have a state in the parent called componentList which is an array of type object. Each object will have data about one child component. This gives me syntax error and if I change and try something else it gives me undefined value for componentList.
export class Parent extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
componentList: []
};
this.onClick = this.onClick.bind(this);
this.onDataChange = this.onDataChange.bind(this);
}
public onDataChange(index: number) {
return function(data: Data) {
this.setState({
this.state.componentList[index].name = data.name;
});
};
}
In child component I am updating the name onChange as below:
interface Props {
name?: string;
age?: number;
onDataChange: (data: Data) => void;
}
export class Child extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.state = {};
this.onNameChange = this.onNameChange.bind(this);
}
public onNameChange(event) {
this.props.onDataChange({
name: event.target.value,
age: this.props.age
});
}
Getting error: "TypeError: Cannot read property 'componentList' of undefined"
As my comment seemed to help you, I've decided to post an answer. Your onDataChange function is returning another function to be invoked by the child.
public onDataChange(index: number) {
return function(data: Data) { // A function gets returned
this.setState({
this.state.componentList[index].name = data.name;
});
};
}
and inside the child you are binding the reference of the this keyword to the this keyword of the child:
this.onDataChange=this.onDataChange.bind(this);
which is generally not wrong, but you want to update the state on the parent, therefore you need a reference to the parent this.
This can be easily achieved by simply changing the return "value" of your onDataChange function.
public onDataChange(index: number) {
// An arrow function always set the this reference to where it's declared correctly,
// therefore this inside the function will reference the parent this.
return (data: Data) => {
this.setState({
this.state.componentList[index].name = data.name;
});
};
}
What's also obsolete then is: this.onDataChange = this.onDataChange.bind(this); inside your parent constructor.
It might be helpful to read up on this topic over at MDN - Arrow functions.
Are you doing binding of onDataChange function before sending to the props?
export class parentClass extends Component{
constructor(props){
super(props);
this.onDataChange=this.onDataChange.bind(this);
}
}*/
If not, this keyword in onDataChange invocation points to the wrong context
also replace
this.setState({
this.state.componentList[index].name = data.name;
});
with something like
this.setState({componentList:newComponentList});
I got button component which has another child component to show tooltip. I pass ref to <Tooltip> component where I attach event listener to mouseEnter and mouseLeave event to my button.
<Button
ref={this.buttonRef}
type={this.props.type}
className={this.props.className}
disabled={this.props.operationIsActive || (this.props.enabled == undefined ? undefined : !this.props.enabled)}
onClick={this.props.onClick}
onSubmit={this.props.onSubmit}
icon={this.props.icon}
>
{this.props.children}
</Button>
<Tooltip domRef={this.buttonRef} text={this.props.tooltip} />
Here is my Tooltip component
export class Tooltip extends ComponentBase<TooltipProps, TooltipState> {
private documentRef = null;
public componentDidMount() {
super.componentDidMount();
this.documentRef = this.props.domRef
if (this.props.domRef) {
const dom = this.props.domRef.current;
dom.element.addEventListener("mouseenter", this.showTooltip);
dom.element.addEventListener("mouseleave", this.hideTooltip);
}
}
public componentWillUnmount() {
if (this.documentRef) {
const dom = this.documentRef.current;
if (dom) {
dom.element.removeEventListener("mouseenter", this.showTooltip);
dom.element.removeEventListener("mouseleave", this.hideTooltip);
}
}
}
private showTooltip = (): void => {
this.updateState({ isTooltipVisible: true })
}
private hideTooltip = (): void => {
this.updateState({ isTooltipVisible: false })
}
private getStyles(position: any): React.CSSProperties {
const css: React.CSSProperties = {
left: position[0].left,
top: position[0].top + 40,
}
return css;
}
public render() {
if (!this.props.domRef.current) {
return null;
}
if(!this.state.isTooltipVisible)
{
return null;
}
const position = this.props.domRef.current.element.getClientRects();
const css = this.getStyles(position);
return ReactDOM.createPortal(<div style={css} className="tooltip">{this.props.text}</div>, document.getElementById(DomRoot) )
}
}
Everything works fine, but when onClick event is fired on button (for example I got button which only set new state to some component and after that simple div will be rendered), componentWillUnmount method is triggered and ref is lost so I cannot remove those two listeners in Tooltip component. Is it possible to unmount children before parent or any other way how I can fix this?
A ref is just { current: ... } object. The purpose of this recipe is to be able to update current property after ref object was passed by reference.
In the code above <Button> is mounted and unmounted before <Tooltip>. The ability of a ref to secretly update current is misused here. Tooltip already relies on that a ref contains a reference to specific component. It isn't expected that a ref will change during Tooltip lifespan, so it shouldn't rely on transient ref. It should be:
public componentDidMount() {
super.componentDidMount();
this.reference = this.props.domRef.current;
if (this.reference) {
this.reference.element.addEventListener("mouseenter", this.showTooltip);
this.reference.element.addEventListener("mouseleave", this.hideTooltip);
}
}
public componentWillUnmount() {
if (this.reference) {
dom.element.removeEventListener("mouseenter", this.showTooltip);
dom.element.removeEventListener("mouseleave", this.hideTooltip);
}
}
Considering the following React Component snippet, I need to set a new component state based on the new props and the current state. It's preferred to use an "updater" function when defining new state which is based on the old one, now my confusion is that I have the nextProps parameter given to the componentWillReceiveProps lifecycle method, but the updater function also, gets a props parameter. Which one should I use?
Now I guess this should be specific to React 16.2; I'm guessing the introduction of the new getDerivedStateFromProps(nextProps, prevState) static method in React 16.3 should eliminate this confusion (right?)
class MyComponent extends Component {
constructor(props, ...args) {
super(props, ...args);
this.state = {
value: props.value,
};
}
componentWillReceiveProps(nextProps) {
this.setState((prevState, props) => {
return {
value: (/* `nextProps or props` */).value,
}
});
}
render() {
// ...
}
}
There are exactly the same thing. If your want to access previous props, the this.props remains untouched in componentWillReceiveProps method.
My intuition is that setState doesn't fire immediately in side componentWillReceiveProps.
Let's consider the example below.
After first click, the curProps will get 0, and props, nextProps both return 1.
class Test extends Component {
state = {
value: 0
};
componentWillReceiveProps(nextProps) {
const curProps = this.props;
this.setState((prevState, props) => {
console.log('[curProps]', curProps.num);
console.log('[props]', props.num);
console.log('[nextProps]', nextProps.num);
const value = prevState.value + nextProps.num;
return {
value
};
});
}
render() {
return <h4>{this.state.value}</h4>;
}
}
class App extends Component {
state = {
value: 0
};
render() {
return (
<div>
<Test num={this.state.value} />
<button onClick={this.onClick}>Add</button>
</div>
);
}
onClick = () => {
this.setState(prevState => ({ value: prevState.value + 1 }));
};
}
There is the codesandbox demo https://codesandbox.io/s/zxpovzkywx
I am developing a sample application. Actually when i was Pass the Value Parent Class to Child Class getting in componentWillReceiveProps() method more times re-rendering that Value.
Here this is my Code:
This is my Parent Class:
class ParentClass extends React.Component {
constructor (props) {
this.state = {
BadgeValue: 0,
setTabVal: false
}
}
getBadgeLength (badgeValue) {
this.setState({
badgeLength: badgeValue,
setTabVal: true
})
}
render () {
return (
<ChildClass navigator= {this.props.navigator} getBadgeLength={this.getBadgeLength.bind(this)} />
)
}
}
Now I Assumed Child Class: in this Case i am actually Calling for Update Purpose again same Methods calling in componentWillReceiveProps()
But, My main Issue more times re-rendering in this scenario.
class ChildClass extends React.Component {
componentWillReceiveProps () {
var getBadgeValue = array.length
this.props.getBadgeLength(getBadgeValue)
}
componentWillMount () {
var getBadgeValue = array.length
this.props.getBadgeLength(getBadgeValue)
this.setState({
dataSource: this.state.dataSource.cloneWithRows(array)
}, () => {
this.getshopaddressData()
this.getShopDetails()
}
render(){
return()
}
}
First of all your method name should be setBadgeLength because its updating the value. Secondly you should pass down badge value down to child class as a prop and then compare it in child class :
shouldComponentUpdate(nextProps) {
if (this.props.badgeValue != nextProps.badgeValue) {
return false; // don't update if badgeValue is changing
} else {
return true;
}
}
Given this component :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionView extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<section className="subscription-view">
<h3>Loading...</h3>
</section>
);
}
return (
<section className="subscription-view">
{ this.props.children }
</section>
);
}
};
And another component :
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<SubscriptionView subscription="allFoo">
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
</SubscriptionView>
);
The first Subscription is re-rendered when the data is available, however the second one is rendered only once and nothing more. If I place a console.log(this.props.subscription, ready); inside the render function of SubscriptionView, I see
allFoo false
allFoo true
singleBar false
and that's it.
On the server side, both publish methods are
Meteor.publish('allFoo', function () {
console.log("Subscribing foos");
return Foos.find();
});
Meteor.publish('singleBar', function (id) {
console.log("Subscribing bar", id);
return Bars.find({ _id: id });
});
Both of the publish methods are being called.
Why isn't the second SubscriptionView reactive?
* Solution *
This is based on alexi2's comment :
import React, { Component } from 'react';
import TrackerReact from 'meteor/ultimatejs:tracker-react';
export default class SubscriptionLoader extends TrackerReact(Component) {
constructor(props) {
super(props);
let params = props.params || [];
if (!Array.isArray(params)) {
params = [params];
}
this.state = {
done: false,
subscription: {
collection: Meteor.subscribe(props.subscription, ...params)
}
};
}
componentWillUnmount() {
this.state.subscription.collection.stop();
}
componentDidUpdate() {
if (!this.state.done) {
this.setState({ done: true });
this.props.onReady && this.props.onReady();
}
}
render() {
let loaded = this.state.subscription.collection.ready();
if (!loaded) {
return (
<div>Loading...</div>
);
}
return null;
}
};
Then, inside the parent component's render method :
<section className="inventory-item-view">
<SubscriptionLoader subscription='singleBar' params={ this.props.id } onReady={ this.setReady.bind(this, 'barReady') } />
<SubscriptionLoader subscription='allFoos' onReady={ this.setReady.bind(this, 'foosReady') } />
{ content }
</section>
Where setReady merely sets the component's state, and content has a value only if this.state.barReady && this.state.foosReady is true.
It works!
Try separating out your SubscriptionView Components like this:
import SubscriptionView from './SubscriptionView.jsx';
export const Foo = () => (
<div>
<SubscriptionView subscription="singleBar" params={ 123 }>
<div>Rendered!</div>
</SubscriptionView>
<SubscriptionView subscription="allFoo">
<div>Rendered Again!</div>
</SubscriptionView>
</div>
);
Edit from comments conversation
Not sure if I am on the right track but you could build Foo as a 'smart' component that passes props to each SubscriptionView as required, and then use Foo as a reusable component.
Let's say that what I need to render is FooBarForm, which requires both Foos and Bars to be registered, in that specific use case. How would you do that?
You could create Components Foos and Bars that took props as required and create a parent component FooBarForm that contained those Components and passed the necessary data.
FooBarForm would handle the state and as that changed pass it to the props of its child components.
Now state is being centrally managed by the parent component, and the child components render using props passed from the parent.
The child components would re-render as their props changed depending on whether the state being passed from the parent component had changed.