I have a TextField thats updates state with useState on change. Im facing an issue where upon state change the entire component is re-rendering, I expect only the TextField to change.
const Uploader = ({ onUploadComplete }) => {
const [fields, setFields] = useState({});
const handleName = (event) => {
const { name, value } = event.target;
setFields((fields) => ({
...fields,
[name]: { value: value },
}
<React.Fragment>
<Grid item xs={4}>
<Card>{console.log('Card Media Rendered')}</Card>
</Grid>
<Grid item xs={8}>
<FormControl fullWidth>
<TextField
value={fields[file.id]?.value || ''}
onChange={handleName}
/>
</FormControl>
</Grid>
</React.Fragment>
}
You need to move the useState inside the functional component like this:
const Uploader = ({ onUploadComplete }) => {
const [fields, setFields] = useState({});
const handleName = (event) => {
const { name, value } = event.target;
setFields((fields) => ({
...fields,
[name]: { value: value },
}))
}
return (
<React.Fragment>
<Grid item xs={4}>
<Card>{console.log('Card Media Rendered')}</Card>
</Grid>
<Grid item xs={8}>
<FormControl fullWidth>
<TextField
value={fields[file.id]?.value || ''}
onChange={handleName}
/>
</FormControl>
</Grid>
</React.Fragment>
)
}
Your component did rerender because the variable that it depends on, wasn't in the react scope. So it couldn't trigger the update only to the Textfield
Related
I have a Material UI autocomplete component with a checkbox component. How can I get both of them working such that only when a user selects an option from the autocomplete, the checkbox should get checked. Here is the link to my component:
https://codesandbox.io/embed/material-demo-forked-of2cz?codemirror=1
https://codesandbox.io/s/material-demo-forked-of2cz?from-embed=&file=/demo.tsx
i finally got my autocomplete/checkbox to work and be all checked by default (redux & ts)
const HIDDEN_USERS: HiddenUsersProps[] = store.getState().user.hiddenUsers
// array of objects
//{
// uid:"oKW8wDsasYdssdd1wTLggsas02"
// userName:"Matt"
//}
const [value, setValue] = useState<HiddenUsersProps[]>([])
//the only way i found to wait for the values from the redux store
useEffect(() => {
setValue(HIDDEN_USERS)
}, [HIDDEN_USERS])
const handleChange = (newValue: HiddenUsersProps[]) => {
setValue(newValue)
}
return (
<Autocomplete
options={HIDDEN_USERS}
value={value}
onChange={(_, newValue: HiddenUsersProps[]) => handleChange(newValue)}
getOptionLabel={(option: any) => option.userName}
isOptionEqualToValue={(option, value) => option.userName === value.userName}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox icon={icon}
checkedIcon={checkedIcon}
checked={selected} />
{option.userName}
</li>
)}
renderInput={(params) => (
<TextField {...params}
label="Hidden Users"
placeholder="Name"
variant="outlined" />)} />
)
"#mui/material": "^5.0.6",
You should change your component like this :
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import Checkbox from "#material-ui/core/Checkbox";
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState<string | null>("");
const [inputValue, setInputValue] = React.useState("");
const [checked, setChecked] = React.useState<boolean>(false);
const [text1, setText1] = React.useState("");
const [text2, setText2] = React.useState("");
const isTextFieldsNotEmpty = text1.length > 0;
const handleFirstTextChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setChecked(!checked)
setText1(event.target.value);
};
return (
<div>
<div>{`value: ${value !== null ? `'${value}'` : "null"}`}</div>
<div>{`inputValue: '${inputValue}' '${checked}' '${text1}'`}</div>
<br />
<Checkbox
checked={checked}
onChange={handleFirstTextChange}
inputProps={{ "aria-label": "primary checkbox" }}
/>
<Autocomplete
value={value}
onChange={(event: any, newValue: string | null) => {
setValue(newValue);
setChecked(!checked)
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
id="controllable-states-demo"
options={options}
style={{ width: 300 }}
renderInput={(params) => (
<TextField
{...params}
onChange={handleFirstTextChange}
label="Controllable"
variant="outlined"
/>
)}
/>
</div>
);
}
I just have put the setChecked inside the same event listener than your textfield.
That would create the behaviors you search when the user select a field is checking the checkbox.
I'm currently working on a form (Material-UI) that users fill in to log a set they did for an exercise. Upon submission, it will run a GraphQL mutation. As I also have login and register functionalities that share similarities, I created a form hook for these. The login and register do not need to be reset, as this is done by redirecting them to the home page. However, for the logging set functionality, I want the modal (where the form is) to close after resetting the form back to it's initial state, so that when they choose to log another set, the form does not contain the values from the previous logged set.
Form
const initialState = {
exerciseName: "",
weight: undefined,
reps: undefined,
notes: "",
};
function MyModal() {
const [errors, setErrors] = useState({});
const { onChange, onSubmit, values } = useForm(registerSet, initialState);
const [addSet] = useMutation(ADD_SET, {
update() {
// need to reset form to initial state here
handleClose();
},
onError(err) {
setErrors(err.graphQLErrors[0].extensions.exception.errors);
},
variables: values,
});
function registerSet() {
addSet();
}
return (
<form
onSubmit={onSubmit}
id="addSetForm"
noValidate
autoComplete="off"
>
<TextField
name="exerciseName"
label="Exercise Name"
select
value={values.exerciseName}
error={errors.exerciseName ? true : false}
onChange={onChange}
>
<MenuItem key="Bench Press" value="Bench Press">
Bench Press
</MenuItem>
<MenuItem key="Deadlift" value="Deadlift">
Deadlift
</MenuItem>
<MenuItem key="Squat" value="Squat">
Squat
</MenuItem>
</TextField>
<Grid container spacing={1}>
<Grid item xs={6}>
<TextField
name="weight"
label="Weight"
type="number"
value={values.weight}
error={errors.weight ? true : false}
onChange={onChange}
/>
</Grid>
<Grid item xs={6}>
<TextField
name="reps"
label="Reps"
type="number"
value={values.reps}
error={errors.reps ? true : false}
onChange={onChange}
/>
</Grid>
</Grid>
<TextField
name="notes"
label="Notes (Optional)"
type="text"
multiline={true}
rows="4"
value={values.notes}
onChange={onChange}
/>
</form>
)
}
useForm Hook
export const useForm = (callback, initialState = {}) => {
const [values, setValues] = useState(initialState);
const onChange = (event) => {
setValues({
...values,
[event.target.name]:
event.target.type === "number"
? parseInt(event.target.value)
: event.target.value,
});
};
const onSubmit = (event) => {
event.preventDefault();
callback();
};
return {
onChange,
onSubmit,
passwordVisibility,
confirmPasswordVisibility,
values,
};
};
I'm not sure how I can access setValues from the useForm hook in the update() handler for useMutation to reset the form back to it's initial state.
Step 1: create a resetValues() function inside your useForm hook and export it
const resetValues = () => {
setValues(initialState)
};
return {
// ... OTHER EXPORTS
resetValues,
};
Step 2: Then use this function inside your component
const { onChange, onSubmit, resetValues, values } = useForm(registerSet, initialState);
const [addSet] = useMutation(ADD_SET, {
update() {
resetValues(); // SEE HERE
handleClose();
},
});
I created a component (it's actually a function but I think they're called components?) for a Select Field.
const useStyles = makeStyles((theme) => ({
formControl: {
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}))
export default function SelectField() {
const classes = useStyles()
const [value, setValue] = React.useState("")
return(
<FormControl variant="outlined" className={classes.formControl}>
<InputLabel id="age">Age</InputLabel>
<Select
labelId="age"
id="age"
label="Age"
key="index"
value={value}
onChange={(val) => setValue(val)}
>
<MenuItem value=""><em>None</em></MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
)
}
And I'm using this Select field on another "component" (I'll only show the import for that field).
import SelectField from './fields/select'
export default function CreateGame(){
const [questions, setQuestions] = React.useState([{
question: 'asdasdasd',
answer: '',
score: '',
age: ''
}])
const HandleField = (event, index, field) => {
let temp_questions = questions
temp_questions[index][field] = event.target.value
setQuestions(temp_questions)
console.log(questions)
}
const setAge = (e, index) => {
let questions_temp = questions
questions_temp[index]['age'] = e.target.value
setQuestions(questions_temp)
console.log(questions)
}
return(
<Card>
<CardContent>
<Typography>
Add Questions
</Typography>
{questions.map((value, index) =>
<Grid container spacing={1}>
<Grid item lg={2}>
<TextField
value={value['question']}
key={index}
label='Question'
variant='outlined'
onChange={e => HandleField(e, index, 'question')} />
</Grid>
<Grid item lg={2}>
<TextField
value={value['answer']}
key={index}
label='Answer'
variant='outlined'
onChange={e => HandleField(e, index, 'answer')} />
</Grid>
<Grid item lg={2}>
<TextField
value={value['score']}
label='Score'
key={index}
variant='outlined'
onChange={HandleField} />
</Grid>
<Grid item lg={5}>
<SelectField age={value['age']} setAge={setAge} index={index} />
</Grid>
</Grid>
)}
</CardContent>
<Button>Add</Button>
</Card>
)
}
I want to get my <SelectField />'s selected value and save it in my questions state, specifically in the age field. But I don't know exactly how to capture that value. I tried putting a value prop, but I don't think that's the correct way.
Keep the state only in the parent, and pass it down as a prop, as well as another prop - a function which, when called, sets the age in the parent.
You also need to correct the shape of onChange - it accepts an argument of the event, not of the new value.
// in CreateGame
const setAge = i => (newAge) => {
setQuestions([
questions.slice(0, i),
{ ...questions[i], age: newAge },
questions.slice(i + 1),
]);
};
// ...
<Grid item lg={5}>
<SelectField age={questions.age} setAge={setAge(index)} />
</Grid>
export default function SelectField({ age, setAge }) {
// ...
<Select
value={age}
onChange={e => setAge(e.currentTarget.value)}
You can go through these steps
- Step 1 Make an onchange event listener
<SelectField onChange={handleQuestion}/>
- Step 2 Use Hooks to make a state inside functional component
const [Question, setQuestion] = useState("")
- Step 3 Make the method that you had called in step 1
const handleQuestion = () =>{
//First get the value coming fron the onchange listener
console.log(e.target.value);
//Set the state to the question
setQuestion(e.target.value)
}
- Step 4 Pass the value to the question
<TextField
value={Question}//Pass the questions state value here
/>
I have a form with a field that needs to show suggestions via an api call. The user should be allowed to select one of those options or not and that value that they type in gets used to submit with the form, but this field is required. I am using Formik to handle the form, Yup for form validation to check if this field is required, downshift for the autocomplete, and Material-UI for the field.
The issue comes in when a user decides not to use one of the suggested options and the onBlur triggers. The onBlur always resets the field and I believe this is Downshift causing this behavior but the solutions to this problem suggest controlling the state of Downshift and when I try that it doesn't work well with Formik and Yup and there are some issues that I can't really understand since these components control the inputValue of this field.
Heres what I have so far:
const AddBuildingForm = props => {
const [suggestions, setSuggestions] = useState([]);
const { values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
modalLoading,
setFieldValue,
setFieldTouched,
classes } = props;
const loadOptions = (inputValue) => {
if(inputValue && inputValue.length >= 3 && inputValue.trim() !== "")
{
console.log('send api request', inputValue)
LocationsAPI.autoComplete(inputValue).then(response => {
let options = response.data.map(erlTO => {
return {
label: erlTO.address.normalizedAddress,
value: erlTO.address.normalizedAddress
}
});
setSuggestions(options);
});
}
setFieldValue('address', inputValue); // update formik address value
}
const handleSelectChange = (selectedItem) => {
setFieldValue('address', selectedItem.value); // update formik address value
}
const handleOnBlur = (e) => {
e.preventDefault();
setFieldValue('address', e.target.value);
setFieldTouched('address', true, true);
}
const handleStateChange = changes => {
if (changes.hasOwnProperty('selectedItem')) {
setFieldValue('address', changes.selectedItem)
} else if (changes.hasOwnProperty('inputValue')) {
setFieldValue('address', changes.inputValue);
}
}
return (
<form onSubmit={handleSubmit} autoComplete="off">
{modalLoading && <LinearProgress/>}
<TextField
id="name"
label="*Name"
margin="normal"
name="name"
type="name"
onChange={handleChange}
value={values.name}
onBlur={handleBlur}
disabled={modalLoading}
fullWidth={true}
error={touched.name && Boolean(errors.name)}
helperText={touched.name ? errors.name : ""}/>
<br/>
<Downshift id="address-autocomplete"
onInputValueChange={loadOptions}
onChange={handleSelectChange}
itemToString={item => item ? item.value : '' }
onStateChange={handleStateChange}
>
{({
getInputProps,
getItemProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
}) => (
<div>
<TextField
id="address"
label="*Address"
name="address"
type="address"
className={classes.autoCompleteOptions}
{...getInputProps( {onBlur: handleOnBlur})}
disabled={modalLoading}
error={touched.address && Boolean(errors.address)}
helperText={touched.address ? errors.address : ""}/>
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{suggestions.map((suggestion, index) =>
<MenuItem {...getItemProps({item:suggestion, index, key:suggestion.label})} component="div" >
{suggestion.value}
</MenuItem>
)}
</Paper>
) : null}
</div>
</div>
)}
</Downshift>
<Grid container direction="column" justify="center" alignItems="center">
<Button id="saveBtn"
type="submit"
disabled={modalLoading}
className = {classes.btn}
color="primary"
variant="contained">Save</Button>
</Grid>
</form>
);
}
const AddBuildingModal = props => {
const { modalLoading, classes, autoComplete, autoCompleteOptions } = props;
return(
<Formik
initialValues={{
name: '',
address: '',
}}
validationSchema={validationSchema}
onSubmit = {
(values) => {
values.parentId = props.companyId;
props.submitAddBuildingForm(values);
}
}
render={formikProps => <AddBuildingForm
autoCompleteOptions={autoCompleteOptions}
autoComplete={autoComplete}
classes={classes}
modalLoading={modalLoading}
{...formikProps} />}
/>
);
}
Got it to work. Needed to use handleOuterClick and set the Downshift state with the Formik value:
const handleOuterClick = (state) => {
// Set downshift state to the updated formik value from handleOnBlur
state.setState({
inputValue: values.address
})
}
Now the value stays in the input field whenever I click out.
I have an Autocomplete field which uses:
searchText = {this.state.searchText}
like this;
<AutoComplete
floatingLabelText='agent input'
ref='agentInput'
hintText="type response"
multiLine = {true}
fullWidth = {true}
searchText = {this.state.searchText}
onNewRequest={this.sendAgentInput}
dataSource={this.agentCommands}
/>
But when I update the this.setState({searchText: null })
it will clear the autoComplete once, but not the second time.
Not sure if this is a bug or if there's another way to reset the field.
I also tried looking for the field and adding a ref but no luck.
Filed here in case it's a bug
https://github.com/callemall/material-ui/issues/2615
Try also to change searchText on every input update:
onUpdateInput={this.handleUpdateInput}
This function should change searchText whenever user changes the input:
handleUpdateInput(text) {
this.setState({
searchText: text
})
}
My code looks as follows (ES6):
class MyComponent extends Component {
constructor (props) {
super(props)
this.dataSource = ['a', 'b' ,'c']
this.state = {
searchText: ''
}
}
handleUpdateInput (t) { this.setState({ searchText: t }) }
handleSelect (t) { this.setState( { searchText: '' }) }
render () {
return <AutoComplete dataSource={this.dataSource}
searchText={this.state.searchText}
onNewRequest={this.handleSelect.bind(this)}
onUpdateInput={this.handleUpdateInput.bind(this)}
/>
}
}
Here I want to clear input when user presses enter or chooses some item from the list (so I clear searchText in handleSelect) but I also change state of searchText on every input update (handleUpdateInput).
I hope it will work for you!
Try this :
this.setState({ searchText: "\r" });
because I think the function should test the length of the string (BUG ?)
For freesolo Autocomplete, you can control the inputValue of Autocomplete and set it to an empty string on demand:
const [inputValue, setInputValue] = React.useState('');
return (
<>
<Button onClick={() => setInputValue('')}>clear input</Button>
<Autocomplete
freeSolo
inputValue={inputValue}
onInputChange={(_, v) => setInputValue(v)}
options={top100Films}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</>
);
If you also want to change the currently selected value, not just the value in the input box:
const [inputValue, setInputValue] = React.useState('');
const [value, setValue] = React.useState(null);
return (
<>
<Button
onClick={() => {
setInputValue('');
setValue(null);
}}
>
clear input
</Button>
<Autocomplete
freeSolo
value={value}
onChange={(_, v) => setValue(v)}
inputValue={inputValue}
onInputChange={(_, v) => setInputValue(v)}
options={top100Films}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</>
);