React, working with select component with objects - javascript

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.

Related

Updating material ui select options from input

I'm new to Reactjs, here i have material ui select element, as you can see i have default values for select element, and also by clicking 'ADD USER' button and submitting, i can add new values to select element, and from select element i can also delete options, my question here is how can i edit specific option from select element, i have added EditUser component for that when option is clicked, but dont know how to update it, any advice ?
my code:
https://codesandbox.io/s/material-ui-multiple-select-with-select-all-option-forked-ysglz8?file=/src/AddUser.js
App.js:
import React, { useState } from "react";
import Checkbox from "#material-ui/core/Checkbox";
import InputLabel from "#material-ui/core/InputLabel";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import ListItemText from "#material-ui/core/ListItemText";
import MenuItem from "#material-ui/core/MenuItem";
import FormControl from "#material-ui/core/FormControl";
import Select from "#material-ui/core/Select";
import DeleteIcon from "#material-ui/icons/Delete";
import CreateIcon from "#material-ui/icons/Create";
import { MenuProps, useStyles } from "./utils";
import AddUser from "./AddUser";
import {
Button,
List,
ListItem,
Dialog,
DialogTitle,
DialogContent
} from "#material-ui/core";
import EditUser from "./EditUser";
function App() {
const rawOptions = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [options, setOptions] = useState(rawOptions);
const [openAddModal, setOpenAddModal] = useState(false);
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const handleChange = (event) => {
console.log("vals", event.target);
const value = event.target.value;
setSelected(value);
console.log("values", selected);
};
function addUser(newArray) {
setOptions(newArray);
}
const openAddUser = () => {
setOpenAddModal(true);
};
const openUpdateUser = (e) => {
e.stopPropagation();
setOpenUpdateModal(true);
};
const closeAddModal = () => {
setOpenAddModal(false);
};
const closeUpdateModal = () => {
setOpenUpdateModal(false);
};
const updateUser = (updateUser) => {
setOptions(updateUser);
};
return (
<FormControl className={classes.formControl}>
<div>
<InputLabel id="mutiple-select-label">Multiple Select</InputLabel>
<Select
labelId="mutiple-select-label"
multiple
variant="outlined"
value={selected || []}
onChange={handleChange}
renderValue={(selected) => selected}
MenuProps={MenuProps}
>
{options.map((option, index) => (
<MenuItem key={option.id} value={option}>
<ListItemIcon>
<Checkbox checked={selected?.includes(option)} />
</ListItemIcon>
<ListItemText primary={option.title}>{option}</ListItemText>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setOptions(options.filter((o) => o !== option));
console.log("run");
}}
/>
<ListItemIcon>
<CreateIcon onClick={openUpdateUser} />
</ListItemIcon>
</MenuItem>
))}
</Select>
<Button onClick={openAddUser} style={{ backgroundColor: "#287B7A" }}>
Add User
</Button>
</div>
<p>{selected}</p>
<AddUser
openAddModal={openAddModal}
handleClose={closeAddModal}
array={options}
addUser={addUser}
/>
<EditUser
openUpdateModal={openUpdateModal}
handleClose={closeUpdateModal}
array={options}
updateUser={updateUser}
/>
</FormControl>
);
}
export default App;
Currently your users haven't any ids or something that they can be identified with.
Try to make users array of objects like this
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
Make your inputs and etc. accept array of objects.
After this in your Edit component, you should pass there selected user object and set default state for your input value (so your can really edit it and not input a new value)
const [value, setValue] = useState(props.user.name);
And in your someFunction, that acts like handleSubmit function, pass your user object, or user id and new value. It will look like
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.hanldeSubmit(props.user.id, value)
props.handleClose() // Might be better to put it into your handleSubmit in parent component
}
};
And finally in your App.js, create handleSubmit function that accepts user id and value, and modify your state in it. Find user by ID and put a new value.
Do not forget to pass this function into your EditUser component.
Hope that helped you!
UPDATE
Ooookay, so, you might also want to start with less hard examples, but lets stick to what we have. I'll note here some problems that I found, and explain how to make it work.
First of all, always name functions and variables correctly, you should understand what function or variable do only by its name (ideally), I understand that this is just an example code and etc, but this makes this point only more important, because when you learn something new, its good not to make it harder for yourself.
Second thing, just for some case, I don't know if this mistake or not, so I mention this:
<CreateIcon
onClick={() =>
openUpdateUser({ id: option.id, name: option.name })
}
/>
Here you pass object, and in openUpdateUser you accept e (event) as a first parameter. Just for you to know, you will get event in your anonymous function and it wouldn't be passed further in openUpdateUser, to pass it, you should write it like this:
<CreateIcon
onClick={(e) =>
openUpdateUser(e, { id: option.id, name: option.name })
}
/>
Okay, let's get back to business.
The first real problem here: You have your options in one state, and selected options in other, so when you add some user, you will see users from selected. What problem does it cause? When you will try to update user in your options, it might be updated, but you wouldn't see any changes in selected, because it two different states.
We will solve it by making one source of information. Now we will store in selected not users, but users ids and in render we will get users from our options by ids.
// before
<p>{selected}</p>
// after
{selected.map(selectedUserId => (
<div>{options.filter(option => option.id === selectedUserId)[0].name}</div>
))}
Now, any changes to options will affect your selected users. Also, update your code to add\remove ids and not user objects.
Let's go further, now you have your selected user and method for updating in edit component, let's go edit:
// EditUser.js
const [value, setValue] = useState(props.edit.name); // set user name as default value to edit it
function changeValue(e) {
setValue(e.target.value);
}
const someFunction = (event) => {
event.preventDefault();
if (value) {
props.updateUser({id: props.edit.id, name: value}) // Pass user id and new value to our update function
props.handleClose();
}
};
So, now we have our new value in update function, the only one thing left is to save those updates. We'll do it in easy way:
// Normally here would be some api call for user update
const updateUser = (updateUser) => {
const temp = [...options] // Not deep copy of our options
temp[temp.findIndex(user => user.id === updateUser.id)].name = updateUser.name;
setOptions(temp)
};
And thats all, now it should work as expected.
Also as improve, you can restructure your options array of objects to make it easier to modify data.
(yep, I know that it was my suggestion, but anyway :) )
Currently it looks like this:
const rawOptions = [
{
id: 0,
name: "Oliver Hansen"
},
];
We can make it object of objects, where key will be id of user:
const rawOptions = {
0: {
id: 0,
name: "Oliver Hansen"
},
};
//Now to get user you can just do
options[userId]
// To get users array
Object.values(options)
// To modify user
const updateUser = (updateUser) => {
setOptions({...options}, [updateUser.id]: updateUser)
};
Just like previously, I wouldn't make those changes into codesandbox, the best way to learn programming is to write some code by yourself :)
If you will find any other issues or questions, feel free to ask, hope it helps :)

