How to update one part of state with setState - reactjs

i'm trying to use setState to update one property of a sub object of the state. What is the correct way to do this? I want to access the state and define which part I want to update, as opposed to update the entire state with a new state. Hope that makes sense...
class BooksApp extends React.Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll().then((books) => {
this.setState({books})
})
}
selectStateUpdate = (book,shelf) => {
this.updateShelf(book, shelf);
}
updateShelf = (book, shelf) => {
BooksAPI.update(book, shelf)
.then(() => {
for (var i=0; this.state.length < i; i++) {
if (this.state.title === book.title) {
this.setState({
books[i].shelf: book.shelf
})
}
}
})
}

Try to change your state changing part to:
this.setState({
books: this.state.books.map((item, index) =>
index === i ? {...item, shelf: book.shelf} : item
)
})

Related

React Native & Flatlist, array items not available (.length === 0)

I'm experience an issue in react native where when the array i have in my state is behaving very strangely. It says i have content in the array, but the array.length === 0. Here is a screenshot of my chatrooms array to show you what i mean (this is at the beginning of my FlatList component (receiving props.chatrooms array)
console.log('props.chatrooms')
console.log(props.chatrooms)
console.log(props.chatrooms.length)
I'm grabbing all the private and group chatIds, then making a call to those chatrooms to get the details of the users. Here is how i'm pulling the information:
componentDidMount() {
this.setState({ isLoading: true })
this.props.navigation.addListener('focus', () => {
//? Refactor to pull userId from Redux Store
let friendsArray = []
let chatrooms = []
let privateChatIds = []
let groupChatIds = []
AsyncStorage.getItem('userId').then(userId => {
let currentUser = {}
firebase.firestore().collection('users').doc(userId).get().then(snapshot => {
currentUser = snapshot.data()
}).then(() => this.setState({ currentUser: currentUser }))
firebase.firestore().collection('users').doc(userId).collection('friends').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
friendsArray.push(doc.data())
privateChatIds.push(doc.data().chatroomId)
});
} else {
console.log('Nothing to Grab')
}
}).then(() => this.setState({ friends: friendsArray }))
// get each array and put into the chatroomsArray
firebase.firestore().collection('users').doc(userId).collection('groupChatrooms').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
groupChatIds.push(doc.data().key)
})
}
//! will want to limit this number to like 10-15 or something
}).then(() => {
for (let i = 0; i < privateChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('private').collection(privateChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
for (let i = 0; i < groupChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('group').collection(groupChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
}).then(() => {
console.log('chatrooms')
console.log(chatrooms)
this.setState({ chatrooms, isLoading: false })
})
})
})
}
is my code perfect? no, probably pretty terrible performance-wise, but i'm just trying to get it to work. I figure i'm resetting the chatrooms array or something, but why would it log as an array of items i want but then immediately after log as .length === 0??? Help!
Update: I'm seeing this content render from the flatlist initially and then it disappears

I cant update my component state.. Do somebody understand how it fix?

