I'm trying to write a simple page slider. Here, when I click a page, it creates a new Page with random content, and re-renders the App component. On App render(), instead of the TransitionGroup holding both state.pages until animation completes, it just switches out the pages, never attaching the enter-leave classes and not performing the css animation. I'm sure I'm messing something up in the LifeCycle, but can't think of it.
Thanks for looking!
var Page = React.createClass({
handleClick: function(){
var pgs = ['page-one','page-two','page-three','page-four']
currentIdx = Math.floor(Math.random() * pgs.length);
var pg = pgs[ currentIdx ];
var newPg = <Page html={pg} title={'Title for ' + pg} />;
React.renderComponent(<App newPage={newPg} />, document.body)
},
render: function(){
return (<div className="content" style={{paddingTop: 44}} onClick={this.handleClick}>{this.props.html}</div>);
}
})
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var App = React.createClass({
getInitialState: function() {
return {pages: [<Page html="initial page" title="initial title" />]};
},
componentWillMount: function(){
this.setState({pages: [this.props.newPage]})
},
componentWillReceiveProps: function(nextProps) {
this.setState({pages: [nextProps.newPage]});
},
render: function() {
var title = this.state.pages.length ? this.state.pages[ this.state.pages.length - 1 ].props.title : 'none';
return (
<div id="body">
<TitleBar title={title} />
<ReactCSSTransitionGroup transitionName="pg" id="transdiv" component={React.DOM.div}>
{this.state.pages}
</ReactCSSTransitionGroup>
</div>
);
}
});
The problem was that I was setting the Page keys in Page.render() (not shown above), and not in App.render() I'm not sure why you can't set keys in the child/owned component as long as they're unique, but this fixed my problem.
var App = React.createClass({
// other methods are same
render: function(){
var title = 'Title';
var pgs = this.state.pages.map(function(pg){
// SET KEY HERE
pg.props.key = pg.props.title;
return pg;
}
return (
<div id="body">
<TitleBar title={title} />
<ReactCSSTransitionGroup transitionName="pg" id="transdiv" component={React.DOM.div}>
{pgs}
</ReactCSSTransitionGroup>
</div>
);
}
}
Also, if anyone can tell me the correct way to set props on unmounted components, please tell me. Setting them directly works, but it doesn't feel right.
Related
I've built a basic restuarant recommendation app that filters by location using the YELP api. The api was responding to my requests with the response object and everything was appending to my divs perfectly, but I realized that for my project, I needed to make a new layer for the data listing. Here are the relevant portions of my two components as they are now:
display-recs:
var DisplayRecs = React.createClass({
render: function() {
var recsLoop = [];
if (this.props.recommendations) {
for (var i=0; i < this.props.recommendations.length; i++) {
recsLoop.push(<Recommendations item={this.props.recommendations[i]} />)
}
}
console.log(this.props.recommendations);
return (
<div className="DisplayRecs">
{recsLoop}
</div>
);
}
});
var mapStateToProps = function(state, props) {
return {
recommendations: state.recommendations
};
};
recommendations:
var Recommendations = React.createClass({
render: function() {
<div id="bizData">
<div id='nameList'>{this.props.item.name}</div>
<div id='phoneList'>{this.props.item.phone}</div>
<div id='ratingList'>{this.props.item.rating}</div>
</div>
}
});
var mapStateToProps = function(state, props) {
return {
recommendations: state.recommendations
};
};
I cannot figure out why the nameList, phoneList, and ratingList will not print onto the dom. When I view the elements tab in my devtools, all i see is an empty displayrecs div. I've tried to just change things by guessing, but it's not been fruitful. Can any of you see an obvious problem with the current code?
Thanks
Your Recommendations react component's render function doesn't have any return statement. Try doing this:
var Recommendations = React.createClass({
render: function() {
return ( <div id="bizData">
<div id='nameList'>{this.props.item.name}</div>
<div id='phoneList'>{this.props.item.phone}</div>
<div id='ratingList'>{this.props.item.rating}</div>
</div>);
}
});
Also add a key to the Recommendations components as #Vikramaditya recommends:
recsLoop.push(<Recommendations key={i} item={this.props.recommendations[i]} />)
Note: I'm using the beautiful library react-rails though it should not impact the answer as far as I understand my problem.
I have a <Component /> which loads a <Map />, which implies client-side rendering as it doesn't make sense on the server side (at least the lib I'm using doesn't do that).
So instead, I want to display an image before the clientside is ready, to apply the Skeuomorphism principle.
Basically, this means I have:
var Component = React.createClass({
render: function() {
var content;
if (this.state.clientSideReady) { // How can I change my component state here?
content = <Map />
} else {
content = <PlaceholderImage />
}
return (<div>{content}</div>)
}
});
From my current understanding, componentDidMount is called on the server-side, when the template string is generated. How can I know the component actually mounted on the clientside so I can replace my image by the actual map?
My mistake. As from this answer componentDidMount isn't called on the server-side.
So I can go:
var Component = React.createClass({
getInitialState: function() {
return {
clientSideReady: false
}
},
componentDidMount: function() {
this.setState({
clientSideReady: true
});
},
render: function() {
var content;
if (this.state.clientSideReady) {
content = <Map />
} else {
content = <PlaceholderImage />
}
return (<div>{content}</div>)
}
});
I have this kind of route:
<Route name="itemDetails" handler={ItemDetails} path="/item/:itemId"/>
It leads to the page, containing full product description.
There are few "related items". Each of them contains the Link to the different "itemDetails" page. When I'm clicking on one of this links, the route is changing in browser address bar, but the content is not.
Why? The rest of routes works well.
Also, if i reload the page, content is refreshing and it's mutching to the route in browser address bar
It might be because of the incorrect url listener
It tried this:
Router.run(routes, function (Handler) {
React.render(<Handler/>, document.getElementById('example'));
});
and this:
Router.run(routes, Router.HashLocation, function (Root) {
React.render(React.createElement(Root, null), document.body);
});
Here the itemDetails code:
var React = require('react');
var Store = require('../../stores/app-store');
var Preloader = require('../template/app-preloader');
var NewEstateItem = require('./app-newestateitem');
var NewEstateRelated = require('./app-newestaterelated');
var NewEstateDetails = React.createClass({
getInitialState: function() {
return { item: null };
},
componentWillMount: function() {
itemId = this.props.params.itemId;
console.log('ItemId:', itemId);
Store.getNewEstateById(itemId,this._getNewEstateDetails);
},
render: function() {
var item = this.state.item;
if(!item) {
return (<Preloader />);
}
return (
<div className="row">
<div className="container flex-container">
<div className="col s12 m12 l8">
<div className="col s12">
<h4>{item.Name}</h4>
</div>
<div className="col l4 hide-on-med-and-down margintop">
<h5 className="center">Related items</h5>
<div className="col s12">
<NewEstateRelated Price={item.Price}/>
</div>
</div>
</div>
</div>
);
},
_getNewEstateDetails: function (item) {
this.setState({item:item});
}
});
module.exports = NewEstateDetails;
And here relatedItem Link code:
<Link to="ItemDetails" params={{itemId: item.objectId, CustomID: item.CustomID}} className="btn</Link>
Here the store:
var getNewEstateData = {
getNewEstateById: function(objectId, callback) {
console.log('store.objectId', objectId);
function NewEstateData() {};
var item = Backendless.Persistence.of( NewEstateData ).findById( objectId );
console.log("store.getNewEstateById", item);
callback(item);
},
getAllNewEstate: function(callback) {
function NewEstateData() {};
var items = Backendless.Persistence.of( NewEstateData ).find().data;
callback(items)
},
getRelatedNewEstateItems: function (priceBottom, priceTop, callback) {
function NewEstateData() {};
var items = Backendless.Persistence.of( NewEstateData );
var dataQuery = {
condition: "Price >= "+priceBottom
}
var query = items.find( dataQuery ).data
var output = query.slice(0,3)
console.log('relatedItems', query);
callback(output)
}
};
module.exports = getNewEstateData;
I think this is the issue:
Your router is expecting :id:
<Route name="itemDetails" handler={ItemDetails} path="/item/:id"/>
But you are sending itemId in the <link>:
<Link to="ItemDetails" params={{itemId: item.objectId, CustomID: item.CustomID}} className="btn</Link>
I am pretty sure it should match and be:
<Link to="ItemDetails" params={{id: item.objectId, CustomID: item.CustomID}} className="btn</Link>
See the docs for it here where the example confirms this:
params: An object of the names/values that correspond with dynamic segments in your route path.
Docs Example
// given a route config like this
<Route name="user" path="/users/:userId"/>
// create a link with this
<Link to="user" params={{userId: "123"}}/>
// though, if your user properties match up to the dynamic segements:
<Link to="user" params={user}/>
Let's say I have 2 components. A parent that contains a child.
The child component is a button like so:
var React = require('react');
var ChildButton = React.createClass({
onSubmitAnswer: function(e) {
this.props.onClick(this);
},
render: function() {
return (
<div className={this.props.visibility}>
<button onClick={this.onSubmitAnswer}>Click Me</button>
</div>
)
}
});
module.exports = ChildButton;
It lives within it's parent, which looks like this:
var React = require('react'),
ChildButton = require('./face-submit-button');
var ParentComponent = React.createClass({
onButtonSubmit: function() {
//Something happens here
},
render: function() {
return (
<div>
//Some more components
<ChildButton text="Submit" onClick={this.onButtonSubmit} />
</div>
)
}
});
module.exports = ParentComponent;
So far so good. Everything works as expected in the UI. But I've encountered some issues in the Jest tests using TestUtils.Simulate.click().
My test for the ChildButton component is straightforward and behaves as I would expect.
jest.dontMock('./child-button');
describe('ChildButton', function() {
var React = require('react/addons'),
ChildButton = require('./child-button'),
TestUtils = React.addons.TestUtils;
describe('events', function() {
var button,
onClickStub;
beforeEach(function() {
onClickStub = jest.genMockFn();
button = TestUtils.renderIntoDocument(
<ChildButton onClick={onClickStub} />
);
});
it('should call onSubmitAnswer when the button is clicked', function() {
var buttonTag = TestUtils.findRenderedDOMComponentWithTag(button, 'button');
TestUtils.Simulate.click(buttonTag);
expect(onClickStub).toBeCalled();
});
});
});
My test for the parent component started out looking the same:
jest.dontMock('./parent-component');
describe('ParentComponent', function() {
var React = require('react/addons'),
ParentComponent = require('./parent-component'),
ChildButton = require('./child-button'),
TestUtils = React.addons.TestUtils;
describe('events', function() {
var parent,
onClickStub;
beforeEach(function() {
onClickStub = jest.genMockFn();
parent = TestUtils.renderIntoDocument(
<ParentComponent onClick={onClickStub} />
);
});
it('should call onButtonSubmit when a click is triggered', function() {
var childButton = TestUtils.findRenderedComponentWithType(parent, ChildButton);
TestUtils.Simulate.click(childButton);
expect(onClickStub).toBeCalled();
});
});
});
But this test fails. The only difference I can see between these two tests is that one uses an HTML tag directly and clicks on it, while the other triggers a click on a React component. Can I not use the click event on React components directly? Is my assumption correct?
And if so, is there a way to trigger a click on React components differently in the tests? I tried using SimulateNative but that had the same effect, the onClickStub doesn't get called on click.
There is currently an open bug for this issue: Let ReactTestUtils.Simulate.click work on non-dom components. So the answer is that due to bugs, you can only use Simulate.click on an actual DOM node. So you can workaround the bug by getting the DOM node until it is fixed.
I'm working with a RadioButtonGroup component which is like radio input but with buttons:
It would be good if using the component was easy like this:
var SelectThing = React.createClass({
render: function render() {
// I would not like to add onClick handler to every button element
// outside the RadioButtonGroup component
return (
<RadioButtonGroup onChange={this._onActiveChange}>
<button>Thing 1</button>
<button>Thing 2</button>
<button>Thing 3</button>
</RadioButtonGroup>
)
},
_onActiveChange: function _onActiveChange(index) {
console.log('You clicked button with index:', index);
}
});
The actual question: How can I achieve that the most elegantly with React? (I found another similar question but it doesn't exactly answer to this).
My first intuition was to add and remove the onClick handlers inside the component to remove boiler plate code from the component's user. Another option that comes to my mind is to give e.g. <p> elements instead of button elements and put them inside button elements which would be created inside the RadioButtonGroup component. I don't like the latter that much because it doesn't make that much sense semantically compared to passing buttons.
Here's what the (obviously not working) component looks like now:
// Radio input with buttons
// http://getbootstrap.com/javascript/#buttons-checkbox-radio
var RadioButtonGroup = React.createClass({
getInitialState: function getInitialState() {
return {
active: this.props.active || 0
};
},
componentWillMount: function componentWillMount() {
var buttons = this.props.children;
buttons[this.props.active].className += ' active';
var self = this;
buttons.forEach(function(button, index) {
// How to dynamically set onclick handler for virtual dom
// element created inside JSX?
button.addEventListener('onClick', function(event) {
self._onAnyButtonClick(index, event);
}
});
},
componentWillUnmount: function componentWillUnmount() {
var buttons = this.props.children;
buttons.forEach(function(button, index) {
button.removeEventListener('onClick');
});
},
render: function render() {
return (
<div className="radio-button-group">
{buttons}
</div>
)
},
_onAnyButtonClick: function _onAnyButtonClick(index, event) {
this.setState({
active: index
});
this.props.onChange(index);
}
});
You don't want to mess with click handlers on each button, just listen for the click on the container. Then update the state based on which child is clicked.
Also, with React it's best to keep all of your DOM stuff in the render function. In this case, defining an element's class name.
Here's how this could work:
var RadioButtonGroup = React.createClass({
getInitialState: function getInitialState() {
return {
active: this.props.active || 0
};
},
clickHandler: function clickHandler(e) {
// Getting an array of DOM elements
// Then finding which element was clicked
var nodes = Array.prototype.slice.call( e.currentTarget.children );
var index = nodes.indexOf( e.target );
this.setState({ active: index });
},
render: function render() {
var buttons = this.children.map(function(child, i) {
if (i === this.state.active) child.props.className += ' active';
return child;
}, this);
return (
<div className="radio-button-group" onClick={ this.clickHandler }>
{ buttons }
</div>
)
}
});
To get an api like this (similar to <input/>), we need to use the cloneWithProps addon.
<RadioButtonGroup onChange={this.handleChange} value={this.state.selected}>
<button>Test 1</button>
<button>Test 2</button>
<button>Test 3</button>
</RadioButtonGroup>
All this does is take each child, add a click handler, and conditionally add a className of 'active' to it. You easily can (and should) modify it to take the active class name as a prop.
var RadioButtonGroup = React.createClass({
render: function(){
return <div>{React.Children.map(this.props.children, this.renderItem)}</div>
},
renderItem: function(button, index){
return React.cloneElement(button, {
className: this.props.value === index ? ' active ' : '',
onClick: function(){
this.props.onChange(index);
}.bind(this),
key: index
});
}
});
demo
If you don't want to use cloneWithProps, you could use a wrapper div, but styling may be a bit more complex.
renderItem: function(button, index){
return React.createElement('div', {
className: this.props.value === index ? ' active ' : '',
onClick: function(){
this.props.onChange(index);
}.bind(this),
key: index
}, button);
}
The reason everything uses index is because you're passing react elements, which are opaque. There's no clean way to get any data out of these buttons, but we do know their index because we're iterating over them using React.Children.map. An alternative api would look like this:
<RadioButtonGroup value={'test1'} onChange={fn} options={{
test1: <button>Test 1</button>,
test2: <button>Test 2</button>,
test3: <button>Test 3</button>
}} />
Here we can iterate over this.props.options, and pass the key to the onChange callback, and take e.g. 'test1' as a value prop.
This is my code .I do not know whether this is the best way ,as i started using react just 5 hrs ago. But it works fine.
var LeftPane = React.createClass({
getInitialState: function() {
return {list:[{name:"Item 1",isactive:1},
{name:"Item 2",isactive:0},
{name:"Item 3",isactive:0},
{name:"Item 4",isactive:0},
{name:"Item 5",isactive:0}]};
},
clickHandler: function(index) {
var current_list = this.state.list;
current_list.map(function(record,i){
if(record.isactive==1){
if(i!=index){
record.isactive =0;
}else{
return;
}
}else{
if(i==index){
record.isactive =1;
}
}
});
this.setState({data:current_list});
},
render: function() {
var active_item = "list-group-item active"
var non_active_item ="list-group-item";
var _this = this;
return (
<div className="list-group">
{this.state.list.map(function(record,i) {
if(record.isactive==1){
return <a className={active_item} onClick={_this.clickHandler.bind(null, i)}>{record.name}</a>
}else{
return <a className={non_active_item} onClick={_this.clickHandler.bind(null, i)}>{record.name}</a>
}
})}
</div>
</div>
)
}
});
I would not like to add onClick handler to every button element
React uses events delegation pattern, so you can freely adding as many onClick handlers as needs.
Another way, if you just want to make code clear, you may create your own button component, and pass _onActiveChange to it props, but i don't sure that it's necessary in your case.
And remember, that React works with synthetic events, and, in some cases, usage of setState within native event handlers may cause an unpredictable behaviour.