Mutate prevState in setState hooks updates the view without re-render. Why? - javascript

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}
}
}
)
}

Related

Why doesn't re-render Vue the component after changing the Pinia state (deleteing an object in Array)?

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)

Dynamic className in map not changing after update

I'm trying to update my react className when the active changes in the sites variable which is mapped to loop through the items.
What happens is that the className 'inactive' does not go away if the active status changes to true or visa versa.
Code:
// Context: this code is inside of the component
const [sites, setSites] = useState([]); <--- Updated dynamically with fetch()
const changeActive = (id) => {
const tmpSites = sites;
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
return (
{sites.length ? sites.map((item, i) => {
return (
<tr className={`${!Boolean(item.active) ? 'inactive' : ''}`} key={item.id}>
// inbetween data
</tr>
)
}) : null}
)
You need to create a copy of the sites array and make changes to the copy and then set it in state. Never mutate state directly as it might not cause a re-render as we are updating the state with the same object reference.
const changeActive = (id) => {
const tmpSites = [...sites];
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
Because you are mutating the original sites Object and not cloning it before making the changes, the useState ("setSites") does not actually re-renders the component because it cannot compare previous Object to current, because they are the same.
You must do a deep-clone of the sites Array of Objects:
const changeActive = (id) => {
setSites(sites => {
sites.map(site => ({ // ← first-level clone
...site // ← second-level clone
active: site.id === id ? !site.active : site.active
}))
})
}
It is imperative to use the setSites function that returns the current state and then you can reliably deep-clone it.

Weird Reselect selector behavior

For some reason, my selector function only gets called when one of the arguments change but not the other.
Here is my selector that gets transactions from state and applies 2 filters to them
export const getFilteredTransactionsSelector = createSelector(
(state) => state.transactions.transactions,
(items) =>
memoize((filterValueFirst, filterValueSecond) =>
items
.filter((item) => {
if (filterValueFirst === "Show All") {
return true;
}
return item["Status"] === filterValueFirst;
})
.filter((item) => {
if (filterValueSecond === "Show All") {
return true;
}
return item["Type"] === filterValueSecond;
})
)
);
In my component's mapStateToProps I pass current state to the selector
const mapStateToProps = (state) => ({
transactions: getTransactions(state),
getFilteredTransactions: getFilteredTransactionsSelector(state),
...
});
And then I call it whenever one of the filter values changes
useEffect(() => {
setFilteredTransactions(
getFilteredTransactions(statusFilterValue, typeFilterValue)
);
}, [transactions, statusFilterValue, typeFilterValue]);
The problem is that I get filtered data only when I change the first filter's value (statusFilterValue). If I change the 2nd one, nothing happens despite the fact that the useEffect hook gets called as it should be.
If I put console.log inside memoize function, it will only show the result if I change the first filter but not the second. Any help would be appreaciated
Going from the code in your question you don't need memoize at all. If you did need it then better use the one that is already in reselect and prevent adding unnecessary dependencies.
You also don't need an effect since what selectFilteredTransactions returns will not change as long as items, filterValueFirst or filterValueSecond won't change and if setFilteredTransactions comes from useState then passing the same value to it between renders won't cause any re render.
You can create the selector like so:
export const selectFilteredTransactions = createSelector(
(state) => state.transactions.transactions,
(a, filterValueFirst) => filterValueFirst,
(a, b, filterValueSecond) => filterValueSecond,
(items, filterValueFirst, filterValueSecond) =>
console.log('running selector, something changed') ||
items
.filter((item) => {
if (filterValueFirst === 'Show All') {
return true;
}
return item['Status'] === filterValueFirst;
})
.filter((item) => {
if (filterValueSecond === 'Show All') {
return true;
}
return item['Type'] === filterValueSecond;
})
);
And call it like this:
//don't even need the effect here since filteredTransactions
// only changes when items, filterValueFirst or filterValueSecond
// changes
setFilteredTransactions(//assuming this comes from useState
useSelector((state) =>
selectFilteredTransactions(
state,
statusFilterValue,
typeFilterValue
)
)
);

How do I pass a value from a promise to a component prop in react native?

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
/>
);

React multiple callbacks not updating the local state

I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you

Categories

Resources