React component issues with state, setState and render - reactjs

I am building a React based project for study purposes. I am stuck on making a table component, that renders itself and then sends ajax request to mbaas backend to get all book entries and fill each on a new row. Here is what I've come up so far. Please forgive the large chunk of code, but since I don't yet fully understand the interactions between methods, state and render() here is the whole class:
class BooksTable extends Component {
constructor(props) {
super(props);
this.state = {
books: []
};
this.updateState = this.updateState.bind(this);
this.initBooks = this.initBooks.bind(this);
}
componentDidMount() {
let method = `GET`;
let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}`;
let headers = {
"Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
"Content-Type": `application/json`
};
let request = {method, url, headers};
$.ajax(request)
.then(this.initBooks)
.catch(() => renderError(`Unable to connect. Try again later.`));
}
deleteBook(id) {
let method = `DELETE`;
let url = consts.serviceUrl + `/appdata/${consts.appKey}/${consts.collection}/${id}`;
let headers = {
"Authorization": `Kinvey ${sessionStorage.getItem(`authToken`)}`,
"Content-Type": `application/json`
};
let request = {method, url, headers};
$.ajax(request)
.then(() => this.updateState(id))
.catch(() => renderError(`Unable to delete, something went wrong.`));
}
updateState(id) {
for (let entry of this.state.books.length) {
if (entry.id === id) {
// Pretty sure this will not work, but that is what I've figured out so far.
this.state.books.splice(entry);
}
}
}
initBooks(response) {
console.log(`#1:${this.state.books});
console.log(`#2:${this});
for (let entry of response) {
this.setState({
books: this.state.books.concat([{
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
}])
}, () => {
console.log(`#3${this.state.books}`);
console.log(`#4${this}`);
});
}
}
render() {
return (
<div id="content">
<h2>Books</h2>
<table id="books-list">
<tbody>
<tr>
<th>Title</th>
<th>Author</th>
<th>Description</th>
<th>Actions</th>
</tr>
{this.state.books.map(x =>
<BookRow
key={x.id}
name={x.name}
author={x.author}
description={x.description}
price={x.price}
publisher={x.publisher} />)}
</tbody>
</table>
</div>
);
}
}
Now the BookRow is not very interesting, only the onClick part is relevant. It looks like this:
<a href="#" onClick={() => this.deleteBook(this.props.id)}>{owner? `[Delete]` : ``}</a>
The Link should not be visible if the logged in user is not publisher of the book. onClick calls deleteBook(id) which is method from BookTable. On successful ajax it should remove the book from state.books (array) and render.
I am particularly confused about the initBooks method. I've added logs before the loop that populates the state and as callbacks for when the state is updated. Results from log#1 and log#3 are identical, same for logs#2#4. Also if I expand log#2 (before setState) or log#4, both of those show state = [1]. How does this make sense? Furthermore if you take a look at logs#1#3 - they print [ ]. I am probably missing some internal component interaction, but I really cant figure out what.
Thanks.

The setState doesn't immediately update the state. So in the second iteration of your for loop, you wont be getting the new state. So make your new book list first and then set it once the new list is prepared.
Try this:
initBooks(response) {
console.log(this.state.books, "new books not set yet")
let newBooks = []
for (let entry of response) {
newBooks.push({
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
})
}
this.setState({books: [...this.state.books, newBooks]}, () => {
console.log(this.state.books, "new books set in the state")
})
}

try this:
initBooks(response = {}) {
const books = Object.keys(response);
if (books.length > 0) {
this.setState((prevState) => {
const newBooks = books.reduce((acc, key) => {
const entry = response[key];
return [ ...acc, {
id: entry._id,
name: entry.name,
author: entry.author,
description: entry.description,
price: Number(entry.name),
publisher: entry.publisher
}];
}, prevState.books);
return { books: newBooks };
});
}
}
What I did here?
setState only if needed (ie only if there is data from API response)
avoids state mutation, no setState inside loop
using a function ((prevState, props) => newState) to ensure atomic
update for reliability.
Points to ponder:
Don't mutate your state
Avoid setState inside a loop; instead prepare the new state object and do a one-time setState)
If you want to access the previous state while calling setState, it is advised to do it like this:
this.setState((prevState, props) => {
return { counter: prevState.counter + props.increment };
});
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value.

Related

Why can't I push this information from one react component to another?

