React app showing form in dialog results in a "findDOMNode is deprecated" error - reactjs

I have a react app, the parent component has a button which when clicked shows a simple dialog with one text input and a submit button. Strict mode is enabled. There are two issues
The form input is set to show an initial value (formik initialValues is set) in the input but that is not being set
When the button is clicked I see an error in the console;
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
The dialog component comes from Material UI and the form comes from Formik. I've created a simple repro here. The error is in the dev tools console. What would cause that error and why is the value not initialising?
Here's the parent component;
import React, { useState } from "react";
import { Button, Typography } from "#material-ui/core";
import ProfileEditor from "./ProfileEditor";
function ProfileManager() {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
return (
<div>
<Typography variant="h5">Profile Manager</Typography>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open profile editor dialog
</Button>
<ProfileEditor open={open} onClose={handleClose}></ProfileEditor>
</div>
);
}
export default ProfileManager;
and the dialog component displayed when the button is clicked in the component above;
import React from "react";
import {
Button,
Dialog,
DialogContent,
LinearProgress,
TextField
} from "#material-ui/core";
import { Formik, Form } from "formik";
interface Props {
open: boolean;
onClose: () => void;
}
function ProfileEditor(props: Props) {
return (
<Dialog open={props.open}>
<DialogContent>
<Formik
// initial value not being displayed !!! 😢
initialValues={{
firstName: "Billy"
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
>
{({ submitForm, isSubmitting }) => (
<Form>
<TextField name="firstName" type="text" label="First name" />
{isSubmitting && <LinearProgress />}
<br />
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
>
Submit
</Button>
<Button variant="contained" onClick={props.onClose}>
Close
</Button>
</Form>
)}
</Formik>
</DialogContent>
</Dialog>
);
}
export default ProfileEditor;

You need to include a value prop to the form field to have it initialized properly.
{({ submitForm, isSubmitting, values }) => (
<Form>
<TextField
name="firstName"
type="text"
label="First name"
value={values.firstName} /* you need this prop */
/>
...
CodeSandBox: https://codesandbox.io/s/so-react-formik-inside-material-dialog-sfq4e?file=/ProfileEditor.tsx
Regarding your issue on the console, I'm not entirely sure at this point what is causing it, but if it bothers you or is causing additional problems, perhaps you can opt to move out of strict mode
<React.Fragment>
<ProfileManager></ProfileManager>
</React.Fragment>

Related

Required Field in React Form not Requiring Text Entry

I have a subscription dialog form. I want the email field to be required, but I am currently able to submit my form with a blank email address (which would be a major problem for the client!). I have it marked as required in my code, but that doesn't seem to be translating to my UI.
I am using Material UI for styling.
Any pointers are sincerely appreciated :)
In the picture, see how I was able to click subscribe with no email address (the submit message appears after clicking subscribe).
import React from 'react';
import Button from '#material-ui/core/Button';
import { makeStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
import Grid from '#material-ui/core/Grid';
import { Typography } from '#material-ui/core';
import { Form } from 'react-final-form';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
padding: theme.spacing(2)
},
divider: {
marginBottom: theme.spacing(2)
}
}));
export default function SubscribeFormResults() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const [formSubmitted, setFormSubmitted] = React.useState(false);
const onSubmit = async values => {
console.log('Submitting subscribe form!');
console.log('Subscribe form values:', values);
setFormSubmitted(true);
};
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
setFormSubmitted(false);
};
const validate = values => {
const errors = {};
if (!values.userEmail) {
errors.userEmail = 'Required';
}
return errors;
};
return (
<div>
<Button size="small" color="primary" onClick={handleClickOpen}>
Subscribe
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Subscribe</DialogTitle>
<DialogContent>
<Form
onSubmit={onSubmit}
initialValues={{ userEmail: 'johndoe#example.com', arn: 'Connect to Backend!' }}
validate={validate}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit} noValidate>
<DialogContentText>
To subscribe to this website, please enter your email address here. We will send updates
occasionally.
</DialogContentText>
<TextField
label="Email Address"
name="userEmail"
margin="none"
required={true}
fullWidth
/>
{formSubmitted && <Grid item xs={12}>
<Typography name='submitMessage' variant='subtitle1'>You have subscribed to AA-01-23-45-678901-2. {/* Connect to backend here */}</Typography>
</Grid>}
<DialogActions>
<Button /* onClick={handleClose} */ color="primary" type="submit" disabled={submitting}>
Subscribe
</Button>
<Button onClick={handleClose} color="primary">
Close
</Button>
</DialogActions>
</form>
)}
/>
</DialogContent>
</Dialog>
</div>
);
}
For future readers, I fixed this by removing the validation parameter from the Material UI, uppercase Form tag and enforced validation using the standard, lowercase form tag.

