Refactoring almost identical components, formik react - reactjs

I have two identical components, and only few differences (one). There are two many repetitive code and boilerplate, but I am unsure how to refactor this so that I only need to supply a config probably.
LoginPage.js
import React from 'react';
import { Link } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';
import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import { LoginValidationSchema } from 'validations/AuthValidationSchema';
function LoginPage({ username, onChangeUsername, onSubmitForm }) {
return (
<div>
<Helmet>
<title>Login</title>
</Helmet>
<Formik
initialValues={{ username, password: '' }}
validationSchema={LoginValidationSchema}
onSubmit={onSubmitForm}
render={({ isSubmitting, isValid, handleChange }) => (
<Form>
<FastField
type="text"
name="username"
render={({ field }) => (
<input
{...field}
onChange={e => {
handleChange(e);
onChangeUsername(e);
}}
/>
)}
/>
<ErrorMessage name="username" component="div" aria-live="polite" />
<FastField type="password" name="password" />
<ErrorMessage name="password" component="div" aria-live="polite" />
<button type="submit" disabled={isSubmitting || !isValid}>
Login
</button>
<FormDebug />
</Form>
)}
/>
<Link to="/auth/forgot_password">Forgot Password</Link>
</div>
);
}
LoginPage.propTypes = {
username: PropTypes.string,
onSubmitForm: PropTypes.func.isRequired,
onChangeUsername: PropTypes.func.isRequired,
};
export default LoginPage;
ForgotPasswordPage.js
import React from 'react';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';
import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import { ForgotPasswordValidationSchema } from 'validations/AuthValidationSchema';
function ForgotPasswordPage({ username, onChangeUsername, onSubmitForm }) {
return (
<div>
<Helmet>
<title>Forgot Password</title>
</Helmet>
<Formik
initialValues={{ username }}
validationSchema={ForgotPasswordValidationSchema}
onSubmit={onSubmitForm}
render={({ isSubmitting, isValid, handleChange }) => (
<Form>
<FastField
type="text"
name="username"
render={({ field }) => (
<input
{...field}
onChange={e => {
handleChange(e);
onChangeUsername(e);
}}
/>
)}
/>
<ErrorMessage name="username" component="div" aria-live="polite" />
<FormDebug />
<button type="submit" disabled={isSubmitting || !isValid}>
Reset Password
</button>
</Form>
)}
/>
</div>
);
}
ForgotPasswordPage.propTypes = {
username: PropTypes.string,
onSubmitForm: PropTypes.func.isRequired,
onChangeUsername: PropTypes.func.isRequired,
};
export default ForgotPasswordPage;
How to you refactor this, if you were me.
I am thinking HOC., but I am unsure how to call pass the "children" which is different

Apologies if you're not looking for a general answer, but I don't think you'll improve maintainability by generalising what may just be seemingly correlated components. I expect these components will drift further apart as you mature your application, e.g. by adding social login, option to 'remember me', captchas, option for retrieving both username and password by email, different handling of an unknown username when retrieving password vs signing in, etc. Also, this is a part of your component you really don't want to get wrong, so KISS and all. Finally, consider if there really is a third use case for such a semi-generalised login-or-retrieve-password form component.
Still, minor improvement could be made by e.g. creating a reusable UsernameField component, the usage of which will be simple and consistent to both cases. Also consider a withValidation HOC adding an error message to a field. If you really want to stretch it, you could have a withSubmit HOC for Formik, passing all props to Formik, rendering children (which you would pass handleChange prop) and a submit button. I assume form itself uses context to pass state to ErrorMessage and FastField.