Still getting used to React and wrapping my head around using props properly.
But essentially I have this react component and i'm trying to take all the information in "drinkInfo" and use props to send it all to the CardInfo component so I can render it there. Yet i'm getting "Props is not defined" and i'm not really sure why.
class Render extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
// call the drinks endpoint (via axios)
let options = {
method: 'GET',
url: MYURL,
};
// let drinks = array of drinks
let drinksFromDatabase = [];
// axios request
axios.request(options)
.then( (response) => {
console.log(response);
drinksFromDatabase = response.data
console.log("Drinks from Database are:")
console.log(drinksFromDatabase)
this.setState({ drinks : drinksFromDatabase})
})
.catch(function (error) {
console.log(error);
});
}
render() {
console.log("this.state is",[this.state])
let stateArray = [this.state]
if (stateArray[0] == null) {
console.log("returning with nthing")
return <div></div>
}
let firstElement = stateArray[0];
let drinks = firstElement.drinks;
let drinkChoice = this.props.reduxState.drinkChoice
console.log("Drink Choice is: " , drinkChoice)
let drinkInfo = {
type: props.drinks.type,
name: props.drinks.name,
manufacturer: props.drinks.manufacturer,
rating: props.drinks.rating,
date: props.drinks.date,
description: props.drinks.description,
favorite: props.drinks.favorite
}
console.log("Drink info is: " , drinkInfo)
let cardComponents = drinks.map((drink) =>{
if (drink.type === drinkChoice) {
return (<InfoCard {props.drinkInfo} />)
} else {
return <div>Nothing to Report</div>
}})
return (
<div>
<div>{cardComponents}</div>
</div>
)
}
}
export default Render
I see you are passing in props to your component. But you are getting the response for your drinks after the component has mounted with ComponentDidMount().
Within ComponentDidMount() this.SetState({drinks: drinksFromDatabase}) is initialized so when you console.log(drinks), the drinks state is logged.
You may want to re-read the docs:
https://reactjs.org/docs/components-and-props.html
on props and states. Since you set the state you will need to change:
let drinkInfo = {
type: props.drinks.type,
name: props.drinks.name,
manufacturer: props.drinks.manufacturer,
rating: props.drinks.rating,
date: props.drinks.date,
description: props.drinks.description,
favorite: props.drinks.favorite
}
to:
let drinkInfo = {
type: this.state.drinks.type,
name: this.state.drinks.name,
manufacturer: this.state.drinks.manufacturer,
rating: this.state.drinks.rating,
date: this.state.drinks.date,
description: this.state.drinks.description,
favorite: this.state.drinks.favorite
}
Since you're iterating by using drinks.map(drink => {...}) you can send the values of each iterable drink within the component:
let cardComponents = drinks.map((drink) =>{
if (drink.type === drinkChoice) {
return (<InfoCard props={drinkInfo} />)
} else {
return <div>Nothing to Report</div>
}})
You then can access the props from your InfoCard props... but honestly drinkInfo seems to contain the same info as your state drink so I'm not sure why you create an object of an objects values which you can then change the former to:
let cardComponents = drinks.map((drink) =>{
if (drink.type === drinkChoice) {
return (<InfoCard props={this.state.drinks} />)
} else {
return <div>Nothing to Report</div>
}})
Which would pass all your drink values without having to create another object. Hopefully, this should get you in the right direction.
you are using react class component, so you forgot adding this to access the props.
this.props.drinks.type
this.props.drinks.name
.....

React - Render happening before data is returned and not updating component

I can't get this to work correctly after several hours.
When creating a component that needs data from Firebase to display, the data is returning after all actions have taken place so my component isn't showing until pressing the button again which renders again and shows correctly.
Currently my function is finishing before setState, and setState is happening before the data returns.
I can get setState to happen when the data is returned by using the callback on setState but the component would have already rendered.
How do i get the component to render after the data has returned?
Or what would the correct approach be?
class CoffeeList extends Component {
constructor(props) {
super(props);
this.state = {
coffeeList: [],
}
}
componentDidMount() {
this.GetCoffeeList()
}
GetCoffeeList() {
var cups = []
coffeeCollection.get().then((querySnapshot) => {
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
})
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
}
render() {
const coffeeCups = this.state.coffeeList;
console.log("Rendering component")
return (
<div className="coffee">
<p> This is the Coffee Component</p>
{coffeeCups.map((c) => {
return (
<CoffeeBox name={c.name} />
)
})}
</div >
)
}
}
Thanks
The problem is that you set the state before the promise is resolved. Change the code in the following way:
GetCoffeeList() {
coffeeCollection.get().then((querySnapshot) => {
const cups = []
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
})
}

React Native: Duplicate items in component state

