componentWillReceiveProps and callback issues - javascript

I've got a big issue - probably a lack of understanding - in React. I've got a child component that makes a callback and has a componentWillReceiveProps function. The issue is that I cannot write on the text input the child is holding. I'll explain myself better in code.
The problem
I have a child with a text input. That child is inside his parent.
I need to:
Type manually on the text input.
Notify the parent that the text was changed.
Modify the input from the parent under certain circumstances.
The child
Has a text prop for the text to display.
textChanged is the callback the parent will suscribe to.
The parent
Contains the child, has a function for the child's callback that does not update the child's state and holds a button to modify the child's text.
The issue
If I write text into the input, it doesn't work well.
If I remove the callback calling in the child (the this.props.textChanged(event.target.value); part) the input works well but the parent doesn't receive child updates.
If I remove the componentWillReceiveProps part, I cannot update the child's text from his parent.
You can play with the code snippet - it "works".
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='container'></div>
<script type="text/babel">
var Child = React.createClass({
propTypes: {
text: React.PropTypes.string,
textChanged: React.PropTypes.func
},
getInitialState: function () {
return ({
text: this.props.text
});
},
handleChange: function (event) {
if (event.target.value != null) {
this.setState({ text: event.target.value });
if (this.props.textChanged !== undefined) {
this.props.textChanged(event.target.value);
}
}
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.text != this.state.text) {
this.setState({
text: nextProps.text
});
}
},
render: function () {
return (
<div>
<input className="form-control" value={this.state.text} onChange={this.handleChange} />
</div>
);
}
});
var Parent = React.createClass({
propTypes: {
childText: React.PropTypes.string,
},
getInitialState: function () {
return ({
childText: this.props.childText,
stateChangingProperty: true
});
},
handleTextChange: function (event) {
this.setState({
stateChangingProperty: !this.state.stateChangingProperty
});
},
handleButton: function () {
this.setState({
childText: "SOMETHING NEW"
});
},
render: function () {
return (
<div>
<Child text={this.state.childText} textChanged={this.handleTextChange} />
<button type="button" onClick={this.handleButton}>Write SOMETHING NEW on child</button>
</div>
);
}
});
ReactDOM.render(<Parent childText="text" />,
document.getElementById('container')
);
</script>
The question
How can I achieve the desired behaviour? Is my aim wrong?
Thanks in advance!

Not exactly sure of what you're trying to do, but I'd get your Parent component to choreograph the rules and handle events like text change and button clicks, just letting the Child component be effectively 'dumb', concentrating on rendering its data.
As an example, I've implemented this here
as you will see, most of the lifecycle methods in the Child component can be removed. Now if you need to apply any 'business logic' to amend what has been typed in, you can do this in the controlling Parent component's handleTextChange function (say, convert to upper case and set the state).
This container pattern is very common and idiomatic in React:
https://medium.com/#learnreact/container-components-c0e67432e005#.jg5yiorko

Related

Reactjs - parent, child action and functions

I am working on a reactjs application - and I am breaking up a big component to have a child component. I've created a callback function in the child that will go back to the parent. When a checkbox is checked -- the child component does the callback and this goes back into the parent shell -- however I want to now jump out of the event callback and push the data to an original parent function.
my application kind of looks like this on a streamlined level.
var Parent = React.createClass({
onSelect: function(value, flag){
this.updateSelected(value, flag);
}
updateSelected: function(value, flag) {
let array = this.state.selectedArray;
array.push({"value": value, "flag": flag});
this.setState({
selectedArray: array
});
},
render: function() {
return (
<div>
<Child onSelect={this.changeHandler} />
<span>{this.state.value}</span>
</div>
);
}
});
var Child = React.createClass({
selectHandler: function(e) {
this.props.onSelect(e.target.value, false);
},
render: function() {
return (
<input type="checkbox" onSelect={this.selectHandler} />
);
}
});
but I can not just invoke the "this.updateSelected(value, flag);" as its inside the this scope.
You should use ES6 syntax and JSX syntax instead of pure React API, that help you a lot to reduce this kind of error about scoping and become your code more readable, but If you even want to use that syntax, so you should bind the function once you pass down to the child component , you can do it like this:
var Parent = React.createClass({
onSelect: function(value, flag){
this.updateSelected(value, flag);
}
updateSelected: function(value, flag) {
let array = this.state.selectedArray;
array.push({"value": value, "flag": flag});
this.setState({
selectedArray: array
});
},
render: function() {
return (
<div>
<Child onSelect={this.changeHandler.bind(this)} />
<span>{this.state.value}</span>
</div>
);
}
});
as you can see adding the .bind method and passing as argument the context that you want your function execute, then once your function be invoked, the scope of that function will be the parent component instead of child component.
Remember that this approach bind method could affect the improve of your component if your Parent component rerenders many times.
If you are using JSX syntax you should do something like
class YourComponent extends React.Component {
changeHandler = (value, flag) => {
this.updateSelected(value, flag);
}
render() {
return (
<div>
...
<Child onSelect={this.changeHandler} />
</div>
);
}
}

