Update Table Contents on API Response - javascript

I'm very new to React and although made some progress that I'm happy with, I've hit a snag that I'm stuck with and have been for over a day.
I'm creating a small app that uses React at the frontend and a .NET Core API server-side to supply the data.
So far, the following code works:
import React, { Component } from 'react';
export class LGSRValidation extends Component {
displayName = LGSRValidation.name
constructor(props) {
super(props);
this.state = { lgsr: [], loading: true };
fetch('api/FormValidation/LGSR')
.then(response => response.json())
.then(data => {
this.setState({
lgsr: data,
loading: false
});
});
}
static renderLGSRTable(lgsr) {
return (
<table className='table'>
<thead>
<tr>
<th>Job Number</th>
<th>Job Type</th>
<th>UPRN</th>
<th>Gas Register Number</th>
<th>Service Date</th>
</tr>
</thead>
<tbody>
<tr key={lgsr.jobnumber}>
<td>{lgsr.jobNumber}</td>
<td>{lgsr.jobType}</td>
<td>{lgsr.uprn}</td>
<td>{lgsr.gasRegisterNumber}</td>
<td>{lgsr.serviceDate}</td>
</tr>
</tbody>
</table>
);
}
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: LGSRValidation.renderLGSRTable(this.state.lgsr);
return (
<div>
<div>
<h1>{this.displayName}</h1>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
</div>
);
}
}
What it quite simply does is call my API to return a single object to the client-side code and render each property within the object into a separate table column.
What I'm trying to do is introduce a button that onClick will call the API again, pull a new object and change the values in the table for the new property values. I'm happy with my server-side code and the creation of the random objects but my client side code is just not working.
I'm introducing the button within the parent tags of the component like this:
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: LGSRValidation.renderLGSRTable(this.state.lgsr);
return (
<div>
<div>
<h1>{this.displayName}</h1>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
<button onClick={this.getData}>
Next
</button>
</div>
);
}
Which seems to render fine. I'm also introducing a getData function like this further up:
getData() {
fetch('api/FormValidation/LGSR')
.then(response => response.json())
.then(data => {
this.setState({
lgsr: data,
loading: false
});
});
this.setState({
contents : this.state.lgsr
});
}
I suspect it's at this part where I'm going wrong.
Please can you advise how I can click my new button to call the API, return some data and pass the properties of the lgsr object into the corresponding table columns; replacing the previous data with the new data.

You need to bind your getData() to your constructor so you can use this for your component rather than the current function.
this.getData = this.getData.bind(this);
A quicker way is to use arrow function like the following...
getData = () => {....}
This should hopefully fix your problem and allow you to use this.state

I see 4 ways to improve your code:
Load data in the componentDidMount lifecycle method instead of the component's constructor (more info). This should not be related to you problem but it's safer to ensure the component did mount before you possibly set the state.
As SGhaleb mentioned be sure that your getData function is bound, either using an arrow function or the bind alternative, otherwise this.getData is undefined and the button's onClick should be a no op. You could use getData for your initial request to not have duplicated code in your class.
In getData you set the state's content property, but you do it outside of the request's callback. It's value may be either the new or the old lgsr object of the new request. Check that you do want this or move it to the fetch callback.
Use a dedicated component to render the LGSR table instead of using a static render method and pass the required data via props to it. Again, nothing to do with your question.

