ReactJS rerender after AJAX call - javascript

Learning react and whilst I understand the life cycle of a component, I don't understand that whilst my JSON call is successful it does not render the list of items I've pulled initially. It does render when I add a new item to the list, but that requires me to click the edit button and add a new element.
var InterBox = React.createClass({
getInitialState: function(){
return {
"content" : [],
"editing" : false,
"pulled" : false
}
},
componentDidMount: function(){
var url = "http://jsonplaceholder.typicode.com/posts";
var tempdata = this.state.content;
$.getJSON(url, function(data){
for (var i = 0; i < data.length; i++) {
var element = data[i];
tempdata.push(element.body);
console.log(tempdata);
}
});
this.setState({"content":tempdata});
this.forceUpdate();
},
addContent: function(e){
var currentContent = this.state.content;
currentContent.push(this.refs.content.value);
this.refs.content.value = "";
this.setState({"content" : currentContent, "editing" : false});
this.render();
},
changeMode: function(e){
this.setState({"editing": true});
this.render();
},
render: function(){
if (this.state.editing)
return(
<div>
<h1>{this.props.title}</h1>
<input type="text" ref="content"/>
<button onClick={this.addContent}>Add</button>
</div>
);
else{
var items = [];
for (var i = 0; i < this.state.content.length; i++) {
var element = this.state.content[i];
items.push(<li>{element}</li>);
}
return(
<div>
<h1>{this.props.title}</h1>
<button onClick={this.changeMode}>Edit</button>
<ul>
{items}
</ul>
</div>
);
}
}
});
ReactDOM.render(
<InterBox title="Hello"></InterBox>,
document.getElementById('main')
);
This is the initial output
This what it should look like

1.) Since $.getJSON func is async, therefore you should wait response, and after that invoke setState in callback.
let {content} = this.state;
$.getJSON(url,data=>{
let result = data.map(e=>e.body);
this.setState({content:[...content, ...result]});
});
2.) You don't need forceUpdate() and render(), because setState invoke rendering component for you.
3.) You shouldn't mutate your state by push().It will causing problems and bugs. Instead you may use spread operator or concat, etc
//concat returns new array
let {content} = this.state;
this.setState({content:content.concat(result)});
//spread operator
let {content} = this.state;
this.setState({content:[...content, ...result]});
4.) If you don't want to use arrow func in callback, you should use bind(this), otherwise you will have undefined for setState in callback
let {content} = this.state;
$.getJSON(url,function(data){
let result = data.map(e=>e.body);
this.setState({content:content.concat(result)});
}.bind(this));

Dont call render function manually i.e. supposed to call by react.
2.You were setting setState outside your AJAX call response.
var InterBox = React.createClass({
getInitialState: function(){
return {
"content" : [],
"editing" : false,
"pulled" : false
}
},
componentDidMount: function(){
var url = "http://jsonplaceholder.typicode.com/posts";
var tempdata = this.state.content;
$.getJSON(url, function(data){
for (var i = 0; i < data.length; i++) {
var element = data[i];
tempdata.push(element.body);
console.log(tempdata);
}
this.setState({"content":tempdata});
});
},
addContent: function(e){
var currentContent = this.state.content;
currentContent.push(this.refs.content.value);
this.refs.content.value = "";
this.setState({"content" : currentContent, "editing" : false});
},
changeMode: function(e){
this.setState({"editing": true});
},
render: function(){
if (this.state.editing)
return(
<div>
<h1>{this.props.title}</h1>
<input type="text" ref="content"/>
<button onClick={this.addContent}>Add</button>
</div>
);
else{
var items = [];
for (var i = 0; i < this.state.content.length; i++) {
var element = this.state.content[i];
items.push(<li>{element}</li>);
}
return(
<div>
<h1>{this.props.title}</h1>
<button onClick={this.changeMode}>Edit</button>
<ul>
{items}
</ul>
</div>
);
}
}
});
ReactDOM.render(
<InterBox title="Hello"></InterBox>,
document.getElementById('main')
);

There is no need for a this.forceUpdate() or this.render() call. The screen will be refreshed, i.e. render() called by React after this.setState().
The reason the data isn't being shown is because you're calling this.setState() immediately after calling $.getJSON() and not in the success callback of getJSON().
This should work:
$.getJSON(url, function(data){
for (var i = 0; i < data.length; i++) {
var element = data[i];
tempdata.push(element.body);
console.log(tempdata);
this.setState({"content":tempdata});
}
});

Related

