React set button to disabled with state - reactjs

I am in a React class-based component and a property on my state like so:
state = {
controls: {
email: {
validation: {
required: true,
isEmail: true
},
invalid: false,
},
password: {
validation: {
required: true,
minLength: 6
},
invalid: true,
},
disabledSubmit: true,
}
};
I have an inputChangedHandler triggered from an input component:
inputChangedHandler = (event, controlName) => {
console.log("[STATE]", this.state.controls);
const updatedControls = {
...this.state.controls,
[controlName]: {
...this.state.controls[controlName],
value: event.target.value,
invalid: !this.checkValidity(
event.target.value,
this.state.controls[controlName].validation
)
},
touched: true
};
console.log("[UPDATEDCONTROLS]", updatedControls);
this.setState({ controls: updatedControls }, () => {
this.disabledSubmitHandler();
});
};
And a disabledSubmitHandler that should be being called from within my inputChangedHandler :
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid &&
!this.state.controls["password"].invalid
) {
this.setState(
{ disabledSubmit: true },
console.log("[DISABLEDHANDLER] TRUE")
);
}
}
The prop is set on my button component in my JSX like so:
<Button
value="submit"
clicked={this.submitHandler}
disabled={this.state.disabledSubmit}
/>
This does not work, but I'm not sure what's happening?

I think maybe this bit seems to need fixing:
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid && //if email is not invalid
!this.state.controls["password"].invalid //if password is not invalid
) {
this.setState(
{ disabledSubmit: true },
console.log("[DISABLEDHANDLER] TRUE")
);
}
}
That code says if the email and password are valid, disable the input. I think it should be:
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid &&
!this.state.controls["password"].invalid
) {
this.setState(
{ disabledSubmit: false },
console.log("[DISABLEDHANDLER] FALSE")
);
}
}
Plas as #an0nym0us mentioned, disabledSubmit is nested inside controls.
Also, on a side note, it seems a little odd that you would call a function which sets state, only to call another function which sets state inside that function, as a callback to set state (inputChangedHandler calling disabledSubmitHandler'). It seems you could call that disabled check from withininputChangedHandlerpassing it yourupdatedControls, and return true/false fordisabledSubmit, resulting in a single call tosetState`

Related

React checkboxes. State is late when toggling the checkboxes

I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

Immutable object is not being set in state

The immutable object being returned is correct but this.setState({...}) does not seem to set it.
I've tried the various ways to set state and all seem to have failed.
state = {
onboarding3: fromJS({
selection: {
neverBeen: false,
noConvert: false,
both: false
},
noConvertOptions: {
dynamicCreative: false,
noPurchase: false,
abandoned: false
},
bothOptions: {
dynamicCreative: false,
noPurchase1: false,
noPurchase2: false
}
})
};
updateToggle = (field, option) => {
return () => {
const currentValue = this.state.onboarding3.getIn([field, option]);
const onboarding3 = this.state.onboarding3.setIn([field, option], !currentValue);
this.setState({ onboarding3 });
};
};
<Component
roundedSwitchFunc={this.updateToggle("noConvertOptions", "dynamicCreative")}
defaultChecked={onboarding3.getIn(["noConvertOptions", "dynamicCreative"])}
/>
It was an issue of event bubbling and the data not being in sync. The purpose of this component was for my state to mimic the store but since each component has their own 'store', it does not accurately mimic Redux.

SetState not working with spread operator

I have a language switcher in a React native app, it has the selected language ticked, which is stored in state. However this is not working, any advice appreciated. The code is as below. The selectedLanguage is being populated so it knows what needs updating but it's not updating the state object.
constructor(props) {
super(props);
this.state = {
languages: [
{
language: 'Dutch',
selected: false,
},
{
language: 'English',
selected: true,
},
{
language: 'French',
selected: false,
},
{
language: 'German',
selected: false,
},
{
language: 'Polish',
selected: false,
}
],
};
};
changeLanguage(selectedLanguage){
this.state.languages.map((language) => {
if(language.language === selectedLanguage){
this.setState(prevState => ([
...prevState.languages, {
language: selectedLanguage,
selected: true,
}
]));
}
});
}
The spread operator isn’t going to deep compare objects in an array. You’ve got to either move from a languages array to a languages object, as in:
languages: {
Dutch: false,
English: true,
...
}
Or, you need to copy and replace:
changeLanguage(selectedLanguage){
this.state.languages.map((language, index) => {
if(language.language === selectedLanguage){
this.setState(prevState => {
const newLangs = [...prevState.languages];
newLangs[index].selected = true;
return newLangs;
});
}
});
}
First need to find out the right object for given selectedLanguage from an array.
Use Array#map to traverse and update the matched object selected prop value with boolean true and rest to false.
const {languages} = this.state;
const updatedLang = languages.map((lang, key) => {
if (lang.language == selectedLanguage) {
lang.selected = true;
} else {
lang.selected = false;
}
return lang;
})
Now do setState.
this.setState(prevState => ({languages: updatedLang}));

Adding a ref to cloneElement to then set focus of cloneElement

I've not used cloned Elements before, but am now trying to create a button that when clicked opens up an input box of an appropriate type and, crucially, focuses on the input box. What I have seems to basically work except that it doesn't focus (I get a console error that _this.refs.content.focus is not a function). I have tried reading documentation and other questions but am no further forward. Any help appreciated
getInitialState() {
return {
isEditing: false,
.....
};
},
_onEdit() {
if (this.state.isEditing) return;
this.setState({
isEditing: true,
editedValue: this.props.value,
}, () => {
console.dir(this.refs);
this.refs.content.focus();
});
},
.....
render() {
.....
// button clicked
if (this.state.isEditing) {
return React.cloneElement(
this.props.children[0], {
onBlur: this.onBlur,
value: this.state.editedValue,
onChange: this.onChange,
ref: "content",
}
);
} else {
const child = this.props.children[1];
// Editable but in read mode - display clickable button
return pug`
button.btn-block.btn-editable(
onClick=this._onEdit,
type='button',
)= React.cloneElement(child, {}, this.state.editedValue || (child.props.children || ""))
`;
}
},

Resources