React not updating component props? - javascript

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/

Related

componentWillReceiveProps and callback issues

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

What is a good way of testing React component callbacks to parent components?

I am currently moving toward more of a TDD approach and want to get better at testing React components. One aspect of testing React components that I am struggling with is that of testing callbacks of a child component to a parent component.
What is an effective way of testing internal React component communication, such as callbacks to parent components?
The response to this question seems to offer a possible solution, though I didn't quite understand it (eg I'm not completely familiar with how to use function chaining in a Jasmine test.)
Thanks in advance for any tips and advice!
Example
(The following example uses Meteor, though I am not necessarily looking for Meteor-specific solutions.)
Repo with the complete example.
Let's say I have a component that accepts text input and passes it via props on submit:
SingleFieldSubmit = React.createClass({
propTypes: {
handleInput: React.PropTypes.func.isRequired
},
getDefaultProps() {
return {
inputValue: ""
};
},
getInitialState() {
return {
inputValue: this.props.inputValue
};
},
updateInputValue(e){
this.setState({inputValue: e.target.value});
},
handleSubmit(e) {
e.preventDefault();
this.handleInput();
},
handleInput(){
this.props.handleInput(this.state.inputValue.trim());
},
render() {
return (
<form className="single-field-submit" onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.inputValue}
onChange={this.updateInputValue}
/>
</form>
)
}
});
Here, I want to test if the component passes the user input on submit. My currently, somewhat clunky, solution is to create a mock parent component, with the component I want to test included as a child:
MockParentComponent = React.createClass({
getInitialState: function() {
return {
callbackValue: null
};
},
handleCallback: function(value) {
this.setState({callbackValue: value});
},
render: function() {
return (
<div className="container">
<SingleFieldSubmit handleInput={this.handleCallback} />
</div>
)
}
});
Then, my (Jasmine) test looks like this. The test passes. However, it seems like there should be a simpler way of doing this....
describe('SingleFieldSubmit Component', function () {
it('should, on submit, return the value input into the form', function () {
//SETUP
let mockUserInput = 'Test input';
let parentComponent = TestUtils.renderIntoDocument(
React.createElement(MockParentComponent)
);
let node = ReactDOM.findDOMNode(parentComponent);
let $node = $(node);
expect(parentComponent.state.callbackValue).toBe(null);
//TEST
Simulate.change($node.find('input')[0], { target: { value: mockUserInput } });
Simulate.submit($node.find('form')[0]);
expect(parentComponent.state.callbackValue).toBe(mockUserInput);
});
});
One method that doesn't require the parent component is to use jasmine spies.
describe('SingleFieldSubmit Component', function () {
it('should call handleInput prop with value of the input on submit', function () {
//SETUP
let callbackSpy = jasmine.createSpy('callbackSpy');
let mockUserInput = 'Test input';
let component = TestUtils.renderIntoDocument(<SingleFieldSubmit handleInput={callbackSpy} />);
let form = TestUtils.findRenderedDOMComponentWithTag(component, 'form');
let input = TestUtils.findRenderedDOMComponentWithTag(component, 'input')
//TEST
Simulate.change(imput, { target: { value: mockUserInput } });
Simulate.submit(form);
expect(callbackSpy).toHaveBeenCalledWith(mockUserInput);
expect(callbackSpy.calls.count()).toEqual(1);
});
});

Resetting state causing issues with React mixin

