could any React expert here please helps me answer this weird issue I have with my react component?
I am trying to create a function to remove an item in an array, however, the setState does not update new array but console.log shows the array is correctly updated with the items properly removed.
constructor (props) {
super (props);
this.state = {
data: dummyData,
chosenData:[],
chosenOption:{}
}
}
onDeleteOptionButtonClick = (id, titleId) => {
const data = this.state.data.map(item => item.id === titleId ? {...item, possibleAnswers: item.possibleAnswers.filter(i => i.id !== id)} : item)
this.setState({ data }, () => {console.log('log of data from state', this.state.data[0].possibleAnswers)})
console.log('log of data from data',data[0].possibleAnswers)
}
here is the picture of the console.log
as you can see that the const data properly updates new array with the item removed, but the state stays the same.
Please help, thanks
Please Use this.setState inside the filter function itself.
Related
I have a deleteHandler function, which changes the users array in pinia. However, in the devtools in vue, the state is changed, but the component didn't re-render, but if I instead deleting the object from the array, just change some values, then vue recognize it and re-render the component, only by deleting the object from the array doesn't work.
const deleteHandler = (user) => {
//doesn't renders
useUser.users = useUser.users.filter(usr => usr.id !== user.id)
//it works, the component is re-rendered
useUser.users.forEach(usr => {
usr.points += 1
})
}
I thinks it some kind of reference issue.
Please try this one
useUser.users = [...useUser.users.filter(usr => usr.id !== user.id)];
insted of
useUser.users = useUser.users.filter(usr => usr.id !== user.id)
My changeProductName function called setState which return a "mutated prevState". I pass the function to and call it in children component via ContextAPI. The function successfully updated a product name displayed in children and parent, but the parent did to fire a re-render. How does the parent updated the view without re-rendering? Can anyone explain what the prevState actually is in setState?
const App = () => {
const [products, setProducts] = useState(initialValues);
const changeProductName = (id, newName) => {
setProducts((prevState) => { //is preState a copy of state?
prevState.products.filter(
(product) => product.id === id
)[0].name = newName; //Mutates prevState
return prevState; //Did I return a new state?
});
};
useEffect(() =>
console.log("I would know when App re-renders")); //No re-render!
return (
<> //Some React Switch and Routers
<div>
{product.map(product=>product.name)} //Successfully Updated!
</div>
<ProductContext value={(products, changeProductName)}>
<ProductPage /> //call changeProductName and it works!
</ProductContext>
</>
);
};
If I change the function not touching prevState, the parent re-renders as expected. Is this method better?
//this will trigger parent re-render.
const changeProductName = (id, newName) => {
setProducts((prevState) => {
prevState.products.filter(
(product) => product.id === id
)[0].name = newName;
return prevState;
});
};
Can anyone explain what the prevState actually is in setState?
prevState is a reference to the previous state. It is not a copy of the state, it is a reference of the object that sits inside the state. So changing that object will not alter the object reference.
Therefore it should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState.
For example, if you do a check inside your changeProduct name like:
setProducts(prevState => {
prevState.filter(product => product.id == id)[0].name = newName;
console.log(prevState === products); // This will console true
return prevState;
});
Also, as you are using hooks, when you write setProducts((prevState) => { prevState.products}... the prevState itself is already the products. So you will get an undefined error in your example when trying to access .products.
So I would recommend you to do:
const changeProductName = (id, newName) => {
setProducts(prevProducts =>
prevProducts.map(product =>
product.id === id ? { ...product, name: newName } : product
)
);
};
.map will build a new array based on prevState, and change the name of the products that have the id called in the function.
As far as I know, mutating the state is generally a bad idea.
According to this answer, mutating the state may not result in a re-render, since the reference to the state object is not changed during the mutation.
I'd rather use some kind of redux-like immutable pattern:
const changeProductName = (id, newName) => {
setProducts((prevState) => (
prevState.map(product=>{
if(product.id!==id){
// name is not changed since the id does not match
return product;
} else {
// change it in the case of match
return {...product, name:newName}
}
}
)
}
I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.
I am using useState which has 2 array imageList and videoList and then in useEffect hook i am using forEach on data then if type is image then push to item to image .
But at last i am not getting imagelist or video list of array.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (()=>{
//data is list of array
dataList.forEach(item =>{
if(!item.images) {
setDataType({...list, imageList:item})
}
else if (item.images[0].type === "video/mp4")
{
setDataType({...list, videoList :item})
}
else if((item.images[0].type === "images/gpeg")
{
setDataType({...list, imageList:item})
}
})
},);
Here type check is working correctly but at last i get last fetch data only which can be videolist or imageList
In last i should get list of all imageList whose type is image and same video list whose type is video
It is not a proper way to call setState in a loop. Below is an attempted solution using array method filter to functionally construct the list.
const [list, setDataType] = useState({ imageList: [], videoList: [] });
useEffect(() => {
let videos = dataList.filter(item => item.images[0].type === "video/mp4")
let images = dataList.filter(item => item.images[0].type === "images/gpeg")
setDataType({imageList: images, videoList: videos})
}, []);
Ideally you don't want to be calling setState constantly inside your loop. Build the state up and then set it.
Two filters might work for you here..
eg.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (()=>{
//data is list of array
setDataType({
videoList: dataList.filter(item => item.images[0].type === 'video/mp4'),
imageList: dataList.filter(item => item.images[0].type === 'images/gpeg')
});
},);
Don't use the useEffect & useState hooks for data transformation, which is not part of an asynchronous flow.
This case is better handled with useMemo. When the datalist changes, useMemo will produce the list, and if it doesn't change, the memoized value would be used. In addition, it won't cause an infinite loop, because it won't cause a rerender while listening to its own state.
To create list, use a Map that will hold the mime-types and their respective types. Reduce the array of items, and according to the type you get from the Map, push them in the images or videos sub-arrays.
/** external code (not in the hook) **/
const types = new Map([['video/mp4', 'video'], ['images/gpeg', 'image']])
const getListType = ({ type }) => types.has(type) ?
`${types.get(type)}List` : null
/** hook code **/
const list = useMemo(() => dataList.reduce((r, item) => {
const list = getListType(item.images[0])
is(list) r[list].push(item)
return r;
}, { imageList:[], videoList:[] }), [dataList])
Avoid set state inside a loop. On each setState, React will re-render the component. So recommended to prepare your state first and then update the state.
const [list, setDataType] = useState({imageList:[], videoList:[] });
useEffect (() =>{
let newState = {imageList:[], videoList:[] };
dataList.forEach(item =>{
if(!item.images) {
newState.imageList.push(item);
} else if (item.images[0].type === "video/mp4") {
newState.videoList.push(item);
} else if((item.images[0].type === "images/gpeg") {
newState.imageList.push(item);
}
});
setDataType(prevState => {
return {
imageList: newState.imageList,
videoList: newState.videoList
}
});
},[]);
Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.
I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.
getMsg = id => {
const m = fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(msg => {
return msg;
});
return m;
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={this.getMsg(item.id)} // failed prop type
bottomDivider
chevron
/>
);
You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)
Instead, the component will need to have two states:
The subtitle isn't loaded yet
The subtitle is loaded
...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.
If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:
//put this is a library of custom hooks you may want to use
// this in other places
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const ListItem = ({
title,
bottomDivider,
chevron,
onPress,
id, //hae to pass id to ListItem
}) => {
const [lastMessage, setLastMessage] = useState(null);
const isMounted = useIsMounted();
React.useEffect(() => {
async function get() {
const m = await fireStoreDB.getUserLastMessage(
fireStoreDB.getUID,
id
);
//before setting state check if component is still mounted
if (isMounted.current) {
setLastMessage(m);
}
}
get();
}, [id, isMounted]);
return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.
componentDidMount() {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, message }))
)
)
)
.then(usersInfo => {
this.setState({ usersInfo });
});
}
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={item.message}
bottomDivider
chevron
/>
);