How to reset Material UI Checkbox on Dialog close

I've created a Dialog component via Material UI, dynamically imported from another file.
It works fine, except the checkboxes (also created with Material UI) inside this Dialog do not reset after each time Dialog closes. They only reset on page refresh. Other types of input, such as text or password do reset themselves automatically.
Please notice that the Dialog component does not have keepMounted = true, and that's how it resets its inputs automatically. Because, this value is false by default.
Here is the code for the original Dialog modal component:
import React, {useState, useEffect} from "react";
import Button from "#material/react-button";
import Divider from "#material-ui/core/Divider";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
import DialogTitle from "#material-ui/core/DialogTitle";
import TextField from "#material-ui/core/TextField";
import FormGroup from "#material-ui/core/FormGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
const SearchModal = (props) => {
const [checkState, setCheckState] = useState({
checkedQuestions: true,
checkedAnswers: true,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
useEffect(() => {
if(
checkState.checkedQuestions === false &&
checkState.checkedAnswers === false
){
setCheckState({
...checkState,
checkedQuestions: true,
checkedAnswers: true
});
}
if(
checkState.checkedVerified === false &&
checkState.checkedPending === false &&
checkState.checkedDisputed === false
){
setCheckState({
...checkState,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
}
});
const checkSet = (event) => {
setCheckState({
...checkState,
[event.target.name]: event.target.checked
});
}
return(
<Dialog
open={props.searchOpen}
onClose={props.handleClose}
aria-labelledby="searchModalTitle"
aria-describedby="searchModalDescription"
id="searchModal"
>
<DialogTitle id="dialog">{"Search tolodire."}</DialogTitle>
<DialogContent>
<DialogContentText className="marginBottom-17" id="searchModalDescription">
Search for questions or answers.
</DialogContentText>
<TextField
required
type="search"
id="searchQuery"
label="Enter keywords or sentences"
placeholder="Required"
variant="outlined"
data-owner="searchModal"
autoFocus
/>
<DialogContentText className="marginTop-20 marginBottom-10">
Use filters to search in detail.
</DialogContentText>
<FormGroup row className="marginTop-5">
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedQuestions}
onChange={(e) => checkSet(e)}
name="checkedQuestions"
/>
}
label="Questions"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedAnswers}
onChange={(e) => checkSet(e)}
name="checkedAnswers"
/>
}
label="Answers"
/>
</FormGroup>
<Divider/>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedVerified}
onChange={(e) => checkSet(e)}
name="checkedVerified"
/>
}
label="Verified"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedPending}
onChange={(e) => checkSet(e)}
name="checkedPending"
/>
}
label="Pending Verification"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedDisputed}
onChange={(e) => checkSet(e)}
name="checkedDisputed"
/>
}
label="Disputed"
/>
</FormGroup>
</DialogContent>
<DialogActions>
<Button raised className="button regularButton font-body" onClick={props.handleClose}>
Search
</Button>
</DialogActions>
</Dialog>
);
}
export default SearchModal
I've already tried searching this issue on Google and StackOverflow, yet, I haven't found any solution. Any contribution is appreciated.
P.S: The handleClose const is on another file;
const [searchOpen, setSearchOpen] = useState(false);
const handleSearchOpen = () => {
setSearchOpen(true);
};
const handleClose = () => {
setSearchOpen(false);
};
I've found the solution myself.
In the same file for SearchModal, I've created another useEffect field to check if the modal is open or not. When I was trying the same inside the pre-existing useEffect field, it created infinite loop.
For the reset process, I'm assigning the default values from scratch independently from other consts.
I am aware that the implementation could be much better, with more code-reuse or dynamic elements. But, this is just a primitive solution until a better one appears any time soon.
So, the solution is:
useEffect(() => {
if(props.searchOpen === false){
setCheckState({
checkedQuestions: true,
checkedAnswers: false,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
}
}, [props.searchOpen]);

How to set values in a multi-step Formik form with components that implement useField()

I'm implementing the multi-step wizard example with Material-UI components and it works well with the useField() hook but I cannot figure out how to bring setFieldValue() into scope, so I can use it from a wizard step.
I've seen suggestions to use the connect() higher-order component but I have no idea how to do that.
Here is a snippet of my code: CodeSandbox, and the use case:
A wizard step has some optional fields that can be shown/hidden using a Material-UI Switch. I would like the values in the optional fields to be cleared when the switch is toggled off.
I.e.
Toggle switch on.
Enter data in Comments field.
Toggle switch off.
Comments value is cleared.
Toggle switch on.
Comments field is empty.
Hoping someone can help! Thanks.
I came across this answer the other day but discarded it because I couldn't get it working.
It does actually work but I'm in two minds as to whether it's the right approach.
const handleOptionalChange = (form) => {
setOptional(!optional)
form.setFieldValue('optionalComments', '', false)
}
<FormGroup>
<FormControlLabel
control={
// As this element is not a Formik field, it has no access to the Formik context.
// Wrap with Field to gain access to the context.
<Field>
{({ field, form }) => (
<Switch
checked={optional}
onChange={() => handleOptionalChange(form)}
name="optional"
color="primary"
/>
)}
</Field>
}
label="Optional"
/>
</FormGroup>
CodeSandbox.
I believe this is what you're after: CodeSandbox. I forked your CodeSandbox.
I tried to follow your code as closely as possible and ended up not using WizardStep. The step variable is returning a React component that is a child to Formik. Formik is rendered with props e.g. setFieldValue, which can be passed down to its children. In order to pass the setFieldValue as a prop to step, I had to use cloneElement() (https://reactjs.org/docs/react-api.html#cloneelement), which allows me to clone the step component and add props to it as follows.
// FormikWizard.js
<Formik
initialValues={snapshot}
onSubmit={handleSubmit}
validate={step.props.validate}
>
{(formik) => (
<Form>
<DialogContent className={classes.wizardDialogContent}>
<Stepper
className={classes.wizardDialogStepper}
activeStep={stepNumber}
alternativeLabel
>
{steps.map((step) => (
<Step key={step.props.name}>
<StepLabel>{step.props.name}</StepLabel>
</Step>
))}
</Stepper>
<Box
className={classes.wizardStepContent}
data-cy="wizardStepContent"
>
{React.cloneElement(step, {
setFieldValue: formik.setFieldValue
})}
</Box>
</DialogContent>
<DialogActions
className={classes.wizardDialogActions}
data-cy="wizardDialogActions"
>
<Button onClick={handleCancel} color="primary">
Cancel
</Button>
<Button
disabled={stepNumber <= 0}
onClick={() => handleBack(formik.values)}
color="primary"
>
Back
</Button>
<Button
disabled={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{isFinalStep ? "Submit" : "Next"}
</Button>
</DialogActions>
</Form>
)}
</Formik>
To access the setFieldValue prop in the child component, in App.js, I created a new component called StepOne and used it to wrap around the inputs, instead of using WizardStep. Now I am able to access setFieldValue and use it in the handleOptionalChange function.
// App.js
import React, { useState } from "react";
import "./styles.css";
import { makeStyles } from "#material-ui/core/styles";
import Box from "#material-ui/core/Box";
import CssBaseline from "#material-ui/core/CssBaseline";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import FormGroup from "#material-ui/core/FormGroup";
import Switch from "#material-ui/core/Switch";
import FormikTextField from "./FormikTextField";
import { Wizard, WizardStep } from "./FormikWizard";
const useStyles = makeStyles((theme) => ({
content: {
display: "flex",
flexFlow: "column nowrap",
alignItems: "center",
width: "100%"
}
}));
const initialValues = {
forename: "",
surname: "",
optionalComments: ""
};
const StepOne = ({ setFieldValue }) => {
const classes = useStyles();
const [optional, setOptional] = useState(false);
const displayOptional = optional ? null : "none";
const handleOptionalChange = () => {
setFieldValue("optionalComments", "");
setOptional(!optional);
};
return (
<Box className={classes.content}>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="forename"
label="Forename"
type="text"
/>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="surname"
label="Surname"
type="text"
/>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={optional}
onChange={handleOptionalChange}
name="optional"
color="primary"
/>
}
label="Optional"
/>
</FormGroup>
<FormikTextField
style={{ display: displayOptional }}
fullWidth
size="small"
variant="outlined"
name="optionalComments"
label="Comments"
type="text"
/>
</Box>
);
};
function App(props) {
return (
<>
<CssBaseline />
<Wizard
title="My Wizard"
open={true}
initialValues={initialValues}
onCancel={() => {
return;
}}
onSubmit={async (values) => {
console.log(JSON.stringify(values));
}}
>
<StepOne />
<StepTwo />
</Wizard>
</>
);
}
export default App;
Alternative
To use setFieldValue in Formik, the easiest way would be to have the all input elements within the <Formik></Formik tags. You could conditionally render the input elements based on what step you're on as follows. This gives the inputs a direct access to setFieldValue so you can call setFieldValue("optionalComments", "") on the Switch input which will clear the comments on each toggle. Although this may mean you'll have a longer form, I don't think this is necessarily a bad thing.
<Formik>
<Form>
{step === 1 && <div>
// Insert inputs here
</div>}
{step === 2 && <div>
<TextField
onChange={(event) => setFieldValue("someField", event.target.value)}
/>
<Switch
checked={optional}
onChange={() => {
setFieldValue("optionalComments", "");
setOptional(!optional);
}}
name="optional"
color="primary"
/>
</div>}
</Form>
</Formik>

ForwardRef warning React-hook-forms with Material UI TextField

I am trying to build a form with react-hook-forms with Material UI's inputs (my custom variant of TextField in this case). Although the form seems to work completely fine, it triggers a warning message in the console when rendering the form.
Warning: Function components cannot be given refs. Attempts to
access this ref will fail. Did you mean to use React.forwardRef()?
I am using react-hook-form's Controller to wrap my TextField (as suggested by the docs)
Any suggestions or solutions are very welcome!
Below both the TextField component and the form where this issue occurs:
Component TextField
const TextField = props => {
const {
icon,
disabled,
errors,
helperText,
id,
label,
value,
name,
required,
...rest
} = props;
const classes = useFieldStyles();
return (
<MuiTextField
{...rest}
name={name}
label={label}
value={value || ''}
required={required}
disabled={disabled}
helperText={helperText}
error={errors}
variant="outlined"
margin="normal"
color="primary"
InputProps={{
startAdornment: icon,
classes: {
notchedOutline: classes.outline,
},
}}
InputLabelProps={{
className: classes.inputLabel,
}}
/>
)
};
TextField.propTypes = {
icon: PropTypes.node,
disabled: PropTypes.bool,
label: PropTypes.string,
id: PropTypes.string,
value: PropTypes.any,
required: PropTypes.bool,
helperText: PropTypes.string,
};
export default TextField;
Component LoginForm
const LoginForm = () => {
const { handleSubmit, errors, control } = useForm();
const onSubmit = values => console.log(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Typography variant="h5" color="primary" gutterBottom>
Login
</Typography>
<Box py={3} height="100%" display="flex" flexDirection="column">
<Controller
as={TextField}
label="Username"
name="username"
control={control}
errors={errors}
required
/>
<Controller
as={TextField}
label="Password"
type="password"
name="password"
control={control}
errors={errors}
required
/>
<Link>
Forgot your password?
</Link>
</Box>
<Button variant="contained" color="primary" fullWidth type="submit">
Submit
</Button>
</form>
)
};
Try to use Controller's render prop instead of as, because TextField's exposed ref is actually called inputRef, while Controller is trying to access ref.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useForm, Controller } from "react-hook-form";
import Header from "./Header";
import { TextField, ThemeProvider, createMuiTheme } from "#material-ui/core";
import "react-datepicker/dist/react-datepicker.css";
import "./styles.css";
import ButtonsResult from "./ButtonsResult";
let renderCount = 0;
const theme = createMuiTheme({
palette: {
type: "dark"
}
});
const defaultValues = {
TextField: "",
TextField1: ""
};
function App() {
const { handleSubmit, reset, control } = useForm({ defaultValues });
const [data, setData] = useState(null);
renderCount++;
return (
<ThemeProvider theme={theme}>
<form onSubmit={handleSubmit((data) => setData(data))} className="form">
<Header renderCount={renderCount} />
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField"
control={control}
rules={{ required: true }}
/>
</section>
<section>
<label>MUI TextField</label>
<Controller
render={(props) => (
<TextField
value={props.value}
onChange={props.onChange}
inputRef={props.ref}
/>
)}
name="TextField1"
control={control}
rules={{ required: true }}
/>
</section>
<ButtonsResult {...{ data, reset, defaultValues }} />
</form>
</ThemeProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
you can click the following link for actual behavior, now with ref assigned properly with Controller, we can successfully focus on the field when there is an error for better accessibility.
https://codesandbox.io/s/react-hook-form-focus-74ecu
The warning is completely right as suggested by the official docs it think you did not reach to the functional components part. Link to the offical docs
You cannot give ref to functional components as they do not have instances
If you want to allow people to take a ref to your function component, you can use forwardRef (possibly in conjunction with useImperativeHandle), or you can convert the component to a class.
You can, however, use the ref attribute inside a function component as long as you refer to a DOM element or a class component like this:
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

Material-UI AutoComplete freeSolo in Form

https://codesandbox.io/s/naughty-bogdan-hvhsd?file=/src/searchfield.tsx
As you can see in this SandBox, I'm using Material AutoComplete as a multiple input with free options. The component should return to Formik ["term1","term2","term3"] and the user can see each string as a label in a Chip. This will be used as filters in a search.
This all happens, but only if the input is already in InitialValues. If the user does an input manually and press enter or tab it craches in a error "value.map" is not a function.
The error points to this line in the material autocomplete component code
"getInputProps = _useAutocomplete.getInputProps,"
Does anyone has any ideia on how to make AutoComplete and Forms work together?
Something like this (searchfield.tsx):
import React, { useState } from "react";
import { TextField } from "#material-ui/core";
import { Formik, Form } from "formik";
import Autocomplete from "#material-ui/lab/Autocomplete";
export default function SearchBar() {
const [searchValues] = useState(["ola", "como"]);
return (
<Formik
initialValues={{
search: []
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
JSON.stringify(values, null, 2);
console.log(values);
setSubmitting(false);
}, 400);
}}
>
{({ values, handleChange, handleBlur, handleSubmit }) => (
<Form onSubmit={handleSubmit}>
<Autocomplete
autoSelect
freeSolo
id="search"
limitTags={4}
multiple
onBlur={handleBlur}
onChange={handleChange}
options={searchValues}
getOptionLabel={option => option}
filterSelectedOptions
renderInput={params => (
<TextField
{...params}
id="search"
name="search"
variant="outlined"
label="Busca"
placeholder="Termos de Busca"
/>
)}
/>
<h6>{searchValues}</h6>
</Form>
)}
</Formik>
);
}

Resources