How do I give React methods to the onClick handler of an html component?

I'm trying to change the HTML received from a database to respond to custom onClick handlers. Specifically, the HTML I pull has divs called yui-navsets which contain yui_nav page selectors and yui_content page contents. I want to click an li in yui_nav, set that li's class to "selected", set the existing content to display:none, and set the new content to style="".
To do this, I have created a function updateTabs which inputs the index of the chosen yui and the new page number, set that li's class to "selected", set the existing content to display:none, and set the new content to style="". This function works: I tried running updateTabs(2, 3) in componentDidUpdate, and it worked fine, changing the content as requested. I want to assign updateTabs to each of the lis, and I attempt to do so in my componentDidMount after my axios request.
However, I keep getting the error: TypeError: this.updateTabs is not a function. Please help?
Page.js:
import React, { Component } from 'react';
import axios from 'axios';
class Page extends Component {
constructor(props) {
super(props);
this.state = {
innerHTML: "",
pageTags: [],
};
console.log(this.props.url);
}
componentDidMount() {
console.log(this.props.url);
axios
.get(
this.props.db_address + "pages?url=" + this.props.url,
{headers: {"Access-Control-Allow-Origin": "*"}}
)
.then(response => {
this.setState({
innerHTML: response.data[0].html,
pageTags: response.data[1]
});
console.log(response);
// Check for yui boxes, evade the null scenario
var yui_sets = document.getElementsByClassName('yui-navset');
if (yui_sets !== null) {
let yui_set, yui_nav, yui_content;
// Iterate through the navs of each set to find the active tabs
for (var yui_set_count = 0; yui_set_count < yui_sets.length; yui_set_count ++) {
yui_set = yui_sets[yui_set_count];
yui_nav = yui_set.getElementsByClassName('yui-nav')[0].children;
yui_content = yui_set.getElementsByClassName('yui-content')[0].children;
let tab_count;
// Give each nav and tab and appropriate ID for testing purposes
for (tab_count = 0; tab_count < yui_nav.length; tab_count ++) {
yui_nav[tab_count].onclick = function() { this.updateTabs(yui_set_count); }
yui_nav[tab_count].id = "nav-"+ yui_set_count.toString() + "-" + tab_count.toString()
yui_content[tab_count].id = "content-"+ yui_set_count.toString() + "-" + tab_count.toString()
}
}
}
})
.catch(error => {
this.setState({ innerHTML: "ERROR 404: Page not found." })
console.log(error);
});
}
updateTabs(yui_index, tab_index){
// Get all yuis
var yui_sets = document.getElementsByClassName('yui-navset');
let yui_set, yui_nav, yui_content
yui_set = yui_sets[yui_index];
yui_nav = yui_set.getElementsByClassName('yui-nav')[0].children;
yui_content = yui_set.getElementsByClassName('yui-content')[0].children;
// Identify the current active tab
var current_tab_found = false;
var old_index = -1;
while (current_tab_found == false) {
old_index += 1;
if (yui_nav[old_index].className === "selected") {
current_tab_found = true;
}
}
// Identify the new and old navs and contents
var yui_nav_old = yui_nav[old_index]
var yui_nav_new = yui_nav[tab_index]
var yui_content_old = yui_content[old_index]
var yui_content_new = yui_content[tab_index]
// Give the new and old navs and contents their appropriate attributes
yui_nav_old.className = "";
yui_nav_new.className = "selected";
yui_content_old.style = "display:none";
yui_content_new.style = "";
}
render() {
return (
<div className="Page">
<div className="Page-html col-12" dangerouslySetInnerHTML={{__html:this.state.innerHTML}} />
<div className="Page-footer">
<div className="d-flex flex-wrap btn btn-secondary justify-content-around">
{this.state.pageTags.map(function(pageTag){return(
<div className="pd-2" key={pageTag.id}>
{pageTag.name}
</div>
)})}
</div>
<div className="d-flex justify-content-center" >
<div className="p-2">Discuss</div>
<div className="p-2">Rate</div>
<div className="p-2">Edit</div>
</div>
<div className="d-flex justify-content-around App">
<div className="p-2">
Unless otherwise stated, the content
of this page is licensed under <br />
<a href="http://creativecommons.org/licenses/by-sa/3.0/"
target="_blank" rel="noopener noreferrer">
Creative Commons Attribution-ShareAlike 3.0 License
</a>
</div>
</div>
</div>
</div>
)
}
}
export default Page
Instead of function with function keyword use arrow functions and it will be solved as follows
You have
yui_nav[tab_count].onclick = function() { this.updateTabs(yui_set_count); }
But use
yui_nav[tab_count].onclick = () => { this.updateTabs(yui_set_count); }
Use this in componentDidMount method
You have to bind the updateTabs method in the constructor:
constructor(props) {
super(props);
...
this.updateTabs = this.updateTabs.bind(this);
}
You should use arrow functions in order to call this method with the correct contetxt:
yui_nav[tab_count].onclick = () => { this.updateTabs(yui_set_count); }