I cant understand why my renderMovies() function dont wanna update my component state.data and i cant render component on my screen ?!
Everithing goes ok until renderMovies function.. I think this.setState(newState) in my fetchPostData function is working incorrect... Do somebody know how to fix it? I tried different ways but i cant solve this issue.
class Movies extends React.Component {
constructor(props) {
super(props)
this.state = { data: {}}
this.fetchPostData = this.fetchPostData.bind(this)
this.renderMovies = this.renderMovies.bind(this)
this.populatePageAfterFetch = this.populatePageAfterFetch.bind(this)
}
componentDidMount() {
this.fetchPostData()
}
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
let objLength = Object.keys(myJSON).length
let newState = this.state;
for (let i = 0; i < objLength; i++) {
let objKey = Object.values(myJSON)[i].title.rendered;
// console.log(objKey)
let currentMovie = newState.data[objKey];
currentMovie = {};
currentMovie.name = Object.values(myJSON)[i].title.rendered;
currentMovie.description = Object.values(myJSON)[i].content.rendered;
currentMovie.featured_image = Object.values(myJSON)[i]['featured_image_url'];
currentMovie.genre = Object.values(myJSON)[i]['genre'];
}
this.setState(newState)
})
}
renderMovies() {
if(this.state.data) {
const moviesArray = Object.values(this.state.data)
console.log(moviesArray)
return Object.values(moviesArray).map((movie, index) => this.populatePageAfterFetch(movie, index))
}
}
populatePageAfterFetch(movie, index) {
if (this.state.data) {
return (
<div key={index} index={index}>
<h2>{movie.title}</h2>
<h3>{movie.genre}</h3>
<p>{movie.description}</p>
</div>
)
}
}
render() {
return (
<div>
<h1>Movies</h1>
<div>{this.renderMovies()}</div>
</div>
)
}
}
When i try to console.log(moviesArray) it show me:
Issue
You save current state into a variable named newState, never update it, and then save the same object reference back into state. React state never really updates.
let newState = this.state;
for (let i = 0; i < objLength; i++) {
...
}
this.setState(newState);
Additionally you mutate state
let currentMovie = newState.data[objKey];
currentMovie = {};
But this doesn't work either since initial state is an empty object so newState.data[objKey] is aways undefined. (so nothing is ever actually mutated)
Solution
It appears as though you intended to map the myJSON data/values into movie objects to update this.state.data. May I suggest this solution. The key is to always create new object references for any object you update.
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
this.setState(prevState => ({
// array::reduce over the JSON values
data: Object.values(myJSON).reduce((movies, movie) => {
// compute movie key
const name = movie.title.rendered;
return {
...movies,
[name]: {
...movies[name], // copy any existing movie properties
// merge in new/updated properties
name,
description: movie.content.rendered,
featured_image: movie.featured_image_url,
genre: movie.genre,
},
}
}, { ...prevState.data }) // use previous state as initial value for reduce
}))
})
}

React - update nested state (Class Component)

