Let's say I have the following rendered by a React.Component implementing material-ui components:
{data.map(value => (
<ListItem
key={data.indexOf(value)}
primaryText={value}
leftCheckbox={
<Checkbox
onCheck={this.props.handleAddOption}>
</Checkbox>}>
</ListItem>
When the Checkbox is chosen, I want to push the value into the array in state
handleAddOption = (value) => {
this.setState((....))
}
How do I go about doing that?
UPDATE
found the solution here Passing a function with parameters through props on reactjs
You need to pass the value from the CheckBox component to the function prop. You can do this by:
<ListItem
key={data.indexOf(value)}
primaryText={value}
leftCheckbox={
<Checkbox
onCheck={(e, isChecked) => this.props.handleAddOption(value, isChecked)}>
</Checkbox>}>
</ListItem>
And for your handler:
handleAddOption(value, isChecked) {
this.setState((prevState, props) => {
// Get the old state's value, sticking with immutable pattern
let yourProperty = prevState.yourProperty;
// Determine if the value already exists in your property's array
const exists = yourProperty.find(v => v === value);
if (isChecked) {
// If the checkbox is checked...
// If the property exists, don't do anything
// If it isn't there, add it
!exists && yourProperty.push(value);
} else {
// If the checkbox is NOT checked...
// If the property exists, filter the array to remove it
// If it isn't there, do nothing
exists && (yourProperty = yourProperty.filter(v => v !== value));
}
// Return the new state
return { yourProperty };
});
}
UPDATE
I've updated the solution a bit with documentation and a couple of typos, and created a working example on CodeSandBox here: https://codesandbox.io/s/pj0m4w3qp7
Related
I'm implementing a shopping cart for a ecommerce website. The shopping cart is a state variable shopCart represented by an array of objects. Each object contains information about a product, such as title and price. I am trying to implement a remove button, which is actually doing what is intended from it, which is to remove items from the shopCart state, but the changes are not represented on the screen render. I can empty the cart, but the screen still shows the products. Here is the main code of the shopping cart page:
return (
<div class={styles.container}>
<h1>Product</h1><h1>Quantity</h1><h1>Unit price</h1><h1>Total price</h1><div></div>
{
shopCart.map((product, i, array) => <CartItem key={product.id} product={product} index={i} array={array}/>)
}
</div>
)
And here is the implementation of CartItem.js
const CartItem = (props) => {
let { shopCart, setShopCart } = useContext(Context);
let product = props.product;
// takes the identification of a shopping cart product and removes it from the cart
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart;
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={() => {
decrease(product.id);
}}>Remove</button>
</div>
)
}
Why isn't it rendering the cart correctly, even though the cart items are being removed after each click of the remove button ?
Issue
You are mutating state. You save a reference to state, mutate it, then save it back into state, so the array reference never changes. React uses shallow equality when checking if state or props update.
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart; // <-- Saved state ref
aux.splice(i, 1); // <-- mutation
setShopCart(aux); // <-- Saved ref back to state
}
}
})
}
Solution
The correct way to update arrays in react state is to copy the array elements into a new array reference. This can be easily accomplished by filtering the current cart by item id. I also suggest changing the argument name so it is clearer what it represents.
const decrease = (id) => {
setShopCart(shopCart => shopCart.filter(item => item.id !== id));
}
You're modifying the shopCart (aux is a reference) directly which is both the context and the collection that you're iterating over. You need to make sure you're updating a copy of the shopping cart and resetting the context. Minimally, you can do the following:
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart.slice(); // makes a copy
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
However, I suggest using the approach Drew recommended. The current approach isn't ideal.
The solution is much simpler than you think. You can use array.filter to remove the matching product by id.
const CartItem = (props) => {
const { product} = props;
let { shopCart, setShopCart } = useContext(Context);
// takes the identification of a shopping cart product and removes it from the cart
const handleClick = (e) => {
const filteredShopCart = shopCart.filter(item => item.id !== product.id);
setShopCart(filteredShopCart);
};
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={handleClick}>Remove</button>
</div>
);
};
I have a React hook I call useToggles that I use for various checkboxes and radio buttons in my app. So far I have been able to get away with something like the following:
const useToggles = (initialValues = {}) => {
const [toggleValues, setToggleValues] = useState(initialValues);
const handleToggle = e => {
const name = e.currentTarget.attributes.name.value;
const value = toggleValues[e.currentTarget.attributes.name.value];
setToggleValues(values => ({ ...values, [name]: !value }));
};
return {
toggleValues,
setToggleValues,
handleToggle,
};
};
export default useToggles;
An example checkbox component:
<CheckBox
checked={toggleValues.gluten || false}
label="Gluten"
onChange={handleToggle}
name="gluten"
/>
So although my "toggleValues" object starts off as {}, any time a checkbox is checked, it populates the object. So we might have:
{
gluten: true,
soy: false
}
Because there's only one layer to this object, spreading out the values and using [name]: !value to flip the Boolean value will work.
However, this falls apart when there is the need for more organization. On another page, I have several groups of checkboxes, the values of which I will need to group together to populate individual database fields. To handle this, I've added a layer of organization to the checkboxes:
<CheckBox
checked={toggleValues.dietType.paleo || ''}
label="Paleo"
onChange={handleToggle}
name="dietType.paleo"
/>
We have used this method of organization elsewhere in our app in order to group data, and have parsed the string with dot-object. Example from useFormValues(): dot.str(e.target.name, value, tmp);
This method does not work with useToggles because we rely on previously existing data in the toggleValues object. Using dot-object consistently creates new layers of the object every time you click the checkbox. But I haven't found a way of using [name] to select a second or third level of an object.
To visualize this, what I need to be able to do is take this object and flip the value of paleo to true based on the function receiving the name "dietType.paleo":
{
dietType: {
paleo: true;
},
intolerances: {}
}
You can use currying and pass the checkbox group name as parameter to handleToggle:
const handleToggle = sub => e => {
const name = e.currentTarget.attributes.name.value;
if (!sub) setToggleValues({ ...toggleValues, [name]: !toggleValues[name] });
else {
const newSub = { ...toggleValues[sub], [name]: !toggleValues[sub][name] };
setToggleValues({ ...toggleValues, [sub]: newSub });
}
};
Now use onChange={handleToggle()} for the top level and onChange={handleToggle("dietType")} for the paleo checkbox.
Edit:
Another way is to check if the name has a period in it and branch accordingly:
const handleToggle = e => {
const name = e.currentTarget.attributes.name.value;
if (!~name.indexOf("."))
setToggleValues({ ...toggleValues, [name]: !toggleValues[name] });
else {
const [sub, prop] = name.split(".");
const newSub = { ...toggleValues[sub], [prop]: !toggleValues[sub][prop] };
setToggleValues({ ...toggleValues, [sub]: newSub });
}
};
This way you can keep your existing JSX 1:1.
i have a function that selects a row of a table, this function is below, it is within my component of SupplierSearchComponent
const handleAddressCheck = item => {
const itemIndex = supplierAddress.findIndex(supplierItem => supplierItem.id === item.id);
let newAddresses;
if (itemIndex >= 0) {
//the reasoning behind using a filter is that splice will not return an updated array, filter however does
newAddresses = supplierAddress.filter(x => x.id !== item.id);
} else {
newAddresses = [...supplierAddress, item];
}
setSupplierAddress(newAddresses);
props.onSelectAddress(newAddresses);
console.log('this is the item => ', newAddresses);
};
However i also pass this function into another component as
const onSelectedAddressesChange = (addresses) => {
props.setFieldValue('supplierAddresses', addresses);
};
I then pass the value of this into another component as
<SearchSuppliers
name="supplierSearch"
label="Supplier Search"
id="supplierSearch"
onSelectAddress={onSelectedAddressesChange}
/>
then from search suppliers i pass the prop into the original component of
<SupplierSearchComponent
onSelectAddress={props.onSelectAddress}
/>
Now finally within my SupplierSearchComponent i should be able to pass this prop as
<SupplierContactDetails
onSelectAddress{props.onSelectAddress}
/>
However within my ContactDetails i cannot actually console.log(onSelectAddress) as it will result as undefined, how could i successfully pass this function or rather its result through?
I suppose you skip "=" when passed props at last component "SupplierContactDetails".
So I got a list of buttons that looks like this
The functionality that I aim for is when you press a button its background will change to another color.
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
I use this function to return a list of selected items. Currently I'm only returning one item but I made it an array so I can handle multiple items in the future.
In the render function I have something like this:
<View style={feed_back_page_styles.buttons_wrapper}>
{
feedbackButtons.map((item, i) => (
<TouchableOpacity style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.pressedStyle:feed_back_page_styles.inputStyle}
onPress={()=>this.onButtonPress(item.key)}>
<Text style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.option_text_style_pressed:feed_back_page_styles.option_text_style}>{item.data}</Text>
</TouchableOpacity>
))
}
</View>
feedbackButtons is just an array with a key and text.
The onButtonPress method looks like this:
onButtonPress = (key) =>{
updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems,key);
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
The problem is that the view does not update on state change. When I click the button the state gets updated but the view stays the same.
I think this is wrong
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
Since you are passing the this.state.selectedItems as 1st argument from your onButtonPress, actually its not creating new array, but using the same reference of state and state should not be modified directly, always use setState().
So basically what you are doing is :
const getUpdatedSelectedItemsArray = (id) => {
this.state.selectedItems = []
this.state.selectedItems.push(id);
return selectedItems;
};
Which is completely wrong and might be the actual issue.
what you can do instead is :
const getUpdatedSelectedItemsArray = (selectedItems=[], id) => {
const items = [...selectedItems]
items.push(id);
return items;
};
and then :
onButtonPress = (key) =>{
const updatedItems = getUpdatedSelectedItemsArray(key); // since currently you want to keep only 1 item in the list
/* Incase more than 1 items, you can then use this
const updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems, key);
*/
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
Hope this resolves your issue.
Also, if you can share your component, it can help if there is some other issue with your component like if you are using PureComponent.
I'm building a dynamic form in react.
I try to bind checked value of a radio button but the comparaison seems to be executed immediatelly.
How do I differ comparasion ?
buildRadio(field) {
const radios = field.questionnairefieldlabelSet.map((label) => {
const radioValue = `${field.id}-${label.id}`
return <Form.Radio
key={radioValue}
label={label.text}
checked={this.state[field.id] === radioValue} // evaluated once, never changes on state change
value={radioValue}
onChange={(e, { value }) => {
this.setState({ [field.id]: value })}} />
})
return <Form.Group key={field.id}>
{radios}
</Form.Group>
}
I've tried to pass a function instead but I get an error message
checked={() => this.state[field.id] === radioValue}
Failed prop type: Invalid prop checked of type function supplied to Checkbox, expected boolean