I have a separate module that I'm working on, this module is meant to contain formik supporting HTML input elements.
The issue is I'm unable to use the useFields() hook since my module component doesn't connect to the formik context.
Here's my component that resides in a different module:
import React from "react";
import PropTypes from "prop-types";
import { useField } from "formik";
export function TextField({ label, ...props }) {
const [field, meta] = useField(props);
return <input {...field} {...meta} />;
}
TextField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
showErrors: PropTypes.bool
};
TextField.defaultProps = {
label: "",
showErrors: true
};
export default TextField;
and here is my Formik form:
<Formik
initialValues={{
firstName: "firstName"
}}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
>
{formik => (
<Form>
<TextField name="firstName" />
<button type="submit">Submit</button>
</Form>
)}
</Formik>
No matter what I do I get the following error:
TypeError: Cannot read property 'getFieldProps' of undefined
Anyone have an idea what I'm doing wrong?
Looking at the useField docs I would expect:
<input {...field} {...props} />
The input component does not expect the {...meta} props.
other than that I could not reproduce your error.
Related
I want to perform navigation on certain user actions, say onSubmit of a button. suppose a user clicks on the Add contact button I want react-router to redirect in "/" which is the home page. At the moment I am facing this problem--> TypeError: Cannot read properties of undefined (reading 'push'). As a beginner, I would really appreciate experts' help.
AddContacts.js
import React, { Component } from "react";
import { Consumer } from "../../context";
import TextInputGroup from "../layout/TextInputGroup";
import { v4 as uuidv4 } from "uuid";
import { useNavigate } from "react-router-dom";
class AddContacts extends Component {
state = {
name: "",
email: "",
phone: "",
errors: {},
};
onSubmit = (dispatch, e) => {
e.preventDefault();
const { name, email, phone } = this.state;
//Check for errors
if (name === "") {
this.setState({ errors: { name: "Name is required" } });
return;
}
if (email === "") {
this.setState({ errors: { email: "Email is required" } });
return;
}
if (phone === "") {
this.setState({ errors: { phone: "Phone is required" } });
return;
}
const newContact = {
id: uuidv4(),
name,
email,
phone,
};
dispatch({ type: "ADD_CONTACT", payload: newContact });
this.setState({
name: "",
email: "",
phone: "",
errors: {},
});
this.props.navigate.push("/");
};
onChange = (e) => this.setState({ [e.target.name]: e.target.value });
render() {
const { name, email, phone, errors } = this.state;
return (
<Consumer>
{(value) => {
const { dispatch } = value;
return (
<div className="card mb-3">
<div className="card-header">Add Contacts</div>
<div className="card-body">
<form onSubmit={this.onSubmit.bind(this, dispatch)}>
<TextInputGroup
label="Name"
name="name"
placeholder="Enter Name..."
value={name}
onChange={this.onChange}
error={errors.name}
/>
<TextInputGroup
label="Email"
name="email"
type="email"
placeholder="Enter Email..."
value={email}
onChange={this.onChange}
error={errors.email}
/>
<TextInputGroup
label="Phone"
name="phone"
placeholder="Enter Phone..."
value={phone}
onChange={this.onChange}
error={errors.phone}
/>
<input
type="submit"
value="Add Contact"
className="btn btn-light btn-block mt-3"
/>
</form>
</div>
</div>
);
}}
</Consumer>
);
}
}
export default AddContacts;
Here is the App.js file
import React, { Component } from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Contacts from "./components/contacts/Contacts";
import Header from "./components/layout/Header";
import AddContacts from "./components/contacts/AddContacts";
import About from "./components/pages/About";
import { Provider } from "./context";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";
function App() {
return (
<Provider>
<BrowserRouter>
<div className="App">
<Header branding="Contact manager" />
<div className="container">
<Routes>
<Route path="/" element={<Contacts />} />{" "}
<Route path="/contact/add/*" element={<AddContacts />} />{" "}
<Route path="about/*" element={<About />} />{" "}
</Routes>{" "}
</div>{" "}
</div>{" "}
</BrowserRouter>{" "}
</Provider>
);
}
export default App;
Issue
TypeError: Cannot read properties of undefined (reading 'push')
This is cause by you attempting to navigate from a navigate prop that doesn't exist, it's undefined.
this.props.navigate.push("/");
The useNavigate hook is only compatible with function components, so of you want/need to use navigate with a class component you must either convert AddContacts to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const navigate = useNavigate();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
navigate={navigate}
// etc...
/>
);
};
And decorate the AddContacts component with the new HOC.
export default withRouter(AddContacts);
This will now pass a navigate prop (and any others you set up) to the decorated components and this.navigate will now be defined.
Additionally, the navigation API changed from v5 to v6, it's no longer the direct history object being used. navigate is a function instead of an object. To use you invoke the function and pass 1 or 2 arguments, the first is the target path, the second is an optional "options" object with replace and/or state key/values.
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: State }
): void;
(delta: number): void;
}
To navigate now as follows:
this.props.navigate("/");
I want to perform navigation on certain user actions, say onSubmit of a button. suppose a user clicks on the Add contact button I want react-router to redirect in "/" which is the home page. At the moment I am facing this problem--> TypeError: Cannot read properties of undefined (reading 'push'). As a beginner, I would really appreciate experts' help.
AddContacts.js
import React, { Component } from "react";
import { Consumer } from "../../context";
import TextInputGroup from "../layout/TextInputGroup";
import { v4 as uuidv4 } from "uuid";
import { useNavigate } from "react-router-dom";
class AddContacts extends Component {
state = {
name: "",
email: "",
phone: "",
errors: {},
};
onSubmit = (dispatch, e) => {
e.preventDefault();
const { name, email, phone } = this.state;
//Check for errors
if (name === "") {
this.setState({ errors: { name: "Name is required" } });
return;
}
if (email === "") {
this.setState({ errors: { email: "Email is required" } });
return;
}
if (phone === "") {
this.setState({ errors: { phone: "Phone is required" } });
return;
}
const newContact = {
id: uuidv4(),
name,
email,
phone,
};
dispatch({ type: "ADD_CONTACT", payload: newContact });
this.setState({
name: "",
email: "",
phone: "",
errors: {},
});
this.props.navigate.push("/");
};
onChange = (e) => this.setState({ [e.target.name]: e.target.value });
render() {
const { name, email, phone, errors } = this.state;
return (
<Consumer>
{(value) => {
const { dispatch } = value;
return (
<div className="card mb-3">
<div className="card-header">Add Contacts</div>
<div className="card-body">
<form onSubmit={this.onSubmit.bind(this, dispatch)}>
<TextInputGroup
label="Name"
name="name"
placeholder="Enter Name..."
value={name}
onChange={this.onChange}
error={errors.name}
/>
<TextInputGroup
label="Email"
name="email"
type="email"
placeholder="Enter Email..."
value={email}
onChange={this.onChange}
error={errors.email}
/>
<TextInputGroup
label="Phone"
name="phone"
placeholder="Enter Phone..."
value={phone}
onChange={this.onChange}
error={errors.phone}
/>
<input
type="submit"
value="Add Contact"
className="btn btn-light btn-block mt-3"
/>
</form>
</div>
</div>
);
}}
</Consumer>
);
}
}
export default AddContacts;
Here is the App.js file
import React, { Component } from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import Contacts from "./components/contacts/Contacts";
import Header from "./components/layout/Header";
import AddContacts from "./components/contacts/AddContacts";
import About from "./components/pages/About";
import { Provider } from "./context";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";
function App() {
return (
<Provider>
<BrowserRouter>
<div className="App">
<Header branding="Contact manager" />
<div className="container">
<Routes>
<Route path="/" element={<Contacts />} />{" "}
<Route path="/contact/add/*" element={<AddContacts />} />{" "}
<Route path="about/*" element={<About />} />{" "}
</Routes>{" "}
</div>{" "}
</div>{" "}
</BrowserRouter>{" "}
</Provider>
);
}
export default App;
Issue
TypeError: Cannot read properties of undefined (reading 'push')
This is cause by you attempting to navigate from a navigate prop that doesn't exist, it's undefined.
this.props.navigate.push("/");
The useNavigate hook is only compatible with function components, so of you want/need to use navigate with a class component you must either convert AddContacts to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const navigate = useNavigate();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
navigate={navigate}
// etc...
/>
);
};
And decorate the AddContacts component with the new HOC.
export default withRouter(AddContacts);
This will now pass a navigate prop (and any others you set up) to the decorated components and this.navigate will now be defined.
Additionally, the navigation API changed from v5 to v6, it's no longer the direct history object being used. navigate is a function instead of an object. To use you invoke the function and pass 1 or 2 arguments, the first is the target path, the second is an optional "options" object with replace and/or state key/values.
interface NavigateFunction {
(
to: To,
options?: { replace?: boolean; state?: State }
): void;
(delta: number): void;
}
To navigate now as follows:
this.props.navigate("/");
I'm trying to reset the inputs in my formik Form on submit. It seems I'm supposed to use resetForm() to do that but i get the error:
src\components\CommentSubmition\inCommentSubmition.js
Line 19:13: 'resetForm' is not defined no-undef
Here's my component:
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import {createComment} from '../../services/CommentLocalStorage.js'
import * as Yup from 'yup';
function CommentForm(props){
return (
<Formik
initialValues={{ autor: '', content: ''}}
validationSchema={Yup.object({
autor: Yup.string().required('Required'),
content: Yup.string().required('Required')
})}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
createComment(props.pageEnum, props.articleId, values.autor, values.content)
setSubmitting(false);
},400);
resetForm();
}}
>
<Form>
<label htmlFor="autor">Nome</label>
<Field name="autor" type="autor" placeholder="Nome"/>
<ErrorMessage name="autor" />
<br/>
<label htmlFor="content">Comentário</label>
<Field name="content" type="content" placeholder="Comentário" />
<ErrorMessage name="content" />
<br/>
<button type="submit">Submit</button>
</Form>
</Formik>
);
};
export default CommentForm;
It seems most people make something like this:
const formik = some configuration
And then they use it like
formik.resetForm()
And instead I'm using the Formik component with all the stuff inside it (I did it based on an example available on the official tutorials). If possible i'd like to keep it like that and still make the form reset.
Pass resetForm as a parameter to your onSubmit function. That should give your function access to the resetForm method from Formik thereby getting rid of the error and successfully reset the form. If you want to use any methods from the formik library inside your onSubmit function, first pass a parameter to the function so you can have access to the formik method. Let me know if this helps
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import {createComment} from '../../services/CommentLocalStorage.js'
import * as Yup from 'yup';
function CommentForm(props){
return (
<Formik
initialValues={{ autor: '', content: ''}}
validationSchema={Yup.object({
autor: Yup.string().required('Required'),
content: Yup.string().required('Required')
})}
onSubmit={(values, { setSubmitting, {resetForm }) => { //<--- See here for code added
setTimeout(() => {
createComment(props.pageEnum, props.articleId, values.autor, values.content)
setSubmitting(false);
},400);
resetForm();
}}
>
<Form>
<label htmlFor="autor">Nome</label>
<Field name="autor" type="autor" placeholder="Nome"/>
<ErrorMessage name="autor" />
<br/>
<label htmlFor="content">Comentário</label>
<Field name="content" type="content" placeholder="Comentário" />
<ErrorMessage name="content" />
<br/>
<button type="submit">Submit</button>
</Form>
</Formik>
);
};
export default CommentForm;
I have 2 separate views. Both of them utilize the same form. The form lives in a Modal. The form works perfectly inside the first view (The Create Mode).
Now I want to add another one, for the (Edit Mode). The only difference is that the Edit Mode needs the values of the actual Item (The user in our case).
I understand how Higher-Order Components work, in general. But my current configuration is using Redux, Formik and Yup for getting Data, Form handling and Validation respectively.
I tried a few examples online, but none of them fit my case scenario. And here lies the problem. How much functionality should I move to the Higher-Order Component?
Functionality One:
Where the actual form should live. The component, or the HOC? Code Below:
Should I move it, in the HOC? Since it is the exact same form. If yes why?
Functionality 2:
I am doing form validation with Yup. I have moved this functionality into the HOC, but now, I am not able to have ValidatioSchema passed to the Form Component. Code Below:
Functionality 3:
How to deal with the Modal. The Modal is a little bit tricky. I have decided to use it stand alone, in each separate screen. Is this correct or should I include it in the HOC?
My HOC:
import React, { Component } from 'react';
import { withFormik } from 'formik';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { createUser, updateUser } from './service';
import { listGroups } from '../groups/service';
const AddEditUserHOCForm = WrappedComponent => {
class ViewUser extends Component {
static propTypes = {
user: PropTypes.object,
onCancel: PropTypes.func,
onSave: PropTypes.func,
status: PropTypes.string,
values: PropTypes.object,
errors: PropTypes.object,
isSubmitting: PropTypes.bool,
handleChange: PropTypes.func,
handleSubmit: PropTypes.func,
setStatus: PropTypes.func
};
static defaultProps = {
user: {},
onCancel: () => { },
onSave: () => { },
status: '',
values: {},
errors: {},
isSubmitting: false,
handleChange: () => { },
handleSubmit: () => { },
setStatus: () => { }
};
state = {
groups: [],
isRetrievingData: false
};
componentDidMount() {
this.setState({
isRetrievingData: true
});
return new Promise([listGroups()])
.then(values => values.map(res => res.data))
.then(([groups]) => {
this.setState({
isRetrievingData: false,
groups
});
})
.catch(({ message = 'Could not retrieve data from server.' }) => {
this.setState({
isRetrievingData: false
});
this.props.setStatus(message);
});
}
handleCancel = () => {
const { onCancel } = this.props;
if (onCancel) {
onCancel();
}
};
render() {
return (
<WrappedComponent
{...this.props}
{...this.state}
onSubmit={this.handleSubmit}
onCancel={this.handleCanel}
/>
);
}
}
const UserValidationSchema = Yup.object().shape({
username: Yup.string('Provide a Username').required('Username is Required'),
password: Yup.string().email('Provide a Valid email Address'),
confirmPassword: Yup.string('Enter your password again')
.required('Password Confirmation is Required')
.oneOf([Yup.ref('password')], 'Passwords do not match')
});
const NewUserFormHOC = withFormik({
mapPropsToValues: ({ user }) => ({ ...user }),
UserValidationSchema,
handleSubmit: (values, { props, setSubmitting, setStatus }) => {
const saveUser = values.username ? updateUser : createUser;
return saveUser(values)
.then(() => {
setSubmitting(false);
setStatus('');
props.onSave(values);
props.onCancel();
})
.catch(({ message, response: { data } }) => {
setSubmitting(false);
setStatus(data || message);
});
},
displayName: 'ViewUser'
})(ViewUser);
return NewUserFormHOC;
};
export default AddEditUserHOCForm;
And here is my Form Component:
import React, { Component, Fragment } from 'react';
import { Formik, Form, Field } from 'formik';
import PropTypes from 'prop-types';
import AddEditUserHOCForm from './components/AddEditUserHOC'
import LensesSelect from './data/ReactSelectComponent';
import formPropTypes from '../constants/formPropTypes';
import { listGroups } from '../groups/service';
class UserCreateForm extends Component {
static propTypes = {
...formPropTypes,
username: PropTypes.string,
email: PropTypes.string,
password: PropTypes.string,
confirmPassword: PropTypes.string,
groupSelect: PropTypes.func
};
static defaultProps = {
email: ''
};
state = {
type: 'password',
groups: []
};
componentDidMount() {
this.fetchListGroups();
}
fetchListGroups = () => {
listGroups().then(({ data }) => {
this.setState({ groups: data });
});
};
mapListGroupToSelect = () => {
const { groups } = this.state;
return groups.map(group => ({
label: group.name,
value: group.name
}));
};
togglePasswordMask = e => {
const { type } = this.state;
e.preventDefault();
this.setState(prevState => ({
passwordIsMasked: !prevState.passwordIsMasked,
type: type === 'password' ? 'input' : 'password'
}));
};
selectOnChangeCallback = response => {
// eslint-disable-next-line no-console
console.log('selectOnChangeCallback', response);
};
render() {
const { type } = this.state;
const selectData = this.mapListGroupToSelect();
return (
<Fragment>
<Formik
initialValues={{
username: '',
email: '',
password: '',
confirmPassword: ''
}}
// validationSchema={createUserValidationSchema}
onSubmit={values => {
// same shape as initial values
console.log(values);
}}
>
{({ errors, touched }) => (
<Form>
<div className="my-3">
<label>
Username <span className="text-danger">*</span>
</label>
<Field name="username" type="text" className="form-control rounded-0" />
{errors.username && touched.username ? (
<div className="text-danger">{errors.username}</div>
) : null}
</div>
<div className="my-3">
<label>email</label>
<Field name="email" type="email" className="form-control rounded-0" />
{errors.email && touched.email ? (
<div className="text-danger">{errors.email}</div>
) : null}
</div>
<div className="my-3">
<label>
Password <span className="text-danger">*</span>
</label>
<div className="d-flex align-items-center">
<Field type={type} name="password" className="form-control rounded-0 mr-2" />
<span
className={type === 'password' ? 'fa fa-eye fa-lg' : 'fa fa-eye-slash fa-lg'}
onClick={this.togglePasswordMask}
/>
</div>
{errors.password && touched.password ? (
<div className="text-danger">{errors.password}</div>
) : null}
</div>
<div className="my-3">
<label>
Confirm Password <span className="text-danger">*</span>
</label>
<Field name="confirmPassword" type={type} className="form-control rounded-0" />
{errors.confirmPassword && touched.confirmPassword ? (
<div className="text-danger">{errors.confirmPassword}</div>
) : null}
</div>
<div className="my-3">
<label>
Select Group <span className="text-danger">*</span>
</label>
<ReactSelectComponent
isMulti={false}
options={selectData}
onChangeCallback={this.selectOnChangeCallback}
/>
</div>
<button type="submit" className="btn btn-primary rounded-0 float-right my-5">
<span className="mx-2">Create User</span>
</button>
</Form>
)}
</Formik>
</Fragment>
);
}
}
export default AddEditUserHOCForm(UserCreateForm);
I understand this is a lot of code to pure through. But I am at a loss. Don't really know what I should include where.
To me, I need the form(Formik) and Yup, along with Redux on the HOC. And then Add the Data depending on the view. Please I really need some guidance and some examples. Thank you.
React is great, but it gives you too much freedom. In my opinion, you have to have SOLID principles applied to your components.
As you can read through the web, your component should only do one thing and do it well.
So. If i were to do it, i would have the form in one folder with one file representing its presentational configuration and another/s with business logic. You can do the second part as one HOC.
Let´s say, UserForm.js, withUserValidations, withUserActions. Then wrapp the whole thing.
Then you would just have to use it where you want. I hope it helps.
I have the following CustomInput component:
import React from 'react';
const CustomInput = props => (
<div className="form-group">
<label className="form-label">{props.title}</label>
<input
className="form-input"
name={props.name}
type={props.inputType}
value={props.content}
onChange={props.controlFunc}
placeholder={props.placeholder}
/>
</div>
);
CustomInput.propTypes = {
inputType: React.PropTypes.oneOf(['text', 'number']).isRequired,
title: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
controlFunc: React.PropTypes.func.isRequired,
content: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
]).isRequired,
placeholder: React.PropTypes.string,
};
export default CustomInput;
And this is my form:
import React, { PropTypes } from 'react';
import { Field, reduxForm } from 'redux-form';
import CustomInput from '../components/CustomInput';
const renderMyStrangeInput = field => (
<CustomInput
inputType={'number'}
title={'How many items do you currently own?'}
name={'currentItems'}
controlFunc={param => field.input.onChange(param.val)}
content={{ val: field.input.value }}
placeholder={'Number of items'}
/>
);
class ItemsForm extends React.Component {
constructor(props) {
super(props);
}
render() {
const { handleSubmit } = this.props;
return (
<form className="container" onSubmit={handleSubmit}>
<h1>Nuevo Pedido</h1>
<Field name="myField" component={renderMyStrangeInput} />
<div className="form-group">
<button type="submit" className="btn btn-primary input-group-btn">Submit</button>
</div>
</form>
);
}
}
ItemsForm.propTypes = {
};
ItemsForm = reduxForm({
form: 'Items',
})(ItemsForm);
export default ItemsForm;
I can render my component, but there are some issues with the content type. First, if I set the CustomInput to accepts numbers I get:
he specified value "[object Object]" is not a valid number. The value must match to the following regular expression: -?(\d+|\d+\.\d+|\.\d+)([eE][-+]?\d+)?
Second, if I set the inputType to text, it renders a:
[object Object]
so, the console is giving the following warning:
Warning: Failed prop type: Invalid prop `content` supplied to `CustomInput`.
How can I set the content properly?
I think the issue is that you are trying pass strings as objects
<CustomInput
inputType="number"
title="How many items do you currently own?"
name="currentItems"
controlFunc={param => field.input.onChange(param.val)}
content={field.input.value}
placeholder="Number of items"
/>
You are passing object via props and you must pass string or number.
content={{ val: field.input.value }} // no!
content={field.input.value} // yes! :)