Reusing react component

I'm with some questions on the use of component react.
Basically I want to use the "Title" component in various parts of the code, but it always has "states" that vary. I do not quite understand the official documentation on this issue, as I do to inherit this component, and only change the "states" for what I want?
I know the question seems silly, but I'm learning and React is very different from everything I saw.
var Title = React.createClass({
displayName: "Title",
getDefaultProps: function () {
return {
className: ""
}
},
render: function () {
return <h1 className={this.props.className}>{this.state.content}</h1>
}
});
You should use props rather than state in this instance. Props are owned by parents, and state is owned by the component itself. Since you would like to use this component in multiple places, then naturally a parent will decide what the text of the Title should be - therefore props are what you want.
React has a special prop children, which takes on the value of whatever is passed inside a component's JSX tag.
For instance, here's a mock component which uses your Title component multiple times:
var MyComponent = React.createClass({
render: function() {
return (
<div>
<Title className="foo">Hello</Title>
<Title className="bar">World</Title>
</div>
);
}
});
Since you are now passing text to the component as the children prop, you must update you Title component to render this:
var Title = React.createClass({
displayName: "Title",
getDefaultProps: function () {
return {
className: ""
}
},
render: function () {
// NOTE: we are now using children prop
return <h1 className={this.props.className}>{this.props.children}</h1>
}
});
A side benefit of this is that you can build more complex titles, containing multiple children, and it will just work:
var MyOtherComponent = React.createClass({
render: function() {
return (
<div>
<Title className="foo">
<span>Hello</span>
World
</Title>
</div>
);
}
});
You might want to replace {this.state.content} by {this.props.children} and use your component like this :
<Title className="myclass">my title</Title>
As a general rule, try to avoid using state when possible, dumb component are more easily reusable.

React not updating component props?