In my React component I'm using the MediaMixin to apply classes based on media queries. Here's a simplified example:
R.createClass({
mixins: [MediaMixin],
render: function () {
var mediaquery = this.state.media;
return (
<Component responsive={mediaquery.small}>
<input value={this.state.formInput1} >
<input value={this.state.formInput2} >
<input value={this.state.formInput3} >
</Compontent>
)
}
});
In the above, {mediaquery.small} returns true or false depending on viewport size.
The component also has an initial state used to set some values for a form in the component.
getInitialState: function () {
return {
formInput1: '',
formInput2: '',
formInput3: null
};
},
When the form is submitted/saved, or the form is cancelled, in order to reset the form we use:
_cancel: function () {
this.setState(this.getInitialState());
}
This causes issues with the mixin however, as it removes the state property containing the mixin's mediaquery.
The solution I'm using now involves resetting the state properties related to the form manually, like:
_cancel: function () {
this.setState({
allowanceType: '',
allowanceAmount: '',
allowanceDocument: null
});
}
Question - How can I reset the state and keep the mixin state properties (without manually resetting each form state property)?
You can use this.
_cancel: function () {
var newState = this.getInitialState();
newState.media = this.state.media;
this.setState(newState);
}
Remember, that you should treat current state as immutable (just for info).
You could store the initial state of your component inside another function that getInitialState as it's shared with the mixin.
Like:
getInitialState: function() {
return this.componentInitialState();
},
componentInitialState: function() {
return {
allowanceType: '',
allowanceAmount: '',
allowanceDocument: null
};
},
cancel: function() {
this.setState(this.componentInitialState());
}

ReactJs - uncheck checkbox from postalJs subscription

I have a component I would like to uncheck from a postal.js subscription. Using my code below the checkbox is always checked.
I am storing the value in state and setting it in ComponentDidMount. Can anyone tell me how I can uncheck it once I receive the subscription:
UPDATED:
var SelectAll = React.createClass({
getInitialState: function() {
return {
checked:false
};
},
handler: function(e) {
var updatedContacts = [],
contacts = this.props.data.contacts,
topic = 'selectAll',
checked = false,
channel = 'contact';
contactChannel.publish({
channel: channel,
topic: topic,
data: {
selectAll: this.state.checked
}});
this.setState({checked: event.target.value});
},
render: function() {
return (
<div className="contact-selector">
<input type="checkbox" checked={this.state.checked}
onChange={this.handler} ref="checkAll" />
</div>
);
},
setUnChecked: function(){
this.setState({ selected: false });
},
componentDidMount: function() {
var self = this;
contactChannel.subscribe("deselectedContact", function(data) {
self.setUnChecked();
});
}
});
module.exports = SelectAll;
If you specify the checked attribute to a form field in ReactJS, you set it as controlled. This means, that simple pressing it by a user won't change it's state (or writing sth in case of text fields won't change the text). You need to set it yourself, for example in your handler() function.
P.S. The data flow inside of the component is imho a bit of a mess - according to what I've just wrote, you should use some this.state.XXX variable in the checked={XXX} attribute and, when ckbx's pressed, update it in the handler() function using `this.setState()'. This will trigger automatic rerendering of the component in the DOM (of course if the state changes).
EDIT
<script src="https://fb.me/JSXTransformer-0.13.3.js"></script>
<script src="https://fb.me/react-0.13.3.js"></script>
<div id="container">
</div>
<script type="text/jsx;harmony=true">void function() {
'use strict';
var MyComponent = React.createClass({
getInitialState: function() {
return {
checked: false
};
},
handle: function(e) {
/*
Do your processing here, you might even prevent the checkbox from changing state - actually that is the main purpose of Facebook implementing the controlled/uncontrolled elements thing - to have full control of the input.
*/
var currentMinute = Math.floor(new Date().getTime() / 1000 / 60);
//The checkbox can be changed only in odd minutes
if(currentMinute % 2 == 1) {
this.setState({checked: e.target.checked});
}
},
render: function() {
return <input type="checkbox" checked={this.state.checked} onChange={this.handle} />;
}
});
React.render(<MyComponent />, document.getElementById('container'));
}()</script>
Something like this maybe ?
setChecked: function(isChecked) {
this.setState({
selected: isChecked
});
},
componentDidMount: function() {
var self = this; // <--- storing the reference to "this" so that it can be later used inside nested functions
contactChannel.subscribe("deselectedContact", function(data) {
self.setChecked(false); // <--- "this" would not refer to the react component hence you have to use self
});
}
You can use componentWillReceiveProps(), it is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.

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