Set state for dynamically generated component in React

I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>

Getting data to be fetched upon page render ReactJS

Still a newbie, so not sure how to solve this issue. The app gets data about movie genres from an API, it then uses those genres to create options in the drop-down selector. The user can choose a type (tv show or movie) and then the genre. When they hit search it will return a random movie or show in the genre. The starting values are tv show and action. I want the user to be able to immediately hit search and find a title to watch. My problem is the data about movies/ shows in the specified type and genre are only fetched when the user changes the selector option from the default one. You can see this hosted on GH Pages here or check the GH repository
So I want the data from the full_url to be fetched upon render. The feedback from the console is that upon render chosenType and chosenGenre are undefined in the fetch method fetchMovieList(). But once I change the type, an array of movies or shows are fetched.
Any help or advice would be appreciated.
Below is the code.
import { createContext, useState, useEffect } from "react";
export const MovieContext = createContext({});
export function MovieProvider({ children }) {
const [movieList, setMovieList] = useState([]);
const [randomMovie, setRandomMovie] = useState({});
const [hasMovie, setHasMovie] = useState(false);
const [genreOptions, setGenreOptions] = useState([]);
const [chosenGenre, setChosenGenre] = useState();
const [typeOptions, setTypeOptions] = useState([]);
const [chosenType, setChosenType] = useState();
const api = "api_key=3fa371d07ffd6838dc488ff081631c5d";
const genres_url =
"https://api.themoviedb.org/3/genre/movie/list?api_key=3fa371d07ffd6838dc488ff081631c5d&language=en-US";
const types = [
{ type: "TV Show", value: "tv" },
{ type: "Movie", value: "movie" },
];
//fetching genres from API to use in selector and for searching and setting types for selector
const fetchGenres = () => {
fetch(genres_url)
.then((res) => res.json())
.then((data) => {
setChosenGenre(data.genres[0].id);
setGenreOptions(data.genres);
setTypeOptions(types);
setChosenType(types[0].value);
console.log(data);
});
};
//getting genres when page loads
useEffect(() => {
fetchGenres();
}, []);
//setting the value of slelector drop downs
const onChangeGenre = (e) => {
setChosenGenre(e.target.value);
};
const onChangeType = (e) => {
setChosenType(e.target.value);
};
//fetching movies or shows from the selected genre
const full_url = `https://api.themoviedb.org/3/discover/${chosenType}?${api}&with_genres=${chosenGenre}`;
const fetchMovieList = () => {
fetch(full_url)
.then((res) => res.json())
.then((data) => {
setMovieList(data.results);
console.log(data.results);
});
};
console.log(chosenType, chosenGenre)
//fetches data from API when type or genre is changed
useEffect(() => {
fetchMovieList();
}, [chosenType, chosenGenre]);
//function that selects a random movie or show from the already fetched data
const getRandomMovie = (e) => {
e.preventDefault();
const randomItem = movieList[Math.floor(Math.random() * movieList.length)];
setRandomMovie(randomItem);
console.log(randomItem);
setHasMovie(true);
};
//passing state and functions to child components
return (
<MovieContext.Provider
value={{
getRandomMovie,
onChangeGenre,
onChangeType,
randomMovie,
hasMovie,
genreOptions,
chosenGenre,
typeOptions,
chosenType,
types,
}}
>
{children}
</MovieContext.Provider>
);
}
The problem is here:
//fetches data from API when type or genre is changed
useEffect(() => {
fetchMovieList();
}, [chosenType, chosenGenre]);
The useEffect hook will be called every time the dependencies change, but also on the initial render of the component. At first, the chosenType and chosenGenre will still be their initial value null. You can fix it with a simple fix like this:
//fetches data from API when type or genre is changed
useEffect(() => {
if(!chosenType || !chosenGenre) return;
fetchMovieList();
}, [chosenType, chosenGenre]);
You can try setting the selected attribute to the dropdown, as shown below. Then your api will have the values to make the request, when the page loads.
Note : This will set the last iterating option as the selected value.
Form.js
<form className={classes["form-row"]} onSubmit={getRandomMovie}>
<p className={classes.label}>Type: </p>
<select value={chosenType} onChange={onChangeType}>
{typeOptions.map((type) => (
<option key={type.value} value={type.value} selected> // <== Here
{type.type}
</option>
))}
</select>
<p className={classes.label}>Genre: </p>
<select value={chosenGenre} onChange={onChangeGenre}>
{genreOptions.map((option) => (
<option key={option.id} value={option.id} selected> // <== Here
{option.name}
</option>
))}
</select>
<button>Search</button>
</form>;
I think the problem is that the default values of chosenGenre are undefined. Set the default values for chosenGenre.
const [chosenGenre, setChosenGenre] = useState("tv");
Solved this issue. It was because I used the API incorrectly, had to use a different url for the tv show type, as opposed to a movie type.

How to set the default selected value while clicking on submit or very first time while loading - React Hook

This is my category state
const [category, setCategory] = useState('');
This's the form element:
<select onChange={e => setCategory(e.target.value)}>
<Options options={categoryList} value={category}/>
</select>
On changing the value, i'm getting category as selected
const handleBusinessInfoSubmit = (e) => {
try{
e.preventDefault();
console.log("category selected is " +category);
}
catch{
console.log("something went wrong!");
}
}
How do I setCategory state when the user doesn't change the value and hits Submit?
For reference sake, here is category list that will come as dynamic later in key value pair
const categoryList = [
{
id: 1,
value: 'Public Services'
}, {
id: 2,
value: 'Automotive'
}
];
// generate select dropdown option list dynamically
function Options({ options }) {
return (
options.map(option =>
<option key={option.id} value={option.value}>
{option.value}
</option>)
);
}
Probably I would add default initial value to useState as instead of '':
const [category, setCategory] = useState(categoryList[0]);
Or maybe if the data is coming dynamically then calling setCategory() with the value from the API result what you would like to have as default.
I hope this helps!

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