Using useState hooks to update based on the previous state successively - reactjs

I'm trying to enable button if both the checkboxes are checked, I have nonworking stackblitz link
I have added the only crux of functionality. Please look into the link for the nonworking demo
import React, { useState } from 'react';
import { render } from 'react-dom';
function App (){
const [checked, toggleCheckbox] = useState({ checkbox1: false, checkbox2: false, disabled: true });
const getDisabled = (state) => {
if (state.checkbox1 || state.checkbox2) {
return false;
} else if (!state.checkbox1 && !state.checkbox2) {
return true;
} else {
return true;
}
};
const handleCheckbox = (checkbox) => {
toggleCheckbox({
...checked,
[checkbox]: !checked[checkbox],
disabled: getDisabled(checked)
});
console.log(checked);
};
const checkDisable = checked.disabled ? 'disabled' : ''
return (
<div>
<div>
<label>
<input
type="checkbox"
className="filled-in"
onChange={() => handleCheckbox('checkbox1')}
checked={checked.checkbox1}
/>
<span className="black-text">Checkbox1</span>
</label>
</div>
<div>
<label>
<input
type="checkbox"
className="filled-in"
onChange={() => handleCheckbox('checkbox2')}
checked={checked.checkbox2}
/>
<span className="black-text">checkbox2</span>
</label>
</div>
<div>
<a className={checkDisable} href="#!">
Next Step
</a>
</div>
</div>
);
}
render(<App />, document.getElementById('root'));
The functionality should be as follows:
The button should be enabled only if both the checkboxes are checked
On unchecking anyone checkboxes it should disable the button

You can simply check the state of both checkbox values.
const isDisabled = !(checked.checkbox1 && checked.checkbox2)
const checkDisable = isDisabled ? 'disabled' : ''
No need to change elsewhere.
Forked stackblitz link.
https://stackblitz.com/edit/react-jscqwr?file=index.js
Answer to the comment.
Hey, that worked! I could see in the log that the state one step below the updated state for an instance after clicking in the first checkbox { checkbox1: false, checkbox: false, disabled: false } after clicking the second checkbox the log { checkbox1: true, checkbox: false, disabled: false }
The reason you are seeing outdated state is because the state updator toggleCheckbox batches the update, thus you'd need to check for the updated status in an effect, which monitors the updated status.
Dynamic number of checkboxes.
I've updated the stack to track dynamic number of checkboxes.
New fork~
https://stackblitz.com/edit/react-pl1e2n
Looks like this.
function App() {
const length = 6;
1️⃣ Generate the initial checkbox states - this prevents un/controlled component error.
const initialCheckboxes = Array
.from({ length }, (_, i) => i + 1)
.reduce((acc, id) => (acc[id] = false, acc), {})
const [checked, toggleCheckbox] = useState(initialCheckboxes);
const [isDisabled, setIsDisabled] = useState(false)
const handleCheckbox = id => {
toggleCheckbox(previous => ({
...previous,
[id]: !previous[id]
}));
};
2️⃣ Update the disable state when the checkbox is selected.
useEffect(() => {
👇 Enable when all checkboxes are not checked - 😅
setIsDisabled(!Object.values(checked).every(_ => !!_))
}, [checked])
3️⃣ Dynamically generate checkboxes
const checkboxElements = Array.from({ length }, (_, i) => i + 1)
.map(id => (
<div key={id}>
<label>
<input
type="checkbox"
className="filled-in"
onChange={() => handleCheckbox(id)}
checked={checked[id]}
/>
<span className="black-text">Checkbox{id}</span>
</label>
</div>
))
return (
<div>
{checkboxElements}
<div>
<a className={isDisabled ? 'disabled' : ''} href="#!">
Next Step
</a>
</div>
</div>
);
}

Related

React when parent component re-render, I loose child state. What I'm missing?