I may be missing some complexity not stated here, but this looks as simple as creating a generic Functional Component that accepts a couple more passed-in properties. I did a diff and you would only need to add 'title', 'buttonText', and, if you like, 'type' to your props, for sure. You could also send initialValues object as a prop, instead of deriving it from 'type'.
I mean, did you try the following?
import React from 'react';
import { Link } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';
import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import * as schema from 'validations/AuthValidationSchema';
function AuthPage({ buttonText, initialValues, title, type, username,
onChangeUsername, onSubmitForm }) {
const authSchema = type === 'login'
? schema.LoginValidationSchema
: schema.ForgotPasswordValidationSchema;
return (
<div>
<Helmet>
<title>{title}</title>
</Helmet>
<Formik
initialValues={initialValues}
validationSchema={authSchema}
onSubmit={onSubmitForm}
render={({ isSubmitting, isValid, handleChange }) => (
<Form>
<FastField
type="text"
name="username"
render={({ field }) => (
<input
{...field}
onChange={e => {
handleChange(e);
onChangeUsername(e);
}}
/>
)}
/>
<ErrorMessage name="username" component="div" aria-live="polite" />
{type === 'forgot' &&
<FastField type="password" name="password" />
<ErrorMessage name="password" component="div" aria-live="polite" />
}
<button type="submit" disabled={isSubmitting || !isValid}>
{buttonText}
</button>
<FormDebug />
</Form>
)}
/>
<Link to="/auth/forgot_password">Forgot Password</Link>
</div>
);
}
AuthPage.propTypes = {
buttonText: PropTypes.string,
initialValues: PropTypes.object,
title: PropTypes.string,
type: PropTypes.oneOf(['login', 'forgot'])
username: PropTypes.string,
onSubmitForm: PropTypes.func.isRequired,
onChangeUsername: PropTypes.func.isRequired,
};
export default AuthPage;
(Only thing I can't remember is whether the conditional render of password field and its ErrorMessage needs to be wrapped in a div or not to make them one element)
If you don't want to pass in initial values:
const initialVals = type === 'login'
? { username, password: ''}
: { username }
...
initialValues={initialVals}
and remove it from propTypes
Only other thing I'm not sure of is why FormDebug is placed differently in the two versions. I've left it after the button.

Those two pages have separate concerns, so I wouldn't suggest pulling logic that can be used for both cases as that tends to add complexity by moving code further apart even if it does remove a few duplicate lines. In this case a good strategy may be to think of things that you reuse within components.
For example you can wrap the formik component in your own wrapper and then wrap for example an input component that works with your new form. A good exercise in reducing boiler plate could be to aim for some variant of this end result
<CoolForm
initialValues={{ username, password: '' }}
validationSchema={LoginValidationSchema}
>
<CoolFormInputWithError someProps={{howeverYou: 'want to design this'}}>
<CoolFormInputWithError someProps={{howeverYou: 'want to design this'}}>
<CoolFormSubmit>
Login
</CoolFormSubmit>
<FormDebug />
</CoolForm>
That's just an idea, but the nice thing about this strategy is that if you want to refactor formik out for whatever reason, its really simple because all your code now is wrapped in CoolForm components, so you only end up changing the implementation of your own components, although that benefit in this case is very small.

Related

Formik form input not accepting decimal number

Here is my complete edit form. I am type checking with prop types. Also using yup for validation with formik. However my input field is not accepting decimal number. Do i need to add custom validation rules for decimal number? Current form's validation and error feedback working properly for number without decimal format. Any hints how can i solve this ? Thanks in advance !
import React from "react";
import PropTypes from "prop-types";
import { Formik, Form, Field } from "formik";
import { Button, Input, FormGroup, Label, FormFeedback } from "reactstrap";
import * as Yup from "yup";
const projectType = PropTypes.shape({
id: PropTypes.number.isRequired,
actual_design: PropTypes.number.isRequired,
actual_development: PropTypes.number.isRequired,
actual_testing: PropTypes.number.isRequired
});
/**
* Custom form field component to make using Reactstrap and Formik together
* easier and less verbose.
*/
const FormField = ({ label, name, touched, errors }) => (
<FormGroup>
<Label for={name}>{label}</Label>
<Input
type="number"
name={name}
id={name}
tag={Field}
invalid={touched[name] && !!errors[name]}
min={0}
required
/>
{touched[name] && errors[name] && (
<FormFeedback>{errors[name]}</FormFeedback>
)}
</FormGroup>
);
FormField.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
touched: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
/**
* Form for editing the actual hours for a project.
*/
const EditProjectForm = ({ project, onSubmit }) => (
<Formik
initialValues={{
actual_design: projectType.actual_design,
actual_development: projectType.actual_development,
actual_testing: projectType.actual_testing
}}
validationSchema={Yup.object().shape({
actual_design: Yup.number()
.min(0)
.required()
.label("Actual design hours"),
actual_development: Yup.number()
.min(0)
.required()
.label("Actual development hours"),
actual_testing: Yup.number()
.min(0)
.required()
.label("Actual testing hours")
})}
onSubmit={onSubmit}
>
{({ touched, errors, isSubmitting }) => (
<Form>
<FormField
name="actual_design"
type="number"
label="Actual design hours"
touched={touched}
errors={errors}
/>
<FormField
name="actual_development"
label="Actual development hours"
touched={touched}
errors={errors}
/>
<FormField
name="actual_testing"
label="Actual testing hours"
touched={touched}
errors={errors}
/>
<Button type="submit" color="primary" disabled={isSubmitting}>
UPDATE
</Button>
</Form>
)}
</Formik>
);
EditProjectForm.propTypes = {
project: projectType.isRequired,
onSubmit: PropTypes.func.isRequired
};
export default EditProjectForm;
I assume that you see a native browser validation message, because type="number" by default only allows integer with step=1, so you can fix it by adding step="any".
More information you can find there - Is there a float input type in HTML5?

react-hook-form when editing doesn't update

I can't find a way to provide the existing information from the DB to the react-hook-form library to revalidate when clicking submits and to update it if the data was changed.
The problem is when clicking on submit button, this object will shown without any value for firstName and lastname:
{firstName: undefined, lastname: undefined}
Simplified version of my component:
import React from "react";
import { useForm } from "react-hook-form";
import { TextField } from "#material-ui/core";
const App = (props) => {
const { register, handleSubmit} = useForm();
const onSubmit = (data) => {
console.log(data); // ---> here
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Step 1</h2>
<label>
First Name:
<TextField {...register("firstName")} defaultValue="firstname" />
</label>
<label>
Last Name:
<TextField {...register("lastname")} defaultValue="lastname" />
</label>
<input type="submit" />
</form>
);
};
export default App
Any idea how to create a form that allows you to edit the fields?
Check out on codesandbox
The problem is with the TextFiled component. the register is not set correctly.
According to the MUI documentation:
props:inputProps type:object description:Attributes applied to the input element.
So you need to pass the register throw the inputProps:
<TextField inputProps={register("firstName")} defaultValue="firstname" />
<TextField inputProps={register("lastname")} defaultValue="lastname" />

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>
);
}

