I have two select boxes, one for country and another for region. When someone selects a country, I need to populate the region select with different values (asynchronously).
I'm aware of react-country-region-selector and react-select, but those solutions seem like overkill for such a simple task.
In the code below, the regions are populated correctly after selecting a country, but the value of the country select is lost. Also, should I be setting state in the constructor or should Formik be handling all state?
import React from 'react';
import { Formik, Form, Field } from "formik";
class App extends React.Component {
constructor(props) {
super(props);
console.log(`props: ${JSON.stringify(props, null, 2)}`)
this.state = {
regions: []
}
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCountryChanged = this.handleCountryChanged.bind(this);
this.getRegions = this.getRegions.bind(this);
}
handleSubmit(values, { setSubmitting }) {
console.log(JSON.stringify(values), null, 2);
};
handleCountryChanged(event) {
const country = event.target.value;
this.getRegions(country).then(regions => {
this.setState({ regions: regions });
console.log(`regions: ${JSON.stringify(regions, null, 2)}`);
});
}
getRegions(country) {
// Simulate async call
return new Promise((resolve, reject) => {
switch (country) {
case "United States":
resolve([
{ value: 'Washington', label: 'Washington' },
{ value: 'California', label: 'California' }
]);
break;
case "Canada":
resolve([
{ value: "Alberta", label: "Alberta" },
{ value: "NovaScotia", label: "Nova Scotia" }
]);
break;
default:
resolve([]);
}
});
}
render() {
return (
<Formik
initialValues={{ country: "None", region: "None", regions: [] }}
onSubmit={this.handleSubmit}
>
{({ isSubmitting }) => (
<Form>
<label htmlFor="country">Country</label>
<Field id="country" name="country" as="select"
onChange={this.handleCountryChanged}>
<option value="None">Select country</option>
<option value="United States">United States</option>
<option value="Canada">Canada</option>
</Field>
<label htmlFor="region">Region</label>
<Field id="region" name="region" as="select">
<option value="None">Select region</option>
{this.state.regions.map(r => (<option key={r.value} value={r.value}>{r.label}</option>))}
</Field>
<button type="submit" disabled={isSubmitting}>Submit</button>
</Form>
)}
</Formik>);
}
}
export default App;```
I think you should handle get regions and set in formik
Here is example code (codesanbox):
Formik handle get regions
Code here:
// Helper styles for demo
import "./helper.css";
import { MoreResources, DisplayFormikState } from "./helper";
import React from "react";
import { render } from "react-dom";
import { Formik, Field } from "formik";
import * as Yup from "yup";
const App = () => {
const getRegions = country => {
// Simulate async call
return new Promise((resolve, reject) => {
switch (country) {
case "United States":
resolve([
{ value: "Washington", label: "Washington" },
{ value: "California", label: "California" }
]);
break;
case "Canada":
resolve([
{ value: "Alberta", label: "Alberta" },
{ value: "NovaScotia", label: "Nova Scotia" }
]);
break;
default:
resolve([]);
}
});
};
return (
<div className="app">
<h1>
Basic{" "}
<a
href="https://github.com/jaredpalmer/formik"
target="_blank"
rel="noopener noreferrer"
>
Formik
</a>{" "}
Demo
</h1>
<Formik
initialValues={{ country: "None", region: "None", regions: [] }}
onSubmit={async values => {
await new Promise(resolve => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.email()
.required("Required")
})}
>
{props => {
const {
values,
dirty,
isSubmitting,
handleChange,
handleSubmit,
handleReset,
setFieldValue
} = props;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="country">Country</label>
<Field
id="country"
name="country"
as="select"
value={values.country}
onChange={async e => {
const { value } = e.target;
const _regions = await getRegions(value);
console.log(_regions);
setFieldValue("country", value);
setFieldValue("region", "");
setFieldValue("regions", _regions);
}}
>
<option value="None">Select country</option>
<option value="United States">United States</option>
<option value="Canada">Canada</option>
</Field>
<label htmlFor="region">Region</label>
<Field
value={values.region}
id="region"
name="region"
as="select"
onChange={handleChange}
>
<option value="None">Select region</option>
{values.regions &&
values.regions.map(r => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</Field>
<button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</button>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
<DisplayFormikState {...props} />
</form>
);
}}
</Formik>
<MoreResources />
</div>
);
};
render(<App />, document.getElementById("root"));
You can set the value of one field to be dependent upon another using Formik's useField
and useFormikContext
hooks. There's a demo in the docs here; here's a simplified example:
const DependentField = (props: FieldAttributes<any>) => {
const { values, touched, setFieldValue } = useFormikContext<ValueType>() // get Formik state and helpers via React Context
const [field, meta] = useField(props) // get the props/info necessary for a Formik <Field> (vs just an <input>)
React.useEffect(() => {
// set the values for this field based on those of another
switch (values.country) {
case 'USA':
setFieldValue(props.name, 'Asia')
break
case 'Kenya':
setFieldValue(props.name, 'Africa')
break
default:
setFieldValue(props.name, 'Earth')
break
}
}, [values.country, touched, setFieldValue, props.name]) // make sure the component will update based on relevant changes
return (
<>
<input {...props} {...field} />
{!!meta.touched && !!meta.error && <div>{meta.error}</div>}
</>
)
}
// then, use it in your form.
const MyForm = (props: any) => {
// do stuff
return(
<Formik>
<Field name="country">
// options
</Field>
<DependentField name="region"> // this field will now change based on the value of the `country` field.
// options
</DependentField>
</Formik>
)
}
Warning - don't try to do this using onChange
on the field that determines the change. It may seem intuitive, but there are a number of problems along the way, mainly that Formik's handleChange
must fire for the field to work properly; but it's async, but returns no promise, and so will not pick up the other changes in the handler.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With