Updating state in react - reactjs

I couldn't get the idea of updating state in react.
state = {
products : []
};
handleProductUpVote = (productId) => {
const nextProducts = this.state.products.map((product)
=> {
if (product.id === productId) {
return Object.assign({}, product, {
votes: product.votes + 1,
});
} else {
return product;
}
});
this.setState({
products: nextProducts,
});
}
Why we need to clone the object? Can we simply write
if (product.id === productId) {
product.votes =
product.votes + 1;
});

State updates can happen asynchronously, and delegating actual change of the state helps to ensure that things won't go wrong. Passing a new Object with the change also lets react make a (shallow) check to see which parts of the state were updated, and which components relying on it should be rerendered.

Related

In React object updates but does not pass updated object to next function

I am using React and have the following object:
const [records, setRecords] = useState(
[
{id: 1, correct: false},
{id: 2, correct: false},
{id: 3, correct: false},
{id: 4, correct: false},
{id: 5, correct: false}
]);
To update the object I have the following:
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
}
Here's the problem:
I want to run another function called isComplete after it but within the Handler function, that uses the changed records object, but it appears to use the original unchanged 'records' object (which console.log confirms).
e.g.
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
isComplete(records);
}
Console.log(records) confirms this. Why does it not use the updated records since the isComplete function runs after the update, and how can I get it to do this?
Try renaming the function as React sees no change in the object and likewise when you are using an array or object in a state. Try to copy them out by storing them in a new variable.
setRecords(
const newRecords = records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
//seting this now triggers an update
setRecords(newRecords);
);
Then as per react documentation it's better to listen to changes with lifecycle methods and not setting state immediately after they are changed because useState is asynchronous.
so use useEffect to listen to the changes to set is Complete
useEffect(() => {
isComplete(records)
}, [records])
I hope this helps you?
This is due to the fact that setState is not actually synchronous. The stateful value is not updated immediately when setState is called, but on the next render cycle. This is because React does some behind the scenes stuff to optimise re-renders.
There are multiple approaches to get around this, one such approach is this:
If you need to listen to state updates to run some logic you can use the useEffect hook.
useEffect(() => {
isComplete(records)
}, [records])
This hook is pretty straightforward. The first argument is a function. This function will run each time if one of the variables in the dependency array updates. In this case it will run each time records update.
You can modify above function onCorrectAnswerHandler to hold the updated records in temporary variable and use to update state and call isComplete func
const onCorrectAnswerHandler = (id, correct) => {
let _records = records.map((record) => {
if (record.id === id) {
return {
id: id,
correct: true
};
} else {
return record;
}
})
setRecords(_records);
isComplete(_records);
}
try this please.
const onCorrectAnswerHandler = (id) => {
records.forEach(r =>{
if(r.id == id) r.correct=true;
});
setRecords([...records]);
}

React - remove nested array item using setState function call

I am trying to remove a (semi) deeply nested item from an array using setState but it doesn't seem to be working. My state is structured as follows:
state = {
currentSeries: null,
currentRowIndex: null,
rows: [
{
id: shortid.generate(),
nodes: [],
series: [], // array with item I want to remove
},
],
};
and my remove item call:
onRemoveModelElementClick = (rowId, modelElementId) => {
this.setState((prevState) => {
const index = prevState.rows.findIndex(x => x.id === rowId);
const series = prevState.rows[index].series.filter(s => s.id !== modelElementId);
return series;
});
};
I tried spreading the remaining state is several ways but it does not seem to update properly. I the rowId and modelElementId are correct and I can verify they do filter the correct item out. I am just having trouble on what to return. I know it is something simple but for the life of me I can't see it.
My recommendation would be to use .map to make things are bit easier to digest. You can then write it like so:
onRemoveModelElementClick = (rowId, modelElementId) => {
const updatedRowsState = this.state.rows.map(row => {
// this is not the row you're looking for so return the original row
if (row.id !== rowId) {
return row;
}
const filteredSeries = row.series.filter(s => s.id !== modelElementId);
return {
// spread properties (id, node, series)
...row,
// overwrite series with item filtered out
series: filteredSeries,
};
});
// since rest of the state doesn't change, we only need to update rows property
this.setState('rows', updatedRowsState);
}
Hope this helps and let me know if you have any questions.
I think the issue here is how your code uses setState. The setState function must return an object. Assuming your filtering functions are correct as you describe, return an object to update the state:
return { series };
setState documentation
Here is what I did to get it working in case it can help someone else:
onRemoveModelElementClick = (rowId, modelElementId) => {
this.setState((prevState) => {
const updatedRowState = prevState.rows.map((row) => {
if (row.id !== rowId) {
return row;
}
const filteredSeries = row.series.filter(s => s.id !== modelElementId);
return {
...row,
series: filteredSeries,
};
});
return {
rows: updatedRowState,
};
});
};
All credit to Dom for the great idea and logic!

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

Using componentWillUpdate with switch statements

