still new to formik and react hook.
Here is my code in react.
// react
import React, { useEffect } from 'react';
import { withFormik } from 'formik';
import { useDispatch } from 'redux-react-hook';
import { takeEvery, call, put } from 'redux-saga/effects';
// row, col, field, input, buttonGroup
import {
Row,
Col,
FieldWrapper,
Input,
ButtonGroup
} from 'some-tool';
const searchTypeOption = [
....
];
const SearchForm = (props: any) => {
const {
values,
touched,
errors,
handleChange,
handleSubmit,
} = props;
return (
<form onSubmit={handleSubmit}>
<Row>
<Col md="3">
<FieldWrapper required={true}>
<Select name="searchKey" onChange={handleChange} value={values.searchKey} options={searchTypeOption} />
</FieldWrapper>
{errors.searchKey && touched.searchKey && <div>{errors.searchKey}</div>}
</Col>
<Col md="5">
<FieldWrapper>
<Input
placeholder="Search"
type="text"
onChange={handleChange}
value={values.searchValue}
name="searchValue"
/>
</FieldWrapper>
{errors.searchValue && touched.searchValue && <div>{errors.searchValue}</div>}
</Col>
</Row>
<Row>
<ButtonGroup>
<Button>Clear</Button>
<Button type="submit">Search</Button>
</ButtonGroup>
</Row>
</form>
);
};
export const Search = withFormik({
mapPropsToValues: () => ({ searchKey: '', searchValue: '' }),
// Custom sync validation
validate: values => {
let errors = {};
//if (values.hasOwnProperty('searchKey') && !values.searchKey) {
// errors.searchKey = 'Required';
//}
return errors;
},
handleSubmit: (values, { props, setSubmitting }) => {
const payload = {
searchKey: values.searchKey,
searchValue: values.searchValue
};
// NOTE: obj.props is empty.....
console.log(obj);
// How to use dispatch here or some way to fire event
dispatch({ type: 'SEARCH_DOCS', payload: payload });
},
})(SearchForm);
in handleSubmit, how do I dispatch an event, so saga and redux are able to receive them?
In order to do that you must pass a connected component so you can have access to dispatch
wrap this with formik like you do
const SearchFormFormik = withFormik(SearchForm)
Then connect it to redux
const mapDispatchToProps = {
searchDocFun,
};
const ConnectedSearchForm = connect(
null,
mapDispatchToProps
)(SearchFormFormik);
Then you can use the searchDocFun on handle submit
handleSubmit: (values, { props, setSubmitting }) => {
props.searchDocFun(values)
}
Related
I have a Formik form which uses check-boxes and selects. For the select field I am using react-select. There is no problem to submit the form but when I want to reset the form the react-select part is not clearing and also when I want to prefill the form after a push of a button with setFieldValue, again the react-select part is not responding.
How can I make it work this form with react-select within Formik?
https://codesandbox.io/s/wonderful-breeze-dyhi3b
App.js
import React from "react";
import FormikReactSelect from "./components/FormikReactSelect";
function App() {
return <FormikReactSelect />
}
export default App;
FormikReactSelect.js
import React, {useRef} from "react";
import {Form, Field, Formik, FieldArray} from "formik";
import SelectField from "./SelectField";
function FormikReactSelect() {
const formikRef = useRef();
const drinkDist = ["Wine", "Beer", "Whiskey"];
const countryDist = ["US", "FR", "DE", "BE", "IT"];
const arrToValLab = (arr) => {
if (arr !== undefined) {
const valLab = arr.map((t) => ({
value: t,
label: t,
}));
return valLab;
} else {
return null;
}
};
const onClick = () => {
if (formikRef.current) {
formikRef.current.setFieldValue("drinks", ["Whiskey"]);
}
if (formikRef.current) {
formikRef.current.setFieldValue("countries", ["FR"]);
}
};
return (
<>
<Formik
innerRef={formikRef}
enableReinitialize={true}
initialValues={{drinks: [], countries: []}}
>
{({
values,
handleChange,
handleSubmit,
handleBlur,
resetForm,
setFieldValue,
}) => (
<Form noValidate>
Drinks:
<FieldArray
name="drinks"
render={(arrayHelpers) => (
<div>
{drinkDist.map((r, i) => (
<div key={i}>
<label>
<Field
name="drinks"
type="checkbox"
value={r}
checked={values.drinks.includes(r)}
onChange={(e) => {
if (e.target.checked) {
arrayHelpers.push(r);
} else {
const idx = values.drinks.indexOf(r);
arrayHelpers.remove(idx);
}
}}
/>
{" " + r}
</label>
</div>
))}
</div>
)}
/>
Countries:
<Field
component={SelectField}
name="countries"
options={arrToValLab(countryDist)}
/>
<button
type="button"
onClick={() => {
resetForm();
}}
>
Reset
</button>
{JSON.stringify(values, null, 2)}
</Form>
)}
</Formik>
Sending preset values:
<button onClick={onClick}>Set Field</button>
</>
);
}
export default FormikReactSelect;
SelectFireld.js
import React from "react";
import Select from "react-select";
import {useField} from "formik";
export default function SelectField(props) {
const [field, state, {setValue, setTouched}] = useField(props.field.name); // eslint-disable-line
const onChange = (value) => {
let arrValue = [];
value.map((k) => arrValue.push(k.value));
setValue(arrValue);
};
return <Select {...props} isMulti onChange={onChange} onBlur={setTouched} />;
}
I am fetching the data to populate it on the form but when I try to edit the data in the input, the input value will return to its original value and it is because of the get method that is infinitely render on the component. I really need your eyes to see something that have missed or missed up. Thanks in advance y'all.
fetch method
import * as api from '../api/profile';
export const getProfile = () => async (dispatch) => {
try {
const { data } = await api.fetchProfile();
dispatch({ type: 'FETCH_ALL', payload: data });
} catch (error) {
console.log(error.message);
}
}
Profile container
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getProfile } from '../../../actions/profile'; //fetch method
import Profile from './Profile';
function Index() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts);
const currentId = useState(null);
useEffect(() => {
dispatch(getProfile());
}, [currentId, dispatch]);
return (
<div className="custom-container">
{posts.map((profile) => (
<div key={profile._id}>
<Profile profile={profile} currentId={currentId} />
</div>
))}
</div>
);
}
export default Index;
Profile form component
import './Profile.css';
import { React, useState, useEffect } from 'react';
import Button from 'react-bootstrap/Button';
import { TextField } from '#material-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { updateProfile } from '../../../actions/profile';
const Profile = ({ profile, currentId }) => {
const dispatch = useDispatch();
currentId = profile._id;
const [postData, setPostData] = useState(
{
profile: {
name: "",
description: "",
email: "",
number: "",
}
}
);
const post = useSelector((state) => currentId ? state.posts.find((p) => p._id === currentId) : null);
useEffect(() => {
if(post) setPostData(post);
}, [post])
const handleSubmit = (e) => {
e.preventDefault();
if(currentId) {
dispatch(updateProfile(currentId, postData));
}
}
return (
<form autoComplete="off" noValidate className="form" onSubmit={handleSubmit}>
<TextField
id="name"
name="name"
className="name"
label="Full Name"
variant="outlined"
value={postData.profile.name}
onChange={(e) => setPostData({...postData, profile: {...postData.profile, name: e.target.value}})}
/>
<TextField
id="outlined-multiline-static"
label="Multiline"
multiline
rows={4}
variant="outlined"
size="small"
className="mb-3"
name="description"
value={postData.profile.description}
onChange={(e) => setPostData({...postData, profile: {...postData.profile, description: e.target.value}})}
fullWidth
/>
<TextField
id="email"
label="Email"
variant="outlined"
size="small"
className="mb-3"
name="email"
value={postData.profile.email}
onChange={(e) => setPostData({...postData, profile: {...postData.profile, email: e.target.value}})}
/>
<TextField
id="phone"
label="Phone Number"
variant="outlined"
size="small"
name="phone"
value={postData.profile.number}
onChange={(e) => setPostData({...postData, profile: {...postData.profile, number: e.target.value}})}
/>
<Button variant="light" type="submit" className="Save">Save</Button>
</form>
);
}
export default Profile;
In useEffect you have passed like this , in Profile Container
useEffect(() => {
dispatch(getProfile());
}, [currentId, dispatch]);
in params you have passed dispatch also , so it will call dispatch every time dispatch runs , so it is called infinite times , remove it
It might be because of useEffect.
useEffect(() => {
if(post) setPostData(post);
}, [post])
The post will be a different object each time. check for post property in useEffect dependency like:
useEffect(() => {
if(post) setPostData(post);
}, [post.profile.description])
Also, why are you changing prop value below in Profile component?
currentId = profile._id
I'm trying to implement a component that takes a phone number from the redux store, parses the number up on component load with useEffect(), puts the parsed elements (country code and ten digit number) into their own state with useState() and then on onChange of the phone form, update both sections of the number AND update the full phone prop coming from the redux store. I'm obviously not doing something right here:
import React, { useState, useEffect } from 'react';
import { object, func, bool } from 'prop-types';
import { Row, Col } from 'styled-bootstrap-grid';
import { connect } from 'react-redux';
import './BillingDetails.css';
import Button from 'components/Button';
import {
setFirstName,
setLastName,
setPhone,
setEmail,
} from '../../../store/signup/actions';
import {
billingDetailsError,
isValidInput,
regExpValidations,
} from '../validateInput';
import InnerTitle from './InnerTitle';
import Input from './Input';
import PhoneInput from './PhoneInput';
import CountryCodeInput from './CountryCodeInput';
import Dropdown from './Dropdown';
const BillingDetails = ({ signup, onChange, onBlur, onNext, inputError }) => {
const { first_name: firstName, last_name: lastName, email, phone } = signup;
const [countryCode, setCountryCode] = useState('');
const [mobileNumber, setMobileNumber] = useState('');
// Parse the incoming phone prop into country code and the ten digit mobile phone. Sets separate state for both.
useEffect(() => {
const phoneArray = phone.split('');
const cc = phoneArray.splice(0, phoneArray.length - 10).join('');
setCountryCode(cc);
}, '');
useEffect(() => {
const phoneArray = phone.split('');
const tenDigitNumber = phoneArray.join('');
setMobileNumber(tenDigitNumber);
}, '');
useEffect(() => {
interpolatePhone();
}, [countryCode, mobileNumber]);
const interpolatePhone = () => {
const fixedMobile = mobileNumber.replace(/[- )(]/g, '');
const interpolatedPhone = `${countryCode}${fixedMobile}`;
setPhone(interpolatedPhone);
};
// Onsubmit concatenates country code and mobile number, calls setPhone action with concat number and onNext().
const onSubmit = e => {
e.preventDefault();
setPhone(interpolatePhone());
onNext();
};
skipping to relevant JSX:
<Col sm="12" md="12">
<>
<div className="phone-input">
<Col sm="3" md="4">
<CountryCodeInput
label="mobile number"
name="country_code"
onChange={e => setCountryCode(e.target.value)}
onBlur={onBlur}
type="country_code"
value={countryCode}
inputValue={!countryCode ? '+1' : countryCode}
masked
error={
inputError && !isValidInput(phone, regExpValidations.phone)
}
/>
</Col>
<Col sm="1" md="1">
|
</Col>
<Col sm="3" md="4">
<PhoneInput
autoFocus
name="mobile_phone"
onChange={e => setMobileNumber(e.target.value)}
onBlur={onBlur}
type="phone"
value={mobileNumber}
inputValue={mobileNumber}
masked
error={
inputError && !isValidInput(phone, regExpValidations.phone)
}
/>
</Col>
</div>
</>
</Col>
<Button
className="full-width"
onClick={onSubmit}
disabled={billingDetailsError(signup)}
>
Continue
</Button>
</Button>
</Row>
</Col>
);
};
const mapStateToProps = state => ({
signup: state.signup,
});
export default connect(mapStateToProps)(BillingDetails);
I need to fix a memory leak in my app but Im not sure how to. I have a component that uses a modal and I get the error when I am adding an item. The modal is reusable and I use it in other components as well. This is the main component:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Card, Select, Form, Button } from 'antd';
import Table from 'components/Table';
import Modal from '../Modal';
import styles from '../index.module.scss';
const { Item } = Form;
const { Option } = Select;
const PersonForm = ({ details, form }) => {
const [modalVisible, setModalVisible] = useState(false);
const [name, setName] = useState(
details?.name ? [...details?.name] : []
);
useEffect(() => {
form.setFieldsValue({
name: name || [],
});
}, [form, details, name]);
const addName = values => {
setName([...name, values]);
setModalVisible(false);
};
const removeName = obj => {
setName([...name.filter(i => i !== obj)]);
};
const cancelModal = () => {
setModalVisible(false);
};
return (
<div>
<Card
title="Names
extra={
<Button type="solid" onClick={() => setModalVisible(true)}>
Add Name
</Button>
}
>
<Table
tableData={name}
dataIndex="name"
removeName={removeName}
/>
</Card>
<Item name="name">
<Modal
title="Add Name"
fieldName="name"
onSubmit={addName}
visible={modalVisible}
closeModal={cancelModal}
/>
</Item>
</div>
);
};
PersonForm.propTypes = {
details: PropTypes.instanceOf(Object),
form: PropTypes.instanceOf(Object),
};
PersonForm.defaultProps = {
form: null,
details: {},
};
export default PersonForm;
And this is the modal component:
import React from 'react';
import PropTypes from 'prop-types';
import { Input, Form } from 'antd';
import Modal from 'components/Modal';
import LocaleItem from 'components/LocaleItem';
const { Item } = Form;
const FormModal = ({ visible, closeModal, onSubmit, fieldName, title }) => {
const [form] = Form.useForm();
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 15 },
};
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName);
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
const canceledModal = () => {
form.resetFields();
closeModal(fieldName);
};
return (
<Modal
onSuccess={addItem}
onCancel={canceledModal}
visible={visible}
title={title}
content={
<Form {...layout} form={form}>
<Item
name="dupleName"
label="Name:"
rules={[
{
required: true,
message: 'Name field cannot be empty',
},
]}
>
<Input placeholder="Enter a name" />
</Item>
</Form>
}
/>
);
};
FormModal.propTypes = {
visible: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
FormModal.defaultProps = {};
export default FormModal;
I get a memory leak when I am in the test file when adding items in the modal. Can someone point out why this is happening and how to fix this? Thanks
Remove closeModal and form.resetFields from addItem function.
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName); // when this onSubmit resolves it closes the modal, therefor these two lines below will be executed when component is unmounted, causing the memory leak warning
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
// instead maybe just:
const [form] = Form.useForm();
<Modal onOk={form.submit}>
<Form form={form}>
<Form.Item name="foo" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
</Modal>
Also, as far as I know you don't need to call form.validateFields as Ant Design's Form would do that automatically if rules are set in the Form.Item's.
I am looking to fire a submit handler for a LoginForm. However, for some reason, instead of my mock function being called, the actual handler for the component gets fired (calling an external api). How can I ensure that my mock handler gets called instead?
The three components of interest are below (The presentational, container and the test suite)
LoginForm.js
import { Formik, Form, Field } from 'formik';
import { CustomInput } from '..';
const LoginForm = ({ initialValues, handleSubmit, validate }) => {
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={handleSubmit}
>
{({ isSubmitting, handleSubmit }) => {
return (
<Form onSubmit={handleSubmit}>
<div className="d-flex flex-column justify-content-center align-items-center">
<Field
data-testid="usernameOrEmail"
type="text"
name="identifier"
placeholder="Username/Email"
component={CustomInput}
inputClass="mb-4 mt-2 text-monospace"
/>
<Field
data-testid="login-password"
type="password"
name="password"
placeholder="Password"
component={CustomInput}
inputClass="mb-4 mt-4 text-monospace"
/>
<button
data-testid="login-button"
className="btn btn-primary btn-lg mt-3 text-monospace"
type="submit"
disabled={isSubmitting}
style={{ textTransform: 'uppercase', minWidth: '12rem' }}
>
Submit
</button>
</div>
</Form>
)}}
</Formik>
);
};
export default LoginForm;
LoginPage.js
import React, { useContext } from 'react';
import { loginUser } from '../../services';
import { userContext } from '../../contexts';
import { loginValidator } from '../../helpers';
import { setAuthorizationToken, renderAlert } from '../../utils';
import LoginForm from './login-form';
const INITIAL_VALUES = { identifier: '', password: '' };
const LoginPage = props => {
const { handleUserData, handleAuthStatus } = useContext(userContext);
const handleSubmit = async (values, { setSubmitting }) => {
try {
const result = await loginUser(values);
handleAuthStatus(true);
handleUserData(result.data);
setAuthorizationToken(result.data.token);
props.history.push('/habits');
renderAlert('success', 'Login Successful');
} catch (err) {
renderAlert('error', err.message);
}
setSubmitting(false);
};
return (
<LoginForm
initialValues={INITIAL_VALUES}
validate={values => loginValidator(values)}
handleSubmit={handleSubmit}
/>
);
};
export default LoginPage;
LoginPage.spec.js
import React from 'react';
import { cleanup, getByTestId, fireEvent, wait } from 'react-testing-library';
import { renderWithRouter } from '../../../helpers';
import LoginPage from '../login-page';
afterEach(cleanup);
const handleSubmit = jest.fn();
test('<LoginPage /> renders with blank fields', () => {
const { container } = renderWithRouter(<LoginPage />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});
test('Clicking the submit button after entering values', async () => {
const { container } = renderWithRouter(<LoginPage handleSubmit={handleSubmit} />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
fireEvent.change(usernameOrEmailNode, { target: { value: fakeUser.username }});
fireEvent.change(passwordNode, { target: { value: fakeUser.password }});
fireEvent.click(submitButtonNode);
await wait(() => {
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});```
To answer your question, you will need to first make the handleSubmit constant accessible outside LoginPage.js so that it may be mocked and then tested. For example,
LoginPage.js
export const handleSubmit = async (values, { setSubmitting }) => {
... code to handle submission
})
And in your tests - LoginPage.spec.js
jest.unmock('./login-page');
import LoginPage, otherFunctions from '../login-page'
otherFunctions.handleSubmit = jest.fn();
...
test('Clicking the submit button after entering values', () => {
...
fireEvent.click(submitButtonNode);
expect(handleSubmit).toHaveBeenCalledTimes(1);
})
I hope the above fixes your problem.
But, going by the philosophy of unit testing, the above components
must not be tested the way you are doing it. Instead your test setup
should be like this -
Add a new test file called LoginForm.spec.js that tests your LoginForm component. You would test the following in this -
Check if all input fields have been rendered.
Check if the correct handler is called on submit and with the correct parameters.
The existing test file called LoginPage.spec.js would then only test if the particular form was rendered and then you could also test
what the handleSubmit method does individually.
I believe the above would make your tests more clearer and readable
too, because of the separation of concerns and would also allow you to
test more edge cases.