I have a two screens in a StackNavigator, one with a FlatList that displays data retrieved from Firestore, and another to add a new data to the database. After returning from the second screen in the stack via navigation.goBack(), the new item should be appended to the list. Instead, the entire state with the new item is being appended to the old state. The database data contains no duplicates and upon refresh, the list contains the correct elements.
I can't tell if I'm misunderstanding the component lifecycle or the query itself so I would appreciate any help.
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
this.setState({chatData: [...this.state.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]})
})
});
})
}
state after adding a course and returning to the list screen (duplicate element)
When setting state try to use the prevState callback function. Like so:
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
// We use the parameter of the first argument of setState - prevState
this.setState(prevState => ({chatData: [...prevState.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]}))
})
});
})
}
Because you want to spread the state that was there previously like an accumulator with the new data you're getting from firestore. If you do it with this.state then you'll be adding it again since it concerns the data that is already in the state and therefore repeated/duplicated. Let me know if it helps.
Try to create a new array with unique values and assign that to chatData
componentDidMount = () => {
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
/**
* create a new array with unique values
*/
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}
Hope this helps you. Feel free for doubts.
Here is my eventual solution. I'm using react-navigation addListener to call the firestore API whenever the first screen is switched to and clearing the state when the second screen is navigated to. I also switched from onSnapshot() to get() for my firestore calls.
class Main extends React.Component {
state = { currentUser: null, chatData:[]}
componentDidMount = () => {
console.log('A - component did mount')
// definitely works
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus',
payload => {
console.log('A- focus')
this.readCourseData()
})
this.willBlurSubscription = this.props.navigation.addListener(
'willBlur',
payload => {
console.log('A- blur')
this.setState({chatData: []})
})
}
componentWillUnmount() {
console.log('A - component will unmount')
this.willFocusSubscription.remove();
this.willBlurSubscription.remove();
// Remove the event listener
}
readCourseData = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).get().then((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).get().then((doc) => {
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}

Duplicate lists printing after adding new document to firestore collection

When the component loads, it pulls all the data from a specific collection in firestore and renders it just fine. then when i add a new document, it adds that document but then prints them all out (including the new one) under the previous list.
This is my first real react project and I am kinda clueless. I have tried resetting the state when the component loads and calling the method at different times.
import React, { Component } from 'react';
// Database Ref
import Firebase from '../../Config/Firebase';
// Stylesheet
import '../View-Styles/views.scss';
// Componenents
import Post from '../../Components/Post/Post';
import EntryForm from '../../Components/EntryForm/EntryForm';
export class Gym extends Component {
constructor() {
super();
this.collection = 'Gym';
this.app = Firebase;
this.db = this.app.firestore().collection('Gym');
this.state = {
posts: []
};
this.addNote = this.addNote.bind(this);
};
componentDidMount() {
this.currentPosts = this.state.posts;
this.db.onSnapshot((snapshot) => {
snapshot.docs.forEach((doc) => {
this.currentPosts.push({
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.setState({
posts: this.currentPosts
});
});
};
addNote(post) {
// console.log('post content:', post );
this.db.add({
body: post
})
}
render() {
return (
<div className="view-body">
<div>
{
this.state.posts.map((post) => {
return(
<div className="post">
<Post key={post.id} postId={post.id} postTitle={post.title} postBody={post.body} />
</div>
)
})
}
</div>
<div className="entry-form">
<EntryForm addNote={this.addNote} collection={this.collection} />
</div>
</div>
)
};
};
export default Gym;
I am trying to get it to only add the new document to the list, rather than rendering another complete list with the new document. no error messages.
Your problem lies with your componentDidMount() function and the use of onSnapshot(). Each time an update to your collection occurs, any listeners attached with onSnapshot() will be triggered. In your listener, you add each document in the snapshot to the existing list. While this list starts off empty, on every subsequent change, the list is appended to with all of the documents in the collection (including the old ones, not just the changes).
There are two ways to handle the listener's snapshot when it comes in - either empty the existing list and recreate it on each change, or only handle the changes (new entries, deleted entries, etc).
As a side note: When using onSnapshot(), it is recommended to store the "unsubscribe" function that it returns (e.g. this.stopChangeListener = this.db.onSnapshot(...)). This allows you later to freeze the state of your list without receiving further updates from the server by calling someGym.stopChangeListener().
Recreate method
For simplicity, I'd recommend using this method unless you are dealing with a large number of items.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = snapshot.docs.map((doc) => {
return {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.currentPosts = postsArray;
this.setState({
posts: postsArray
});
});
};
Replicate changes method
This method is subject to race-conditions and opens up the possibility of desyncing with the database if handled incorrectly.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = this.currentPosts.clone() // take a copy to work with.
snapshot.docChanges().forEach((change) => {
var doc = change.document;
var data = {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
switch(change.type) {
case 'added':
// add new entry
postsArray.push(data)
break;
case 'removed':
// delete potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1)
}
break;
case 'modified':
// update potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1, data)
} else {
postsArray.push(data)
}
}
});
this.currentPosts = postsArray; // commit the changes to the copy
this.setState({
posts: postsArray
});
});
};
As a side note: I would also consider moving this.currentPosts = ... into the this.setState() function.
When you use onSnapshot() in Cloud Firestore,you can print only the added data. For your code, it should be something like:
snapshot.docChanges().forEach(function(change) {
if (change.type === "added") {
console.log("Newly added data: ", change.doc.data());
}
Also, Firestore does not load the entire collection everytime a new data is added, the documents are cached and will be reused when the collection changes again.
For more info, you can checkout this answer.

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Resources