I have an array of data being passed from one json file.
When I click on the list item it triggers a handleClick method that will get the new url and set it to the provided state. That state is then passed onto a child component that makes an ajax call using that link.
The problem is that the ajax call only loads once when the component is mounted and does not make anymore calls after that no matter how many times I click on it.
How would I get the child component to load a new url each time a different item is clicked?
Parent Component:
getInitialState: function() {
return {
gameUrl: ''
};
},
handleClick: function() {
this.setState({
gameUrl: gameUrl
});
},
render: function() {
var gameList = this.props.data.map(function(game) {
var homeTeamName = game.home_team_name;
var awayTeamName = game.away_team_name;
var gameUrl = game.game_directory+'/anotherFile.json';
console.log(homeTeamName+' vs '+awayTeamName+':'+ gameUrl);
var click = this.handleClick;
return (
<li key={game.location} className="list-group-item" onClick={click}>
{homeTeamName}
</li>
);
}, bind);
return (
<div>
<ul>
{gameList}
</ul>
<GameDetail url={this.state.gameUrl}/>
</div>
);
Child Component:
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({
data: data.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
Implement componentWillReceiveProps method.
It will be called when the props have changed and when this is not an initial rendering.Then update the state depending on the existing and upcoming props.
componentWillReceiveProps: function(nextProps) {
this.setState({
// set something
});
}
Thanks to #WitVault managed to do it here is the revised section:
Instead of componentDidMount, I changed it to componentWillReceiveProps in the child component. Make sure the prop you're passing through is the same in the child and parent component. (i.e. in child component the prop is url, in the parent you're passing it to the same prop <GameDetail url={this.state.gameUrl}/>) Then you access it in the componentWillReceiveProps method via nextProps.url
componentWillReceiveProps: function(nextProps) {
// Access the url prop
var newUrl = nextProps.url;
$.ajax({
url: newUrl,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({
data: data.data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
Related
I have three React components: the first is a container (NoteContainer component) which is responsible for rendering my objects (Note component) in the UI. The data is obtained as JSON via AJAX GET. The last component is a form (NoteForm) which creates new objects (via AJAX POST).
The response from the POST is only the JSON representation of the newly created object, not the JSON for all of the objects.
Should the NoteForm send the JSON response from creating a new object to the NoteContainer which would append it to its state.data and re-render , or should the NoteContainer request the full list of objects and update its state date entirely?
I would presume the first way is better since it does not require requesting data which is already present in the state of NoteContainer. However, I'm still not sure of the "best" way to handle this. Should I give NoteContainer another function, something like addNewNote, which would take the JSON data from the NoteForm and append it to state.data?
I'm new to React so I apologize if this is not a clear question. Here are my components:
var NoteContainer = React.createClass({
getInitialState: function(){
return {data: []};
},
componentDidMount: 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)
});
},
render: function(){
var noteNodes = this.state.data.map(function(note){
return (
<Note title={note.title} body={note.body} />
);
});
return (<div className='noteContainer'>{noteNodes}</div>);
}
});
var Note = React.createClass({
render: function(){
return (
<div className="note" >
<h1>{this.props.title}</h1>
<p>{this.props.body}</p>
</div>
);
}
});
var NoteForm = React.createClass({
getInitialState: function(){
return {'title': '', 'body': ''}
},
handleTitleChange: function(e){
this.setState({title: e.target.value});
},
handleBodyChange: function(e){
this.setState({body: e.target.value});
},
handleSubmit: function(e){
e.preventDefault();
var note = {
title: this.state.title,
body: this.state.body};
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: note,
success: function(data){
// Send data to NoteContainer?
}.bind(this),
error: function(xhr, status, err){
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function(){
return (
<form>
<input
type='text'
placeholder='Title'
value={this.state.title}
onChange={this.handleTitleChange} />
<textarea onChange={this.handleBodyChange}>
{this.state.body}
</textarea>
</form>
);
}
});
What #xCrZx is suggesting is that you pull the state outside of the individual components and have a one or more top-level stores to maintain state. The simplest (i.e. "vanilla") example of this would be if your NoteContainer was a parent of NoteForm. Then you could simply pass a callback from NoteContainer to NoteForm:
var NoteContainer = React.createClass({
createNote: function() {
...
},
render: function() {
return (
...
<NoteForm createNote={this.createNote}>
...
);
}
});
var NoteForm = React.createClass({
props: {
createNote: React.PropTypes.func.isRequired
},
render: function() {
return (
...
onClick={this.props.createNote}
...
);
}
});
However, that of course only works if the relationship actually exists. Now let's take a look at Reflux, where you create central stores (and actions to go with them) to keep data, and components "listen" to the stores.
var NoteActions = Reflux.createActins([
'createNote',
'getNotes'
]);
var NoteStore = Reflux.createStore({
listenables: [NoteActions],
init: {
// Notes is an empty array by default
this.notes = [];
},
getInitialState: function() {
return {
notes: this.notes
};
},
onCreateNote: function(noteFormData) { ...POST here, save note JSON to this.notes on success... },
onGetNotes: function() { ..GET here for the initial load...}
});
var NoteForm = React.createClass({
render: function() {
...
onClick={NoteActions.createNote(noteFormData)}
...
}
});
var NoteContainer = React.createClass({
mixins: [Reflux.connect(NoteStore)],
componentDidMount: function() {
NoteActions.getNotes();
},
render: function() {
return: function() {
.. same that you have now using this.state.notes
}
}
});
Hope this is starting to make sense. Highly recommend looking through the Reflux (or Redux, similar but different) examples.
The best approach is to keep all notes in global state and add new entities there one by one when needed. It can be achieved with help of global stores, like Redux or Reflux.
I'm trying to figure out how to edit comments in ReactJS. I have been following this tutorial.
There are several theories I have on the solution:
Using mutable state instead of immutable props.
Has to do with the CommentBox component which has the loadCommentsFromServer and handleCommentSubmit functions. The loadComments function fires an AJAX request, possibly to my comments.json file.
Here's the relevant code from the server.js file
var COMMENTS_FILE = path.join(__dirname, 'comments.json');
app.get('/api/comments', function(req, res) {
fs.readFile(COMMENTS_FILE, function(err, data) {
if (err) { /* Print error to console */ }
res.json(JSON.parse(data));
});
});
// This snippet of code is probably the most important
app.post('/api/comments', function(req, res) {
fs.readFile(COMMENTS_FILE, function(err, data) {
if (err) { /* Print error to console */ }
var comments = JSON.parse(data);
var newComment = {
id: Date.now(),
text: req.body.text,
};
comments.push(newComment);
fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) {
if (err) { /* Print error to console */ }
res.json(comments);
});
});
});
Here's my main script file where I generate react components
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
// Trying to change the value of the text box on edit
<p onChange={this.handleTextChange()}> {this.props.text} </p>
</div>
);
}
});
var CommentBox = React.createClass({
When the component is first created, we need to get JSON data from the server and update the state with the latest data. this.setState() allows for dynamic updates. The old array of comments is being replaced by the new one
loadCommentsFromServer: 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)
});
},
This function passes data from the child components up to the parent components. It's supposed to submit to the server and refresh the list of comments
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return <Comment key={comment.id}>{comment.text}</Comment>
});
return <div className="commentList"> {commentNodes} </div>
);
}
});
Here a component is being created for filling out forms. this.state is used to save user's input as it's entered. I'm trying to accomplish this with the edit functionality.
var CommentForm = React.createClass({
getInitialState: function() {
return {text: ''};
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
// This is also probably an important function
handleSubmit: function(e) {
e.preventDefault();
var text = this.state.text.trim();
this.props.onCommentSubmit({text: text});
this.setState({text: ''});
},
The value property of the input elements will reflect the state of the component and attach onChange handlers to them
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" value={this.state.text} onChange={this.handleTextChange} />
<input type="submit" value="Post" />
</form>
);
}
});
Finally, I am rendering the CommentBox component. The url attribute fetches dynamic data from the server. The pollInterval reloads the page every 2 seconds.
ReactDOM.render(
<CommentBox url="/api/comments" pollInterval={2000} />,
document.getElementById('content')
);
Here were my thoughts on how to implement the edit functionality
setTimeout(function() {
$('.edit').on('click', function() {
$(this).prev().prop('contentEditable', 'true');
$(this).prev().focus();
});
},1000);
I had to use setTimeout because it would take some time before the component's file to be loaded. I would then listen for clicking on the edit button, and change the html5 contentEditable property to true.
The problem I have is in updating the changes to the JSON file once it's edited.
I'd also like to know if there's a more react way of accomplishing this onclick functionality
As you can see in my component's file, I added an onChange handler to the paragraph that renders the body of the text.
render: function() {
return (
<div className="comment">
<p onChange={this.handleTextChange()}> {this.props.text} </p>
</div>
);
}
I've searched the internet extensively for examples of editing functionality but couldn't find anything.
My goal was to make this code as readable as possible. I tried to trim down code that is not immediately relevant to the problem at hand. I have removed the following code:
Declaration of npm variables and app.use
The listening of the server
The author fields for the text form. We only need the text field
It's usually not a very good idea to have jQuery mucking with React components (though jQuery + React can play nice with each other for certain tasks); we are running a large scale React application and have spent many hours removing instances of this from our early days.
In terms of saving the comments, you need a new endpoint to handle that functionality, it should look almost exactly like app.post('/api/comments') except instead of getting data from reading the file, it should get data from req.body, which is the data posted to it. To keep the same url this.props.url you could set it up as a PATCH endpoint: app.patch('/api/comments' ...). I'll leave that implementation up to you. The React save functionality should happen like this: the Comment component should use state to manage it's...state. Clicking "Edit" should switch that state to have the contentEditable set to true, "Edit" become "Save", etc. The actual saving part should be defined in the parent component CommentBox and should be passed down to the Comment component. Here is a basic idea of the changes you should make to allow editing, it's 100% untested but hopefully helps out some.
// changes to Comment component
var Comment = React.createClass({
getInitialState: function() {
return {
contentEditable: false,
buttonText: 'Edit',
text: this.props.text
};
},
handleButton: function() {
var commentTag = this.refs.comment;
// if the component is currently editable and the text is different from the original, save it
if (this.state.contentEditable && commentTag.textContent != this.state.text) {
this.props.onUpdateComment(this.props.id, commentTag.textContent);
}
// invert current contentEditable state Save => Edit or Edit => Save
var editable = !this.state.contentEditable;
this.setState({
contentEditable: editable,
// update the state to reflect the edited text
text: commentTag.textContent,
// change button text based on editable state
buttonText: editable ? 'Save' : 'Edit'
});
},
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">{this.props.author}</h2>
<p ref="comment" contentEditable={this.state.contentEditable}>{this.state.text}</p>
<button onClick={this.handleButton}>{this.state.buttonText}</button>
</div>
);
}
});
// changes to CommentList
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) {
return <Comment onUpdateComment={this.props.onUpdateComment} {...comment} />
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
// changes to CommentBox
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.getJSON(this.props.url)
.then(function(newComments) {
this.setState({ data: newComments });
}.bind(this))
.fail(function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
});
},
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.post(this.props.url, comments)
.then(function(data) {
this.setState({ data: data });
}.bind(this))
.fail(function(xhr, status, err) {
this.setState({ data: comments });
console.error(this.props.url, status, err.toString());
}.bind(this));
},
onUpdateComment: function(id, comment) {
// clone state, we don't want to alter this directly
var newData = this.state.data.slice(0);
newData.forEach(function(item) {
if(item.id === id) {
item.text = comment;
}
});
$.ajax({
url: this.props.url,
dataType: 'json',
method: 'PATCH',
data: newData
}).then(function(data) {
this.setState({ data: data });
}.bind(this));
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
I am trying to get the data from a .json file and displaying the results through a view. Below is the scoreboard.json file.
{
"subject":"scoreboard",
"copyright":"Copyright 2015",
"data":
"games":{
"next_day_date":"2015-07-22",
"modified_date":"2015-08-04T07:34:50Z",
"month":"07",
"year":"2015",
"game":[
{
"game_type":"R",
"double_header_sw":"N",
"location":"Some City, State",
},
{
"game_type":"R",
"double_header_sw":"N",
"location":"Another City, Another State"
}
]
}
I only want the "data" field and not the "subject" or "copyright" fields and am able to do so through the ajax request below.
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
console.log(data.data);
this.setState({data: data.data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
I pass the state down to the receiving file but, when I try to call it I receive a TypeError: this.props.data.map is not a function
render: function() {
var gameDate = this.props.data.map(function(data) {
return (
<h1>{data.modified_date} </h1>
);
});
I want to be able to get all the information that is in the "data" field of the scoreboard.json eventually (i.e. data.games.game[] array). The .map function Does not seem to work for this either.
How would I get the "data" fields (more specifically the modified_date) in the .map function?
Edit: I can log the modified_date by calling data.games.modified_date but, when trying to set the state of data: this.state.data = data.games.game I receive a new warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of "GameDate"
As pointed out by azium my data is an object and not an array like my results were expecting.
To get the "game" array inside the "games" field I set the state as the following:
this.setState({data: data.data.games.game});
To get the other fields I simply created a new state and passed it through a prop i.e. modified_date:
this.state.date = data.data.games.modified_date
The warning: Each child in an array or iterator should have a unique "key" prop. was fixed by adding a "key" property inside the .map method:
render: function() {
var gameDate = this.props.data.map(function(data) {
return (
<h1 key={data.location} >{data.location} </h1>
);
});
So say I have a React parent component that does an AJAX request on the componentDidMount that sets state and then in the render I have a child component that will be taking some of that data from the state and will be doing another AJAX request inside the child component with that information, how do I go about this?
For instance:
var Parent = React.createClass({
getInitialState: function() {
return { num: '' };
},
componentDidMount: function() {
$.ajax({
url: '/urltogetdata',
method: 'GET',
dataType: 'json',
success: function(data) {
if (this.isMounted()) {
this.setState({num: data['num']});
}
}.bind(this)
});
},
render: function() {
return (
<div>
<Child num={this.state.num}/>
</div>
);
},
});
var Child = React.createClass({
getInitialState: function() {
return { childInfo: {} };
},
componentDidMount: function() {
$.ajax({
url: '/urltogetdata/' + this.props.num,
method: 'GET',
dataType: 'json',
success: function(data) {
if (this.isMounted()) {
this.setState({childInfo: data});
}
}.bind(this)
});
},
renderChildChild: function() {
return (<div><ChildChild /></div>);
},
render: function() {
return (
<div>
{this.state.childInfo.map(this.renderChildChild)}
</div>
);
},
});
Now the problem I'm getting is that all these renders are happening before I get information. I can usually get the data and display it on the Parent side, but when it gets back down to the Child with the Parent's state (that is set from the AJAX request), I lose a render when I try to setState, because it is already set.
I've tried it with shouldComponentUpdate, componentWillUpdate and various other things and I'm at a loss. I don't really know what else to try or if it's just something I have to deal with because AJAX and asynchronous rendering in React just isn't there yet?
I'm still new to ReactJS, and I've run into a bit of a snag. I'm trying to implement pagination in a React component; however, even though my pagination function is being called successfully the state change is not causing the component to Render again.
I'm using one function to get the initial data (getMainFeed) and then a second one (getMoreItems) for the pagination. The second function is called by my 'handleScroll' function. Interestingly enough, when I replace the 'getMoreItems' function with the 'getMainFeed' function, the state change causes the component to render the additional data perfectly. Unfortunately I need to hit these two separate APIs and I don't think it would be in good form to combine the two calls into one function. So is there a way that I can get 'getMoreItems' to render the new items to the screen?
var data = [];
var GridView = React.createClass({
getInitialState: function() {
window.addEventListener("scroll", this.handleScroll);
return {
data: [],
page: 0, //for pagination
loadingFlag: false,
};
},
getMainFeed: function() {
var nextPage = 1; //increase the page count
ajax_url = "http://127.0.0.1:8200/api/content/main/";
ajax_type = "GET";
ajax_data = {
'BatchCount': "20"
};
$.ajax({
url: ajax_url,
type: ajax_type,
contentType: 'application/x-www-form-urlencoded',
data: ajax_data,
dataType: 'json',
success: function(data) {
this.setState({
data: data,
loadingFlag:false,
page: 2
});
//loading("off");
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
}, //end function
getMoreItems: function() {
var nextPage = this.state.page+1; //increase the page count
ajax_url = "http://127.0.0.1:8200/api/content/page/1/";
ajax_type = "GET";
ajax_data = {
'BatchCount': "20"
};
$.ajax({
url: ajax_url,
type: ajax_type,
contentType: 'application/x-www-form-urlencoded',
data: ajax_data,
dataType: 'json',
success: function(data) {
this.setState({
data: data,
loadingFlag:false,
page: nextPage
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
}, //end function
componentDidMount: function() {
//loading("on");
this.getMainFeed();
},
handleScroll:function(e){
//this function will be triggered if user scrolls
var windowHeight = $(window).height();
var inHeight = window.innerHeight;
var scrollT = $(window).scrollTop();
var totalScrolled = scrollT+inHeight;
if(totalScrolled+1200>windowHeight){ //user reached at bottom
if(!this.state.loadingFlag){ //to avoid multiple request
this.setState({
loadingFlag:true,
});
//loading("on");
this.getMoreItems();
}
}
},
componentDidUpdate: function() {
$('#grid-container').imagesLoaded( function() {
MasonryInit();
});
},
render: function() {
return (
<div id="feed-container-inner">
<GridMain data={this.state.data} />
</div>
);
}
});
When the state change it will re-render. Also you saw that it was working with your getmainfeed function.
So I think that your getmoreitems function just do not success. Have you verified that this call succeed ?
The problems turns out to be that I wasn't concatenating the data to the end of the existing data like so data: this.state.data.concat(data),.
This surprises me, because I would have expected the getMoreItems function to simply replace the existing data.