React refs do not update between render - javascript

So I have this component
var LineItemRowsWrapper = React.createClass({
current_lineitem_count: 0,
getAjaxData: function(){
var lineitem_data = [];
for(var i = 0; i < this.current_lineitem_count; i++){
var data = this.refs['lineitem_'+i].getAjaxData();
lineitem_data.push(data)
}
return lineitem_data;
},
getLineitems: function(){
var self = this;
var lineitem_components = [];
this.current_lineitem_count = 0;
if(this.props.shoot){
var preview = this.props.preview;
var lineitems = this.props.shoot.get_lineitems();
lineitem_components = lineitems.map(function (item, index) {
var ref_str = 'lineitem_'+self.current_lineitem_count;
self.current_lineitem_count++;
return (
<LineItemRow item={item} key={index} ref={ref_str} preview={preview} onChange={self.props.onChange} />
)
});
}
return lineitem_components;
},
render: function() {
var lineitems = this.getLineitems();
return (
<div>
{lineitems}
</div>
)
}
})
the first time lineitems are rendered the refs work like expected. But if I add a lineitem to this.props.shoot the refs object of this component does not change.
So for example say I had an array of 3 lineitems
[i1,i2,i3]
this.refs would be
{lineitem_0:{}, lineitem_1:{}, lineitem_2:{}}
and when I update my lineitem array to be
[i1,i2,i3,i4]
this.refs does not change, it will still be
{lineitem_0:{}, lineitem_1:{}, lineitem_2:{}}
why doesn't the refs object update between renders?
The LineItemRow components update properly so I know its not something wrong on that front. Any insights would be much appreciated!
____Edit____ (requested to add more code for context)
var DocumentContent = React.createClass({
contextTypes: {
router: React.PropTypes.func.isRequired
},
getParams: function(){
return this.context.router.getCurrentParams()
},
getInitialState: function() {
return {
shoot: ShootStore.get_shoot(this.getParams().shoot_id),
}
},
componentWillMount: function() {
ShootStore.bind( 'change', this.onStoreUpdate );
},
componentWillUnmount: function() {
ShootStore.unbind( 'change', this.onStoreUpdate );
},
onStoreUpdate: function(){
this.setState(this.getInitialState());
},
addLineItem: function() {
ShootActions.create_lineitem(this.state.shoot.id);
},
update_shoot_timeout: null,
update_shoot:function(){
var self = this;
window.clearTimeout(this.update_shoot_timeout)
this.update_shoot_timeout = window.setTimeout(function(){
var lineitem_data = self.refs.lineitems.getAjaxData()
if(self.props.shoot){
ShootActions.update_shoot(self.state.shoot.id, lineitem_data )
}
}, 500)
},
render: function() {
var shoot = this.state.shoot;
return (
<div className='document__content'>
<div className='row'>
<div className='document__expenses'>
<h3 className='lineitem__title'> Expenses </h3>
<LineItemRowsWrapper shoot={shoot} onChange={this.update_shoot} ref='lineitems'/>
</div>
<button onClick={this.addLineItem} className="btn-small btn-positive">
+ Add Expense
</button>
</div>
);
}
})

Under the section "Caution" in the react documentation about refs https://facebook.github.io/react/docs/more-about-refs.html
"Never access refs inside of any component's render method - or while
any component's render method is even running anywhere in the call
stack."
Which is exactly what you're doing.
Instead you should store state about the component in this.state or properties of the component in this.props

Remove all your refs and iteration to read the data.
The onchange handler you pass all the way down to the LineItem component should be called and passed only the data that changes. (The single LineItem data)
This is then handled back at the component with the state handling (DocumentContent).
Create an action ShootActions.updateLineItem() that updates the relevant line item in the store, which then emits the change and everything renders again.

Related

Trying to edit data using react

