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

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.

Related

Passing properties to child component does not work

I am trying to pass properties to a child component, but when I am trying to access them from within the child component, they are undefined.
I am creating a few forms and my idea was to create a parent component LoginPanel that is used by all forms, in this case ResetForm
This is my parent component
var LoginPanel = React.createClass({
getInitialState: function() {
return {status: false};
},
render() {
// add props to children
const childrenWithProps = React.Children.map(this.props.children,
(child) => React.cloneElement(child, {
status: this.state.status
})
);
console.log(childrenWithProps[0]);
// the new props (status) are copied, but previous properties (resetUrl) don't exist
// this is the shortened html
return (
<div className="container">
{childrenWithProps}
</div>
);
}
})
This is my child component
var ResetForm = React.createClass({
componentDidMount() {
console.log("done:", this.props.status); // this.props.status is undefined
},
render()
console.log("status:", this.props.status); // this.props.status is undefined
// shortened html
return (
<LoginPanel>
<p>...</p>
</LoginPanel>
);
}
});
ReactDOM.render(
<ResetForm resetUrl={resetUrl}/>,
document.getElementById('content')
);
This is my console output
status: undefined
child: Object {...}
done: undefined
I am also getting an error message from React in the console (I am actually trying to pass more properties, just restricted my example to the status property) but I don't understand what's the problem. The properties are unknown. Then how can I let React know about the props?
"Warning: Unknown props `statuses`, `status`, `message`, `updateStatus` on <form> tag. Remove these props from the element. For details, see fb.me/react-unknown-prop
in form (created by ResetForm)
in div (created by LoginPanel)
in div (created by LoginPanel)
in div (created by LoginPanel)
in LoginPanel (created by ResetForm)
in ResetForm"
I already found other SO answers that explain how to pass props to child components and I tried to do the same. But apparently I am doing something wrong and I can't find the problem. Any ideas?
I would recommend you to make two corrections one in the LoginPanel you seem to have forgotten to define the state status. Include it as:
getInitialState: function() {
return {status: false};
},
also include getDefaultProps() in your child component as
getDefaultProps: function() {
return {state: null};
}
Okay, so it turned out that when rendering the child component, the parent component has to be placed in the ReactDOM.render() function, not in the render() function of the component itself. This is how I fixed my child component:
var ResetForm = React.createClass({
componentDidMount() {
console.log("done:", this.props.status);
},
render()
console.log("status:", this.props.status);
// move <LoginPanel> from here ...
return (
<p>...</p>
);
}
});
ReactDOM.render(
// ... to here
<LoginPanel>
<ResetForm resetUrl={resetUrl}/>
</LoginPanel>,
document.getElementById('content')
);

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.

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/Rails - getInitialState not called

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.

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