How to bind onclick handlers to `this` properly on React

Explanation to why this is not a duplicate: My code is already working, I have included as a comment. The question is why the this context change when I include it to click handler function.
I'm attempting a calculator project in React. The goal is to attach onclick handlers to number buttons so the numbers are displayed on the calculator display area. If the handler is written directly to render method it is working, however, if I'm trying from the ComponentDidMount I get an error this.inputDigit is not a function. How do I bind this.inputDigit(digit) properly?
import React from 'react';
import './App.css';
export default class Calculator extends React.Component {
// display of calculator initially zero
state = {
displayValue: '0'
}
//click handler function
inputDigit(digit){
const { displayValue } = this.state;
this.setState({
displayValue: displayValue+String(digit)
})
}
componentDidMount(){
//Get all number keys and attach click handler function
var numberKeys = document.getElementsByClassName("number-keys");
var myFunction = function() {
var targetNumber = Number(this.innerHTML);
return this.inputDigit(targetNumber); // This is not working
};
for (var i = 0; i < numberKeys.length; i++) {
numberKeys[i].onclick = myFunction;
}
}
render() {
const { displayValue } = this.state;
return (
<div className="calculator">
<div className="calculator-display">{displayValue}</div>
<div className="calculator-keypad">
<div className="input-keys">
<div className="digit-keys">
{/*<button className="number-keys" onClick={()=> this.inputDigit(0)}>0</button> This will Work*/}}
<button className="number-keys">0</button>
<button className="number-keys1">1</button>
<button className="number-keys">2</button>
<button className="number-keys">3</button>
<button className="number-keys">4</button>
<button className="number-keys">5</button>
<button className="number-keys">6</button>
<button className="number-keys">7</button>
<button className="number-keys">8</button>
<button className="number-keys">9</button>
</div>
</div>
</div>
</div>
)
}
}
Thats because you are writing it inside a function which is not bound,
Use
var myFunction = function() {
var targetNumber = Number(this.innerHTML);
return this.inputDigit(targetNumber);
}.bind(this);
or
const myFunction = () => {
var targetNumber = Number(this.innerHTML);
return this.inputDigit(targetNumber);
}
After this you need to bind the inputDigit function as well since it also uses setState
//click handler function
inputDigit = (digit) => {
const { displayValue } = this.state;
this.setState({
displayValue: displayValue+String(digit)
})
}
Since you want to use the button text as well, in that case you should use a separate variable in place of this to call the inputDigit function like
class Calculator extends React.Component {
// display of calculator initially zero
state = {
displayValue: '0'
}
//click handler function
inputDigit(digit){
const { displayValue } = this.state;
this.setState({
displayValue: displayValue+String(digit)
})
}
componentDidMount(){
//Get all number keys and attach click handler function
var numberKeys = document.getElementsByClassName("number-keys");
var that = this;
var myFunction = function() {
var targetNumber = Number(this.innerHTML);
console.log(targetNumber);
return that.inputDigit(targetNumber); // This is not working
};
for (var i = 0; i < numberKeys.length; i++) {
numberKeys[i].onclick = myFunction;
}
}
render() {
const { displayValue } = this.state;
return (
<div className="calculator">
<div className="calculator-display">{displayValue}</div>
<div className="calculator-keypad">
<div className="input-keys">
<div className="digit-keys">
{/*<button className="number-keys" onClick={()=> this.inputDigit(0)}>0</button> This will Work*/}
<button className="number-keys">0</button>
<button className="number-keys">1</button>
<button className="number-keys">2</button>
<button className="number-keys">3</button>
<button className="number-keys">4</button>
<button className="number-keys">5</button>
<button className="number-keys">6</button>
<button className="number-keys">7</button>
<button className="number-keys">8</button>
<button className="number-keys">9</button>
</div>
</div>
</div>
</div>
)
}
}
ReactDOM.render(<Calculator/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Bind it in the constructor
constructor(props) {
super(props);
this.inputDigit = this.inputDigit.bind(this);
}

React Redux only displaying one element

I'm new to React and am having some trouble getting it to work.
I have a react class that puts a bunch of JSON in the store as an object, a PushNotification with two elements: pushId and count. So, the store should have a list of PushNotifications.
However, when I try and display that information to the screen, it only outputs one of them.
My React code is:
socket.onmessage = function(event) {
console.log("Received message" + event.data.toString());
store.dispatch(receivedPushNotification(event.data));
};
var App = React.createClass({
render: function () {
var pushNotifications = _.map(this.props.pushNotifications, function(value, key, notification) {
var percentage = (notification.count / 50) * 100;
return (
<div className="row" key={notification.pushid}>
<div className="col-sm-12">
<Card>
<h1 className="marB15">{notification.pushid}</h1>
<div className="clearfix">
<div className="progress progress-striped active marB10">
<div className="progress-bar" style={{'width': percentage + '%'}}></div>
</div>
<div className="pull-right">
<p>Total: {notification.count}</p>
</div>
</div>
</Card>
</div>
</div>
)
});
}
});
My Reducer is:
var pushNotificationDefaultState = {};
var pushNotificationReducer = function(state, action) {
switch(action.type) {
case 'RECEIVED_PUSH_NOTIFICATION':
var obj = JSON.parse(action.PushNotification);
console.log(obj.pushid);
console.log(obj.count);
return obj;
default:
if (typeof state === 'undefined') {
return pushNotificationDefaultState;
}
return state;
}
};
module.exports = Redux.combineReducers({
pushNotifications: pushNotificationReducer
});
Thanks in advance,
The problem is, that you are storing only one notification in redux state. Instead of this, you should store an array of them.
// Using an emty array as default state, instead of object.
var pushNotificationDefaultState = [];
var pushNotificationReducer = function(state, action) {
switch(action.type) {
case 'RECEIVED_PUSH_NOTIFICATION':
var obj = JSON.parse(action.PushNotification);
// Returning new array, which contains previous state and new notification.
return [].concat(state, [obj]);
default:
if (typeof state === 'undefined') {
return pushNotificationDefaultState;
}
return state;
}
};
module.exports = Redux.combineReducers({
pushNotifications: pushNotificationReducer
});
Also, you are not returning notifications elements from render function:
socket.onmessage = function(event) {
console.log("Received message" + event.data.toString());
store.dispatch(receivedPushNotification(event.data));
};
var App = React.createClass({
render: function () {
// To render notifications, return it array from render function
return _.map(this.props.pushNotifications, function(value, key, notification) {
var percentage = (notification.count / 50) * 100;
return (
<div className="row" key={notification.pushid}>
<div className="col-sm-12">
<Card>
<h1 className="marB15">{notification.pushid}</h1>
<div className="clearfix">
<div className="progress progress-striped active marB10">
<div className="progress-bar" style={{'width': percentage + '%'}}></div>
</div>
<div className="pull-right">
<p>Total: {notification.count}</p>
</div>
</div>
</Card>
</div>
</div>
)
});
}
});
add return statement in your render, after map
return (<div>{pushNotifications}</div>);
in reducer you should add new notif in array
case 'RECEIVED_PUSH_NOTIFICATION':
var notif = JSON.parse(action.PushNotification);
return [...state, notif ];

How to turn objects in to HTML

How do i get this :
<li>
<div class='myClass1'>myData1</div>
<div class='myClass2'>myData2</div>
<div class='myClass3'>myData3</div>
<div class='myClass4'>myData4</div>
</li>
from this code
var data1 = {"Columns":[{"Title":"Title1","HTMLClass":"g1_Title"},{"Title":"Title2","HTMLClass":"g2_Title"},{"Title":"Title3","HTMLClass":"g3_Title"}],"Rows":[{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]},{"Cells":["Cell0","Cell1","Cell2"]}]};
var GridRow = React.createClass({
render: function() {
var data = [], columns;
// puts all the data in to a better structure (ideally the props would have this structure or this manipulation would be done on onReceiveProps)
if(this.props.columns){
for(var ii = 0; ii < this.props.columns.length; ii++){
data.push({
class: this.props.columns[i].HTMLClass,
contents: this.props.Cell[i]
})
}
}
// Its best to map JSX elements and not store them in arrays
columns = data.map(function(col) {
return <div className= + {col.class}> {col.contents} </div>;
});
return (
<div>
<li>
{columns}
</li>
</div>
);
}
});
var GridHead = React.createClass({
render: function() {
if(this.props.data){
var cell = this.props.data.Title;
var htmlClass = this.props.data.HTMLClass;
}
return (
<div className={htmlClass}>{cell}</div>
);
}
});
var GridList = React.createClass({
render: function() {
if(this.props.data){
var header = this.props.data.Columns.map(function (columns) {
return (
<GridHead data={columns} />
);
});
var row = this.props.data.Rows.map(function (row, i) {
return (
<GridRow columns={data1.Columns} cells={row.Cells} key={i} />
);
});
}
return (
<ul>
<li>{header}</li>
{row}
</ul>
);
}
});
var GridBox = React.createClass({
render: function(){
return (
<GridList data={data1} />
);
}
});
The output right now is this
In file "~/Scripts/Grid.jsx": Parse Error: Line 26: XJS value should
be either an expression or a quoted XJS text (at line 26 column 35)
Line: 52 Column:3
As your question initially asked was to do with just the GridRow component and nothing else I have not touched any other component.
Your main problem was you were assigning className = + //something in your GridRow component which isn't the correct way to assign. There were other errors like missing div tags.
Better GridRow
When the component mounts a columndata variable is created and is populated with formatted data using formatData();.
I do not recommend you do data formatting in this component (although it is doable). You should either format your data at a top level component and pass down formatted data or accept data in the correct structure.
My GridRow component to this:
var GridRow = React.createClass({
componentWillMount: function() {
this.columndata = [];
this.formatData();
},
formatData: function() { // Formats prop data into something manageable
if (this.props.columns && this.props.cells) {
for(var ii = 0; ii < this.props.columns.length; ii++){
this.columndata.push({
class: this.props.columns[ii].HTMLClass,
contents: this.props.cells[ii]
})
}
this.forceUpdate(); // Forces a rerender
}
},
componentDidUpdate: function(prevProps, prevState) {
// If this component receives the props late
if (!prevProps.cells && !prevProps.columns) {
this.formatData();
}
},
render: function() {
var columns;
// Its best to map JSX elements and not store them in arrays
columns = this.columndata.map(function(col) {
return <div className={col.class}> {col.contents} </div>;
});
return (
<div>
<li>
{columns}
</li>
</div>
);
}
});
I think it's important to note that you should avoid storing JSX elements in arrays.
I think you were basically on the money, except you were missing classname and div tags.