It seems that my component props are not updated the way I thought they ought to be.
var AdditionalInfoBlock = React.createClass({
getInitialState: function() {
return ({
comment: this.props.comment
});
},
onCommentChange: function(e) {
this.setState({comment: e.target.value});
this.props.onCommentChange(e);
},
render: function() {
return (
<ToolBlock>
<div className="container">
<form>
<textarea value={this.props.comment} onChange={this.onCommentChange} />
</form>
</div>
</ToolBlock>
);
}
};
var MainTool = React.createClass({
getInitialState: function () {
return {
comment: undefined
};
},
restart: function (e) {
e && e.preventDefault && e.preventDefault();
this.setState(this.getInitialState());
},
onCommentChange: function(e) {
this.setState({
comment: e.target.value
});
},
render: function() {
return (
<div>
<AdditionalInfoBlock comment={this.state.comment}
onCommentChange={this.onCommentChange} />
</div>
);
}
};
What I want this code to do is basically hold the comment's value until I post it and call restart - then it should reset the value in both AdditionalInfoBlock and MainTool. At the moment after restart is called when I console.log() the state of MainTool, the comment value is undefined. However, if I log AdditionalInfoBlock state and/or props, then the comment value is not reset in neither of those.
(PS. This is obviously a short version of the code, hopefully only incorporating the relevant bits. Although restart is not called in this excerpt, it doesn't mean that I have forgotten to call it at all :))
Since you're handling the same state on both components MainTool and AdditionalInfoBlock, finding value of the comment can get confusing. While you're listening for the comment change, you're setting the state on both components.
The changed state in your parent component MainTool is passing the props to the child AdditionalInfoBlock. getInitialState is invoked once before mounting (getInitialState documentation). Therefore, the passed on property is not handled by you child component on succeeding updates. By using componentWillReceiveProps, you will be able to handle the props sent by MainTool.
var AdditionalInfoBlock = React.createClass({
...
componentWillReceiveProps(nextProps) {
this.setState({comment: nextProps.comment});
},
Working Code: https://jsfiddle.net/ta8y1w1m/1/

How to pass value from parent component to child component (react)

I'm new to learning React, and am trying to pass on a value that I'm getting from user input inside of my ParentComponent (via input box) into its ChildComponent - which I'll use the user input value to run an AJAX call.
I thought that by replacing the state in the ParentComponent would work - but I'm still not able to grab it in the ChildComponent.
I also only want the ChildComponent to run/render only after receiving the input value from the ParentComponent (so that I can run the AJAX call and then render...).
Any tips?
var ParentComponent = React.createClass({
getInitialState: function() {
return {data: []};
},
handleSearch: function(event) {
event.preventDefault();
var userInput = $('#userInput').val();
this.replaceState({data: userInput});
},
render: function() {
return (
<div>
<form>
<input type="text" id="userInput"></input>
<button onClick={this.handleSearch}>Search</button>
</form>
<ChildComponent />
{this.props.children}
</div>
);
}
});
var ChildComponent = React.createClass({
render: function() {
return <div> User's Input: {this.state.data} </div>
}
});
You should pass parent state as a prop to your child: Change your childcomponent inside parent render to:
<ChildComponent foo={this.state.data} />
And then you can access it inside ChildComponent through this.props.foo.
Explanation: Inside ChildComponent, this.state.someData refers to ChildComponent state. And your child doesn't have state. (Which is fine by the way)
Also: this.setState() is probably better than this.replaceState().
And better to initialize parent state with
return { data : null };

Clone a component that has already been mounted

I am trying to build an app that uses drag-and-drop behaviour, and the component being dragged needs to be cloned elsewhere in the DOM. Since the component is already mounted, trying to mount it again causes the browser to hang.
Trying to use cloneWithProps results in a Cannot read property 'defaultProps' of undefined error.
Here's a testcase:
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem} />
{this.state.draggingItem}
</div>
},
setDraggingItem (component) {
// This gives `Cannot read property 'defaultProps' of undefined`
//React.addons.cloneWithProps(component)
// This crashes the browser
//this.setState({ draggingItem: component })
}
})
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
handleOnClick (event) {
this.props.onClick(this)
}
})
React.render(<TestCase />, document.body)
Of course I could simply clone component.getDOMNode() in setDraggingItem, but it really seems like rendering the component or calling cloneWithProps should work?
The two things you need to create an element is: the component class (e.g. ExampleComponent) and its props. cloneWithProps is only to be used in render and only with an element coming from props which was created in another component's render. You shouldn't save elements, or pass them around other than to other components in render. Instead, you pass around objects (props) and component classes.
Since you need to know the props and component class to render it in the first place, you can handle all of this in TestCase.
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null,
draggingItemProps: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem.bind(null,
/* the component class */ ExampleComponent,
/* the props to render it with */ null
)} />
{
this.state.draggingItem && React.createElement(
this.state.draggingItem,
this.state.draggingItemProps
)
}
</div>
},
setDraggingItem (component, props, event) {
this.setState({ draggingItem: component, draggingItemProps: props })
}
});
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
// just defer the event
handleOnClick (event) {
this.props.onClick(event)
}
});
If you wish to make these valid outside this TestCase component, ensure there aren't any functions bound to TestCase in the props. Also ensure there's no children prop with react elements in it. If children are relevant, provide the {componentClass,props} structure needed to recreate them.
It's hard to tell what your actual requirements are, but hopefully this is enough to get you started.
You need be sure you're creating a new component with the same props, not mount the same one multiple times. First, setup a function that returns an instantiated components (easier to drop JSX here):
function getComponent(props) {
return ExampleComponent(props);
}
Then in your TestCase render:
return (<div>
{ getComponent({ onClick: this.setDraggingItem }) }
{ this.state.draggingItem }
</div>);
That will create the first component. Then to create a clone:
setDraggingItem(component) {
var clone = getComponent(component.props);
}
This deals with the cloning part. You still have some dragging and rendering to figure out.

Categories