I am very confused about a problem I am trying to solve. I am able to render data on the page using React, but I want to be able to change the values when an edit button is clicked. I am prompting the user for new data when the button is clicked and I want the new data to replace the old data on the page. The editItem function is where I am attempting to do this. Any suggestions on how to solve this would be extremely helpful.
const NewProduct = React.createClass({
render: function() {
return (
<section>
<div>Name: {this.props.name}</div>
<div>Price: {this.props.price}</div>
<div>Category: {this.props.category}</div>
<button className="deleteButton" onClick={this.deleteItem}>Delete</button>
<button className="editButton" onClick={this.editItem}>Edit</button>
</section>
);
},
deleteItem: function() {
console.log(this.props.id);
this.props.product.destroy();
},
editItem: function() {
var name = prompt('What should the new name be?');
<div>Name: {this.name.value}</div>
}
});
export default NewProduct;
You can make use of the local state and life cycle method to achieve this.
const NewProduct = React.createClass({
constructor(props) {
super(props);
const entity = {
name: '',
price : '',
category: '',
};
this.state = {
entity : entity
};
}
componentWillReceiveProps(newProps){
const entity = newProps.props;
const entity = {
name: entity.name,
price : entity.price,
category: entity.category,
};
this.setState({
entity: entity
});
}
render: function() {
const entity = this.state.entity;
return (
<section>
<div>Name: {entity.name}</div>
<div>Price: {entity.price}</div>
<div>Category: {entity.category}</div>
<button className="deleteButton" onClick={this.deleteItem}>Delete</button>
<button className="editButton" onClick={this.editItem}>Edit</button>
</section>
);
},
deleteItem: function() {
console.log(this.props.id);
this.props.product.destroy();
},
editItem: function() {
var name = prompt('What should the new name be?');
// here you need to just update the state based on the promt values or use a callback function passing the values and update the state.
}
});
export default NewProduct;
There are two approaches you can take for this.
Update props
You are currently always rendering the name based on the this.props.name value. If you'd like to update this, you'd have to notify your parent component when the value should update, and then have the parent component pass the new prop value back down to the child.
Example
editItem: function() {
var name = prompt('What should the new name be?');
/*
handleNewName would be a function passed in as a prop from the parent component.
It will then be on the parent component to update the name, and pass in
the updated name as a prop, which will trigger a re-render and update the
value on the child NewProduct component.
*/
this.props.handleNewName(name);
}
Introduce state
The second way you can handle this is to introduce local state into this component.
Example
const NewProduct = React.createClass({
getInitialState: function () {
// Get initial value from props.
return {
name: this.props.name
}
},
render: function() {
return (
<section>
<div>Name: {this.state.name}</div>
<div>Price: {this.props.price}</div>
<div>Category: {this.props.category}</div>
<button className="deleteButton" onClick={this.deleteItem}>Delete</button>
<button className="editButton" onClick={this.editItem}>Edit</button>
</section>
);
},
deleteItem: function() {
this.props.product.destroy();
},
editItem: function() {
var name = prompt('What should the new name be?');
this.setState({ name })
}
});
export default NewProduct;

React: Binding array to dynamically added fields

