React function component parent state change does not re-render child component - javascript

I have a parent react function component (Products) which shows a list of products and with a state productInfo which is passed as a prop to the child component (AddEditProductModal)
const Products = () => {
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [productInfo, setProductInfo] = useState({});
const showEditModal = async (currentProductInfo) => {
console.log('edit called for key ',currentProductInfo.key)
setIsEditModalVisible(true);
setProductInfo(prevProductInfo => {
return {...prevProductInfo, ...currentProductInfo};
});
};
useEffect(() => {
setIsEditModalVisible(false);
setProductInfo({})
}, []);
return (
<>
<AddEditProductModal
title="Edit Product"
visible={isEditModalVisible}
productInfo={productInfo}
onOk={handleOk}
onCancel={handleCancel}
onFinish={onFinish}
/>
//Table components with columns/actions per row go here
</>
);
};
export default Products;
The child component AddEditProductModal is an antd Modal/Popup which fills the form with prefilled values chosen for current product row as shown below.
const AddEditProductModal = ({ title, visible, productInfo, onOk, onCancel, onFinish }) => {
return (
<Modal
title={title}
visible={visible}
onOk={onOk}
onCancel={onCancel}
>
<Form
name="basic"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
onFinish={onFinish}
initialValues = {productInfo}
>
<Form.Item
label="Key"
name="key"
>
<Input disabled={true} />
</Form.Item>
<Form.Item
label="Image"
name="image"
rules={[
{
required: true,
message: "Please input image!",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Name"
name="name"
rules={[
{
required: true,
message: "Please input name!",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Price"
name="price"
rules={[
{
required: true,
message: "Please input price!",
},
]}
>
<Input />
</Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form>
</Modal>
);
};
export default AddEditProductModal;
The productInfo is an object containing props as shown below:
{"key":19,"name":"Cooker","image":"https://.....d16bfa6d9c2e010cadc3fe6885448cbd.jpg_720x720q80.jpg","price":123}
When I click on any row's Edit button, the AddEditProductModal shows correct default values for the product. But when I click on another product/row, the AddEditProductModal still shows old values even though the productInfo state (seen in the profiler) has changed. Basically, the productInfo state has changed in the parent but the child has not re-rendered is what I am thinking.
Can anyone help why the modal shows the info on the first click but second time, fails to re-render and shows old product info ?

Yes, you are right! First, antd Form's API initialValues only works when it is initialized or reset. Second, antd Modal won't destroy after the close. So there is the result you said.
method A: do what you said,
useEffect(() => {
form.setFieldsValue(productInfo);
}, [productInfo]);
method B: destroyOnClose property of Modal set true will also solve your issue, but this is not a good choice!
const AddEditProductModal = ({ title, visible, productInfo, onOk, onCancel, onFinish }) => {
return (
<Modal
title={title}
visible={visible}
onOk={onOk}
onCancel={onCancel}
+destroyOnClose+
>
...

I got the answer to my problem. Rather than react, this seemed like an issue with the antd Form component initialValues. Seems like the initialValues does not change and we need to explicitly update the values. Adding the following to my AddEditProductModal solved my issue:
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(productInfo);
}, [productInfo]);
Hope this helps some antd user some day.

Related

Handle Reset for specific field formik

I am new to react and I have just started using Formik
I like how simple it makes making forms and handling forms in react.
I have created multiple custom fields using formik, I am putting the react-select field I created as an example here.
import { ErrorMessage, Field } from "formik";
import React from "react";
import Select from 'react-select'
const SelectInput = (props) => {
const { label, name, id,options, required, ...rest } = props;
const defaultOptions = [
{label : `Select ${label}`,value : ''}
]
const selectedOptions = options ? [...defaultOptions,...options] : defaultOptions
return (
<div className="mt-3">
<label htmlFor={id ? id : name}>
{label} {required && <span className="text-rose-500">*</span>}
</label>
<Field
// className="w-full"
name={name}
id={id ? id : name}
>
{(props) => {
return (
<Select
options={selectedOptions}
onChange={(val) => {
props.form.setFieldValue(name, val ? val.value : null);
}}
onClick = {(e)=>{e.stopPropagation}}
{...rest}
// I want someting like onReset here
></Select>
);
}}
</Field>
<ErrorMessage
name={name}
component="div"
className="text-xs mt-1 text-rose-500"
/>
</div>
);
};
export default SelectInput;
This is the usual code I use for submitting form as you can see I am using resetForm() method that is provided by formik, I want to attach the reseting logic in on submit method itself.
const onSubmit = async (values, onSubmitProps) => {
try {
//send request to api
onSubmitProps.resetForm()
} catch (error) {
console.log(error.response.data);
}
};
If you want to reset the selected value after the form is submitted, you need to provide a controlled value for the Select component.
The Formik Field component provides the value in the props object, so you can use it.
For example:
SelectInput.js
import { ErrorMessage, Field } from 'formik';
import React from 'react';
import Select from 'react-select';
const SelectInput = ({ label, name, id, options, required, ...rest }) => {
const defaultOptions = [{ label: `Select ${label}`, value: '' }];
const selectedOptions = options ? [...defaultOptions, ...options] : defaultOptions;
return (
<div className='mt-3'>
<label htmlFor={id ? id : name}>
{label} {required && <span className='text-rose-500'>*</span>}
</label>
<Field
// className="w-full"
name={name}
id={id ? id : name}
>
{({
field: { value },
form: { setFieldValue },
}) => {
return (
<Select
{...rest}
options={selectedOptions}
onChange={(val) => setFieldValue(name, val ? val : null)}
onClick={(e) => e.stopPropagation()}
value={value}
/>
);
}}
</Field>
<ErrorMessage name={name} component='div' className='text-xs mt-1 text-rose-500' />
</div>
);
};
export default SelectInput;
and Form.js
import { Formik, Form } from 'formik';
import SelectInput from './SelectInput';
function App() {
return (
<Formik
initialValues={{
firstName: '',
}}
onSubmit={async (values, { resetForm }) => {
console.log({ values });
resetForm();
}}
>
<Form>
<SelectInput
name='firstName'
label='First Name'
options={[{ label: 'Sam', value: 'Sam' }]}
/>
<button type='submit'>Submit</button>
</Form>
</Formik>
);
}
export default App;
Therefore, if you click the Submit button, value in the Select component will be reset.
You can also make a useRef hook to the Fromik component and then reset the form within the reset function without adding it as a parameter to the function.
https://www.w3schools.com/react/react_useref.asp
It's one of the really nice hooks you'll learn as you progress through React :)
So if I understood you correctly you want to reset a specif field value onSubmit rather than resetting the whole form, that's exactly what you can achieve using actions.resetForm().
Note: If nextState is specified, Formik will set nextState.values as the new "initial state" and use the related values of nextState to update the form's initialValues as well as initialTouched, initialStatus, initialErrors. This is useful for altering the initial state (i.e. "base") of the form after changes have been made.
You can check this in more detail here.
And here is an example of resetting a specific field using resetForm() whereby you can see as you input name, email and upon submit only email field will get empty using resetForm.
import "./styles.css";
import React from "react";
import { Formik } from "formik";
const initialState = {
name: "",
email: ""
};
const App = () => (
<div>
<h1>My Form</h1>
<Formik
initialValues={initialState}
onSubmit={(values, actions) => {
console.log(values, "values");
actions.resetForm({
values: {
email: initialState.email
}
});
}}
>
{(props) => (
<form onSubmit={props.handleSubmit}>
<input
type="text"
onChange={props.handleChange}
onBlur={props.handleBlur}
value={props.values.name}
name="name"
/>
<br />
<input
type="text"
onChange={props.handleChange}
onBlur={props.handleBlur}
value={props.values.email}
name="email"
/>
<br />
<br />
{props.errors.name && <div id="feedback">{props.errors.name}</div>}
<button type="submit">Submit</button>
</form>
)}
</Formik>
</div>
);
export default App;

#Antd# How can I bind self-made component with Form.item

I want to use my component like this
<Form.item>
<MyComponent />
</Form.item>
I can't pass the defaultValue to MyComponent and the Form can't get the value from MyComponent when I hit the submit button.
How can I fix this?
For using a default value you can use initialValues from antd's Form together with the in Form.Item defined name.
E.g.:
<Form
{...layout}
name="basic"
initialValues={{
myComponent: "IntialTestValue"
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="MyComponent"
name="myComponent"
To use custom or third party component you need bind your component to a value and onChange.
E.g.:
const MyComponent = ({ value = {}, onChange }) => {
const [inputValue, setInputValue] = useState(value);
const onInputChange = e => {
setInputValue(e.target.value);
onChange(e.target.value);
};
return (
<>
<Input defaultValue={value} value={inputValue} onChange={onInputChange} />
</>
);
};
Here is a working CodeSandbox.
Another good example can be found int antd Form docs: CodeSandbox

Setting the default value inside the input field in the antd library after calling the function in useEffect

I have the loadProfile function that I want to call in useEffect. If the loadProfile function is called within useEffect, the name Mario should be displayed inside the input name field. How can I set the default value in the antd library inside input? I try to use defaultValue but the input field remains empty.
Example here: https://stackblitz.com/edit/react-311hn1
const App = () => {
const [values, setValues] = useState({
role: '',
name: '',
email: '',
password: '',
buttonText: 'Submit'
});
useEffect(() => {
loadProfile();
}, []);
const loadProfile = () => {
setValues({...values, role, name="Mario", email});
}
const {role, name, email, buttonText} = values;
const updateForm = () => (
<Form
{...layout}
name="basic"
initialValues={{
remember: true,
name: name
}}
>
<Form.Item
label= 'Name'
name='name'
rules={[
{
required: true,
message: 'Please input your name!',
},
]}
>
<Input
defaultValue= {name}
/>
</Form.Item>
<Form.Item
label="Email"
name="email"
value={email}
rules={[
{
type: 'email',
required: true,
message: 'Please input your email!',
},
]}
>
<Input
/>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>
</Form.Item>
</Form>
);
return (
<>
<Row>
<Col span={24} style={{textAlign: 'center'}}>
<h1>Private</h1>
<p>Update Form</p>
</Col>
</Row>
{updateForm()}
</>
);
};
You have to make three changes to your code. This is a working component, I also extracted your component and put the appropriate state in there. I also made it functional.
https://stackblitz.com/edit/react-tlm1qg
First change
setValues({...values, role, name="Mario", email});
to
setValues({...values, name: "Mario"});
This will properly set the state.
Second change:
Next, you should notice that if you set defaultValue="test123" it still won't work, something is up. Remove name from Form.Item and boom it works. test123 shows up. But if you put values.name in there, it still doesn't work!
Third Change:
That's because defaultValue only sets that value right when the component is created, not on mount. So you have to use value={values.name} and it will set that value once on mount per your useEffect
In the demo component I also added a change handler for you so the user can type in there, if you wanted that.
If you look at the FAQ for Ant Design it says:
Components inside Form.Item with name property will turn into
controlled mode, which makes defaultValue not work anymore. Please try
initialValues of Form to set default value.
Ant Design is taking over control - so you don't have to set value={name} and onChange.
You want to set the values AFTER the component is created. So to do that you need
const [form] = Form.useForm();
React.useEffect(() => {
form.setFieldsValue({
username: 'Mario',
});
}, []);
and in you Form make sure you add:
<Form
{...layout}
name="basic"
initialValues={{
remember: true,
}}
form={form} // Add this!
>
I updated my online example.
Big picture - when you want to set the value after creation, use this hook and form.setFieldsValue

formik warning, A component is changing an uncontrolled input of type text to be controlled

I am developing a dynamic form in React JS, when the user clicks on the Add button, the Register form is added to screen. In the Register form I am using formik for validation.
The form is dynamically adding successfully but whenever I start entering any input in the input box, I am getting the following error in the console:
Warning: A component is changing an uncontrolled input of type text to be
controlled. Input elements should not switch from uncontrolled to controlled
(or vice versa). Decide between using a controlled or uncontrolled input element
for the lifetime of the component.
I am not sure where it is wrong. Below is my dynamic form rendering code -
Account.js
import React, { Component } from 'react';
import axios from 'axios';
import {
Card, CardBody, CardHeader,Button,Col, Row, Form, Container
} from 'reactstrap';
import { Formik, Field, ErrorMessage } from 'formik';
import WrapperForm from './WrapperForm'
class Account extends Component {
state = {
wrapperForm: [{}]
}
constructor(props) {
super(props);
}
addUser = () => {
this.setState((prevState) => ({
wrapperForm: [...prevState.wrapperForm, {}],
}));
}
render() {
let { wrapperForm } = this.state
return (
<Form>
<Container className="themed-container" fluid={true}>
<button type="button" onClick={this.addUser}>Add User</button>
<WrapperForm wrapperForm={wrapperForm} />
</Container>
</Form>
);
}
}
export default Account;
WrapperForm.js
const WrapperForm = (props) => {
return (
props.wrapperForm.map((val, idx)=> {
return (
<Formik
key={idx}
initialValues={{
email: props.wrapperForm[idx].email || '',
password: props.wrapperForm[idx].password || '',
firstName: props.wrapperForm[idx].firstName || '',
lastName: props.wrapperForm[idx].lastName || '',
zipCode: props.wrapperForm[idx].zipCode || ''
}}
>
{({ values }) => (
<Row style={{ marginBottom: "2em" }}>
<Card>
<CardHeader>Register</CardHeader>
<CardBody>
<Temp index={idx} />
</CardBody>
</Card>
</Row>
)}
</Formik>
)
})
)
}
Temp.js
const Temp = ({ index }) => {
let passwordId = 'password-'+ index;
let firstNameId = 'firstName-'+ index;
let lastNameId = 'lastName-'+ index;
let zipCodeId = 'zipCode-'+ index;
return (
<Card body outline color="primary">
<CardTitle>Create Profile</CardTitle>
<Row form>
<Col md={6}>
<Field
className="email"
component={customInputForm}
data-id={index}
id="email"
label="Email"
name="email"
placeholder="Email"
type="email"
/>
</Col>
</Row>
</Card>
)
}
I found one solution, maybe it will help someone. You need to create dynamic initialValues for formik as:
let passwordId = 'password-'+ idx ;
let firstNameId = 'firstName-'+ idx;
let lastNameId = 'lastName-'+ idx;
let zipCodeId = 'zipCode-'+ idx;
return (
<Formik
key={idx}
initialValues={{
email: props.wrapperForm[idx].email || '',
[passwordId]: props.wrapperForm[idx].password || '',
[firstNameId]: props.wrapperForm[idx].firstName || '',
[lastNameId]: props.wrapperForm[idx].lastName || '',
[zipCodeId]: props.wrapperForm[idx].zipCode || ''
}}
>
)
Reading through all the above comments was really helpful especially in understanding the basic difference between uncontrolled and controlled component in form element within react. However,I got the same error after adding InitialValues as a property to Formik. This opened my mind to the fact that the error occured whenever react view a form input as uncontrolled (controlled within the DOM) combined with state changing function within react component.
In fact,I was getting the error because the variable names initialized in my react useState hook are different from the name attribute on my form which I was using as the key to my form inputs. If you get the same error after adding InitialValues property on Formik,you should make sure your state variables are the same with the name attributes on each form input.
I had a field on my form but no corresponding initial value for it. Once I added the field to initial values (I used a static value, didn't have to be a prop) this error message disappeared.
I notice ppb's answer changes the field names in initialValues from the question, perhaps that made them match?
I presume the component is controlled when an initial value is specified and uncontrolled otherwise.

Having trouble manually adding 'Other' option in React. What gives?

I'm having a minor problem with respect to the functionality of my React Component illustrated below. Specifically, I would like for the user to be able to enter (via an input field) the poll category, if not listed under the options of the select component. This happens when the user selects the 'Other' option, which renders said input field as seen below. The issue I'm having is when the user starts entering a new category (triggering the onChange listener), this.setState results in 'category' no longer being 'Other' and, thus, the input field is no longer rendered. As such, it is impossible to enter another category. Below is the relevant code.
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Button, Form, Grid, Header, Icon } from 'semantic-ui-react';
class NewPoll extends Component {
state = {
title: '',
category: '',
choice: '',
choices: [],
};
onChange = e =>
this.setState({
[e.target.name]: e.target.value,
});
onSelect = e =>
this.setState({
category: e.target.value,
});
onAddChoice = e => {
//...
};
onRemoveChoice = id => {
//...
};
onPollSubmit = e => {
//...
};
render() {
const { title, category, choices, choice } = this.state;
const categories = [
'Sport',
'Travel & Tourism',
'Education',
'Technology',
'Automotive',
'Other',
];
// Preview and edit poll before saving
const shouldShowPreview = () => {
if (title && choices.length > 0) return true;
else return false;
};
return (
<Fragment>
<Grid.Row>
<Grid.Column>
<Header size="large" textAlign="center">
Create New Poll
</Header>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={shouldShowPreview() ? 2 : 1}>
<Grid.Column>
<Form style={styles.form} onSubmit={this.onPollSubmit}>
<Form.Field>
...
</Form.Field>
<Form.Field
placeholder="Category"
label="Poll Category"
control="select"
value={category}
onChange={this.onSelect}
>
<option disabled>Category</option>
{categories.map((pollCategory, index) => (
<option value={pollCategory} key={index}>
{pollCategory}
</option>
))}
</Form.Field>
{category === 'Other' && (
<Form.Field>
<label>If "Other"</label>
<input
name="category"
value={category}
placeholder="Enter category"
onChange={this.onChange}
/>
</Form.Field>
)}
<Form.Field>
<label>Poll Choices</label>
<div style={styles.choice}>
<input
name="choice"
value={choice}
placeholder="Enter poll choice"
onChange={this.onChange}
/>
<Button className="add-choice" onClick={this.onAddChoice}>
<Icon style={styles.icon} name='add' size='large' />
</Button>
</div>
</Form.Field>
<Form.Field control={Button} type="submit">
Submit Poll
</Form.Field>
</Form>
</Grid.Column>
</Grid.Row>
</Fragment>
);
}
}
NewPoll.propTypes = {
addNewPoll: PropTypes.func.isRequired,
};
export default NewPoll;
This issue is being caused because you are using the same variable - category to do two things:
Store what the actual category is
Determine whether to show the additional textbox
You have two options:
Create a different variable to show the additional textbox. For example, something like:
const showCategoryTextBox = ...//whether category belongs in the categories list.
// and then use this to control the display of the textbox.
OR
Modify your condition like:
(category!== 'Sport' && category!=='Travel & Tourism' &&...)
// include all the values in your categories list except 'Other'

Categories