Getting ng-show value as reference

I have the following html:
<div ng-show=showMarketingNav>
...
</div>
<div ng-show=showProductsNav>
...
</div>
<div ng-show=showSettingsNav>
...
</div>
What I want to do is to easily be able to hide all but one of the divs from another controller. I thought I could be clever and do the following:
var subNavMenuDisplays = {
Marketing: $scope.showMarketingNav,
Products: $scope.showProductsNav,
Settings: $scope.showSettingsNav
}
$rootScope.hideContextMenu = function () {
for (var category in subNavMenuDisplays) {
subNavMenuDisplays[category] = false;
}
}
$rootScope.setContextMenu = function (name) {
$rootScope.hideContextMenu();
subNavMenuDisplays[name] = true;
}
but this obviously does not work as $scope.showMarketingNav etc. will be passed as value, not reference.
The following works, but is not exactly nice to work with:
$rootScope.hideContextMenu = function () {
$scope.showMarketingNav = false;
$scope.showProductsNav = false;
$scope.showSettingsNav = false;
}
$rootScope.setContextMenu = function (name) {
$rootScope.hideContextMenu();
if (name == "Marketing") {
$scope.showMarketingNav = true;
}
if (name == "Products") {
$scope.showProductsNav = true;
}
if (name == "Settings") {
$scope.showSettingsNav = true;
}
}
Is there a way to grab $scope.showMarketingNav by reference, or another clever way around this?
I'd prefer not using eval to concatenate variable names.
You can place an object on the $scope and then toggle it dynamically:
$scope.show = {};
$rootScope.setContextMenu = function (name) {
$scope.show = {};
$scope.show[name] = true;
}
And the Html:
<div ng-show="show.Marketing">
...
</div>
<div ng-show="show.Products">
...
</div>
<div ng-show="show.Settings">
...
</div>
Here's a plunker demonstrating the change.
You can assign simple updater functions in that object:
Marketing: function(val) { $scope.showMarketingNav = val },
Products: function(val) { $scope.showProductsNav = val},
Settings: function(val) { $scope.showSettingsNav = val}
Then call it:
subNavMenuDisplays[name](true);

Categories