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.
Related
I am learning Redux. I cannot figure out how to set state.
I need to set state (I'm assuming with useDispatch) by using a login form. On the component Fake1, I am able to console.log the "user" passed with useSelector. If i hardcode a change in state on user.js ({ i.e., username: "beanbag", password: "122345" }), the change in state appears on Fake1, telling me that the mechanics of my setup are good, and that the problem is that state is not being set inside loginOnSubmit().
My code:
const initialStateValue = { username: "", password: "" };
export const userSlice = createSlice({
name: "user",
initialState: { value: initialStateValue },
reducers: {
login: (state, action) => {
state.value = action.payload;
},
logout: (state) => {
state.value = initialStateValue;
},
},
});
export const { login, logout } = userSlice.actions;
export default userSlice.reducer;
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Visibility from "#mui/icons-material/Visibility";
import VisibilityOff from "#mui/icons-material/VisibilityOff";
import InputAdornment from "#mui/material/InputAdornment";
import IconButton from "#mui/material/IconButton";
import Input from "#mui/material/Input";
import Button from "#mui/material/Button";
import LoginIcon from "#mui/icons-material/Login";
import AddCircleOutlineIcon from "#mui/icons-material/AddCircleOutline";
import Stack from "#mui/material/Stack";
import "./LoginForm.css";
import { useDispatch } from "react-redux";
import { login } from "../features/user";
function LoginForm() {
const [user, setUser] = useState(null);
const [loginUsername, setLoginUsername] = useState("");
const [loginError, setLoginError] = useState([]);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [values, setValues] = useState({
password: "",
showPassword: false,
});
const dispatch = useDispatch();
const navigate = useNavigate();
const handleChange = (prop) => (event) => {
setValues({ ...values, [prop]: event.target.value });
};
const handleClickShowPassword = () => {
setValues({
...values,
showPassword: !values.showPassword,
});
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
// useEffect(() => {
// fetch("/authorize_user")
// .then((res) => res.json())
// .then(setUser);
// }, []);
const loginOnSubmit = (e) => {
e.preventDefault();
const newUser = {
username: loginUsername,
password: values.password,
};
// dispatch(login({username: loginUsername, password: values.password}))
fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
}).then((res) => {
if (res.ok) {
res.json().then((newUser) => {
setUser(newUser);
setIsAuthenticated(true);
setLoginUsername("");
dispatch(login({ newUser }));
navigate("/fake1");
});
} else {
res.json().then((json) => setLoginError(json.error));
}
});
};
const handleSignupRoute = () => {
navigate("/signup");
};
return (
<form onSubmit={loginOnSubmit}>
<div>
<br></br>
<Input
className="test1"
value={loginUsername}
onChange={(e) => setLoginUsername(e.target.value)}
type="text"
label="Username"
placeholder="Username"
/>
<br></br>
<br></br>
<Input
id="standard-adornment-password"
type={values.showPassword ? "text" : "password"}
value={values.password}
// onChange={(e) => setValues(e.target.value)}
onChange={handleChange("password")}
placeholder="Password"
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
>
{values.showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
/>
<br></br>
<br></br>
<br></br>
<div className="test2">
<Stack direction="row" spacing={2}>
<Button
type="submit"
variant="outlined"
endIcon={<LoginIcon />}
className="btn-login"
>
Login
</Button>
<Button
onClick={handleSignupRoute}
variant="outlined"
endIcon={<AddCircleOutlineIcon />}
className="btn-signup"
>
Signup
</Button>
</Stack>
<br></br>
<br></br>
</div>
</div>
</form>
);
}
export default LoginForm;
import React from 'react'
import {useSelector} from 'react-redux'
const Fake1 = () => {
const user = useSelector(state => state.user.value)
console.log(user)
return (
<div>Fake1</div>
)
}
export default Fake1
The code below is using antd v3,for the antd v4 the form.create() is already not available, I read the documentation,it shows to replace the form.create() by useform.I tried to put it in the code(the second code) but still not working. Hhow can I use useForm in the code?
import React from 'react'
import {Form, Input} from 'antd'
const EditableContext = React.createContext();
const EditableRow = ({ form, index, ...props }) => (
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
);
export const EditableFormRow = Form.create()(EditableRow);
export class EditableCell extends React.Component {
state = {
editing: false,
};
toggleEdit = () => {
const editing = !this.state.editing;
this.setState({ editing }, () => {
if (editing) {
this.input.focus();
}
});
};
save = e => {
const { record, handleSave } = this.props;
this.form.validateFields((error, values) => {
if (error && error[e.currentTarget.id]) {
return;
}
this.toggleEdit();
handleSave({ ...record, ...values });
});
};
}
The useForm code
export const EditableFormRow = () => {
const { form, index, ...props } = useForm();
return (
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
)
};
useForm() return array, you can get form like this:
import React from "react";
import ReactDOM from "react-dom";
import { Form, Input, Button } from "antd";
const App = () => {
return <>{["a#x.com", "b#x.com"].map((email) => EditRow({ email }))}</>;
};
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 }
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 }
};
const EditRow = ({ email }) => {
const [form] = Form.useForm();
const onFinish = (values) => {
console.log(values);
};
const onClick = () => {
console.log(form.getFieldValue("email"));
};
return (
<Form form={form} layout={layout} onFinish={onFinish}>
<Form.Item
name="email"
label="email"
rules={[{ required: true }]}
initialValue={email}
>
<Input />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button onClick={onClick}>getFieldValue</Button>
</Form.Item>
</Form>
);
};
I am new to react. I have a task for importing a component inside another one dynamically.
as you can see I have an AutoComplete component that uses react-select; what I want is when user searches for a value and the value is not provided by the list, the user clicks on Create ... option and a dialog opens containing the needed component ( the component need to by imported at that time). Now I am passing the component as child but I want it more dynamic like passing the path of Component as prop and the AutoComplete loads by the path. any solution?
sample component uses AutoComplete:
<Autocomplete
apiUrl="/coreums/api/provinces"
onChange={(field, value) => {
console.log(`${field} ${value}`);
}}
onCreate={(field) => {
console.log(field);
}}
name="province"
componentName=""
entity={null}
>
{/* <ProvinceUpdate></ProvinceUpdate> */}
<h1>Hello</h1>
</Autocomplete>
as you see I am passing a h1 and it renders but for another component like ProvinceUpdater just want to pass my components path and it will be rendered.
which changes need my AutoComplete?
here is my AutoComplete:
import React, {
Component,
FC,
Suspense,
lazy,
useEffect,
useState,
} from 'react';
import Axios from 'axios';
import CreatableSelect from 'react-select/creatable';
import Select from 'react-select';
import { AvForm } from 'availity-reactstrap-validation';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
interface Props {
name: string;
apiUrl: string;
entity: any;
filterBy?: string;
onChange: (field: string, value: any) => void;
onCreate: (field: string) => void;
componentName?: string;
}
const Autocomplete: FC<Props> = ({
name,
apiUrl,
entity,
onChange,
children,
onCreate,
filterBy = 'name',
componentName,
}) => {
const [myOptions, setMyOptions] = useState([]);
const [childDialogVisible, setChildDialogVisible] = useState(false);
useEffect(() => {
Axios.get(`services/${apiUrl}?cacheBuster=${new Date().getTime()}`).then(
(res) => {
// console.log(res)
setMyOptions(
res.data.map((obj) => {
return { value: obj.id, label: obj[filterBy] };
})
);
}
);
}, []);
const handleChange = (newValue: any, actionMeta: any) => {
if (newValue) onChange(name, newValue.value);
};
const handleInputChange = (inputValue: any, actionMeta: any) => {
Axios.get(
`services/${apiUrl}?name.contains=${inputValue}&cacheBuster=${new Date().getTime()}`
).then((res) => {
setMyOptions(
res.data.map((obj) => {
return { value: obj.id, label: obj.name };
})
);
});
};
const handleCreate = (inputValue) => {
setChildDialogVisible(true);
onCreate(inputValue);
};
const renderFooter = () => {
return (
<div>
<Button
label="No"
icon="pi pi-times"
onClick={() => setChildDialogVisible(false)}
className="p-button-text"
/>
<Button
label="Yes"
icon="pi pi-check"
onClick={() => setChildDialogVisible(false)}
autoFocus
/>
</div>
);
};
return (
<>
<CreatableSelect
options={myOptions}
onChange={handleChange}
onCreateOption={handleCreate}
onInputChange={handleInputChange}
menuPortalTarget={document.body}
/>
<Dialog
header="Header"
visible={childDialogVisible}
style={{ width: '50vw' }}
footer={renderFooter()}
onHide={() => setChildDialogVisible(false)}
>
<Suspense fallback={() => <p>Loading</p>}>{children}</Suspense>
</Dialog>
</>
);
};
export default Autocomplete;
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)
}
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.