React/Rails - getInitialState not called - ruby-on-rails

I'm following this intro to React guide as well as the original fb react tutorial, using Rails as a backend. I've split out my files and they all work together correctly up to this point.
Upon trying to set the getInitialState to provide props for my CommentBox, I get errors of Cannot read property 'map' of undefined, meaning React can't find the empty array [] that was allegedly set, and sent to CommentList. How do I ensure getInitialState actually sets the data prop?
var CommentBox = React.createClass({
getInitialState: function(){
return {data: []};
},
render: function(){
return(
<div className="commentBox">
<h1> Comments! </h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
var ready = function(){
React.render(
<CommentBox />,
document.getElementById('content')
);
};
$(document).ready(ready);
Then entirety of the code is hosted in this repo. Thanks!
EDIT: CommentList code:
var CommentList = React.createClass({
commentNodes: function(){
var nodes = this.props.data.map(function(d){
return(
<Comment author={d.author}>
{d.text}
</Comment>
);
});
return nodes;
},
render: function(){
return(
<div className="commentList">
This is the comment list.
{this.commentNodes()}
</div>
);
}
});

Figured it out. Essentially:
I was trying to pass in a data props, which doesn't exist because it is a state.
Upon rendering the CommentList, it then becomes a prop and it can be accessed from there.
Learnings were had.

Related

how to avoid key/id problems in reactjs and make props pass from parent to child?

I keep hitting a wall when trying to get the parent data passed down to the child component.
My view:
<%= react_component 'Items', { data: #items } %>
My Items component makes an ajax call, sets state, and renders Item. Leaving key={this.props.id} out of the Item instance passed into the mapping function makes it so that the component html renders to the page. But add the key in, and I get a console error: Uncaught TypeError: Cannot read property 'id' of undefined
Here's 'Items':
var Items = React.createClass({
loadItemsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
var itemNodes = this.props.data.map(function() {
return (
<Item key={this.props.id} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
My item.js.jsx component just formats each Item:
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
The React dev tools extension shows the props and state data inside Items. The children, however, are empty.
I'm aware of this, but I'm setting key with this.props.id. I'm not sure what I'm missing?
I found a couple of problems with the code you posted, in the Items component
You're rendering this.props.data while in fact this.state.data is the one being updated with the ajax request. You need to render this.state.data but get the initial value from props
The map iterator function takes an argument representing the current array element, use it to access the properties instead of using this which is undefined
The updated code should look like this
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
var Items = React.createClass({
getInitialState: function() {
return {
// for initial state use the array passed as props,
// or empty array if not passed
data: this.props.data || []
};
},
loadItemsFromServer: function() {
var data = [{
id: 1,
artist: 'abc'
}, {
id: 2,
artist: 'def'
}]
this.setState({
data: data
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
// use this.state.data not this.props.data,
// since you are updating the state with the result of the ajax request,
// you're not updating the props
var itemNodes = this.state.data.map(function(item) {
// the map iterator function takes an item as a parameter,
// which is the current element of the array (this.state.data),
// use (item) to access properties, not (this)
return (
// use key as item id, and pass all the item properties
// to the Item component with ES6 object spread syntax
<Item key={item.id} {...item} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
And here is a working example http://codepen.io/Gaafar/pen/EyyGPR?editors=0010
There are a couple of problems with your implementation.
First of all, you need to decide: Do you want to render the #items passed to the Items component from your view? Or do you want to load them asynchronous?
Because right now I get the impression you are trying to do both...
Render items passed from view
If you want to render the items from your view passed to the component, make sure it's proper json. You might need to call 'as_json' on it.
<%= react_component 'Items', { data: #items.as_json } %>
Then, in your Component, map the items to render the <Item /> components. Here is the second problem, regarding your key. You need to define the item variable to the callback function of your map function, and read the id from it:
var itemNodes = this.props.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);
});
Note, I also added the author as prop, since you are using it in your <Item /> Component.
You can remove your componentDidMount and loadItemsFromServer functions, since you are not using them.
Load items asynchronous
If you want to load the items asynchronously, like you are trying to do in your loadItemsFromServer function, first of all, pass the url from your view and remove the {data: #items} part, since you will load the items from your component, something like:
<%= react_component 'Items', { url: items_path(format: :json) } %>
If you want to render the asynchronous fetched items, use:
var itemNodes = this.state.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);loadItemsFromServer
});
Note I changed this.props.map to this.state.map
You can now use your componentDidMount and loadItemsFromServer functions to load the data and save them to state.

How to properly initialize a store with the props of a component

I'm relatively new to the React/Reflux thing so excuse me if it's a dumb question.
I have a React component that I want to use many times in my application.
I'd like to initialize the state of this component with the props.
How can I set the initial state of my store with the props of my component with Reflux?
I read in the ReactJS doc that it could be an anti-pattern but I think in my case it's not.
I tried the code bellow but it renders my component twice since I set a new state on ComponentDidMount function.
I have no idea how to pass the props of my component to my store at initialization.
The parent component:
var ParentComponent = React.createClass({
render: function(){
return (
<div className="parent-component">
<OrderComponent order={parent.order} />
</div>
);
}
});
My component:
var OrderComponent = React.createClass({
mixins: [Reflux.connect(OrderStore, "order")],
componentDidMount: function(){
OrderActions.update(this.props.order);
},
...
render: function(){
<div>{this.state.order}</div>
}
})
My store:
var OrderStore = Reflux.createStore({
listenables: [OrderActions],
onUpdate: function(order){
this.update(order);
},
...
update: function(order){
this.order = order;
this.trigger(order);
}
});
To answer your question, in your store you could just not trigger an update.
var OrderStore = Reflux.createStore({
listenables: [OrderActions],
onUpdate: function(order, ignoreTrigger){
this.order = order;
if (!ignoreTrigger) {
this.update(order);
}
},
...
update: function(order){
this.trigger(order);
}
});
You are right that this is definitely an anti-pattern. Your store should initialize itself. I'd suggest using the init function on your store to initialize your order so your components can just update based on the state coming from your store.

React-Rails: Load initial array state with ajax

I've been following along some tutorials with React and i'm starting out building an application on my own. I've come across a situation regarding components and i'm wondering if theres a best practice for this scenario. Please note, I'm just using react-rails; no flux or whatever for now.
setting the initial state with an array whose values get set through ajax and have that array display in the initial render
Here's what i'm trying to do: (stripped down for simplicity)
var ShoutList = React.createClass({
getInitialState: function(){
return {shouts: []};
},
componentDidMount: function(){
var component = this;
$.get('/api/shouts.json', function(data){
component.setState({shouts: data});
});
},
render: function(){
return (
<div>
{this.state.shouts[0].shout}
</div>);
}
});
So if I have this right, the order in which things are run go as follows:
On load, getInitialState sets shouts to an empty array
Render gets called and errors out because of trying to access the shout property on an empty array
ComponentDidMount gets called and sets the state of shouts to the data received from the ajax call. **I get an error when I try to do this in ComponentWillMount **
Render gets called again because the state has changed, but this time shouts[0].shout would contain data.
So I error out at step 2 and my work around is as follows:
var ShoutList = React.createClass({
getInitialState: function(){
return {shouts: []};
},
componentDidMount: function(){
var component = this;
$.get('/api/shouts.json', function(data){
component.setState({shouts: data});
});
},
emptyShouts: function(){
return(<div>No Shouts Yet!</div>);
},
shoutsList: function(){
return(<div>{this.state.shouts[0].shout}</div>);
},
render: function(){
if(this.state.shouts.length > 0){
return this.shoutsList();
}else {
return this.emptyShouts();
}
}
});
This works exactly like I need it to, but is there a better way of setting the initial state's array value with ajax and having it load in the initial render without having to do this if statement?
Thanks!
Without using Flux, I'd say your implementation is one of the few ways to get around this problem. Another way would be to have the logic before your render's return:
...
render: function () {
var renderedShout;
if (typeof this.state.shouts[0] === "undefined") {
renderedShout = <div>No Shouts Yet!</div>;
} else {
renderedShout = <div>{this.state.shouts[0].shout}</div>;
}
return renderedShout;
}
The advantage of doing it this way is that you will only have one return which could make it clearer for a reader in the long run.
If you want, you can try this change in your pre-edit code:
var ShoutList = React.createClass({
getInitialState: function(){
return {shouts: []};
},
componentDidMount: function(){
var component = this;
$.get('/api/shouts.json', function(data){
component.setState({shouts: data});
}.bind(this), 'json');
},
render: function(){
return (
<div>
{this.state.shouts[0].shout}
</div>);
}
});
bind your $.get call to the component making the call. It should work as expected from there.

React.NET uncaught TypeError: undefined is not a function

I am trying to learn ReactJs and found React.NET.
Followed the tutorial on the author's website with the only change being an MVC 5 app instead of MVC 4.
Here is the jsx:
/** #jsx React.DOM */
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
React.renderComponent(
<CommentBox data={data} />,
document.getElementById('content')
);
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {
return <Comment author={comment.Author}>{comment.Text}</Comment>;
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
var CommentForm = React.createClass({
render: function() {
return (
<div className="commentForm">
Hello, world! I am a CommentForm.
</div>
);
}
});
var data = [
{ Author: "Daniel Lo Nigro", Text: "Hello ReactJS.NET World!" },
{ Author: "Pete Hunt", Text: "This is one comment" },
{ Author: "Jordan Walke", Text: "This is *another* comment" }
];
It gives this error:
Uncaught TypeError: undefined is not a function
Any clues on this one?
Regards.
There are three steps in the snippet.
First, define the CommentBox:
var CommentBox = React.createClass...
Second, render the CommentBox and the CommentList:
React.renderComponent...
Third, define the CommentList:
var CommentList = React.createClass...
So, the problem is that the CommentList is rendered before the CommentList is defined. If the last two steps were switched around then it would work fine. The CommentList class needs to be defined before it can be rendered.
CommentForm is also defined in the wrong place - it needs to be defined before it's referred to. The React tutorial has it the wrong way round: http://facebook.github.io/react/docs/tutorial.html
The correct order for the definition of the classes in the Javascript is:
var Comment = React.createClass
...
var CommentList = React.createClass
...
var CommentForm = React.createClass
...
var CommentBox = React.createClass
...
I had this happen whilst working through the tutorial and found that declaring the var data = [...] at the top of the jsx script resolved the issue. So it would appear that the engine is not hoisting variables properly?

Backbone.js understanding: fetch and display with templating

I've read many tutorials and made a search on the .net... but still I'm in trouble with Backbone.js. This is my simple scenario:
A Rails application responds to a GET request with a JSON collection of objects.
I want to dynamically build a list of table-rows with Backbone collections, when DOM is ready. This is the code is confusing me:
HTML part:
<script type="text/template" id="tmplt-Page">
<td>{{=title}}</td>
<td>{{=description}}</td>
</script>
Backbone's script:
$(function(){
var Page = Backbone.Model.extend({});
var Pages = Backbone.Collection.extend({
model: Page,
url: '/pages'
});
var pages = new Pages([
{title: 'ProvA1', description: ''},
{title: 'ProvA2', description: ''}
]);
var PageView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#tmplt-Page').html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
}
});
var AppView = Backbone.View.extend({
el: $("#results"),
initialize: function () {
_.bindAll(this, 'render');
pages.on('reset', this.render)
},
render: function() {
this.$el.empty();
pages.each( function( page ) {
var view = new PageView({
model : page
});
this.$el.append(view.render().el);
});
return this;
}
});
var appview = new AppView;
});
Nothing renders on the screen.
There seem to be 2 problems:
1) fetch() is asynchronous, so the code is executed before the end of the ajax round-trip.
2) If I manually load some objects into the collection, this piece of code "this.template(this.model.toJSON())" does not substitute jSON attributes
EDIT :
To use mustache tags I wrote this code before all:
First, as you said, fetch() is asynchronous, but it triggers the 'reset' event when it completes, so you should add this in AppView.initialize:
pages.on('reset', this.render)
Second, you never insert the HTML of PageView anywhere. Add this in AppView.render:
// at the beginning
var self = this;
// and in the forEach loop
self.$el.append(view.el);
Third, at the beginning of AppView.render, you should clear the content of this.$el.
EDIT:
You still had a couple issues:
You are using underscore templates with mustache tags ({{ }} -> <%= %>)
Missing var self = this in render
You are not calling appview.render() ! :)
Here's your code working on jsfiddle: http://jsfiddle.net/PkuqS/

Resources