How to bind parameters in React hooks' setState() function? - javascript

I have a list of country names and when clicked on one of the countries, the state of the picked country should be updated with the name of the newly selected country. This state then triggers other changes in a useEffect(). The state of pickedCountry is handled in a parent component, but the setPickedCountry() function is passed to the child component (which creates the country list) as a prop.
When I now add a onPress={props.setCountry.bind(this, country.name)} to each of the list items I am getting a warning stating:
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Right now I don't know how a useEffect would help me here. Could somebody help me out?
These are my components:
Country Data
[
{
"name": "Germany",
"data": [*SOME DATA*]
},
{
"name": "France",
"data": [*SOME DATA*]
}, ...
]
Parent Component
const [pickedCountry, setPickedCountry] = useState(null);
useEffect(() => {
if (pickedCountry!= null) {
//Do Something
}
}, [pickedCountry]);
return (
<Child setPickedCountry={setPickedCountry} />
);
Child Component
const [child, setChild] = useState([]);
useEffect(() => {
const myComponents= [];
for (country of myData) {
myComponents.push(
<TouchableOpacity
onPress={props.setCountry.bind(this, country.name)} />
)
}
setChild(myComponents);
}, [someProps];
return (
{child.map((x) => x)}
)

Functional components are instanceless, therefore, there is no this to do any binding anyway. It looks as if you simply want to pass a country name as a new state value, i.e. onPress={() => props.setCountry(country.name)}.
useEffect(() => {
const myComponents= [];
for (country of myData) {
myComponents.push(<TouchableOpacity onPress={() => props.setCountry(country.name)} />)
}
setChild(myComponents);
}, [someProps];
Or create a curried function handler so only a single handler is defined and passed. The country name as saved in the enclosure.
useEffect(() => {
const myComponents= [];
const onPressHandler = name => () => props.setCountry(name);
for (country of myData) {
myComponents.push(<TouchableOpacity onPress={onPressHandler(country.name)} />)
}
setChild(myComponents);
}, [someProps];

Related

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.

Array is initially empty, but after an entry in a text field which is used for filtering, it is full

I verushc an array from one component to another component.
The initial array is filled by a DB and is not empty.
If I try to map over the array in my second component, it is empty (length = 0);
However, after I wrote a value in a search box to filter the array, all articles appear as intended.
What is that about?
export default function Einkäufe({ alleEinkäufe, ladeAlleEinkäufe, url }) {
const [searchTerm, setSearchTerm] = React.useState("");
const [searchResults, setSearchResults] = React.useState(alleEinkäufe);
const listeFiltern = (event) => {
setSearchTerm(event.target.value);
};
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, []);
React.useEffect(() => {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}, [searchTerm]);
[...]
{searchResults.map((artikel, index) => {
return ( ... );
})}
}
The problem is with your useEffect hook that sets the list of searchResults, it's not rerun when alleEinkäufe property is updated. You need to add alleEinkäufe as it's dependency.
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, [alleEinkäufe]);
My bet is that the parent component that renders Einkäufe is initially passing an empty array which is used as searchResults state and then never updated since useEffect with empty dependencies array is only run once on the component's mount.
I would also advise you to use English variable and function names, especially when you ask for assistance because it helps others to help you.
Your search term intially is "". All effects run when your components mount, including the effect which runs a filter. Initially, it's going to try to match any article to "".
You should include a condition to run your filter.
React.useEffect(() => {
if (searchTerm) {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}
}, [searchTerm]);
BTW, "" is falsy.

React, working with select component with objects

Im new to react and I have a question about select component of material ui.
The thing is like this, I have a funcionality that is for creating and editing an User, this User is an object, it has primary key and some data, between this data there is a relation with other object that is a role, so in this case I use a Select component to select the role.
So, I have the role list that I bring from the backend:
const [rolList, setRolList] = React.useState([]);
const searchRoles = async () => {
try {
setRolList(await api.post('/v1/roles/buscar', filtroRol));
} catch (error) {
snackbar.showMessage(error, "error");
}
}
And the select component that is inside a formik:
<Mui.Select
label="Role"
value={values.usuRolPk}
onChange={(opt) =>{handleChange(opt);
}}
>
<Mui.MenuItem disabled key={0} value=''>
Select role
</Mui.MenuItem>
{rolList.map((e) => {
return <Mui.MenuItem key={e.rolPk} value={e.rolPk}>{e.rolName}</Mui.MenuItem>;
})}
</Mui.Select>
As you can see, for the value of the select I use the pk of role, so when the user is persisted I have to search in the list of roles and atach the selected object to the users object and send it to the backend.
Something like this (usuRolPk is the value of the select, usuRol is the relation with object role):
const save = async (values) => {
try {
if(values.usuRolPk==null){
values.usrRole=null;
}else{
values.usrRole=rolList.filter(element=>''+element.rolPk==values.usuRolPk)[0];
}
...
if (values.usrPk == null) {
await api.post('/v1/users', values);
} else {
await api.put('/v1/users/' + values.usrPk, values);
}
handleClose();
snackbar.showMessage("GUARDADO_CORRECTO", "success")
} catch (error) {
snackbar.showMessage(error, 'error');
}
return;
}
The thing is, I want to skip that last step of having to search in the list of roles with the selected Pk.
Is there a way of working just with the object as the selected value instead of the pk? I tried just changing the value to have the whole object like this:
<Mui.Select
label="role"
value={values.usuRol}
onChange={(opt) =>{handleChange(opt);
}}
>
<Mui.MenuItem disabled key={0} value=''>
Select role
</Mui.MenuItem>
{rolList.map((e) => {
return <Mui.MenuItem key={e.rolPk} value={e}>{e.rolName}</Mui.MenuItem>;
})}
</Mui.Select>
This works just when Im creating a new object, but when I try to edit an object that already exists and already has a role, when I pass the role to the select It says something like I initialize the Select with a value that doesnt exist in the list of roles.
Is there a way to achieve this?
Thanks!
Per conversation in the comments on the question:
I'm doing this with a normal select and options purely for convenience, but you can replace them easily enough with their mui equivalents:
import React, { useState, useEffect, useCallback } from 'react';
const SomeComponent = () => {
const [list, setList] = useState({}); // { "1": someObjWithId1, etc }
const [selected, setSelected] = useState();
useEffect(() => {
const getList = async () => {
const resp = await fetch('/some/url'); // get object list from backend
const data = await resp.json();
setList(data);
};
if (!list.length) getList();
}, []);
const handler = useCallback((evt) => {
setSelected(list[evt.target.value]);
}, [list, setSelected]);
return (
<select onChange={handler}>
{Object.entries(list).map(([id, obj]) => selected && id === selected.id
? <option selected key={id} value={id}>{obj.text}</option>
: <option key={id} value={id}>{obj.text}</option>
)}
</select>
);
};
The component will render a select element with the options once they've been passed from the backend. The change handler will update the state with the entire object (keyed by the id/value) selected. In real life you'd likely have the state in a parent form component and pass it with the setter through props, but you get the idea.

How to sort React components based on specific value?

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.

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

Categories

Resources