Lets say I have a class with a state level array
ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
addElement: function() {
var element = {
name: ""
};
},
render() {
return (
{this.state.elements.map(function (element, i) {
return <input value={element.name} />
}
)}
)
}
The idea being that I can dynamically add to the elements array and have a new input field appearing.
How do I bind the data so that I am able to change the value in the input field and have that reflect automatically in the correct element in the elements array?
To dynamically sync your inputs with your state array you can use someting called linkState from the react-catalyst package. Once you've installed it with npm you can use it in the following way:
//need to import
import Catalyst from 'react-catalyst';
ElementsClass = React.createClass({
// mixin the linkedstate component
mixins : [Catalyst.LinkedStateMixin],
getInitialState: function() {
return {
elements: []
}
},
addElement: function() {
var element = {
name: ""
};
//add to elements array
this.state.elements.push(element);
//let react know to rerender necessary parts
this.setState({
elements : this.state.elements
});
},
render() {
return (
{this.state.elements.map(function (element, i) {
//use the linkState method
return <input valueLink={this.linkState('elements.'+i+'.name')} />
}
)}
)
}
The reason we need the react-catalyst package is that natively React's valueLink will only link top level state items, in your case elements. Obviously this isn't particularily useful but thankfully it's a fairly easy problem to solve.
Note: for iterated items like your element inputs, you need to provide a unique key. Something like the following (might need modifying to be more specific):
{this.state.elements.map(function (element, i) {
//use the linkState method
return <input valueLink={this.linkState('elements.'+i+'.name')} key={'elinput' + i} />
}
)}
This doesn't have any outward effect on your app, it's mostly to help react target the element internally.
If you want to do this with just ES5 and React, one solution would be this:
var ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
createElement: function(){
var element = {
name: ''
};
this.setState({elements: this.state.elements.concat(element)});
},
updateElement: function(pos, event) {
var value = event.target.value;
var updatedElements = this.state.elements.map(function(element, i){
if (i === pos){
return {name: value};
}
return element;
});
this.setState({elements: updatedElements});
},
render: function() {
console.log(this.state.elements);
return (
<div>
{this.state.elements.map(function (element, i) {
var boundClick = this.updateElement.bind(this, i);
return <input key={i} onKeyUp={boundClick}/>
}.bind(this))}
<button onClick={this.createElement}>Add Element</button>
</div>
)
}
});
React.render(<ElementsClass />, document.getElementById('app'));
You want to treat component state as immutable, so you don't want to call a mutating method like push on elements.
These situations are handled easily with custom links packages.
State and Forms in React, Part 3: Handling the Complex State
import Link from 'valuelink';
// linked inputs will be deprecated, thus we need to use custom wrappers
import { Input } from 'valueLink/tags.jsx'
const ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
render() {
// Take link to the element
const elementsLink = Link.state( this, 'elements' );
return (
<div>
{ elementsLink.map( ( elementLink, i ) => (
<Input key={ i } valueLink={ elementLink.at( 'name' ) } />
))}
<button onClick={ elementsLink.push({ name : '' })}>
Add Elements
</button>
</div>
);
}
});

Giving a state to parent component in React

I know you can pass down states and props in React from a parent component to a child component, but is there any way to do this the opposite way?
For example:
Given some child component:
var Child = React.createClass({
getInitialState: function(){
return {
data: ''
};
},
componentDidMount: function(){
this.setState({data: 'something'});
},
render: function() {
return (
<div>
...
</div>
);
}
});
and given some parent component:
var Parent = React.createClass({
render: function() {
return (
<div>
<Child />
...
</div>
);
}
});
Is there any way for me to give Parent the value of the state data from Child?
No.
But yes. But really no.
You cannot "pass" anything from a child to a parent in React. However, there are two solutions you can use to simulate such a passing.
1) pass a callback from the parent to the child
var Parent = React.createClass({
getInitialState: function() {
return {
names: []
};
},
addName: function(name) {
this.setState({
names: this.state.names.push(name)
});
},
render: function() {
return (
<Child
addName={this.addName}
/>
);
}
});
var Child = React.createClass({
props: {
addName: React.PropTypes.func.isRequired
},
handleAddName: function(event) {
// This is a mock
event.preventDefault();
var name = event.target.value;
this.props.addName(name);
},
render: function() {
return (
...
onClick={this.handleAddName}
...
);
}
});
The second option is to have a top-level state by using a Flux-style action/store system, such as Reflux or Redux. These basically do the same thing as the above, but are more abstract and make doing so on much larger applications very easy.
One way to do this is through a 'render props' pattern I was recently introduced to. Remember, this.props.children is really just a way for React to pass things down to a component.
For your example:
var Parent = React.createClass({
render: function() {
return (
<div>
<Child>
{(childState) => {
// render other 'grandchildren' here
}}
</Child>
</div>
);
}
});
And then in <Child> render method:
var Child = React.createClass({
propTypes: {
children: React.PropTypes.func.isRequired
},
// etc
render () {
return this.props.children(this.state);
}
});
This is probably best suited for cases where the <Child /> is responsible for doing something but doesn't really care much at all about the children that would be rendered in its place. The example the react training guys used was for a component that would fetch from Github APIs, but allow the parent to really control what / if anything was rendered with those results.

