I'm still learning about state and lifecycle with ReactJS and have run into a scenario where I have a form that on submit should save the form value and then append the returned JSON object to the end of an array which would re-render the component storing the original array.
With my current setup, I have the components setup and form submit with returned JSON object, but the state contains an empty array rather than the object spread {...comment} and it doesn't look like the setState is updating component, but that could be due to the empty array mentioned before. Can anyone point me in the right direction?
Comment:
import React from 'react';
import fetch from 'node-fetch';
//record Comment - Comment Form Handle POST
class CommentForm extends React.Component {
constructor(props){
super(props);
this.state = {
value: '',
comments: []
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
postComment(comment, recordId, csrfToken) {
var body = { comment: comment };
var route = 'http://localhost:3000/record/' + recordId + '/comment';
fetch(route,
{
method: 'POST',
body: JSON.stringify(body),
headers: {
'X-CSRF-Token': csrfToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
let commentsArr = this.state.comments;
this.setState({comments: commentsArr.concat(data)});
})
.catch(err => {
console.log(err);
});
}
onChange(e){
this.setState({
value: e.target.value
});
}
handleSubmit(e){
e.preventDefault();
this.postComment(this.state.value, this.props.recordId, this.props.csrf);
}
render(){
return (
<div className="record-comment__form">
<div className="row">
<form action={"/record/" + this.props.recordId + "/comment"} method="post" onSubmit={this.handleSubmit}>
<input type="hidden" name="_csrf" value={this.props.csrf}/>
<textarea name="comment" className="record-comment__form-text-area" onChange={e => this.setState({ value: e.target.value })} value={this.state.value}></textarea>
<button type="submit" className="record-comment__form-button" disabled={!this.state.value}>Comment</button>
</form>
</div>
</div>
)
}
}
//record Comment - Comment
const Comment = props => {
return (
<div className="row">
<div className="col-md-12">
<h5>{props.user_id}</h5>
<h4>{props.comment}</h4>
<h3>{props.synotate_user.fullNameSlug}</h3>
</div>
</div>
)
}
//record Comment - Container
export default class Comments extends React.Component {
render() {
return (
<div className="record-comment-container">
<CommentForm recordId={this.props.recordId} csrf={this.props.csrf}/>
{ this.props.record_comments.map((comment, i) =>
<Comment {...comment} key={this.props.recordCommentId}/>
)}
</div>
);
}
}
Record (Parent component)(Where Comment is being set):
//GET /api/test and set to state
class RecordFeedContainer extends React.Component{
constructor(props, context) {
super(props, context);
this.state = this.context.data || window.__INITIAL_STATE__ || {records: []};
}
fetchList() {
fetch('http://localhost:3000/api/test')
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
this.setState({ records: data.record, user: data.user, csrf: data.csrfToken });
})
.catch(err => {
console.log(err);
});
}
componentDidMount() {
this.fetchList();
}
render() {
return (
<div className="container">
<h2>Comments List</h2>
<RecordFeed {...this.state} />
</div>
)
}
};
//Loop through JSON and create Record and Comment Container Component
const RecordFeed = props => {
return (
<div>
{
props.records.map((record, index) => {
return (
<div className="row">
<div className="col-md-6 col-md-offset-3 record-card">
<RecordCard {...record} key={record.recordIdHash} user={props.user} />
<Comments {...record} key={index} recordId={record.recordIdHash} csrf={props.csrf}/>
</div>
</div>
);
})
}
</div>
)
}
Your problem is that when rendering <Comments>, the this.props.record_comments is not the comments you've updated in the state of the <CommentForm> component. Each component has it's own internal state.
You need to pass the state along to your <Comments> component. You will need to move your state up to the top level or use a state management system like Redux which will allow you to access a shared state which could contain your comments array.
From the top level component you could manage the state there, like so:
this.state = {
comments: [],
// other shared state
};
You can pass along an update comments function, named for example updateCommentsFunc() to <CommentForm> like so:
<CommentForm updateComments={this.updateCommentsFunc} recordId={this.props.recordId} csrf={this.props.csrf}/>
Which will allow you to pass the updated comments back up to the parent component via something like:
const updateCommentsFunc = (newComments) => {
this.setState({comments: [...this.state.comments, newComments]});
}
Your postComment() function doesn't appear to be properly bound to your enveloping <CommentForm/> component's this. As a result; calling this.setState() from within the function isn't really doing anything.
Try binding it within your constructor method.
constructor(props) {
// ...
this.postComment = this.postComment.bind(this)
}
Or by declaring it using an arrow function.
postComment = (comment, recordId, csrfToken) => {
// ...
}
See this article for more info on React binding patterns.
Related
What would be a way to call an API in a component class (do not use a functional component) and render the received data in a new component. I am asking for the order of operation to setState. My current solution includes setState and a function as a second argument that set anther state. Function getStockInfo calls API and then I want to render the GetQuote component switching showComponent to true. Note that in ComponentDidMount I call an initial API. Please let me know if this is a valid solution or there is a neater way.
class Portfolio extends Component {
constructor(props) {
super(props);
this.state = {
res: " ",
data: [],
showComponent: false,
};
this.showStockiaDetails = this.showStockDetails.bind(this);
this.getStockInfo = this.getStockInfo.bind(this);
this.handleClick = this.handleClick.bind(this);
}
async getStockInfo(item) {
const stock = item.symbol;
const API_KEY = "1D";
const PATH = `https://www.alphavantage.co/query?function=OVERVIEW&symbol=${stock}&apikey=${API_KEY}`;
await axios
.get(PATH)
.then((response) => {
this.setState({ res: response.data }, () => this.showStockDetails());
console.log(response.data);
})
.catch(function (error) {
console.log(error);
});
}
handleClick(item) {
this.getStockInfo(item);
}
showStockDetails() {
console.log("-->", this.state.res);
this.setState({ showComponent: true });
}
componentDidMount() {
axios
.get("http://127.0.0.1:8000/api/data")
.then((response) => {
this.setState({ data: response.data });
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div className="container">
<h1>Day Eight</h1>
<div className="row col-12">
<div className="col-md-5 col-lg-4 ">
<ul className="list-group">
{this.state.dayeightData.map((s) => (
<li
key={s.id}
onClick={() => this.handleClick(s)}
className="list-group-item"
aria-current="true"
>
{s.symbol}
</li>
))}
</ul>
</div>
<div className="col-md-7 col-lg-8 order-md-last">
{this.state.showComponent ? (
<GetQuote stock={this.state.res} />
) : null}
</div>
</div>
</div>
);
}
}
export default Portfolio;
You did it right. There would be many other alternatives too depending on what GetQuote component is rendering. To make it look a little bit more cleaner you can do something like this:
{
this.state.res ? <GetQuote stock={this.state.res} /> : ''
}
But make sure to initialize res as "" (without any space) as "" will be false and " " will be true.
In that way, you don't really have to call showStockDetails function.
I followed a tutorial to make an Asp.Net Core MVC app with a ReactJs front end (https://reactjs.net/tutorials/aspnetcore.html). I've been adding additional functionality to the project after completing the tutorial to see what else I can do with it.
My <AddColourForm> component assembles a <Colour> object and posts it off via an XmlHttpRequest to my API controller which in turn persists it to local storage. The submitUrl for the controller is passed in through the props. This works.
I've since tried to add the <SoftDeleteColour> component to each colourNode rendered in the <ColourList> which I intend to behave in more-or-less the same manner as the <AddColourForm> component. Each colourNode rendered in the <ColourList> has it's own delete button and I want the <SoftDeleteColour> component to take the colour.id from the selected colour and pass it to the softDelete action on the API controller so that can be handled in turn (it'll find the colour by id and append a DateDeleted to it, the API will then ignore any colours where DateDeleted != null) and the <SoftDeleteColour> component can then call loadColoursFromServer() to bring back the refreshed list from the storage. I want <SoftDeleteColour> to receive the softDeleteUrl from props in the same way that the add form does.
When I run the project in debug the softDeleteUrl is coming in as undefined and when I inspect the props in the browser it doesn't contain the softDeleteUrl. Also the "colour" is undefined so I feel like my <SoftDeleteColour> component isn't receiving the props or state. I'm new to React and struggling conceptually with props/state binding a little bit so I suspect this is the source of my problem.
How can I pass the softDeleteUrl and the properties of the colour from the <ColourList> that I am selecting for deletion to the <SoftDeleteColour> component? Do I need to call something like <SoftDeleteColour HandleDeletion=this.HandleDeletion.bind(this) /> or something?
class ColourDisplay extends React.Component {
constructor(props) {
super(props);
this.state = { data: [] };
this.handleColourSubmit = this.handleColourSubmit.bind(this);
}
loadColoursFromServer() {
const xhr = new XMLHttpRequest();
xhr.open('get', this.props.url, true);
xhr.onload = () => {
const data = JSON.parse(xhr.responseText);
this.setState({ data: data });
};
xhr.send();
}
handleColourSubmit(colour) {
const data = new FormData();
data.append('name', colour.name);
data.append('brand', colour.brand);
data.append('expiry', colour.expiry);
data.append('serialNumber', colour.serialNumber);
const xhr = new XMLHttpRequest();
xhr.open('post', this.props.submitUrl, true);
xhr.onload = () => this.loadColoursFromServer();
xhr.send(data);
}
componentDidMount() {
this.loadColoursFromServer();
}
render() {
return (
<div className="colourDisplay">
<h1>Colours</h1>
<ColourList data={this.state.data}/>
<AddColourForm onColourSubmit={this.handleColourSubmit}/>
</div>
);
}
}
class ColourList extends React.Component {
render() {
const colourNodes = this.props.data.map(colour => (
<Colour name={colour.name} key={colour.id}>
<div>Brand: {colour.brand}</div>
<div>Exp: {colour.expiry}</div>
<div>Serial #: {colour.serialNumber}</div>
<div>Date Added: {colour.dateAdded}</div>
<SoftDeleteColour />
</Colour>
));
return <div className="colourList">{colourNodes}</div>;
}
}
class SoftDeleteColour extends React.Component {
constructor(props) {
super(props)
this.state = {
colour: this.props.colour
};
}
HandleDeletion(colour) {
var xhr = new XMLHttpRequest();
var url = this.props.softDeleteUrl + colour.id;
xhr.open('DELETE', url, true);
xhr.onreadystatechange = () => {
if (xhr.status == 204) {
this.loadColoursFromServer();
}
}
xhr.send();
}
render() {
return (
<button onClick={() => { this.HandleDeletion(this.state.colour); }}>Delete</button>
)
}
}
class AddColourForm extends React.Component {
constructor(props) {
super(props);
this.state = { name: '', brand: '', expiry: '', serialNumber: '' };
this.handleNameChange = this.handleNameChange.bind(this);
this.handleBrandChange = this.handleBrandChange.bind(this);
this.handleExpiryChange = this.handleExpiryChange.bind(this);
this.handleSerialNumberChange = this.handleSerialNumberChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleNameChange(e) {
this.setState({ name: e.target.value });
}
handleBrandChange(e) {
this.setState({ brand: e.target.value });
}
handleExpiryChange(e) {
this.setState({ expiry: e.target.value });
}
handleSerialNumberChange(e) {
this.setState({ serialNumber: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
const name = this.state.name.trim();
const brand = this.state.brand.trim();
const expiry = this.state.expiry.trim();
const serialNumber = this.state.serialNumber.trim();
if (!name || !brand || !expiry || !serialNumber) {
return;
}
this.props.onColourSubmit({
name: name,
brand: brand,
expiry: expiry,
serialNumber: serialNumber
})
this.setState({
name: '',
brand: '',
expiry: '',
serialNumber: ''
});
}
render() {
return (
<form className="addColourForm" onSubmit={this.handleSubmit}>
<h2>Add a colour to your list</h2>
<div>
<input
type="text"
placeholder="Colour"
value={this.state.name}
onChange={this.handleNameChange}
/>
</div>
<div>
<input
type="text"
placeholder="Brand"
value={this.state.brand}
onChange={this.handleBrandChange}
/>
</div>
<div>
<input
type="text"
placeholder="Expiry MM/YY"
value={this.state.expiry}
onChange={this.handleExpiryChange}
/>
</div>
<div>
<input
type="text"
placeholder="Serial #"
value={this.state.serialNumber}
onChange={this.handleSerialNumberChange}
/>
</div>
<input type="submit" value="Post" />
</form>
);
}
}
class Colour extends React.Component {
render() {
return (
<div className="colour">
<h2 className="colourName">{this.props.name}</h2>
{this.props.children}
</div>
);
}
}
ReactDOM.render(
<ColourDisplay
url="/colours"
submitUrl="/colours/new"
softDeleteUrl="/colours/softDelete"
/>,
document.getElementById('content')
);
class App extends Component{
constructor(props){
super(props);
this.state = {
users:[]
};
}
componentDidMount() {
axios.get(`http://localhost:3000/employees`)
.then(res => {
const users = res.data;
this.setState({ users });
})
}
render(){
return(
<div>
<Main users= {this.state.users}/>
<Form/>
</div>
);
}
}
class Main extends Component{
state = {
id: ''
}
handleChange = event => {
this.setState({ id: event.target.value });
}
handleSubmit = event => {
event.preventDefault();
axios.delete(`http://localhost:3000/employees/${this.state.id}`)
.then(res => {
console.log(res);
console.log("this is" + res.data);
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<label>
Person Name:
<input type="text" name="name" onChange={this.handleChange} />
</label>
<button type="submit">Delete</button>
</form>
</div>
)
}
}
Can someone tell me why after the Axios delete request, how I can render the new state in the users array from App component?
In the App component, I am trying to make this.state.users as a props to send it to the Form component. My guess is put this.setState({users: res.data}). The delete request is fine with 200, but I need to refresh the page to get the new result. How can I update immediatly?
// this is a json object
"employees": [
{
"id": 8,
"first_name": "Lteve",
"last_name": "Palmer",
"email": "steve#codingthesmartway.com"
},
As Dave mentions in a comment you want to have single responsibility for state between components.
This topic is also discussed in the blog post You Probably Don't Need Derived State, where one solution to your problem is to have Main "report back" to App in order to update state. So either App passes down a onDeleteUser function, or a callback for when a user is removed, such as onUserWasDeleted.
The latter can be done with the least amount of changes to your code I suppose.
class App extends Component {
constructor(props) {
super(props);
this.onUserWasDeleted = this.onUserWasDeleted.bind(this);
}
onUserWasDeleted(userId) {
// remove user that was successfully removed
this.setState({ users: this.state.users.filter(user => user.id !== userId) });
}
render() {
return (
<Main
users={this.state.users}
// pass down a callback to Main
onUserDeleted={this.onUserWasDeleted}
/>
);
}
}
class Main extends Component {
handleSubmit = event => {
event.preventDefault();
axios.delete(`http://localhost:3000/employees/${this.state.id}`)
.then(res => {
console.log(res);
console.log("this is" + res.data);
// call our callback function in App
this.props.onUserWasDeleted(this.state.id);
})
}
}
I am beginner to React and I am facing some problems. I am trying to get React to render one user, but I don't know how. Every attempt so far has been a failure and I really do need help.
Error message and console.log(this.state.user)
class SelfEdit extends React.Component {
constructor(props){
super(props);
this.state = {
isLoaded: false,
user: null,
loggedUser: props.loggedUser // Username of a logged account
};
this.getUser = this.getUser.bind(this);
}
render(){
console.log(this.state.user);
const div = (
<div className="oneUser">
<h2> {this.state.loggedUser} </h2> <br />
{this.state.user} //<-- Here I would like to get user's email or such
</div>
);
return div;
}
getUser(){
const url = 'http://localhost:3000/api/user/' + this.state.loggedUser;
fetch(url)
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded : true,
user: result
});
},
(error) => {
this.setState({
isLoaded : true,
error
});
}
)
};
componentDidMount() {
this.getUser();
}
}
So how can I make 'user' usable in the rendering?
Thank you in advance, now i have to go get some sleep.
Welcome to stackoverflow Dr.Pippis. As the error suggests, you simply cannot just render a javascript object as a React child. As is this.state.user is a raw object. React wants to interpret a string or number to display, so to do that you can use of the object's properties like email.
class SelfEdit extends React.Component {
constructor(props){
super(props);
this.state = {
isLoaded: false,
user: null,
loggedUser: props.loggedUser // Username of a logged account
};
this.getUser = this.getUser.bind(this);
}
render(){
console.log(this.state.user);
const div = (
<div className="oneUser">
<h2> {this.state.loggedUser} </h2> <br />
{ this.state.user && (<div>{this.state.user.email}</div>) }
</div>
);
return div;
}
getUser(){
const url = 'http://localhost:3000/api/user/' + this.state.loggedUser;
fetch(url)
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded : true,
user: result
});
},
(error) => {
this.setState({
isLoaded : true,
error
});
}
)
};
Since user is an object you cannot render it as is. Instead need to either extract the properties you would like displayed before rendering them or reference them directly e.g.:
Directly:
render(){
console.log(this.state.user);
const div = (
<div className="oneUser">
<h2> {this.state.loggedUser} </h2>
<div>{this.state.user.email}</div>
<div>{this.state.user.username}</div>
</div>
);
return div;
}
or extracted:
render(){
console.log(this.state.user);
// Destructure the properties you would like
const {
email,
username,
} = this.state.user;
const div = (
<div className="oneUser">
<h2> {this.state.loggedUser} </h2>
<div>{email}</div>
<div>{username}</div>
</div>
);
return div;
}
I am developing a simple website which displays data from an API(JSON) into a page using react.
I'm using the fetch() API.
I am able to get the data from the API and set it into the 'App' component state but I'm not able to pass is down to Table and Row components that I've manually created.
class App extends React.Component {
constructor (props) {
super(props)
this.state = {ticker: {}, volume: {}}
this.loadData = this.loadData.bind(this)
this.loadData()
}
loadData () {
fetch(ticker)
.then((resp) => resp.json())
.then((data) => {
this.setState({
ticker: data
})
})
.catch((err) => {
console.log(err)
})
fetch(volume)
.then((resp) => resp.json())
.then((data) => {
this.setState({
volume: data
})
})
.catch((err) => {
console.log(err)
})
}
render () {
return (
<div>
<Navbar />
<div className='container'>
<div className='align'>
<div className='element' />
</div>
<Table volume={this.state.volume} ticker={this.state.ticker} />
</div>
</div>
)
}
}
BOTTOM LINE:
I have an API with data, and I have 3 components, Table, which also has a row component.
I want to display variables in the Row component
which looks like this
<Row img='eth' name='Ethereum' price='' volume='' change='' marketCap='' />
You constructor :
constructor (props) {
super(props);
this.state = {ticker: {}, volume: {}}
this.loadData = this.loadData.bind(this);
}
For fetching data you need always to use lifecycle component, as componentDidMount or componentWillMount, thus :
componentDidMount(){
this.loadData()
}
And then in your state you will have the data.
In your render method pass it as props to the Table component:
render(){
return(
<Table volume={this.state.volume} ticker={this.state.ticker} />
)
}
Then in from the Table component pass it to Row component as props, thus :
render(){
return(
<Row img='eth' name='Ethereum' price='' volume={this.props.volume} change='' marketCap='' />
)
}
If you have array of objects, something like :
this.state = {
volume: [ {name: "One", size: 1 }, {name: "Two", size: 2 }, ..... ]
}
You'll need to loop through the array and show the Row component for each object.
Thus, your Table component should be something as follows:
render(){
return (
<div>{this.props.volume.map(vol => <Row img='eth' name='Ethereum' price='' volume={vol} change='' marketCap='' />) }</div>
)
}
If you make the ajax call in componentDidMount, then React will rerender when the state changes (https://facebook.github.io/react/docs/react-component.html#componentdidmount). But you still have to anticipate that the volume and ticker props will be empty until the request resolves and React rerenders.