I'm sure I'm missing something about how state works in React.
My component GenericApiFilter is a list of checkboxes, result of API call:
State filters: filters available, result of API call in useEffect()
State selected: list of selected filters by the user
Prop onChange: invoked when the selection changes
export const GenericFilter = ({
apiUrl,
apiParams,
onChange = () => {},
}: GenericFilterProps) => {
const [filters, setFilters] = useState<Filter[]>([]);
const [selected, setSelected] = useState<Filter[]>([]);
useEffect(() => {
axios.get(apiUrl, { params: apiParams }).then(res => setFilters(res.data));
}, [apiUrl, apiParams]);
return (
<>
{filters.map(filter =>
<div key={filter.id}>
<label>{filter.name} ({filter.count})</label>
<input
type='checkbox'
checked={selected.includes(filter)}
disabled={!filter.enabled}
onChange={({ target: { checked } }) => {
const selection = (checked ? [...selected, filter] : selected.filter(f => f !== filter));
setSelected(selection);
onChange(selection);
}}
/>
</div>
)}
</>
);
}
The parent FiltersPanel uses the GenericFilter. What's the problem?
I check one or more checkbox and selection is retained
When I update the state calling setSizeParams (click on the button),i lost the selection: the filter re-renders without retaining the state
export const FiltersPanel= () => {
const [sizeParams, setSizeParams] = useState({});
return (
<>
<button className='btn btn-primary' onClick={() => {
setSizeParams(prev => ({ ...prev, time: Math.random() }));
}}>Update size params</button>
<GenericFilter apiUrl='/api/filter/size' apiParams={sizeParams} />
</>
);
};
What I'm missing here?
The problem is not about React, is about JS:
selected.filter(f => f !== filter));
Here you are comparing objects, and this will match just if you are comparing the same objects. As soon as you render the component again, those object change and the comparison will always return false.
Solution: work with primitive values (e.g. the id), and it should work:
return (
<>
{filters.map((filter) => (
<div key={filter.id}>
<label>
{filter.name} ({filter.count})
</label>
<input
type="checkbox"
checked={selected.includes(filter.id)}
disabled={!filter.enabled}
onChange={({ target: { checked } }) => {
const selection = checked
? [...selected, filter.id]
: selected.filter((f) => f !== filter.id);
setSelected(selection);
onChange(selection);
}}
/>
</div>
))}
</>
);
This is the problem
checked={selected.includes(filter)}
You are checking with includes against an object filter.
But when effect fires again in that component, the new filter objects arrive, hence that above includes check won't work anymore.
So you should use something like an id instead of storing object references.

React.js working code breaks on re-render

I have a weird bug, where my code works on first attempt, but breaks on page re-render.
I've created a filter function using an object with filter names and array of filter values:
const filterOptions = {
'size': ['s', 'm', 'l'],
'color': ['black', 'white', 'pink', 'beige'],
'fit': ['relaxed fit','slim fit', 'skinny fit', 'oversize'],
'pattern': ['patterned', 'spotted', 'solid color'],
'material': ['wool', 'cotton', 'leather', 'denim', 'satin']
}
The idea was to create a separate object with all the values and corresponding 'checked' attribute and than use it to check if checkbox is checked:
const [checkedValue, setCheckedValue] = useState({})
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))}, [])
FilterValue here is array of values from FilterOptions:
<div className='popper'>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
There is onChange function as wel, which could be a part of problem:
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}}
I've tried keeping filterOptions in parent component and in Context, but it gives exactly the same result. It always work as planned on first render, and on next render it shows this error, until you delete the checked attribute of input. I've noticed that on re-render the 'checkedValue' object returns as empty, but I can't find out why. Would be really helpful if somebody could explain me a reason.
Uncaught TypeError: Cannot read properties of undefined (reading 'checked')
Edit: full code looks like this:
Parent Component
const Filter = () => {
return (
<div className='filter'>
<div className="price-filter">
<p>Price: </p>
<Slider onChange={handleSliderChange} value={[min, max]} valueLabelDisplay="on" disableSwap style={{width:"70%"}} min={0} max={250} />
</div>
<Divider />
<ul className='filter-list'>
{Object.entries(filterOptions).map((filter, i) => {
return (
<Fragment key={`${filter[0]}${i}`}>
<FilterOption className='filter-option' filterName={filter[0]} filterValue={filter[1]} />
<Divider key={`${i}${Math.random()}`} />
</Fragment>
)
})}
</ul>
</div>
)
}
Child Component
const FilterOption = ({ filterName, filterValue }) => {
const { checkedValue, setCheckedValue, activeFilters, setActiveFilters, filterOptions } = useContext(FilterContext)
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))
}, [])
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}
}
return (
<div className='popper' key={filterName}>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
)

How to remove checkbox state on button click

