I'm working on implementation of a multi step form with react-hook-form and my problem is that input fields do not get reinitialized with the form data when I return to the previous page.
I'm using <FormProvider /> component from react-hook-form to inject the form data into the pages and my input components are registered with register method from useFormContext() hook
const CreateAccount = () => {
const [currentStep, setCurrentStep] = useState(0);
const methods = useForm<FormData>({
mode: "onChange",
});
const onSubmit = (data) => console.log(data);
const handleNextStep = () => {
if (currentStep >= 5) return;
setCurrentStep(currentStep + 1);
};
const handlePreviousStep = () => {
if (currentStep <= 0) return;
setCurrentStep(currentStep - 1);
};
const renderContent = () => ({
[RegistrationSteps.UsernameEmail]: <UsernameEmail handleNextStep={handleNextStep} handlePreviousStep={handlePreviousStep} />,
[RegistrationSteps.Password]: <CreatePassword handleNextStep={handleNextStep} handlePreviousStep={handlePreviousStep} />,
});
return (
<Container maxWidth="sm">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{renderContent()[currentStep]}
</form>
</FormProvider>
</Container>
);
};
export default CreateAccount;
Here is what the input fields look like
const {
register
} = useFormContext();
<TextField
label="Email"
{...register("email")}
/>
Even though the form still holds the data in its state, it does not populate into corresponding fields when I switch back and forth between the form pages.
Instead of a single form at a global level, I recommend creating each component in your step as a form with its own instance of useForm() and wrapping steps in a state provider to store data across different steps. That way, you can assign values to the step forms from the respective state using defaultValues option of useForm on initialization.
You can check out this for the basic architecture that I'm trying to explain.
defaultValues in useForm
Related
I have some clearable select, and I want to reset the applets field in state to an empty array.
const defaultFormValues = { device: { ...initialDevice }, applets: [] };
const { control, getValues, setValue, reset, handleSubmit } = useForm<CreateDeviceFormData>({
mode: "all",
reValidateMode: "onChange",
defaultValues: defaultFormValues,
resolver: yupResolver(validationSchema),
});
const onChangeHandler = React.useCallback(
(value: Experience | null) => {
if (value) {
setValue("applets", getApplets(value));
} else {
setValue("applets", []);
// reset(defaultFormValues);
}
setValue("device.experience_id", value ? value.id : undefined);
},
[templateSelector, setValue],
);
console.log("current data", getValues(), control);
return (
<>
<SomeAutocompleteComponent control={control} onChange={onChangeHandler} />
<SelectAppletsComponent control={control} />
</>
);
export const SelectAppletsComponent = ({ control, onChange }) => {
const applets = useWatch({ control, name: "applets" }) as Applet[];
const device = useWatch({ control, name: "device" }) as Device;
if (!applets.length) {
return null;
}
return (
<SpanWrapper className="p-col-8">
{applets.map((applet) => (
<LabelRadio
key={applet.id}
inputId={applet.applet_type}
value={applet.applet_type}
label={applet.name}
checked={device.applet_type === applet.applet_type}
onChange={onChange}
/>
))}
</SpanWrapper>
);
};
the problem is that clearing the selection on UI with setValue("applets", []); not working for some reason, and I don't understand why, and how to do it without reset method, which resets the whole state, not just single property as I understand
You should always register fields if you want to use them as RHF's form state.
React.useEffect(() => {
register("applets");
}, [register]);
This fixes an issue.
Update:
Also a new method resetField is available
Just to follow up on this, it is indeed the right solution provided by AuthorProxy.
Using defaultValues doesn't register the fields (it seems that they are still added to the formData on submit, but since they are not registered, any user triggered changes to these fields won't reflect on the formData).
You have to register every field you want the user to be able to interact with.
We usually register fields via inputs in the JSX, but we also need to register the array since there is no input for it in the JSX.
As per shown by the author of the react hook form library.
https://github.com/react-hook-form/react-hook-form/discussions/3160
And sandbox
https://codesandbox.io/s/inspiring-wood-4z0n0?file=/src/App.tsx
English is not my mother language so sorry for mistakes, i'm react beginner, my question:
I am at route /#/billingInfo/
when user clicks 'sender' button payers name will be senders name and same for 'receiver' button, but the problem is i'm pushing same url which is this same page, i get senders or receivers name in my input as i should but only when i refresh the page, my question is how to not refresh the page and get those in my input (need to render/forceUpdate < Form > or that specific input when user clicks those buttons
??)
my code:
const [form] = Form.useForm();
const query = window.location.toString().split("?")[1];
const urlParamss = new URLSearchParams(query);
const payer = new URLSearchParams(query).get("payer");
const bNameUrl = urlParamss.get("bName");
const PickUpName = urlParamss.get("pName");
const DeliveryName = urlParamss.get("dName");
let bName;
if (payer === "sender") {
bName = PickUpName;
} else if (payer === "receiver") {
bName = DeliveryName;
}
const senderPays = () => {
history.push(`/billingInfo/${customerId}?${query}&payer=sender`);
};
const receiverPays = () => {
history.push(`/billingInfo/${customerId}?${query}&payer=receiver`);
};
return (
<div>
<Form>
<div>
<Button onClick={senderPays}>sender</Button>
<Button onClick={receiverPays}>receiver</Button>
</div>
<Form.Item
label="payers name"
name="bName"
initialValue={bNameUrl || bName}
>
<Input type="string" />
</Form.Item>
</Form>
</div>
);
}
export default BillingInfo;
If the payer query parameter is the only difference in urls when the new routes are pushed, then you can "listen" for changes on that value in an useEffect hook.
Create a custom query parameter hook. From the React-Router docs Query Parameters.
const useQuery = () => {
return new URLSearchParams(useLocation().search);
};
Use this hook to get the query parameters.
const {
payer,
bName: bNameUrl,
dName: DeliveryName,
pName: PickUpName
} = useQuery();
Move bName into component state.
const [name, setName] = useState(bNameUrl || bName);
Use useEffect to handle any changes to payer and update name state.
useEffect(() => {
setName(payer === 'sender' ? PickUpName : DeliveryName);
}, [payer]);
Render name as the default input value (retains whatever form logic you may have). Use a react key on the form to help "re-initialize" it by indicating to react it's a "new" component and needs to be mounted. See Fully uncontrolled component with a key.
<Form key={name}>
<div>
<Button onClick={senderPays}>sender</Button>
<Button onClick={receiverPays}>receiver</Button>
</div>
<Form.Item
label="payers name"
name="bName"
initialValue={name}
>
<Input type="string" />
</Form.Item>
</Form>
React beginner here, English is not my mother language so sorry for mistakes. I have url from there I want to get information I will show how I have done other pages(using antd form), but now I'm in different page where at the end of my url is payer=sender, so if payer is "sender" then do this. that 'payer=sender' at the end of my url comes by clicking a sender button, then when it is clicked form input should fill accordingly
const senderPays = () => {
history.push(`/bilInfo/${custId}?${query}&payer=sender`);
};
<Button onClick={senderPays}>Sender</Button>
In previous pages I could get pickUpName from url like this :
const query = window.location.toString().split("?")[1];
const urlParams = new URLSearchParams(query);
const PickUpName = urlParams.get("pickUpName") || props.customer?.name;
<Form.Item
label="pickUpName"
name="pickUpName"
initialValue={PickUpName || ""}
>
<Input type="string" />
</Form.Item>
This is my url: https://localhost:5001/#/billingInfo/5dc9d690-a549-4baf-868e-a441c6c4ff47?pickUpName=gholam&pickUpContactPerson=&pickUpPhone=8744444&pickUpAddress=gerbytnie&pickUpPostalCode=65230&pickUpPostalRegion=helsnki&deliveryName=shammms&deliveryContactPerson=kalle&deliveryPhone=2658265&deliveryAddress=&deliveryPostalCode=65230&deliveryPostalRegion=&payer=sender
As you can see end of url is &payer=sender
so if payer is sender then have these information and if payer is receiver then have these information:
this is my code:
useEffect(() => {
if (payer === "sender") {
billingName = PickUpName;
} else if (payer === "receiver") {
billingName = DeliveryName;
}
}, [payer]);
Component
<Form.Item
label="billingName"
name="billingName"
initialValue={billingName || ""}
>
<Input type="string" />
</Form.Item>
So I need somehow to get that billingName in my forms initialValue, depending who is payer.
Use the useForm react hook to manually initialize fields:
Summary:
Initialize a form instance using useForm().
Pass that FormInstance to your Form
Use useEffect hook to dynamically set the value of the Form.Item based on the value of payer
import { Form } from 'antd';
const Demo = () => {
// Note that `useForm` is a React Hook and will only works in functional components
const [form] = Form.useForm();
const payer = new URLSearchParams(window.location.search).get('payer');
// Manually initialize billingName based on `payer` value
useEffect(() => {
if (payer === "sender") {
form.setFieldsValue({ billingName: request.pickUpName });
} else if (payer === "receiver") {
form.setFieldsValue({ billingName: request.deliveryName });
}
}, [payer])
// Pass the FormInstance `form` to your Form
return (
<Form form={form}>
<Form.Item
label="billingName"
name="billingName"
>
...
</Form.Item>
...
)
Here's some documentation to help you out:
Antd: manually setting form field values
React: useEffect hooks
In the following code i take in an Object. I pull out the object keys that i would like to use as labels for the form. I have written several different for loops that can create an array of const variables. My problem is building the [idProps, idMeta] variable to pass into my form. [idProps, idMeta] need to change for each form input [{label}Props, {label}Meta] The useField() is tricky. Any suggestions or reference some other code would be welcome.
import React, { useCallback } from 'react';
import { useField } from 'fielder';
import Form from 'react-bootstrap/Form';
import { Container, Row, Col } from 'react-bootstrap';
import BrewFormGroup from '../FormComponents/BrewFormGroup';
import BrewButton from '../Buttons/BrewButtons';
function BrewFormComponent(props) {
console.log("row ===> ",props.data);
// Variables and const
const data =props.data;
console.log(data);
const keys = Object.keys(data);
const test = [];
const createVars = ()=>{
for (let i = 0; i < keys.length; i++) {
const element = keys[i];
test.push(element);
}
}
createVars();
const [idProps, idMeta] = useField({
name: 'id',
initialValue: props.data.id,
});
// ========== Functions ============
// const keys = HandleData.getObjectKeysprops2(data);
const handleSubmit = useCallback((event) => {
}, []);
const changeLocation = () => {
alert("Submit to local storage")
console.log("Hobnob===> "+test[2]);
}
// =============== Return ===================
return (
<Container><Row className="mb-2 justify-right">
<Col></Col><Col><BrewButton
variant="success"
type="submit"
onClick={changeLocation}
value="Submit"></BrewButton></Col>
</Row>
<Form autoComplete="off" onSubmit={handleSubmit}>
<BrewFormGroup
props={idProps}
meta={idMeta}
label="id"
type="text"
/>
</Form></Container>)
};
export default BrewFormComponent;
So, I think you can use other features of Formik to accomplish your goal. Instead of dynamically building fields with useField, you can instead map over your props in your output itself. Something like:
return (
<Form autoComplete="off" onSubmit={handleSubmit}>
{Object.keys(props.data).map((key) => (
<Field id={key} name={key} initialValue={props.data[key]} />
)}
</Form>
I hate to upload a code snippet with no sandbox, but this particular instance I use firebase so wasn't sure how to make one. Apologies for the verbose code. I'm a beginner React developer and I've been stuck on this state management issue for 2 weeks now, and I tried so many different methods but to no fruit. Please help.
My goal is to click AddLinkButton to make multiple input forms one by one, each input form would be different links, and by clicking Apply Button it would collect all the link values and store it to firebase's firestore. Once the storing is complete, it would display a preview by passing in multiple updated hook values to <UserPreview />.
If I run this particular code below, the key which is supposed to be the value of the link input forms, is null and does not update on onChange.
Please help... much appreciated. Thank you.
EDIT: changed variable name key to keyHook but to no success. Same issue
const AdminCustomizer = () => {
const [username, setUsername] = useState(null);
const [linkForm, setlinkForm] = useState([]);
const [spotlightLabel, setSpotlightLabel] = useState('');
const [spotlightLink, setSpotlightLink] = useState('');
const [refresh, setRefresh] = useState(false);
const [keyHook, setKeyHook] = useState(null);
const [startCollect, setStartCollect] = useState(false);
const linkRef = useRef();
const userInfo = {username, linkRef, spotlightLabel, spotlightLink, pfpURL, refresh};
// on initial load, load database to page
if (!username) {
firebase.getAuth().onAuthStateChanged(user => {
if (user) {
setUsername(user.displayName);
firebase.getUserInfo(user.displayName).then(result => {
setSpotlightLabel(result.spotlightLabel);
setSpotlightLink(result.spotlightLink);
linkRef.current = result.links;
if (result.links) {
Object.values(result.links).forEach(link => {
AddLinks(link);
});
}
})
}
});
}
//on refresh (when clicking apply changes button) reload page values with updated database
useEffect(() => {
if (refresh) {
firebase.getAuth().onAuthStateChanged(user => {
if (user) {
firebase.getUserInfo(user.displayName).then(result => {
linkRef.current = result.links;
Object.values(result.links).forEach(link => {
AddLinks(link);
});
})
setRefresh(false);
}
});
}
}, [refresh])
// adding AddLink button will add a new input form
// adding AddLink with firebase database value will add a new input form with values loaded
const AddLinks = url => {
const hooks = { refresh, startCollect, keyHook, setKeyHook };
if (url) setKeyHook(url);
setlinkForm([ ...linkForm, <AddLink key={keyHook} keyHook={keyHook} hooks={hooks} /> ]);
}
// add link input form
const AddLink = props => {
const handleChange = e => setKeyHook(e.target.value);
return (
<form noValidate autoComplete="off">
<br />
<Link label="Social" onChange={handleChange} value={props.keyHook} />
</form>
)
}
// when apply changes is clicked, collect input values from all link input forms
if (startCollect) {
linkForm.forEach(form => {
linkRef.current = {
...linkRef.current,
link: form.keyHook,
}
});
firebase.addLinksToUser({ spotlightLabel, spotlightLink, linkRef }).then(() => {
//force refresh to update userInfo for UserPreview
setStartCollect(false);
setRefresh(true);
});
}
return (
<>
<LinkBox>
<ApplyButton onClick={() => setStartCollect(true)}>Apply Changes</ApplyButton>
<Link label="Website Title" onChange={e => setSpotlightLabel(e.target.value)} value={spotlightLabel} />
<Link label="Website URL" onChange={e => setSpotlightLink(e.target.value)} value={spotlightLink}/>
<AddLinkButton onClick={() => AddLinks(null)} />
<div>{linkForm ? linkForm.map(child => child) : null}</div>
</LinkBox>
<div>
<PhoneOutline>
<UserPreview userInfo={userInfo}/>
</PhoneOutline>
</div>
</>
);
}
export default AdminCustomizer;
In AddLink, the key is a restricted keyword and doesn't get propagated as props. Try a different prop name instead of key.
See this link
Try:
<AddLink key={keyHook} keyHook={keyHook} hooks={hooks} />