Checkbox onChange event is not handled by handleChange props by Formik

I was building simple Form using React and Formik library.
I have added check box inside the form tag which is wrapped by withFormik wrapper of formik library.
I have tried to changing from
<input
type="checkbox"
name="flag"
checked={values.flag}
onChange={handleChange}
onBlur={handleBlur}
/>
to
<input
type="checkbox"
name="flag"
value={values.flag}
onChange={handleChange}
onBlur={handleBlur}
/>
but none is working.
the component is as following
import { withFormik } from 'formik';
...
const Form = props => (
<form>
<input
type="checkbox"
name="flag"
checked={props.values.flag}
onChange={props.handleChange}
onBlur={props.handleBlur}
/>
<input
type="text"
name="name"
checked={props.values.name}
onChange={props.handleChange}
onBlur={props.handleBlur}
/>
</form>
);
const WrappedForm = withFormik({
displayName: 'BasicForm',
})(Form);
export default WrappedForm;
It should change props.values when clicking checkbox.
but it doesn't change props data at all.
Btw, it changes props data when typing in text input box.
This only happens with checkbox.
Using the setFieldValue from Formik props, you can set the value of the check to true or false.
<CheckBox
checked={values.check}
onPress={() => setFieldValue('check', !values.check)}
/>
My answer relates to react-native checkbox.
This article is very helpful. https://heartbeat.fritz.ai/handling-different-field-types-in-react-native-forms-with-formik-and-yup-fa9ea89d867e
Im using react material ui library, here is how i manage my checkboxes :
import { FormControlLabel, Checkbox} from "#material-ui/core";
import { Formik, Form, Field } from "formik";
<Formik
initialValues={{
check: false
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ values, setFieldValue }) => (
<Form className="gt-form">
<FormControlLabel
checked={values.check}
onChange={() => setFieldValue("check", !values.check)}
control={<Checkbox />}
label="save this for later"
/>
</Form>
)}
</Formik>

