Weird Reselect selector behavior - javascript

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

Related

Time from the change of variable in useSelector and triggering useEffect takes too long

I am using a useSelector in redux to save a variable. For example, in this case a variable called inFocus. This variable contains an array of objects. When this variable is updated (objects are being inserted into the array), it should trigger a useEffect which should execute a certain function. However, this trigger takes a very long time (up to 6000ms), making the app looks laggy. How can I improve on this?
The code:
function setItemOcrInFocus() {
if (selections && selections.clicks) {
const clicks = selections.clicks.filter(
(c) =>
c?.selectedItem === item.id || c?.selectedItemIndex === item.index
);
if (clicks.length > 0) {
dispatch({
type: SET_STATE,
payload: {
inFocus: clicks,
},
});
}
}
}
return (
<Component onMouseEnter={() => setItemOcrInFocus()} />
)
Component.js
function Component() {
const [inFocus] = useSelector((state) => [
state.item.inFocus,
]);
useEffect(() => {
// execute a function here
}, [inFocus])
}
The function setItemOcrInFocus is triggered when a specific component is being hovered over.
Am I missing something here?

React child component not re-rendering on updated parent state

I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.

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

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

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

How do I modify the value of an array of Bool in React?

I have an array of boolean as a state in my component. If it is false, I want to set it as true.
this.state = {
checkedPos: []
}
handleChange(index, reaction) {
if (!this.state.checkedPos[index])
{
this.state.checkedPos[index] = true;
this.addReaction(reaction);
this.forceUpdate();
}
}
It works, but the only problem I encounter is that it show this warning:
Do not mutate state directly. Use setState()
So I tried changing it and putting it like this:
this.setState({
checkedPos[index]: true
})
But it does not compile at all.
One solution would be to map the previous array in your state. Since you should never modify your state without setState I will show you how you can use it in this solution :
handleChange(index) {
this.setState(prev => ({
checkedPos: prev.checkedPos.map((val, i) => !val && i === index ? true : val)
}))
}
Here, map will change the value of your array elements to true only if the previous value was false and if the index is the same as the one provided. Otherwise, it returns the already existing value.
You can then use the second argument of setState to execute your function after your value has been updated :
handleChange(index) {
this.setState(prev => ({
checkedPos: prev.checkedPos.map((val, i) => !val && i === index ? true : val)
}), () => {
this.addReaction(reaction);
})
}
You can use the functional setState for this.
this.setstate((prevState) => ({
const checkedPos = [...prevState.checkedPos];
checkedPos[index] = true
return { checkedPos };
}))

Categories

Resources