Please know that I am new to ReactJS with Redux.
I have list of passengers, and each passenger has list of flights. I would like to update the flight property, checkedIn with the action property isCheckedIn. How can I achieve that with in reducer?
reducer.js
export default function passengerReducer(
state = initialState.passengers,
action
) {
switch (action.type) {
case types.LOAD_PASSENGERS_SUCCESS:
return action.passengers;
case types.UPDATE_PASSENGER_SUCCESS:
console.log("action ", action.passengerData.passengerId);
console.log("state ", state);
return state
.filter(x => x.id == action.passengerData.passengerId)
.map(f => {
f.flights[0].checkedIn = action.passengerData.isCheckedIn
});
default:
return state;
}
}
The state contains array of objects. Each object also contains flights. At the moment, I am only focusing the first flight with in flights array.
The action contains isCheckedIn property. I would like to update checkedInproperty of the flights with isCheckedIn property from action.
So this is the piece of code in question I presume:
case types.UPDATE_PASSENGER_SUCCESS:
console.log("action ", action.passengerData.passengerId);
console.log("state ", state);
return state
.filter(x => x.id == action.passengerData.passengerId)
.map(f => {
f.flights[0].checkedIn = action.passengerData.isCheckedIn
});
You're (a) filtering the array only for the element you want to change, and (b) mapping that but not returning anything from your map function
Just (a) alone is bad -- you're going to change your entire state to only include the filtered items? I don't think that's what you intended. But then (b) means you're returning an array full of undefined
What you want to do, instead, is create a new array, var newArray = state.slice(0);
Then, find the index of the item you want to change the checked_in property of,
var index = newArray.findIndex(x => x.id == action.passengerData.passengerId);
var newPassenger = Object.assign({}, newArray[index]);
newPassenger.flights[0].checkedIn = action.passengerData.isCheckedIn;
newArray[index] = newPassenger;
return newArray;
So you've found the item you wanted to change, changed it, put it back in the array (this is the immutable way to do things, I think), and then returned the FULL array
Related
I have a state set as
const [filteredProducts, setFilteredProducts] = useState([]);
I want to be able to append to the end of that state. I am currently trying
products.forEach((product) => {
if (product.category === category) {
setFilteredProducts([...filteredProducts, product]);
}
});
It it looping through the products array correctly. I can even log the product after the setFilteredProducts and it logs the correct ones I want. I am calling this with an onClick.
Find all the products you want to add:
const productsToAdd = products.filter(product => product.category === category)
Then append them
setFilteredProducts((currentFilteredProducts) => ([...currentFilteredProducts, ...productsToAdd]));
The issue with your example is that filteredProducts may get stale after the first iteration. setFilteredProducts will not run synchronously, and filteredProducts keep the original value, until the re-render happen.
You would only append the last match to the existing filteredProducts array.
You can add all matches like so:
setFilteredProducts([...filteredProducts, ...products.filter((product) => product.category === category)]);
I'd recommend you do this in 2 steps:
Create an array of the new products you plan to add
let productsToAdd = [];
products.forEach((product) => {
if (product.category === category) {
productsToAdd.push(product);
}
});
Then combine the arrays and set state
setFilteredProducts([...filteredProducts, ...productsToAdd]);
I think you want what the ES6 built-in function does. You can rewrite your code to give you the the products that match the category like this:
const filteringTheProducts = products.filter(product => {
return product.category === category
})
setFilteredProducts(filteringTheProducts)
The result of the filtering will be the array of all the products that match that criteria.
Here is the documentation for .filter()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
The problem is, that setFilteredProducts doesn't immediately affect products. It's React's job to decide when to update the state. So when you loop over products, you'll probably ending up adding just the last item, because filteredProducts wasn't updated yet.
What you can do, is preparing an array of products to add:
const productsToAdd = products.filter(product => product.category === category);
And then append them:
setFilteredProducts([...products, ...productsToAdd]);
when I click on the button to sort the data about countries after the website is loaded then the data displays correclty. But when I want to sort it again by different value, the data doesn't update and the state also doesn't update, however, the function works well, as I can see that the results in the console are sorted correctly. What can be the reason for this weird behaviour? Thank you for your time.
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useState([]);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (val) => {
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={() => sortResults("deaths")}>Deaths</button>
<button onClick={() => sortResults("cases")}>Cases</button>
<button onClick={() => sortResults("recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;
Array.sort() method doesn't return new reference, it returns the same reference. So I assume in sortedCountries state is stored the same props.countries reference, that is why second button click doesn't set the state (it is the same reference), I can give you simple solution just change this line setSortedCountries(sortedResults) with this setSortedCountries([...sortedResults]) in this case copy of the sortedResults will be passed to setSortedCountries (new reference).
let sortedResults = countries.sort
this line is sorting the countries props and setting sortedResults to that pointer
Let us assume that pointer is held at mem |0001| for simplicity
After your first click, the function is fired, the prop is sorted, and sortedResults is set via setSortedCountries (set state).
This fires off the render that you desire, because the previous state was undefined, and now the state is pointing to |0001|
When your function runs again, the sort function fires off, does its work on |0001| and returns -- you guessed it -- |0001| but with your new sorted array.
When you go to set the state a second time, there wasn't actually any state changed because the previous country is |0001| and the country you want to change to is |0001|
So what can we do my good sir?
First I need you to think about the problem for a second, think about how you could solve this and try to apply some changes.
What you can try to do is copy the countries props to a new array pointer with the same values, and then sort it, and then set sortedCountries state to that list.
Does that make sense?
By the way, this is for similar reasons why if you try to setState directly on an new object state, the new object state will be exactly equal to the one you set. There isn't any real magic going on here. React does not automagically merge your previous object state and your new one, unless you tell it explicitly to do so.
In some sense you have told react to check differences between two states, an old country and a new country state (behind the scenes with the Vdom), but to react, those two states have no difference. And since the two object states have no difference, there will be no actual DOM changes.
You setting a pointer twice will therefore produce no actual changes.
You must therefore set state to an actual new pointer with new values, so the react virtual dom can compare the two states (the previous countries list) and the new countries list.
Hope that helps
-
It's a bit problem in React and another solution is by using the useReducer instead. such as below:
function reducer(state, action) {
return [...state, ...action];
}
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useReducer(reducer, []);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (e, val) => {
e.preventDefault();
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={(e) => sortResults(e, "deaths")}>Deaths</button>
<button onClick={(e) => sortResults(e, "cases")}>Cases</button>
<button onClick={(e) => sortResults(e, "recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(item){
item.expand = true;
this.setState({})
}
this.state.myArray.map((item) =>{
return <div onClick={()=>this.clickItem(item)}>{item.name}</div>
})
In React, i have a simple array of objects,
when i click on one of theses object, i want to change their prop and update the state, what is the proper way of doing this.
i feel like there could be a better way
You need to copy your state, update the copied state and the set the state.
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(key){
let items = this.state.myArray;
items[key].expand = true;
this.setState({items})
}
this.state.myArray.map((key, item) =>{
return <div onClick={()=>this.clickItem(key)}>{item.name}</div>
})
Okay, a couple of things.
You're mutating the state directly which is going to fail silently and you're also missing the key prop on your <div.
This is easily resolved though by using the data you have available to you. I don't know whether each name is unique but you can use that as your key. This helps React decide which DOM elements to actually update when state changes.
To update your item in state, you need a way to find it within the state originally, so if name is unique, you can use Array.prototype.find to update it.
clickItem(item) {
const targetIndex = this.state.items.find(stateItem => stateItem.name === item.name)
if (targetIndex === -1)
// Handle not finding the element
const target = this.state.items[targetIndex]
target.expand = !target.expand // Toggle instead of setting so double clicking works as expected.
this.setState({
items: this.state.items.splice(targetIndex, 1, target) // This replaces 1 item in the target array with the new one.
})
}
This will update state and re-render your app. The code is untested but it should work.
I trying trying to achieve the following: There is a textfield and once a user enters in a text, an object is created with the text assigned to a state property called 'commentText' which is located inside the 'comments' array which is inside the object (todo[0]) of 'todos' array. 'commentInput' is just a temporary storage for the input entered in the textfield, to be assigned to the 'commentText' of 'todo[0]' object's 'comments' array.
I retrieve the current state object via following:
const mapStateToProps=(state, ownProps)=>({
todo:state.todos.filter(todo=>todo.id==ownProps.params.id)
});
and dispatch and actions via:
function mapDispatchToProps(dispatch){
return{
actions: bindActionCreators(actions, dispatch)
}
So the retrieved object 'todo' has an array property named comments. I have a text field that has:
onChange={this.handleCommentChange.bind(this)}
which does:
handleCommentChange(event){
this.props.actions.updateComment(event.target.value)
}
Before handleCommentChange is called, the object 'todo[0]' is first fetched correctly:
But as soon as a text is inputted into the text field, onChange={this.handleCommentChange.bind(this)} is called and all of a sudden, 'todo[0]' state is all lost (as shown in the 'next state' log):
What may be the issue? Tried solving it for hours and hours but still stuck. Any guidance or insight would be greatly appreciated. Thank you in advance.
EDIT **:
{
this.props.newCommentsArray.map((comment) => {
return <Comment key={comment.id} comment={comment} actions={this.props.actions}/>
})
}
EDIT 2 **
case 'ADD_COMMENT':
return todos.map(function(todo){
//Find the current object to apply the action to
if(todo.id === action.id){
//Create a new array, with newly assigned object
return var newComments = todo.comments.concat({
id: action.id,
commentTxt: action.commentTxt
})
}
//Otherwise return the original array
return todo.comments
})
I would suspect that your reducer is not correctly updating the todo entry. It's probably replacing the contents of the entry entirely, rather than merging the incoming value in in some fashion.
edit:
Yup, after seeing your full code, your reducer is very much at fault. Here's the current code:
case 'ADD_COMMENT':
return todos.map(function(todo){
if(todo.id === action.id){
return todo.comments = [{
id: action.id,
commentTxt: action.commentTxt
}, ...todo.comments]
}
})
map() should be returning one item for every item in the array. Instead, you're only returning something if the ID matches, and even then, you're actually assigning to todo.comments (causing direct mutation) and returning the result of that statement (which might be undefined?).
You need something like this instead (which could be written shorter, but I've deliberately written it out long-form to clarify what's happening):
case 'ADD_COMMENT':
return todos.map(function(todo) {
if(todo.id !== action.id) {
// if the ID doesn't match, just return the existing objecct
return todo;
}
// Otherwise, we need to return an updated value:
// Create a new comments array with the new comment at the end. concat() will
// You could also do something like [newComment].concat(todo.comments) to produce
// a new array with the new comment first depending on how you want it ordered.
var newComments = todo.comments.concat({
id : action.id,
commentTxt : action.commentTxt
});
// Create a new todo object that is a copy of the original,
// but with a new value in the "comments" field
var newTodo = Object.assign({}, todo, {comments : newComments});
// Now return that instead
return newTodo;
});
I'm trying to add immutableJS to Mern.io. When I try to remove a post from my list of post then set it back in my state the state doesn't update.
case ActionTypes.DELETE_POST :
const removeArray = state.get('posts')
.filter((post) => post._id !== action.post._id)
console.log(removeArray.length)
state.set('posts', removeArray)
console.log(state)
return state;
In this example if I have an array of 5 I should be able to filter it out then set "posts" again with the new array. What I don't understand is that I can remove the object from the array and the removeArray will be one less than state.posts. But when I console log state it's the same. What am I missing?
When you call state.set(...) it returns a new object. The original state is unchanged. I changed 3 lines in your snippet:
case ActionTypes.DELETE_POST :
const removeArray = state.get('posts')
.filter((post) => post._id !== action.post._id)
console.log(removeArray.length)
const newState = state.set('posts', removeArray) // assign to another variable
console.log(newState) // and log here
return newState; // and you'll probably want to return the new state