What are you trying to achieve here?
Can you paste your final code?
I see something terrible here. Your 'loading' is already declared in your component state and you're updating 'contents' state with the lgsr data. And in your render, you have declared contents as this.state.loading
constructor(props) {
super(props);
this.state = { lgsr: [], loading: true };
/*some code here*/
getData () {
fetch('api/FormValidation/LGSR')
.then(response => response.json())
.then(data => {
this.setState({
lgsr: data,
loading: false
});
});
this.setState({
contents : this.state.lgsr
});
}
render() {
let contents = this.state.loading

Related

Cannot map data correctlly

I'm new to React.js development and need help with this issue. I created a service using FastAPI, which executes a number of SQL SELECT declarations against an Oracle database, takes the results, and exposes them as a REST API using JSON data format.
In a Chrome Web-browser, I see this information when I execute a GET to the API:
[API GET results][1]
Therefore, the API is returning results correctly. To show them all in my React app, I created this component:
import React, {Component} from 'react';
export default class App extends React.Component {
state = {
loading: true,
pedidos: []
}
async componentDidMount() {
const url = "http://localhost:8000/pedidos-bloqueados/";
fetch(url)
.then(response => {
return response.json();
})
.then(d => {
this.setState({ pedidos: [d], loading: false });
})
.catch(error => console.log(error))
}
render() {
return (
<div>
{ this.state.loading? <div>Carregando...</div> : <div><li> Cliente: * {(this.state.pedidos.map((pedido, index)=>(this.state.pedidos[index]))).map((p, i)=> p['cliente'])}</li></div> } *
</div>
);
}
}
It turns out that, when a map() function inside render() method gets executed, the only information shown is:
Cliente:
Without any more data. And the component is not allowed to do a for() loop inside render() method, what could, possibly, render the objects encoded in the JSON list.
I think the problem is in the snippet:
{ this.state.loading? <div>Loading...</div> : <div><li> Cliente: {(this.state.pedidos.map((pedido, index)=>(this.state.pedidos[index]))).map((p, i)=> p['cliente'])}</li></div> }
How should I change my code to show the properties of each object in the list when rendering the component? (Note: cliente is ***just one of the JSON object attributes - there are others like vendedor, pedido, and so on... But, if I manage to list this field, I can replicate the behavior to the others).
I thank you VERY MUCH for any assistance with this matter!
[1]: https://i.stack.imgur.com/XHuN3.png

infinite api calls in componentDidUpdate(prevProps) despite conditional that compares current prop with previous prop

My problem is in my subcomponent files where every state update in the parent component keeps re-rendering the subcomponents infinitely by making infinite api calls with the default or updated props value passed to the child components. I have a User directory page which contains multiple components in a single page.
class Users extends Component {
constructor(props) {
super(props);
this.state = {
user: "",
listOfUsers: [],
socialData:[],
contactData:[],
videoData:[],
detailsData:[]
}
}
componentDidMount(){
//api call that gets list of users here
//set response data to this.state.listOfUsers
}
userHandler = async (event) => {
this.setState({
user: event.target.value,
});
};
render() {
return (
<div>
<div>
<select
value={this.state.user}
onChange={this.userHandler}
>
// list of users returned from api
</select>
</div>
<div>
<Social Media user={this.state.user} />
<Contact user={this.state.user}/>
<Video user={this.state.user}/>
<Details user={this.state.user}/>
</div>
</div>
);
}
}
I have 1 API call for the parent component Users, and 4 for each of the subcomponents: Social Media, Contact, Video, and Details. The Users api will return a list of users in a dropdown and the value of the user selected is then fed to the other four API's. i.e. https://localhost:3000/social_media?user=${this.state.user}. Thus, the four subcomponents' API is dependent on the Users API. I currently have the parent api call in a componentDidMount() and the other 4 api calls in their respective subcomponents and use props to pass down the value of the user selected in the parent to the subcomponents. Each of the api calls is in a componentDidUpdate(prevProps). All the subcomponents follow this structure:
class Social Media extends Component {
constructor(props) {
super(props);
this.state = {
user: "",
socialData:[],
}
}
componentWillReceiveProps(props) {
this.setState({ user: this.props.user })
}
componentDidUpdate(prevProps){
if (this.props.user !== prevProps.user) {
// make api call here
fetch (`https://localhost:3000/social_media?user=${this.state.user}`)
.then((response) => response.json())
.catch((error) => console.error("Error: ", error))
.then((data) => {
this.setState({ socialData: Array.from(data) });
}
}
render() {
return (
{this.socialData.length > 0 ? (
<div>
<Social Media data={this.state.socialData}/>
</div>
)
:
(<div> Loading ... </div>)
);
}
}
Abortive attempt to answer your question
It's hard to say exactly what's going on here; based on the state shown in the Users component, user should be a string, which should be straightforward to compare, but clearly something is going wrong in the if (this.props.user !== prevProps.user) { comparison.
If we could see the results of the console.log(typeof this.props.user, this.props.user, typeof prevProps.user, typeof prevProps.user) call I suggested in my comment, we'd probably have a better idea what's going on here.
Suggestions that go beyond the scope of your question
Given the moderate complexity of your state, you may want to use some sort of shared state like React's Context API, Redux, or MobX.. I'm partial toward the Context API, as it's built into React and requires relatively less setup.
(Then again, I also prefer functional components and hooks to classes and componentDidUpdate, so my suggestion may not apply to your codebase without a rewrite.)
If this.props.user is an object, then this.props.user !== prevProps.user will evaluate to true because they are not the same object. If you want to compare if they have the same properties and values (i.e. they are shallowly equal) you can use an npm package like shallow-equal and do something like:
import { shallowEqualObjects } from "shallow-equal";
//...
componentDidUpdate(prevProps){
if (!shallowEqualObjects(prevProps.user, this.props.user)) {
// make api call here
fetch (`https://localhost:3000/social_media?user=${this.state.user}`)
.then((response) => response.json())
.catch((error) => console.error("Error: ", error))
.then((data) => {
this.setState({ socialData: Array.from(data) });
}
}

Infinity loop after componentDidUpdate() in ReactJS

I have React JS app, which updating boostrap grid table with entries from ASP .NET Core Web API.
And I need to update the grid with new entries after inserting. I did that with help of componentDidUpdate() and using refreshList() function there, but I am getting an infinity loop of refreshList() function.
Any thoughts about how to update the grid without that loop?
import React, {Component} from 'react';
import {Table, Container} from 'react-bootstrap';
import {Button, ButtonToolbar} from 'react-bootstrap';
import {AddDepModal} from './AddDepModal';
import {EditDepModal} from './EditDepModal';
export class Department extends Component {
constructor(props){
super(props);
this.state = {deps:[], addModalShow: false, editModalShow: false}
}
componentDidMount()
{
this.refreshList();
}
refreshList()
{
fetch("https://localhost:5001/api/departments")
.then(response=> response.json())
.then(data=> {
this.setState({deps:data});
});
}
componentDidUpdate()
{
this.refreshList();
}
render(){
const {deps, depid, depname} = this.state;
let addModalClose = () => this.setState({addModalShow:false})
let editModalClose = () => this.setState({editModalShow:false})
return (
<div>
<Table className = "mt-4" striped bordered hover size ="sm">
<thead>
<tr>
<th>DepartmentID</th>
<th>DepartmentName</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{deps.map(dep=>
<tr key = {dep.id}>
<td>{dep.id}</td>
<td>{dep.name}</td>
<td>
<ButtonToolbar>
<Button
className ="mr-2" variant ="info"
onClick = {() => this.setState({editModalShow:true, depid: dep.id, depname: dep.name})}
>
Edit
</Button>
<EditDepModal
show = {this.state.editModalShow}
onHide = {editModalClose}
depid = {depid}
depname = {depname}/>
</ButtonToolbar>
</td>
</tr>
)}
</tbody>
</Table>
<ButtonToolbar>
<Button variant = "primary" onClick = {() => this.setState({addModalShow: true})}>Add Department</Button>
<AddDepModal
show ={this.state.addModalShow}
onHide ={addModalClose}/>
</ButtonToolbar>
</div>
)
}
}
Remove componentDidUpdate() because refreshData doesn't depend from props to fetch data and there aren't any checks with prevProps and newProps.
You can call refreshData method from Add or Save Button callback.
I image that you are saving data from modal code, add setState callback.
Modal save data, onhide set show state to false and call refreshData from once.
let addModalClose = () => this.setState({addModalShow:false}, this.refreshData)
You're calling this.refreshList(); which does some thing and then set the state. After the state is set, render functions is called and in turn componentDidUpdate is called again, setting the infinite loop. To make it work, compare from the previous props and then call the this.refreshList(); if needed.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.depid !== prevProps.depid) { //Replace `depid` with the props you like to compare changes. If that is changed, then only call
this.refreshList();
}
}
You may call setState() immediately in componentDidUpdate() but note
that it must be wrapped in a condition like in the example above, or
you’ll cause an infinite loop. It would also cause an extra
re-rendering which, while not visible to the user, can affect the
component performance. If you’re trying to “mirror” some state to a
prop coming from above, consider using the prop directly instead. Read
more about why copying props into state causes bugs.
see the docs: https://reactjs.org/docs/react-component.html#componentdidupdate
The problem is you are calling refreshList in componentDidMount and componentDidUpdate. As described in the documentation: https://reactjs.org/docs/react-component.html#componentdidupdate
you should at some condition to avoid an infinity loop in componentDidUpdate.

How to add event to react function and re-render function

I have a function that renders content to page based on a state populated by API data, but I need to have an onClick event to refine that content;
So currently getPosts returns information from the state 'posts' which is provided with data from our API, but i want to filter this content further, so my idea is to have some sort of event listener, and if actioned, change the data coming out of getPosts.
constructor() {
super();
this.state = {
posts: ""
}
this.getPosts = this.getPosts.bind(this);
}
async componentWillMount(){
var data = await api.posts();
this.setState({posts: data.data});
console.log(this.state.posts);
}
getPosts(type){
if(this.state.posts.length){
return this.state.posts.map((content,index) => {
var url = content.Title.replace(/[^\w\s]/gi, '');
url = url.replace(/\s+/g, '-').toLowerCase();
if(type === content.PostType){
//output something different
}
else{
return(
<Col md={4} className="mb-4" key={index}>
{content.title}
</Col>
);
}
})
}
}
render() {
return (
<div>
<p><button onClick={()=>{this.getPosts('blog')}}>blog</button> <button onClick={()=>{this.getPosts('news')}}>news</button></p>
{this.getPosts()}
</div>
)
}
So my getPosts works fine without any type, how to do tell it to re-output the function on the page based in the onClick event?
Without getting into the complexities of context and keys, a component requires a change in props or state to re-render. To read more about state and component life-cycle, the docs have a great explanation for class components.
Your component does not re-render after the onClick event handler's call to getPosts because getPosts does not update internal component state. getPosts works within render because those values are being returned to React. By using getPosts as an onClick event handler, you are creating React elements and trying to return them to the window.
What follows should be treated as psuedo code that shows how to trigger your component to render different posts:
Consider adding another key to state in your constructor,
constructor(props) {
super(props);
this.state = {
posts: "",
type: null
};
this.getPosts = this.getPosts.bind(this);
this.onClick = this.onClick.bind(this);
}
and creating a click handler that doesn't try to return React elements
function onClick(evt) {
this.setState({ type: evt.target.value });
}
and values to your buttons
<button onClick={this.onClick} type="button" value="blog">blog</button>
Now your button will update state with your new post type, causing your component to re-render:
render() {
return (
<div>
<p>
<button onClick={this.onClick} type="button" value="blog">blog</button>
<button onClick={this.onClick} type="button" value="news">news</button>
</p>
{this.getPosts()}
</div>
);
}
With the content type being stored in state, you can now implement your getPosts call in any way that works for you. Good luck!
It strays from the question asked, but it is worth noting componentWillMount is being deprecated, and componentDidMount is a preferable life-cycle function for side-effects and asynchronous behavior. Thankfully, the documentation has lots of details!
Ok so you should start by changing your default this.state to
this.state = {
posts: []
}
remember that you want to iterate over an array of data instead of iterate a string, that will throw an error if you do that, so better keep from the beginning the data type you want to use.
Then you need to separate the responsibility for your getPosts method, maybe getPostByType is a better name for that, so you have
getPostByType(type) {
// if type is same as content.PostType then return it;
const nextPosts = this.state.posts.filter((content) => type === content.PostType);
this.setState({ posts: nextPosts });
}
and finally you can iterate over posts, like this
render() {
// better use content.id if exists instead of index, this is for avoid problems
// with rerender when same index key is applied.
const posts = this.state.posts.map((content, index) => (
<Col md={4} className="mb-4" key={content.id || index}>
{content.title}
</Col>
));
return (
<div>
<button onClick={() => this.getPostByType('blog')}>Show Blog Posts</button>
{posts}
</div>
);
}
Then you can use getPostsByType any time in any click event passing the type that you want to render.

Accessing data from Axios GET request

I'm having trouble accessing data from an Axios GET request. I'd like to be able to iterate through all of the data I get and create a table that displays the username, avatar, and score for each user. The only way I'm able to currently render a single username is with the following code:
this.setState({name: res.data[0].username});
But the above code only gives me access to the first object in the URL I use in the GET request. If I use this code:
this.setState({name: JSON.stringify(res.data)});
I'm able to render the entire object as a string. So that makes me think I need to update my render method, but I'm not sure how.
What steps should I take so that I can map over the state that I'm setting and render each user's data in a table or list?
class LeaderBoard extends React.Component {
constructor(props) {
super(props)
this.state = {
name: []
}
}
componentDidMount(){
axios.get('https://fcctop100.herokuapp.com/api/fccusers/top/recent').then(res =>
{
this.setState({name: res.data[0].username});
});
}
render () {
return (
<div>
<h1>{this.state.name}</h1>
</div>
)
}
}
ReactDOM.render(
<LeaderBoard/>, document.getElementById("app"));
You're on the right track, you're just missing a few key pieces.
Step 1: Add the entire array to your state
I'm not sure of the exact structure of your data, but you likely want to do this
componentDidMount(){
axios.get('https://fcctop100.herokuapp.com/api/fccusers/top/recent').then(res =>
{
this.setState({data: res.data});
});
}
Now you have all the data you want to work with in your state, ready to be iterated over.
Step 2: Use .map() to create the JSX elements you want
This code here:
render () {
return (
<div>
<h1>{this.state.name}</h1>
</div>
)
}
}
The big fault here is that it will only ever render one thing, because, well, that's all there is. What you want to do is .map() through and create a bunch of names from your data, right?
That would look more like this.
render () {
const namesToRender = this.state.data.map(val => {
return (
<h1>val.username</h1>
)
})
return (
<div>
{namesToRender}
</div>
)
}
}
All this is saying is "Go through that data and give me the name of each person and wrap it in some <h1> tags and spit that out".

Categories

Resources