I'm trying to update a nested state. See below. The problem is that upon clicking on a category checkbox, instead of updating the {categories: ....} object in state, it creates a new object in state:
class AppBC extends React.Component {
constructor(props) {
super(props)
this.state = {
products: [],
categories: []
}
this.handleSelectCategory = this.handleSelectCategory.bind(this);
}
componentDidMount() {
this.setState({
products: data_products,
categories: data_categories.map(category => ({
...category,
selected: true
}))
});
}
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState.categories.map(
category => {
if(category.id === id){
return {
...category,
selected: !category.selected,
}
}else{
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
} // handleSelectCategory
render() {
return(
<div className="bc">
<h1>Bare Class Component</h1>
<div className="main-area">
<Products categories={this.state.categories} products={this.state.products} />
<Categories
categories={this.state.categories}
handleSelectCategory={this.handleSelectCategory}
/>
</div>
</div>
);
};
Initial state before clicking (all categories are selected):
After clicking on an a checkbox to select a particular category, it saves a new object to state (correctly reflecting the category selection) instead of updating the already existin categories property:
Change your update to:
handleSelectCategory(id) {
this.setState(prevState => ({
...prevState,
categories: prevstate.categories.map(
category => {
if (category.id === id) {
return {
...category,
selected: !category.selected,
}
} else {
return category;
} // else
} // category
) // map
}) // prevState function
) // setState
}
I prefer this way, it's more easy for reading
handleSelectCategory(id) {
const index = this.state.categories.findIndex(c => c.id === id);
const categories = [...this.state.categories];
categories[index].selected = !categories[index].selected;
this.setState({ categories });
}
If your purpose is to only change selected property on handleSelectCategory function,
Then you could just do it like
run findIndex on array and obtain index for id match from array of objects.
update selected property for that index
Code:
handleSelectCategory(id) {
let targetIndex = this.state.categories.findIndex((i) => i.id === id);
let updatedCategories = [...this.state.categories];
if (targetIndex !== -1) {
// this means there is a match
updatedCategories[targetIndex].selected = !updatedCategories[targetIndex].selected;
this.setState({
categories: updatedCategories,
});
} else {
// avoid any operation here if there is no "id" matched
}
}

Comparing PrevProps in componentDidUpdate

I am trying to detect when a prop has changed inside componentDidUpdate of a mounted component. I have a test (refreshData in the code below) that is working fine. Is it possible to SOMEHOW pass props in a way that aren't detected by componentDidUpdate(prevProps)?
In component.js:
componentDidUpdate(prevProps){
//works fine
if ( this.props.refreshData !== prevProps.refreshData ) {
if ( this.props.refreshData )
this.refreshData();
}
//these two arent calling
if ( this.props.selectedCountries !== prevProps.selectedCountries ) {
if ( this.props.selectedCountries )
console.log('updated selected countries');
}
if ( this.props.selectedLocations !== prevProps.selectedLocations ) {
console.log('updated selected locations');
}
}
and in App.js passing the props like:
selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {
//console.log(type);
//console.log(lng);
//console.log(lat);
//console.log(polydata);
let selectedType = 'selected' + type;
let previousState = [];
if (clear) {
this.setState({
selectedCountries: [],
selectedLocations: [],
selectedServices: [],
selectedPoints: [],
mapCenter: [lng, lat],
locationGeoCoords: [polydata]
})
previousState.push(id);
} else {
previousState = this.state[selectedType];
if (previousState.indexOf(id) === -1) {
//push id
previousState.push(id);
} else {
//remove id
var index = previousState.indexOf(id)
previousState.splice(index, 1);
}
}
if (type === "Countries") {
this.setState({
selectedCountries: previousState,
refreshData: true,
})
} else if (type === "Locations") {
this.setState({
selectedLocations: previousState,
refreshData: true
})
} else if (type === "Points") {
this.setState({
selectedPoints: previousState,
refreshData: true
})
}
}
render() {
return (
<component
selectedCountries={this.state.selectedCountries}
selectedLocations={this.state.selectedLocations}
refreshData={this.state.refreshData} />
}
}
Hi :) as noted in my comment, the issue is in your App.js file - you are mutating an array. In other words, when you THINK you are creating a new array of selected countries to pass down, you are actually updating the original array, and so when you go to do a comparison you are comparing the two exact same arrays ALWAYS.
Try updating your App.js like so -
selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {
//console.log(type);
//console.log(lng);
//console.log(lat);
//console.log(polydata);
let selectedType = 'selected' + type;
let previousState = [];
if (clear) {
this.setState({
selectedCountries: [],
selectedLocations: [],
selectedServices: [],
selectedPoints: [],
mapCenter: [lng, lat],
locationGeoCoords: [polydata]
})
previousState.push(id);
} else {
previousState = [].concat(this.state[selectedType]);
if (previousState.indexOf(id) === -1) {
//push id
previousState.push(id);
} else {
//remove id
var index = previousState.indexOf(id)
previousState.splice(index, 1);
}
}
if (type === "Countries") {
this.setState({
selectedCountries: previousState,
refreshData: true,
})
} else if (type === "Locations") {
this.setState({
selectedLocations: previousState,
refreshData: true
})
} else if (type === "Points") {
this.setState({
selectedPoints: previousState,
refreshData: true
})
}
}
render() {
return (
<component
selectedCountries={this.state.selectedCountries}
selectedLocations={this.state.selectedLocations}
refreshData={this.state.refreshData} />
}
}
The only difference is the line where you set previousState - I updated it to be
previousState = [].concat(this.state[selectedType]);
By adding the [].concat I am effectively creating a NEW array each time and so then when you apply your changes to the array via push/splice you will be only modifying the NEW array. Then the comparison will work properly once you pass it down as props :)
For your reading interest, I found a post that talks about this a bit: https://medium.com/pro-react/a-brief-talk-about-immutability-and-react-s-helpers-70919ab8ae7c
selectedCountries and selectedLocations are array objects. The reference of it never changes. Instead check for the length.
componentDidUpdate(prevProps){
if ( this.props.refreshData !== prevProps.refreshData ) {
if ( this.props.refreshData )
this.refreshData();
}
if ( this.props.selectedCountries.length > prevProps.selectedCountries.length ) {
if ( this.props.selectedCountries )
console.log('updated selected countries');
}
if ( this.props.selectedLocations.length > prevProps.selectedLocations.length ) {
console.log('updated selected locations');
}
}
In the code snippet above, you seem to be making changes to this.state directly. State should be immutable. Always make sure, you concat to add and filter to delete the elements as they create a new array instead of mutating the original array in the state. I would do something in these lines.
Also it is a good practice to capitalize the component name.
selectLocation = (id, type, lng, lat, polydata, name, clear = false) => {
//console.log(type);
//console.log(lng);
//console.log(lat);
//console.log(polydata);
let selectedType = "selected" + type;
let previousState = [];
let updatedData = [];
if (clear) {
this.setState({
selectedCountries: [],
selectedLocations: [],
selectedServices: [],
selectedPoints: [],
mapCenter: [lng, lat],
locationGeoCoords: [polydata]
});
} else {
const data = this.state[selectedType];
if (data.indexOf(id) === -1) {
//push id
updatedData = [...data, id];
} else {
updatedData = data.filter((value) => value !== id);
}
}
if(type) {
this.setState({
[selectedType]: updatedData,
refreshData: true
});
}
};
render() {
return (
<component
selectedCountries={this.state.selectedCountries}
selectedLocations={this.state.selectedLocations}
refreshData={this.state.refreshData}
/>
);
}
}
did you make sure that the props of locations & countries are actually changing? If yes, the following code should work:
componentDidUpdate(prevProps) {
if (this.props.selectedCountries.length !== prevProps.selectedCountries.length) {
console.log("updated selected countries");
}
if (this.props.selectedLocations.length !== prevProps.selectedLocations.length) {
console.log("updated selected locations");
}
}
I created a fiddle for showcasing the effect here:
https://codesandbox.io/s/o580n8lnv5
I ran into this very issue. My solution was to send downstream to child components a clone of the state in question. This way when the state changes in App.js again, it will not affect the copy of the state passed down to children since those children were given a clone. In the previous props passed to async componentDidUpdate (prevProps) in child components, prevProps will be the clone that was originally handed down, and current props will be the most recent state changes made in App.js, which again is a clone, but prev and current props will be different.
Below is snipppet from App.render(), notice the value assign to the filter attribute, namely a clone the portion of the state in question:
...
<Routes onCategorySelect={this.handleCategorySelect}
onCategoryUnselect={this.handleCategoryUnselect}
onRouteLoad={this.handleRouteLoad} filter={this.cloneFilter(savedState.filter)}
updatedDimension={this.state.updatedDimension}/>
...
And this is the componentDidUpdate() of the child component:
async componentDidUpdate (prevProps) {
if (this.props.filter !== prevProps.filter && this.props.updatedDimension !== this.dimension) {
await this.updateChart()
}
}