I am using React-Native and React-Native-Firebase and am trying to have my Events component make a different Firebase query (and then update redux store) depending what the value of the activityType prop is.
Here is the parent component which is working just fine. It updates state.eventType when I change the dropdown value and passes the value into <Events />.
let eventTypes = [{value: 'My Activity'}, {value: 'Friend Activity'}, {value: 'All Activity'}];
state = {
showEventFormModal: false,
eventType: 'Friend Activity'
}
<View style={styles.container}>
<Dropdown
data={eventTypes}
value={this.state.eventType}
containerStyle={{minWidth: 200, marginBottom: 20}}
onChangeText={val => this.setState({eventType: val})}
/>
<Events activityType={this.state.eventType}/>
</View>
And here is Events component. Using a Switch statement to determine which activityType was passed into props. The issue I am having is an infinite loop because within each case statement I am dispatching the action to update the store which causes a rerender and the componentWillUpdate() to retrigger. What I am trying to understand is what the optimal way to handle this problem is? Because clearly my method now does not function properly. Is there a common react pattern to achieve this?
// GOAL: when this components props are updated
// update redux store via this.props.dispatch(updateEvents(events))
// depending on which type of activity was selected
componentWillUpdate() {
let events = [];
switch(this.props.activityType) {
case 'Friend Activity': // get events collections where the participants contains a friend
// first get current users' friend list
firebase.firestore().doc(`users/${this.props.currentUser.uid}`)
.get()
.then(doc => {
return doc.data().friends
})
// then search the participants sub collection of the event
.then(friends => {
firebase.firestore().collection('events')
.get()
.then(eventsSnapshot => {
eventsSnapshot.forEach(doc => {
const { type, date, event_author, comment } = doc.data();
let event = {
doc,
id: doc.id,
type,
event_author,
participants: [],
date,
comment,
}
firebase.firestore().collection('events').doc(doc.id).collection('participants')
.get()
.then(participantsSnapshot => {
for(let i=0; i<participantsSnapshot.size;i++) {
if(participantsSnapshot.docs[i].exists) {
// if participant uid is in friends array, add event to events array
if(friends.includes(participantsSnapshot.docs[i].data().uid)) {
// add participant to event
let { displayName, uid } = participantsSnapshot.docs[i].data();
let participant = { displayName, uid }
event['participants'].push(participant)
events.push(event)
break;
}
}
}
})
.then(() => {
console.log(events)
this.props.dispatch(updateEvents(events))
})
.catch(e => {console.error(e)})
})
})
.catch(e => {console.error(e)})
})
case 'My Activity': // get events collections where event_author is the user
let counter = 0;
firebase.firestore().collection('events').where("event_author", "==", this.props.currentUser.displayName)
.get()
.then(eventsSnapshot => {
eventsSnapshot.forEach(doc => {
const { type, date, event_author, comment } = doc.data();
let event = {
doc,
id: doc.id,
type,
event_author,
participants: [],
date,
comment,
}
// add participants sub collection to event object
firebase.firestore().collection('events').doc(event.id).collection('participants')
.get()
.then(participantsSnapshot => {
participantsSnapshot.forEach(doc => {
if(doc.exists) {
// add participant to event
let { displayName, uid } = doc.data();
let participant = { displayName, uid }
event['participants'].push(participant)
}
})
events.push(event);
counter++;
return counter;
})
.then((counter) => {
// if all events have been retrieved, call updateEvents(events)
if(counter === eventsSnapshot.size) {
this.props.dispatch(updateEvents(events))
}
})
})
})
case 'All Activity':
// TODO
// get events collections where event_author is the user
// OR a friend is a participant
}
}
Updating the store is best to do on the user action. So, I'd update the store in the Dropdown onChange event vs. in the componentWillUpdate function.
I've figured out what feels like a clean way to handle this after finding out I can access prevProps via componentDidUpdate. This way I can compare the previous activityType to the current and if they have changed, then on componentDidUpdate it should call fetchData(activityType).
class Events extends React.Component {
componentDidMount() {
// for initial load
this.fetchData('My Activity')
}
componentDidUpdate(prevProps) {
if(this.props.activityType !== prevProps.activityType) {
console.log(`prev and current activityType are NOT equal. Fetching data for ${this.props.activityType}`)
this.fetchData(this.props.activityType)
}
}
fetchData = (activityType) => {
//switch statements deciding which query to perform
...
//this.props.dispatch(updateEvents(events))
}
}
https://reactjs.org/docs/react-component.html#componentdidupdate

How to update state in react.js?

I'd like to ask if there is any better way to update state in react.js.
I wrote this code below but just updating state takes many steps and I wonder if I'm doing in a right way.
Any suggestion?
How about immutable.js? I know the name of it but I've never used it and don't know much about it.
code
toggleTodoStatus(todoId) {
const todosListForUpdate = [...this.state.todos];
const indexForUpdate = this.state.todos.findIndex((todo) => {
return todo.id === todoId;
});
const todoForUpdate = todosListForUpdate[indexForUpdate];
todoForUpdate.isDone = !todoForUpdate.isDone;
this.setState({
todos: [...todosListForUpdate.slice(0, indexForUpdate), todoForUpdate, ...todosListForUpdate.slice(indexForUpdate + 1)]
})
}
You are using an extra step that you don't need. Directly setting value to the cloned object and restting back to state will work
toggleTodoStatus(todoId) {
const todosListForUpdate = [...this.state.todos];
const indexForUpdate = this.state.todos.findIndex((todo) => {
return todo.id === todoId;
});
todosListForUpdate[indexForUpdate].isDone = !todosListForUpdate[indexForUpdate].isDone;
this.setState({
todos: todosListForUpdate
})
}

Resources