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.
Related
I have two states defined like so:
const [productProperties, setProductProperties] = useState<
PropertyGroup[] | null
>(null);
const [originalProductProperties, setOriginalProductProperties] = useState<
PropertyGroup[] | null
>(null);
The first one is supposed to be updated through user input and the second one is used later for a comparison so that only the PropertyGroup's that have changed values will be submitted via API to be updated.
I have done this a thousand times before, but for some reason when I change the name value for a PropertyGroup and update the state for 'productProperties' like so:
(e, itemId) => {
const update = [...productProperties];
const i = update.findIndex((group) => group.id === itemId);
if (i !== -1) {
update[i].name = {
...update[i].name,
[selectedLocale]: e.currentTarget.value,
};
setProductProperties([...update]);
}
}
The state of originalProductProperties also updates. Why? setOriginalProductProperties is never called here, I am also not mutating any state directly and I use the spread operator to be sure to create new references. I am lost.
Preface: It sounds like the two arrays are sharing the same objects. That's fine provided you handle updates correctly.
Although you're copying the array, you're modifying the object in the array directly. That's breaking the main rule of state: Do Not Modify State Directly
Instead, make a copy of the object as well:
(e, itemId) => {
const update = [...productProperties];
const i = update.findIndex((group) => group.id === itemId);
if (i !== -1) {
update[i] = { // *** Note making a new object
...update[i],
[selectedLocale]: e.currentTarget.value,
};;
setProductProperties(update); // (No need to *re*copy the array here, you've already done it at the top of the function)
}
}
Or, since you have that i !== -1 check there, we could copy the array later so we don't copy it if we don't find the group matching itemId:
(e, itemId) => {
const i = productProperties.findIndex((group) => group.id === itemId);
if (i !== -1) {
const update = [...productProperties];
update[i] = { // *** Note making a new object
...update[i],
[selectedLocale]: e.currentTarget.value,
};;
setProductProperties(update);
}
}
FWIW, in cases where you know there will be a match, map is good for this (but probably not in this case, since you seem to indicate the group may not be there):
(e, itemId) => {
const update = productProperties.map((group) => {
if (group.id === itemId) {
// It's the one we want, create the replacement
group = {
...group,
[selectedLocale]: e.currentTarget.value,
};
}
return group;
});
setProductProperties(update);
}
Or sometimes you see it written with a conditional operator:
(e, itemId) => {
const update = productProperties.map((group) =>
group.id === itemId
? { // It's the one we want, create a replacement
...group,
[selectedLocale]: e.currentTarget.value,
}
: group
);
setProductProperties(update);
}
I am getting different behaviour depending on whether I am using a boolvalue on with useState, or whether I am using a bool value inside an object with useState.
This first bit of code will show the hidden text when the button is pressed. It uses contextMenuIsOpen which is a bool directly on the state, to control the visibility of the hidden text.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
setContextMenuIsOpen(true);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuIsOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
This next bit of code uses a property on an object which is on the state. It doesn't show the hidden text when I do it this way.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuState.isOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
React checks objects for equality by checking their reference.
Simply, look at the below example.
const x = { a : 1, b : 2};
x.a = 3;
console.log(x===x);
So when you do the below,
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
You are not changing the reference of contextMenuState. Hence, there is no real change of state and setContextMenuState does not lead to any rerender.
Solution:
Create a new reference.
One example, is using spread operator:
const openChild = ()=>{
setContextMenuState({ ...contextMenuState , isOpen : true });
}
The problem with your second approach is that React will not identify that the value has changed.
const openChild = () => {
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
In this code, you refer to the object's field, but the object reference itself does not change. React is only detecting that the contextMenuState refers to the same address as before and from its point of view nothing has changed, so there is no need to rerender anything.
If you change your code like this, a new object will be created and old contextMenuState is not equal with the new contextMenuState as Javascript has created a new object with a new address to the memory (ie. oldContextMenuState !== newContextMenuState).:
const openChild = () => {
setContextMenuState({
...contextMenuState,
isOpen: true
});
}
This way React will identify the state change and will rerender.
State is immutable in react.
you have to use setContextMenuState() to update the state value.
Because you want to update state according to the previous state, it's better to pass in an arrow function in setContextMenuState where prev is the previous state.
const openChild = () =>{
setContextMenuState((prev) => ({...prev, isOpen: true }))
}
Try change
contextMenuState.isOpen = true;
to:
setContextMenuState((i) => ({...i, isOpen: true}) )
never change state like this 'contextMenuState.isOpen = true;'
Hello i have problem about change state after onClick with this function i dont know why this is doesnt work because console.log displayed difference value and i dont know why i cant set the same state.
`doneUndone = (index) => {
console.log(!this.state.scores[index].done)
const test = !this.state.scores[index].done
this.setState({
scores: test,
})
}`
here will be all code of this aplication https://codepen.io/RetupK/pen/xxKmELd?editors=0010
As per your state scores is an array and in your method of done you are assigning Boolean value to it where as it must be an array itself. Because you're using .map() in your render method which only works with array not boolean.
What you need to do is change the done property of particular object in scores and pass the newly updated scores object to setState method and it will work.
doneUndone = (index) => {
this.state.scores[index].done = !this.state.scores[index].done
this.setState({
scores: this.state.scores,
})
}
If you use this.state to get previously done value you might have problems when you fire doneUndone method multiple times (e.g. clicking button few times in a row). That's why I suggest such solution:
doneUndone = index => {
this.setState(state => ({
scores: state.scores.map((score, idx) =>
idx === index ? { ...score, done: !score.done } : score
)
}));
};
The doneUndone method isn't updating the state properly. You can check the method form here.
doneUndone = (index) => {
const score = this.state.scores[index];
const updatedScore = {...score, done: !score.done};
const updatedScores = [...this.state.scores];
updatedScores[index] = updatedScore;
this.setState({
...this.state,
scores: updatedScores
})
}
doneUndone = (index) => {
let modScores = this.state.scores;
modScores[index].done=!this.state.scores[index].done
this.setState({
scores: modScores
})
}
cleaner way to do it
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.
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