In the following code there are three questions with some checkboxes. There is only one form and the questions change content based on a count state. The values of the chosen checkboxes is saved and displayed in another component.
When I click on the next button I would like to get the new question and the checkboxes to reset. Currently if I check one of the checkboxes and click next, the checkbox still remains checked for the next question. Is it possible to remove the checked state on button click in this scenario?
import React from "react";
import questionsData from "../questions-data";
function AddQuestions({ onAddTwo }) {
const [userinfo, setUserInfo] = React.useState({
options: [],
response: [],
});
const handleChange = (e) => {
// Destructuring
const { value, checked } = e.target;
const { options } = userinfo;
checked === true
? setUserInfo({
options: [...options, value],
response: [...options, value],
})
: setUserInfo({
options: options.filter((e) => e !== value),
response: options.filter((e) => e !== value),
});
};
///submits to list
const [count, setCount] = React.useState(0);
const data = questionsData.data.option;
const ask = data[count].ask;
const one = data[count].one;
const two = data[count].two;
const three = data[count].three;
const onSubmitTwo = (e) => {
e.preventDefault();
console.log("clicked")
const daas = userinfo.response;
!daas.length ? console.log("empty") : onAddTwo({ daas });
!daas.length ? alert("please add task") : setCount(count + 1);
};
//button func
const stylesNext = {
display: count > 2 ? "none" : " ",
};
return (
<>
<form>
<div className="one">
<legend>{ask}</legend>
<input className= "check--input"
type="checkbox"
name= "option"
value={one}
checked ={choice}
onChange={handleChange} />
<label htmlFor="check--input">{one}</label>
<input className="check--input"
type="checkbox"
name="option"
value={two}
onChange={handleChange} />
<label htmlFor="check--input">{two}</label>
<input className="check--input"
type="checkbox"
name="option"
value={three}
onChange={handleChange} />
<label htmlFor="check--input">{three}</label>
</div>
<div className="control--btns">
<button className="next--btn" onClick={onSubmitTwo} style={stylesNext}>
{count === 2 ? "Submit" : "Next"}
</button>
</div>
</form>
);
}
export default AddQuestions;
```

Material UI Autocomplete - Disable listbox after the item selection

I'm creating an Autocomplete component in React.js with the help of Material-UI headless useAutoComplete hook. The component is working properly. When the user tries to type any character, the Listbox will automatically open.
But the problem is that when the user selects anything and pays attention to the input element, the ListBox reopens. How can I prevent this?
Codesandbox:
Code:
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = function () {
if (value.length > 0) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li {...getOptionProps({ option, index })} {...listItem}>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;
You can store the selectedItem in a state and use it in handleOpen to decide whether the list has to be displayed.
For setting selectedItem you can modify the default click provided by getOptionProps
import React from 'react';
import useAutocomplete from '#material-ui/lab/useAutocomplete';
function AutocompleteComponent(props) {
const { data } = props;
const [value, setValue] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const [selectedItem, setSelectedItem] = React.useState('');
const handleOpen = function () {
if (value.length > 0 && selectedItem !== value) {
setIsOpen(true);
}
};
const handleInputChange = function (event, newInputValue) {
setValue(newInputValue);
if (newInputValue.length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
const {
getRootProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: 'form-control',
options: data,
autoComplete: true,
open: isOpen, // Manually control
onOpen: handleOpen, // Manually control
onClose: () => setIsOpen(false), // Manually control
inputValue: value, // Manually control
onInputChange: handleInputChange, // Manually control
getOptionLabel: (option) => option.name
});
const listItem = {
className: 'form-control__item'
};
return (
<div className="app">
<div className="form">
<div {...getRootProps()}>
<input
type="text"
className="form-control"
placeholder="Location"
{...getInputProps()}
/>
</div>
{groupedOptions.length > 0 && (
<ul
className="form-control__box"
aria-labelledby="autocompleteMenu"
{...getListboxProps()}
>
{groupedOptions.map((option, index) => {
return (
<li
{...getOptionProps({ option, index })}
{...listItem}
onClick={(ev) => {
setSelectedItem(option.name);
getOptionProps({ option, index }).onClick(ev);
}}
>
<div>{option.name}</div>
</li>
);
})}
</ul>
)}
</div>
</div>
);
}
export default AutocompleteComponent;
You can modify the props passed to the inputbox, before applying them. That way you can delete the event that happens when the input is clicked.
var newProps = getInputProps();
delete newProps.onMouseDown; //delete the extra MouseDown event
return (
...
<input
...
placeholder="Location"
{...newProps} //use newProps rather than calling getInputProps
And it will stop showing that popup when you click it.
You can check the working code here

How to Dynamically set the attributes of a component, from users input?

Set the attributes of a input field or any component by taking input from the user dynamically?
I would like to know if there is any way, where I would give user an option to choose a component from the list of components i would mention, and allow him to customize the components attributes. For example if the user chooses a Input component, he must be able to set the attributes of that particular component, like "required", "type", "placeholder".
You can achieve it by passing all attributes you want as props to the child component.
You should also add them to state of parent component with change handler.
Each time the user change something of the attributes, the state should update.
As the state updates, the new state will pass as props to child Component and it'll update.
I made a simple example to input: You can change its placeholder, minLength, and requierd.
Check This Example
in the render, method you can do something like this
render() {
// change the name and values base on your user input
userInputtedAttribName = "placeholder";
userInputtedAttribValue = "the placeholder";
// the object to contain your user defined attribute name and values
const dynamicAttributes = {
[userInputtedAttribName]: userInputtedAttribValue
};
return (
<div>
<input type="text" {...dynamicAttributes}></input>
</div>
)
}
the spread operator, {...dynamicAttributes}, will build the attributes and their values dynamically
Probably not even what you're looking for, but I made a medium-sized prototype that can show you how to create Components (input, button, textarea), dynamically.
It's like filling out a form. Choose a type of component you want to make from the select-list. Then define the attributes you want in the proceeding textboxes. Once you're done adding all the attributes, hit Generate to render your customized component.
Sandbox: https://codesandbox.io/s/dynamic-component-generator-mhuh5
Working code:
import React from "react";
import ReactDOM from "react-dom";
import Input from "./Input";
import Button from "./Button";
import TextArea from "./TextArea";
import "./styles.css";
class ComponentGenerator extends React.Component {
state = {
componentInProgress: null,
customizeMode: false,
attributeName: "",
attributeSetting: "",
boolean: false,
builtComponents: []
};
handleSelection = e => {
this.setState({
componentInProgress: { componentType: e.target.value },
customizeMode: true
});
};
createOptions = () => {
const { customizeMode, componentInProgress } = this.state;
return (
<div>
<h4>Choose a Component:</h4>
<select
onChange={this.handleSelection}
value={!customizeMode ? "Select" : componentInProgress.componentType}
>
<option>Select</option>
<option>Input</option>
<option>TextArea</option>
<option>Button</option>
</select>
</div>
);
};
handleOnChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleOnSubmit = e => {
const {
attributeName,
attributeSetting,
boolean,
componentInProgress
} = this.state;
e.preventDefault();
let componentCopy = JSON.parse(JSON.stringify(componentInProgress));
componentCopy.props = {
...componentCopy.props,
[attributeName]: boolean ? boolean : attributeSetting
};
this.setState({
componentInProgress: componentCopy,
attributeName: "",
attributeSetting: "",
boolean: false
});
};
setBoolean = boolean => {
this.setState({
boolean: boolean
});
};
generateComponent = () => {
const { componentInProgress, builtComponents } = this.state;
this.setState({
componentInProgress: null,
customizeMode: false,
builtComponents: [...builtComponents, componentInProgress]
});
};
defineComponentAttributes = () => {
const {
componentInProgress,
attributeName,
attributeSetting,
boolean
} = this.state;
return (
<div>
<h4>
Customizing:{" "}
<span className="highlight">{componentInProgress.componentType}</span>
</h4>
{/*Render form */}
<form onSubmit={this.handleOnSubmit}>
<label>Attribute: </label>
<input
className="form-group"
onChange={this.handleOnChange}
value={attributeName}
name="attributeName"
placeholder="Choose attribute (type)"
/>
<label>Definition: </label>
<input
className="form-group"
onChange={this.handleOnChange}
value={attributeSetting}
name="attributeSetting"
placeholder="Define attribute (text)"
/>
<label>This is a Boolean type: </label>
<input
type="radio"
name="boolean"
onChange={() => this.setBoolean(true)}
/>
True
<input
type="radio"
name="boolean"
checked={boolean === false}
onChange={() => this.setBoolean(false)}
/>
False
<button className="form-group" type="submit">
Add
</button>
</form>
{/*Create List of attributes */}
{componentInProgress.props && (
<div>
<h4>Defined Attributes:</h4>
{Object.entries(componentInProgress.props).map(
([propName, propValue]) => {
return (
<div key={propName}>
<span>{propName}: </span>
<span>{propValue + ""}</span>
</div>
);
}
)}
</div>
)}
<div>
<h4>Click to finish and generate:</h4>
<button onClick={this.generateComponent}>Generate</button>
</div>
</div>
);
};
renderComponents = () => {
const { builtComponents } = this.state;
return builtComponents.map((component, index) => {
let renderedComponent = () => {
switch (component.componentType) {
case "Input":
return <Input {...component.props} />;
case "Button":
return <Button {...component.props} />;
case "TextArea":
return <TextArea {...component.props} />;
default:
return null;
}
};
return (
<div key={index} className="componentSection">
<h4>{component.componentType}</h4>
{renderedComponent()}
<div>
<p>Attributes: </p>
{Object.entries(component.props).map(([propName, propValue]) => {
return (
<div key={propName}>
<span>{propName}: </span>
<span>{propValue + ""}</span>
</div>
);
})}
</div>
</div>
);
});
};
render() {
const { customizeMode } = this.state;
return (
<div>
{this.createOptions()}
{customizeMode && this.defineComponentAttributes()}
<div className="components">{this.renderComponents()}</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<ComponentGenerator />, rootElement);

Resources