react-select onChange event does not render with props - javascript

i'm using the select-react library --> https://react-select.com/home
i have managed to mapped options to the individual items i will need from itemList. the dropdown do register the selected option, however my end goal is such that my bar chart renders the correct data of the selected option.
/**
* Produces a filter list for item sales cards
* #param {*} props
* #returns the filter list of item names along with their ids
*/
export const ItemSelection = (props) => {
const { onChangeValue, itemList } = props;
if (itemList.length === 0) return <></>;
return (
<UncontrolledDropdown>
<Select
options={itemList.map((item) => {
return {
label: item.name,
value: item,
key: item.itemID,
};
})}
onChange={(item)=>onChangeValue(item)}
placeholder="ITEM"
/>
</UncontrolledDropdown>
);
};
itemSelection is being used here:
const [itemID = '1', setItemID] = useState('1');
const [itemName = '', setItemName] = useState('');
const [itemListID = [], setItemListID] = useState([]);
<ItemSelection
onChangeValue={(item) => {
setItemID(item.itemID);
setItemName(item.name);
}}
itemList={itemListID}
/>
onChangeValue is the one registering the option selected to the barchart. Is there anyway to map onChangeValue?

It depends what argument you're actually expecting from the parent element that uses ItemSelection. That said, it also looks like Select takes a function that takes an action of type string and the option value as the second argument. The selected value would then come in the second argument.
Something along the lines of of the below code is what you need if all you care about is the value. On that note, you will get the selected string value passed to onChangeValue which I imagine is what you want without seeing the function definition from an ancestor component.
Updated:
The select code can take the entire option object as the second parameter.
export const ItemSelection = (props) => {
const { onChangeValue, itemList } = props;
if (itemList.length === 0) return null;
return (
<UncontrolledDropdown>
<Select
options={itemList.map((item) => ({
label: item.name,
value: item.name, // note a change from your code here; it would have been an object otherwise
key: item.itemID,
}))}
onChange={(item)=>onChangeValue(item)}
placeholder="ITEM"
/>
</UncontrolledDropdown>
);
};
In the parent component
// note the changes here where I deleted the assignments
const [itemID, setItemID] = useState('1');
const [itemName, setItemName] = useState('');
const [itemListID, setItemListID] = useState([]);
<ItemSelection
onChangeValue={(option) => {
setItemID(option.key);
setItemName(option.value);
}}
itemList={itemListID}
/>

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

force remount on value change

I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
const CartDropdown = () => {
const { setCartProducts, cartProducts } = useContext(CartContext);
const { setProducts, currentProducts } = useContext(ProductsContext);
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts;
console.log(cleanProducts);
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty("quantity")) {
cleanProducts[i].quantity = 0;
}
}
setProducts(cleanProducts);
setCartProducts([]);
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

React state is updating but the component is not

There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.

React child component not updating when props change

I'm trying to build out a local search in React consisting of a parent component with a search input, and a child component containing a list of search results. I've created a React state object that contains the search query and a list of search results. When the input field is changed, the search is ran to generate a new result set, and both properties (query and results) are updated. The search input is updating as expected, but the child component doesn't re-render despite an update to its prop. I've removed some of the code for brevity, but if you need more information please let me know.
export const Search = () => {
let [searchState, setSearchState] = React.useState({});
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value);
searchState['query'] = event.target.value;
setSearchState(searchState);
};
return (
<div>
<FormControl>
<Input
value={searchState.query}
onChange={handleChange}>
Search...
</Input>
</FormControl>
<SearchResults results={searchState.results}></SearchResults>
</div>
);
};
export const SearchResults = (props) => {
return (
<List>
{props.results?.map((product, index) => (
<ListItem key={index}>
<ListItemText primary={product.name}></ListItemText>
</ListItem>
))}
</List>
);
};
My question is: Why doesn't the SearchResults component get re-rendered when searchState.results changes?
You mutate your state object in your handleChange function. The component doesn't rerender because searchState is still the same object reference from the previous render cycle.
let handleChange = (event) => {
searchState['results'] = searchProducts(event.target.value); // mutation!
searchState['query'] = event.target.value; // mutation!
setSearchState(searchState); // safe reference back into state
};
You shouldn't mutate state object directly. Use a functional state update and shallow copy existing state into a new state object reference. Then update the properties you want to update.
let handleChange = (event) => {
const { value } = event.target;
setSearchState(searchState => ({
...searchState,
results: searchProducts(value),
query: value,
}));
};

Categories

Resources