Call parent function passed as prop in React

I'm creating a survey-type app in React. The questions are arranged as items in a carousel.
When the user selects an answer - I AM able to change the state of the question (setting a button as active). However, I would also like to advance the carousel to the next item.
var Question = React.createClass({
getInitialState() {
return {
selectedIndex: -1
};
},
handleClick(index) {
this.setState({selectedIndex: index});
this.props.onQuestionAnswered();
},
render() {
var answerItems = answerChoices.map(function (answer) {
return (
<ReactBootstrap.ListGroupItem
key={answer.text}
text={answer.text}
active={answer.index == this.state.selectedIndex}
onClick={this.handleClick.bind(this, answer.index)}>
{answer.text}
</ReactBootstrap.ListGroupItem>
);
}.bind(this));
return (
<div>
<h3>{this.props.qText.text}</h3>
<ReactBootstrap.ListGroup>
{answerItems}
</ReactBootstrap.ListGroup>
</div>
);
}
});
var Carousel = React.createClass({
getInitialState() {
return {
index: 0,
};
},
handleSelect() {
this.setState({
index: 1
});
},
render() {
var questionItems = questionContent.map(function (question) {
return (
<ReactBootstrap.CarouselItem key={question.text}>
<Question qText={question}/>
</ReactBootstrap.CarouselItem>
);
});
return (
<ReactBootstrap.Carousel interval={false} activeIndex={this.state.index} onQuestionAnswered={this.handleSelect}>
{questionItems}
</ReactBootstrap.Carousel>
);
}
});
var App = React.createClass({
render() {
return (
<div>
<h4>Survey</h4>
<Carousel/>
</div>
);
}
});
React.render(<App/>, document.getElementById('content'));
I have a full JSFiddle available: http://jsfiddle.net/adamfinley/d3hmw2dn/
Console says the following when I try to call the function prop:
Uncaught TypeError: this.props.onQuestionAnswered is not a function
What do I have to do to call the parent function? Alternatively - is there a better pattern I should be using? (first time with React).
It looks like the error is coming from the Question component, which doesn't have the onQuestionAnswered prop. So you simply need to pass it in your questionItems map iteration.
var self = this;
var questionItems = questionContent.map(function (question) {
return (
<ReactBootstrap.CarouselItem key={question.text}>
<Question onQuestionAnswered={self.handleSelect} qText={question}/>
</ReactBootstrap.CarouselItem>
);
});

Passing AJAX Results As Props to Child Component