How to use handleSubmit to submit data in form and some extra data in redux form?

I am a redux-form library newbie. I am trying to write some code to update data to server. The update API that server supports well.
I am using reactjs, redux and redux-form.
My concise code is:
render() {
const { handleSubmit, pristine, reset, submitting, updatePost } = this.props;
return (
<div>
<h4>Update the Post</h4>
<form onSubmit={handleSubmit(updatePost.bind(this))}>
<Field name="title" component={this.renderField} type="text" className="form-control" label="Post Title"/>
<Field name="description" component={this.renderField} type="textarea" className="form-control" label="Post Description"/>
<button type="submit" className="btn btn-primary">Update</button>
<Link to='/template'>Back to List</Link>
</form>
</div>
);
}
When I click on button Submit, the code onSubmit={handleSubmit(updatePost.bind(this))} will send all data in the form to my function updatePost.
My question is: I want to pass some data (like an PostID). How can I do that?
I don't want to use a cheat (like hidden field in my form).
Thank you.
Not sure exactly what your use case is but there are a few approaches you can do...
using setState of parent component, you can pass it as props inside initialValues.
class ParentComponent extends Component {
updatePost(formValues) {
// process data
this.setState({ postId: '...' });
}
render() {
return <FormComponent initialValues={{postId: this.state.postId}} />
}
}
Just make sure you have the enableReinitialize set to true when initializing your reduxForm.
export default reduxForm({ form: 'myFormName', enableReinitialize: true })(FormComponent);
Or
use reduxForm change action creator
import { change } from 'redux-form';
class ParentComponent extends Component {
updatePost(formValues) {
this.props.change('myFormName', 'postId', 'postid_value');
}
...
}
export default connect(mapStateToProps, { change })(ParentComponent);
Or pass postId as props itself (<MyForm postId={...} />) and do the change action inside the MyForm component.
I'm using an onChange function passed to the reduxForm decorator to submit my form after any of the form's fields change.
You can modify the form's values object passed to your onSubmit function in the onChange function. In the example below I'm creating new key/value pairs on the values object before it gets to my onSubmit function using the component's properties.
I haven't tested yet but this should also work for a presentational component as well.
//#flow
import React, { PureComponent } from "react";
import { Field, Form, reduxForm } from "redux-form";
import { FormLabel, FormControl, FormControlLabel } from "material-ui/Form";
import { RadioGroup } from "redux-form-material-ui";
import Radio from "material-ui/Radio";
import Paper from "material-ui/Paper";
type HccTableQuestionProps = {
handleSubmit: Function,
row: Object,
};
class HccTableQuestion extends PureComponent<HccTableQuestionProps> {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(values) {
console.log("hccActionForm submitted ", values);
}
render() {
const { handleSubmit, row } = this.props;
return (
<Paper style={{ padding: 9 }}>
<Form name={"hccActionForm"} onSubmit={handleSubmit(this.handleChange)}>
<FormControl component="fieldset">
<FormLabel component="legend">HCC Action</FormLabel>
<Field component={RadioGroup} name={"hccAction"}>
<FormControlLabel
value="ICD Code Submitted for CY encounter"
control={<Radio />}
label="ICD Code Submitted for CY encounter"
/>
<FormControlLabel
value="Queried Provider"
control={<Radio />}
label="Queried Provider"
/>
<FormControlLabel
value="Condition Not Present"
control={<Radio />}
label="Condition Not Present"
/>
<FormControlLabel
value="No Action"
control={<Radio />}
label="No Action"
/>
</Field>
</FormControl>
</Form>
</Paper>
);
}
}
export default reduxForm({
form: "hccActionForm",
enableReinitialize: true,
onChange: (values, dispatch, props, previousValues) => {
values.hccCode = props.hccCode; // Adding new key/value pair to values
values.patientHccCodingId = 99999999; // Adding new key/value pair to values
props.submit();
},
})(HccTableQuestion);

Resources