Concat object to state object

I am trying to manage my local state whilst also updating an API which holds a list of books. In this setup, when the updateShelf method recieves a book and a shelf, it checks to see if book is already in the book state, if not it should concat the book param on the book state. Struggling to work out how to do this.
class BooksApp extends React.Component {
state = {
books: []
};
componentDidMount() {
console.log("MOUNTING");
BooksAPI.getAll().then(books => {
this.setState({ books });
});
}
selectStateUpdate = (book, shelf) => {
this.updateShelf(book, shelf);
};
updateShelf = (book, shelf) => {
BooksAPI.update(book, shelf).then(() => {
let bookscopy = { ...this.state.books };
console.log(bookscopy);
for (let i = 0; this.state.books.length > i; i++) {
if (this.state.books[i].title === book.title) {
bookscopy[i].shelf = shelf;
this.setState({ bookscopy });
} else
this.setState({
books: bookscopy.concat(book)
});
}
});
};
}
Project for reference > here.
you can leverage setState with function and do it like this
updateShelf = (book, shelf) => {
BooksAPI.update(book, shelf)
.then(() => {
this.setState(prevState => {
const updatedBook = prevState.books
.filter(b => b.title === book.title)
.map(_book => ({
..._book,
shelf
}))
return {
books: [
...prevState.books.filter(b => b.title !== book.title),
updatedBook
]
}
})
})
}
What it does ?
Gets the book and remap it with filter && reduce function
Returns new state composed with all books without the one which is update
Returns the new composed state
You try to update state on every loop iteration:
for (let i=0; this.state.books.length > i; i++) {
if (this.state.books[i].title === book.title) {
bookscopy[i].shelf = shelf;
this.setState({bookscopy})
} else
this.setState({
books: bookscopy.concat(book)
})
}
})
Try something like this:
updateShelf = (book, shelf) => {
BooksAPI.update(book, shelf).then(() => {
this.setState(prevState => {
const bookFromState = prevState.books.find(b => b.title === book.title);
if(bookFromState) {
return null
}
return {
books: [...prevState.books, book]
}
})
}

Resources