I'm trying to create a blog in React. In my main ReactBlog Component, I'm doing an AJAX call to a node server to return an array of posts. I want to pass this post data to different components as props.
In particular, I have a component called PostViewer that will show post information. I want it to by default show the post passed in from its parent via props, and otherwise show data that is set via a state call.
Currently, the relevant parts of my code looks like this.
var ReactBlog = React.createClass({
getInitialState: function() {
return {
posts: []
};
},
componentDidMount: function() {
$.get(this.props.url, function(data) {
if (this.isMounted()) {
this.setState({
posts: data
});
}
}.bind(this));
},
render: function() {
var latestPost = this.state.posts[0];
return (
<div className="layout">
<div className="layout layout-sidebar">
<PostList posts={this.state.posts}/>
</div>
<div className="layout layout-content">
<PostViewer post={latestPost}/>
</div>
</div>
)
}
});
and the child component:
var PostViewer = React.createClass({
getInitialState: function() {
return {
post: this.props.post
}
},
render: function() {
/* handle check for initial load which doesn't include prop data yet */
if (this.state.post) {
return (
<div>
{this.state.post.title}
</div>
)
}
return (
<div/>
)
}
});
The above works if I swap out the if statement and content in my child's render to this.props.* However, this would mean that I couldn't change the content later via state, correct?
TLDR: I want to set a default post to be viewed via props in a child component (results of an AJAX call), and I want to be able to change what post is being viewed by adding onClick events (of another component) that will update the state.
Is this the correct way to go about it?
Current hierarchy of my app's components are:
React Blog
- Post List
- Post Snippet (click will callback on React Blog and update Post Viewer)
- Post Viewer (default post passed in via props)
Thanks!
EDIT:
So what I ended up doing was attaching the props in ReactBlog using a value based on this.state. This ensured that it updates when I change state and renders correctly in child components. However, to do this I had to chain onClick callbacks up through all the various child components. Is this correct? It seems like it could get VERY messy. Here's my full example code:
var ReactBlog = React.createClass({
getInitialState: function() {
return {
posts: [],
};
},
componentDidMount: function() {
$.get(this.props.url, function(data) {
if (this.isMounted()) {
this.setState({
posts: data,
post: data[0]
});
}
}.bind(this));
},
focusPost: function(slug) {
$.get('/api/posts/' + slug, function(data) {
this.setState({
post: data
})
}.bind(this));
},
render: function() {
return (
<div className="layout">
<div className="layout layout-sidebar">
<PostList handleTitleClick={this.focusPost} posts={this.state.posts}/>
</div>
<div className="layout layout-content">
<PostViewer post={this.state.post}/>
</div>
</div>
)
}
});
var PostList = React.createClass({
handleTitleClick: function(slug) {
this.props.handleTitleClick(slug);
},
render: function() {
var posts = this.props.posts;
var postSnippets = posts.map(function(post, i) {
return <PostSnippet data={post} key={i} handleTitleClick={this.handleTitleClick}/>;
}, this);
return (
<div className="posts-list">
<ul>
{postSnippets}
</ul>
</div>
)
}
});
var PostSnippet = React.createClass({
handleTitleClick: function(slug) {
this.props.handleTitleClick(slug);
},
render: function() {
var post = this.props.data;
return (
<li>
<h1 onClick={this.handleTitleClick.bind(this, post.slug)}>{post.title}</h1>
</li>
)
}
});
var PostViewer = React.createClass({
getInitialState: function() {
return {
post: this.props.post
}
},
render: function() {
/* handle check for initial load which doesn't include prop data yet */
if (this.props.post) {
return (
<div>
{this.props.post.title}
</div>
)
}
return (
<div/>
)
}
});
Still hoping to get some feedback / hope this helps!
This is an old question, but I believe still relevant, so I'm going to throw in my 2 cents.
Ideally, you want to separate out any ajax calls into an actions file instead of doing it right inside a component. Without going into using something like Redux to help you manage your state (which, at this point in time, I would recommend redux + react-redux), you could use something called "container components" to do all of the heavy state lifting for you and then use props in the component that's doing the main layout. Here's an example:
// childComponent.js
import React from 'react';
import axios from 'axios'; // ajax stuff similar to jquery but with promises
const ChildComponent = React.createClass({
render: function() {
<ul className="posts">
{this.props.posts.map(function(post){
return (
<li>
<h3>{post.title}</h3>
<p>{post.content}</p>
</li>
)
})}
</ul>
}
})
const ChildComponentContainer = React.createClass({
getInitialState: function() {
return {
posts: []
}
},
componentWillMount: function() {
axios.get(this.props.url, function(resp) {
this.setState({
posts: resp.data
});
}.bind(this));
},
render: function() {
return (
<ChildComponent posts={this.state.posts} />
)
}
})
export default ChildComponentContainer;
A blog is static for the most part, so you could exploit React immutable structures to "render everything" all the time instead of using the state.
One option for this is to use a router (like page.js) to fetch data.
Here is some code http://jsbin.com/qesimopugo/1/edit?html,js,output
If you don't understand something just let me know ;)
Get rid of isMounted and make use of context if you're passing callbacks down several levels

Categories

Resources