I've been tinkering with converting one page in my web application to use react rather than jQuery and Vanilla JS but I'm getting stuck on how to approach it because of how unfamiliar I am with react.
At the moment, on the page I have an html table with rows that relate to a database table and another column with links to edit and delete the rows.
This is done by opening bootstrap modals containing a form which is populated for the appropriate row for the edit action and a delete modal to confirm the deletion, there is also a link on the page to add a new row, also via AJAX, so what I am trying to do is replicate this in react but I can't seem to understand how to go about it.
At the moment I have this (I am using a generic component name (ModelName) for this example to avoid confusion from what it is actually called.):
var ModelNames = React.createClass({
getInitialState: function() {
return {model_names: this.props.model_names};
},
render: function() {
var rows = [];
this.props.model_names.forEach(function(model_name) {
rows.push(<ModelName model_name={model_name} key={model_name.id} />);
});
return (
<div className="row">
<div className="col-md-12">
<table className="table table-striped table-bordered table-hover">
<thead>
<tr role="row" className="heading">
<th>Property 1</th>
<th>Property 2</th>
<th className="text-center">Options</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
</div>
</div>
);
}
});
var ModelName = React.createClass({
handleRemoveModelName: function() {
$.ajax({
url: '/model_name/ajax_delete',
data: { model_name_id: this.props.model_name.id },
type: 'POST',
cache: false,
success: function(data) {
this.setState({ model_names: data }); // this is the part I am having trouble with
}.bind(this),
error: function() {
console.log('error');
}.bind(this)
});
},
render: function() {
return (
<tr id={"model_name_" + this.props.model_name.id}>
<td>{this.props.model_name.property_1}</td>
<td>{this.props.model_name.property_2}</td>
<td className="text-center">
</i> </i>
</td>
</tr>
);
}
});
ReactDOM.render(
// this comes from Rails
<ModelName model_names={<%= #model_names.to_json %>} />,
document.getElementById('react')
);
The deletion via ajax works fine and the server side responds with all the rows in the database table (after the deletion has taken place), so the json response is in exactly the same format as the initial data provided on the page load, I am just trying to figure out how to update the state so that react knows to remove the record I just deleted.
Similarly, I want to then extend it to replicate my editing and creation CRUD features but I am struggling to find the information I need after reading a number of blog posts.
Update
var ModelNames = React.createClass({
handleRemoveModelName: function(id) {
$.ajax({
url: '/model_name/ajax_delete',
data: { model_name_id: id },
type: 'POST',
cache: false,
success: function(data) {
this.setState({ model_names: data });
}.bind(this),
error: function() {
console.log('error');
}.bind(this)
});
},
render: function() {
var rows = [];
this.props.model_names.map(function(model_name) {
return (
<ModelName
model_name={model_name}
key={model_name.id}
onRemove={this.handleRemoveModelName}
/>
);
}, this);
// ...
}
});
var ModelName = React.createClass({
handleRemoveModelName: function() {
this.props.onRemove(this.props.model_name.id);
},
// ...
});
Also, just to try and include everything relevant, the delete button still has onClick={this.handleRemoveModelName} for the ModelName return.
On top of Felix Kling's answer, you would need to somehow communicate between the ModalNames and ModalName to update the state, since the state is managed by ModalNames and the event is called in ModalName.
One solution is to pass function that updates the state to the ModalName:
ModalNames:
var ModelNames = React.createClass({
...
updateState: function(modal_names) {
this.setState({modal_names})
},
render: function() {
var rows = [];
this.state.model_names.forEach(function(model_name) {
rows.push(<ModelName model_name={model_name} key={model_name.id} updateState={this.updateState} />);
});
...
}
}
ModalName:
var ModelName = React.createClass({
handleRemoveModelName: function() {
$.ajax({
url: '/model_name/ajax_delete',
data: { model_name_id: this.props.model_name.id },
type: 'POST',
cache: false,
success: function(data) {
this.props.updateState(data);
}.bind(this),
error: function() {
console.log('error');
}.bind(this)
});
},
...
}
Alternative solution is to define handleRemoveModelName in ModalNames which takes in id, calls ajax, and does setState like you originally had.
You have to decide where the source of truth for your data should be. In your current setup, it appears to be in ModelNames.
Instead of having ModelName perform the deletion, let ModelNames do it, since it owns the data.
Just pass a callback to ModelName that is called when deletion is requested. In this example we are adding a new property to the child component, onRemove which is called when the delete button is clicked:
var ModelNames = React.createClass({
handleRemoveModelName: function(id) {
// make Ajax call here
},
render: function() {
var rows = this.state.model_names.map(function(model_name) {
return (
<ModelName
model_name={model_name}
key={model_name.id}
onRemove={this.handleRemoveModelName}
/>
);
}, this);
// ...
}
});
var ModelName = React.createClass({
handleRemoveModelName: function() {
this.props.onRemove(this.props.model_name.id);
},
// ...
});
The parent component can then decide how it wants to respond to this and make the appropriate Ajax call.
This makes ModuleName a "dump" component. It doesn't have any thing to do with data management and only renders the data it gets passed.
The other issue is that you are reading from the props in ModelNames (this.props.model_names), not the state. State and props are different. You need to read from the state (this.state.model_names) in order for the component to update when the state changes.
Related
I have some search filters for a query in my application. From the filters I want to render the json results into a kendo grid. For that reason I don't use the default DataSource.Read() of the grid but an Ajax request and I bind the results to the grid like that:
Kendo grid:
#(Html.Kendo().Grid<List<SearchSpecialtiesResult>>()
.Name("grid")
.Columns(columns =>
{
columns.Bound(c => c.Id).Hidden();
columns.Bound(c => c.Code).Width(100);
// Some other columns
})
//Some events and filtering options
.DataSource(dataSource => dataSource
.Ajax()
.ServerOperation(false)
.Model(model =>
{
model.Id(p => p.Id);
// other model values
})
)
)
Set the dataSource on Ajax success
var datasource = new kendo.data.DataSource({ data: resultData });
$("#grid").data("kendoGrid").setDataSource(datasource);
The binding happens correctly however to other places that I use the DataSource.Read() the grid shows a loading effect that I can't find how to reproduce to my page and I use to this place some other loading effect we use on Ajax requests. Is there any way to reproduce it in my case?
I had such cases in my application too. The way I handled them was also by kendo.ui.progress($("#gridDame"), true). For the sake of the completion of the answer I will post the way I handled them and the way I am handling them now with the DataSource.Read() of the grid by passing the filter input values as additional data to my Read request.
First way:
I had a general Ajax call for all my gridRequests
function getGridData(uri, payload, gridName) {
return $.ajax({
url: uri,
type: "POST",
contentType: "application/json",
data: payload,
beforeSend: function () {
window.kendo.ui.progress($("#"+ gridName), true);
}
}).done(function (result) {
return result;
}).always(function () {
window.kendo.ui.progress($("#" + gridName), false);
});
}
I called it on my button click with the parameters of my search form
$("#searchFormBtn").bind("click", function (e) {
e.preventDefault();
// ... Get the filter input values and strignify them as json ...
return getGridData(url, filterInputValuesStrignifiedAsJson, "grid")
.done(function (result) {
if (result.success) {
var datasource = new kendo.data.DataSource({ data: result.data });
$("#grid").data("kendoGrid").setDataSource(datasource);
} else {
//Handle error
}
});
});
Second way:
I set my Datasource.Read() like this:
.Read(read => read.Action("ActionName", "ControllerName").Data("getFilterInputValues"))
and always Autobind(false) in order not to read on first load
In the function getFilterInputValues I get my search form values like that:
function searchModelData() {
return {
DateFrom: $("#DateFrom").data("kendoDatePicker").value(),
DateTo: $("#DateTo").data("kendoDatePicker").value(),
Forever: document.getElementById("datesForever").checked === true ? true : false,
SearchCommunicationType: { Id: $("#SearchCommunicationType_Id").val() },
SearchBranch: { Id: $("#SearchBranch_Id").val() },
CompletedById: { Id: $("#CompletedById_Id").val() },
SearchOperationType: { Id: $("#SearchOperationType_Id").val() },
AcademicPeriodSearch: { Id: $("#AcademicPeriodSearch_Id").val() },
AcademicYearSearch: $("#AcademicYearSearch").val(),
Name: $("#Name").val(),
ContactValue: $("#ContactValue").val(),
AddressValue: $("#AddressValue").val()
};
}
Finally I trigger the DataSource.Read() of my grid on the button click
$("#searchFormBtn").bind("click", function () {
var grid = $('#grid').data("kendoGrid");
if (grid.dataSource.page() !== 1) {
grid.dataSource.page(1);
}
grid.dataSource.read();
});
With Datasource.Read() obviously works correctly and the spinning effect you mention in your question.
You're looking for kendo.ui.progress. Click here for Telerik's documentation.
Before running the ajax call add the following to show the loading effect:
kendo.ui.progress($("#gridName"), true);
After success or failure add the following to remove the loading effect:
kendo.ui.progress($("#gridName"), false);
You can do it manualy
<div id="UrGrid"></div>
<div class="chart-loading"></div>
in event:
var loading = $(".chart-loading", e.sender.element.parent());
kendo.ui.progress(loading, true);
...work with data...
kendo.ui.progress(loading, false);
Using React.js I have written a simple app that gets json and uses some of that data returned to build html.
Although, when the JSON changes, the html does not. Am I missing something here?
Here is my code -
<script type="text/jsx">
var classNames = ({
'auditNumber': "auditNumber",
'audit-overview-box': "audit-overview-box"
});
var AuditOverviewBox = React.createClass({
render: function () {
return (
<div className="audit-overview-box">
<h1 className={classNames.auditNumber}>{this.props.auditNo}</h1>
<span>{this.props.creationDate}</span>
</div>
)
}
});
var AuditBoxes = React.createClass({
getInitialState: function () {
return {
data: []
}
},
componentWillMount: function () {
this.dataSource();
},
componentWillReceiveProps: function (nextProps) {
this.state.data(nextProps);
},
dataSource: function (props) {
props = props || this.props;
return $.ajax({
url: '../json.php',
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 data = this.state.data;
console.log("data");
var photos = data.map(function (audit) {
return <AuditOverviewBox key={audit.auditNo.toString()} auditNo={audit.auditNo}
creationDate={audit.creationDate}/>
});
return (
<div className='myAudits'>
{photos}
</div>
)
}
});
ReactDOM.render(<AuditBoxes />, document.getElementById('audits-div'));
</script>
And the JSON -
[{
"auditNo": "a1201",
"creationDate": "21/10/2016"
},
{
"auditNo": "a1221",
"creationDate": "21/10/2016"
},
{
"auditNo": "a1211",
"creationDate": "21/10/2016"
}]
You cannot push changes from the server to the browser (unless you use websockets). If you just need to update once in a while you should setup your code around the ajax request in such a way that it will perform a request every n seconds. The simplest solution would be using setInterval()
setInterval(
function () {
// your ajax code
},
5000
)
that way the request to the server will be done every 5 seconds. Please be aware that you can overload your server if you set the interval to short / have a lot of visitors.
There are only two ways to change the data. You can use .setState method or directly set data to .state property and call .forceUpdate method of component, but this method is stritly unrecommended.
You can read more about it here: https://facebook.github.io/react/docs/state-and-lifecycle.html
When i used to fetch data from json api its throwing "Unexpected token" error. Below, i've added my code what i have tried so far. Get me out from this issue. I'm trying to solve this problem long time.
Here,
var Demo = React.createClass({
render: function() {
getInitialState:function(){
return {
data:[]
};
},
componentDidMount: function () {
$.ajax({
url: "http://www.w3schools.com/angular/customers.php"
}).done(function(data) {
this.setState({data: data})
});
},
return (
<div>
{this.props.data.map(function(el,i) {
return <div key={i}>
<div>{el.Name}</div>
<div>{el.City}</div>
<div>{el.Country}</div>
</div>;
}
</div>
);
}
});
var Stream = React.createClass({
render: function() {
return (
<div>
<div className="scrollContent ">
<Demo />
</div>
</div>
);
}
});
You have several errors in your code
move getInitialState and componentDidMount from render method, these methods should be as children of your component (Demo) class but not as children of render method
add dataType: 'json' to $.ajax, because now it returns string, but in your case you need get json
as you are using this.setState in .done you should set this to .done callback, because now this refers to $.ajax object not Demo, you can use .bind method to do it.
change this.props.data to this.state.data because data located in state object not in props
array with data located in records property use it instead of just data
Example
var Demo = React.createClass({
getInitialState:function() {
return {
data :[]
};
},
componentDidMount: function () {
$.ajax({
url: "http://www.w3schools.com/angular/customers.php",
dataType: 'json'
}).done(function(response) {
this.setState({ data: response.records });
}.bind(this));
},
render: function() {
var customers = this.state.data.map(function(el,i) {
return <div key={i}>
<div>{el.Name}</div>
<div>{el.City}</div>
<div>{el.Country}</div>
</div>
});
return <div>{ customers }</div>;
}
});
I'm facing an infinite loop issue and I can't see what is triggering it. It seems to happen while rendering the components.
I have three components, organised like this :
TimelineComponent
|--PostComponent
|--UserPopover
TimelineComponenet:
React.createClass({
mixins: [
Reflux.listenTo(TimelineStore, 'onChange'),
],
getInitialState: function() {
return {
posts: [],
}
},
componentWillMount: function(){
Actions.getPostsTimeline();
},
render: function(){
return (
<div className="timeline">
{this.renderPosts()}
</div>
);
},
renderPosts: function (){
return this.state.posts.map(function(post){
return (
<PostComponenet key={post.id} post={post} />
);
});
},
onChange: function(event, posts) {
this.setState({posts: posts});
}
});
PostComponent:
React.createClass({
...
render: function() {
return (
...
<UserPopover userId= {this.props.post.user_id}/>
...
);
}
});
UserPopover:
module.exports = React.createClass({
mixins: [
Reflux.listenTo(UsersStore, 'onChange'),
],
getInitialState: function() {
return {
user: null
};
},
componentWillMount: function(){
Actions.getUser(this.props.userId);
},
render: function() {
return (this.state.user? this.renderContent() : null);
},
renderContent: function(){
console.log(i++);
return (
<div>
<img src={this.state.user.thumbnail} />
<span>{this.state.user.name}</span>
<span>{this.state.user.last_name}</span>
...
</div>
);
},
onChange: function() {
this.setState({
user: UsersStore.findUser(this.props.userId)
});
}
});
Finally, there is also UsersStore**:
module.exports = Reflux.createStore({
listenables: [Actions],
users: [],
getUser: function(userId){
return Api.get(url/userId)
.then(function(json){
this.users.push(json);
this.triggerChange();
}.bind(this));
},
findUser: function(userId) {
var user = _.findWhere(this.users, {'id': userId});
if(user){
return user;
}else{
this.getUser(userId);
return [];
}
},
triggerChange: function() {
this.trigger('change', this.users);
}
});
Everything works properly except the UserPopover component.
For each PostComponent is rendering one UserPopOver which fetch the data in the willMount cycle.
The thing is, if you noticed I have this line of code console.log(i++); in the UserPopover component, that increments over and over
...
3820
3821
3822
3823
3824
3825
...
Clearl an infinite loop, but I really don't know where it comes from. If anyone could give me a hint I will be very gratefully.
PS: I already tried this approach in the UsersStore but then all the PostComponent have the same "user":
...
getUser: function(userId){
return Api.get(url/userId)
.then(function(json){
this.user = json;
this.triggerChange();
}.bind(this));
},
triggerChange: function() {
this.trigger('change', this.user);
}
...
And in the UserPopover
...
onChange: function(event, user) {
this.setState({
user: user
});
}
...
Because that your posts is fetch async, I believe that when your UserPopover component execute it's componentWillMount, the props.userId is undefined, and then you call UsersStore.findUser(this.props.userId), In UserStore, the getUser is called because it can't find user in local storage.
NOTE that every time the getUser's ajax finished, it trigger. So the UserPopover component execute onChange function, and call UsersStore.findUser again. That's a endless loop.
Please add a console.log(this.props.userId) in the UserPopover's componentWillMount to find out if it is like what i said above. I actually not 100% sure it.
That is a problem that all UserPopover instance share the same UserStore, I think we should rethink the structure of these components and stores. But I haven't thought out the best way yet.
You can do it like this:
TimelineComponent
|--PostComponent
|--UserPopover
UserPopover just listen for changes and update itself.
UserPopover listens for change at store, which holds which user's data should be in popover and on change updates itself. You can send also coordinates where to render. No need to create Popover for each Post.
I am getting this error can anyone please tell me how I can debug this further?
Warning: setState(...): Can only update a mounted or mounting
component. This usually means you called setState() on an unmounted
component. This is a no-op.
Can anyone help?
This is my component which is causing the error:
var postal = require('postal'),
contactChannel = postal.channel("contact"),
React = require('react');
var ContactSelector = React.createClass({
getInitialState: function() {
return {
selectedContacts:[]
};
},
handleChange: function(e) {
var id = e.target.attributes['data-ref'].value;
if (e.target.checked === true){
contactChannel.publish({
channel: "contact",
topic: "selectedContact",
data: {
id: id
}});
} else{
contactChannel.publish({
channel: "contact",
topic: "deselectedContact",
data: {
id: id
}
});
}
},
render: function() {
var id = this.props.data.id;
var isSelected = this.props.data.IsSelected;
return (
<div className="contact-selector">
<input type="checkbox"
checked={isSelected} data-ref={id}
onChange={this.handleChange} />
</div>
);
}
});
module.exports = ContactSelector;
The contactChannel is a channel I've setup using postal.js, https://github.com/postaljs/postal.js
contactChannel.subscribe("selectedContact",function (data, envelope) {
page.setPersonIsSelectedState(data.id, true);
basketChannel.publish({
channel: "basket",
topic: "addPersonToBasket",
data: {
personId: data.id
}
});
});
I suscribe to the publish in componentDidMount on my parent page:
componentDidMount: function() {
var page = this;
this.loadContacts();
page.subscribeEvents();
},
Listeners:
subscribeEvents: function() {
var page = this;
page.subscribeToChannel(filterChannel, "searchFilterChange", this.listenerForSearchFilterChanged);
contactChannel.subscribe("pageSizeChanged", this.listenerForSizeChanged);
page.subscribeToChannel(filterChannel, "genderFilterChange", this.listnerForGenderFilterChange);
page.subscribeToChannel(filterChannel, "rollModeFilterChange", this.listnerForRollModeFilterChange);
page.subscribeToChannel(filterChannel, "attendanceModeFilterChange", this.listnerForAttendanceModeFilterChange)
page.subscribeToChannel(filterChannel, "messageToFilterChange", this.listnerForMessageToFilterChange);
contactChannel.subscribe("selectAll", function (data) {
page.loadContacts();
});
contactChannel.subscribe("selectedContact",function (data, envelope) {
page.setPersonIsSelectedState(data.id, true);
basketChannel.publish({
channel: "basket",
topic: "addPersonToBasket",
data: {
personId: data.id
}
});
});
contactChannel.subscribe("selectAll", function (data, envelope) {
basketChannel.publish({
channel: "basket",
topic: "selectAll",
data: {
selectAll: data.selectAll
}
});
});
contactChannel.subscribe("refreshContacts", function (data, envelope) {
page.loadContacts();
});
},
Add a ref attribute to your root div, and check that ref value before calling setState . This will make sure the component is mounted.
render: function() {
var id = this.props.data.id;
var isSelected = this.props.data.IsSelected;
return (
<div ref='some_ref' className="contact-selector">
<input type="checkbox"
checked={isSelected} data-ref={id}
onChange={this.handleChange} />
</div>
);
}
then call setState like below
this.refs.some_ref ? this.setState({yourState:value}): null;
You're going about using react incorrectly. React is built to be componentized, so you'll want to be doing everything in components.
When you're setting up your app, you'll want to use postal's subscribe inside of each of your components' getInitialState. Then, unsubscribe from the postal channels in the componentWillUnmount functions.
It seems that the offending code is missing from the snippets in your question, if you post all your code on I could look at it and tell you specifically where you are still "subscribed" to a postal event on a component that is no longer mounted.