Is there a way to set defaultValues in a child component using react hook form with useFieldArray hook - not using the useForm hook? - reactjs

Problem
I want to implement key/value pairs of input fields in a form that can be added by a user.
See animated gif on dynamic fields.
In addition, I want to display saved data when the user has submitted the form and the page is displayed again.
See animated gif on displaying saved dynamic fields.
Preconditions
I'm using react-hook-form V7 (RHF) and its useFieldArray hook.
As I use Material-UI I have to use controlled components.
Working solution
In a simplified app, I have a parent component using the useForm hook and two child components, one for the purpose of demonstration holding normal form fields and an <ArrayFields /> component holding the array fields.
Yesterday I learned by this answer that one way to do it is to set the defaultValues object in the parent's useForm hook like this:
const methods = useForm({
defaultValues: {
email: "john.smith#example.com",
firstName: "John",
lastName: "Smith",
systemRole: "Admin",
envRoles: [ // <-- saved dynamic fields
{ envName: "foo1", envRole: "bar1" },
{ envName: "foo2", envRole: "bar2" }
]
}
});
Here you can see a codesandbox of this working solution.
Question
Nonetheless, I am wondering if it isn't possible to set the defaultValues in the <ArrayFields /> child component?
useFormContext approach
For example like this using the useFormContext hook:
//
const ArrayFields = ({ fieldset }) => {
const { control, getValues } = useFormContext({
defaultValues: {
envRoles: [
{ envName: "foo1", envRole: "bar1" },
{ envName: "foo2", envRole: "bar2" }
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "envRoles"
});
...
}
But this is not displaying the saved fields at all
See here a codesandbox version of the useFormContext approach
Props approach
Next, I tried to pass the fields (alias envRoles) as props and set the defaultValues to the Controller directly
// index.js:30
<ArrayFields
fieldset={[
{ envName: "foo1", envRole: "bar1" },
{ envName: "foo2", envRole: "bar2" }
]}
/>
// ArrayFields.js:35
<Controller
render={({ field }) => <input {...field} />}
defaultValue={item.envName} {/* <-- defaultValue on Controller */}
name={`envRoles[${index}].envName`}
control={control}
/>
This displays the defaultValues
but does not work when clicking on the add or delete button
See this codesandbox of the props approach
Question again
So is it really true that RHF does not allow handling all things that matter to the component inside this component?
Thanks in advance for your hints.

I think I found the answer. 🙊
In the documentation of useFieldArray you'll find the replace method.
replace (obj: object[]) => void Replace the entire field array values.
So using a useEffect hook it's finally easy.
useEffect(() => {
replace(fieldset);
}, [fieldset, replace]);
In this codesandbox you will find the example that finally works with setting the defaultValues in the child component.

Related

react-table v7 within react-hook-form

i am working on a complex react form where it has few controlled inputs along with grid/table. currently i am using react-hook-form for validation. here is my mockup. idea here is show grid as required until user adds some data. user can add/remove data by clicking "+" or "-" buttons.
when i submit here is what i see in submitted data
{
"fname": "sasa",
"lname": "asasasa"
}
here is the expected output
{
"fname": "sasa",
"lname": "asasasa",
"localAddress":[
{
"street1":"street1",
"street2":"street2",
"city":"city"
},
{
"street1":"street2",
"street2":"street2",
"city":"city"
}
]
}
here is my codesanbox
Codesanbox
not sure how can i integrate react-table (or any table component) with react-hook-form (or any react form). building a form using "react-table" is a must for me.
appreciate any help.
As mentioned in the get-started docs in the "Work with UI library" section:
Option 3: we can set up a custom register using the useEffect
Hook and update the value via setValue.
So here's what needs to be done in your case:
export default function App() {
const { register, handleSubmit, setValue } = useForm();
// ...
React.useEffect(() => {
register({ name: "localaddress" });
}, [register]);
const addLocalAddress = function() {
// ...
setValue("localaddress", d);
setLocalAddress(d);
};
// ...
}
And with this in place you need to get rid of Controller by replacing this:
<Controller
name="tag"
control={methods.control}
as={
<Table1
name="tag"
ref={methods.register}
columns={columns}
data={localAddress}
{...methods}
/>
}
/>;
with this:
<Table1 columns={columns} data={localAddress} />
That should be it. And of course the sandbox.
Here you go :
First : we need the input box, with name that can generate the texted output
name="localAddress[0][street1]"
name="localAddress[0][street2]"
name="localAddress[0][city]"
name="localAddress[1][street1]"
name="localAddress[1][street2]"
name="localAddress[1][city]"
// to generate the name you can do something like this
<Input
type="text"
name={`localAddress[${i}][${columns[j]["accessor"]}]`}
placeholder={columns[j]["accessor"]}
innerRef={methods.register}
/>
Second : we need access to methods
// pass it from props
<Table1
name="tag"
ref={methods.register}
columns={columns}
data={localAddress}
methods = {methods} // <---- Pass HERE
/>
// access it via props
export const Table1 = React.forwardRef(({ columns, data, methods }, ref) => {
WORKING DEMO : (for output you can check the console)

Async update of Formik initialValues inherited from parent React component state (leveraging useEffect hook?)

I am currently building a multi-step form during a user onboarding process, which is why I need to centralize all form data in a parent React component state.
I need to update initialValues with user information but this is an async process.
I thought of creating a useEffect hook calling setState, but maybe there is a more elegant way of doing so...
Having initialValues as one of useEffect dependencies seems to create an infinite loop (Maximum update depth exceeded). This is why the working solution I found was to duplicate all initialValues within... 😒
So how could I update only specific values from initialValues after getting async user information?
Here is a simplified version of the implementation:
import React, { useState, useEffect } from 'react'
// Auth0 hook for authentication (via React Context).
import { useAuth0 } from '../../contexts/auth/auth'
import { Formik, Form, Field } from 'formik'
export default () => {
const { user } = useAuth0()
const initialValues = {
profile: {
name: '',
address: '',
// Other properties...
},
personalInfo: {
gender: '',
birthday: '',
// Other properties...
},
}
const [formData, setFormData] = useState(initialValues)
const [step, setStep] = useState(1)
const nextStep = () => setStep((prev) => prev + 1)
useEffect(() => {
const updateInitialValues = (user) => {
if (user) {
const { name = '', gender = '' } = user
const updatedInitialValues = {
profile: {
name: name,
// All other properties duplicated?
},
personalInfo: {
gender: gender,
// All other properties duplicated?
},
}
setFormData(updatedInitialValues)
}
}
updateInitialValues(user)
}, [user, setFormData])
switch (step) {
case 1:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="profile.name" type="text" />
<Field name="profile.address" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
case 2:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="personalInfo.gender" type="text" />
<Field name="personalInfo.birthday" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
// Other cases...
default:
return <div>...</div>
}
}
it's probably late for me to see this question and I just happen to work on a similar project recently.
For my use case, I'm using only one Formik, and using theory similar to Formik Multistep form Wizard: https://github.com/formium/formik/blob/master/examples/MultistepWizard.js for my multistep forms.
And on each step, I need to fetch API to prefill data, I also use useEffect but since I just call the API onetime when I load the specific step, I force it to behave the same as ComponentDidMount(), which is to leave the [] empty with the comment // eslint-disable-next-line so it won't give me warning.
And I use setFieldValue in the useEffect after data is successfully loaded. I feel mine is also not a good way to handle this situation, and I just found something that might be useful: https://github.com/talor-hammond/formik-react-hooks-multi-step-form, it has a Dynamic initialValues. (Though it's typescript)
I am going to refer to this and also try to use for each of my steps, and probably use Context or Wrap them in a parent and store data in the parent Formik.
And getting infinite loop for you might because setFormData should not be in the dependency, since when you setState, the component re-render, the useEffect calls again.
Not sure if this can help you or you already find out how to implement it, I'll look into this deeper.

How to make the options in react-select dropdown accessible?

I am building a reactjs application and I am using a library called react-select for my dropdown which is searchable.
but the problem I am facing is that the options inside the select are not being read out by NVDA screenreader when using arrow keys.
and am not able to set focus on this dropdown as well for some reason.
I tried it via the official documentation but no luck as of now.
The library I am using:
React-select
https://react-select.com/home
The code:
import React, { Component, Fragment } from "react";
import Select from "react-select";
export const flavourOptions = [
{ value: "vanilla", label: "Vanilla", rating: "safe" },
{ value: "chocolate", label: "Chocolate", rating: "good" },
{ value: "strawberry", label: "Strawberry", rating: "wild" },
{ value: "salted-caramel", label: "Salted Caramel", rating: "crazy" }
];
export default class SampleDropdown extends Component {
state = {
isClearable: true,
isDisabled: false,
isLoading: false,
isRtl: false,
isSearchable: true
};
componentDidMount() {
document.getElementById("translate").focus();
}
render() {
const {
isClearable,
isSearchable,
isDisabled,
isLoading,
isRtl
} = this.state;
return (
<Fragment>
<Select
className="basic-single"
classNamePrefix="select"
defaultValue={flavourOptions[0]}
isDisabled={isDisabled}
isLoading={isLoading}
isClearable={isClearable}
isRtl={isRtl}
isSearchable={isSearchable}
name="color"
options={flavourOptions}
id="translate"
/>
</Fragment>
);
}
}
And here is a working example in codesandbox.
https://codesandbox.io/s/focused-clarke-euk0e
Actual result: When I enter the page, the dropdown does not have the focus. and am not able to read out options in the dropdown using arrow keys in NVDA screenreader.the options are being read out as blank.
Expected result: When I enter the page, the dropdown should have the focus. and the options in the dropdown should be read out when using arrow keys when NVDA screenreader is switched on.
I looked at using the same library but ran into accessibility issues as well. I ended up building my custom select element and manually handling the key presses, focus movement, and label announcements. If you're stuck on using react-select you'll probably need to amend it yourself or wait for a PR.
Otherwise, if you're up for the challenge, you can follow my tutorial on creating an accessible select component in React. You can pull apart the code on codesandbox as well. This might make it easier to port to the react-select as well.
And of course, I'd also recommend using the native select element, as that will handle accessibility best.
Reach UI has accessible components. This Combobox could be of use https://reach.tech/combobox

How do I trigger the change event on a react-select component with react-testing-library?

Given that I can't test internals directly with react-testing-library, how would I go about testing a component that uses react-select? For instance, if I have a conditional render based on the value of the react-select, which doesn't render a traditional <select/>, can I still trigger the change?
import React, { useState } from "react";
import Select from "react-select";
const options = [
{ value: "First", label: "First" },
{ value: "Second", label: "Second" },
{ value: "Third", label: "Third" },
];
function TestApp() {
const [option, setOption] = useState(null);
return (
<div>
<label htmlFor="option-select">Select Option</label>
<Select
value={option}
options={options}
onChange={option => setOption(option)}
/>
{option && <div>{option.label}</div>}
</div>
);
}
export default TestApp;
I'm not even sure what I should query for. Is it the hidden input?
My team has a test utility in our project that lets us select an item easily after spending too much time trying to figure out how to do this properly. Sharing it here to hopefully help others.
This doesn't rely on any React Select internals or mocking but does require you to have set up a <label> which has a for linking to the React Select input. It uses the label to select a given choice value just like a user would on the real page.
const KEY_DOWN = 40
// Select an item from a React Select dropdown given a label and
// choice label you wish to pick.
export async function selectItem(
container: HTMLElement,
label: string,
choice: string
): Promise<void> {
// Focus and enable the dropdown of options.
fireEvent.focus(getByLabelText(container, label))
fireEvent.keyDown(getByLabelText(container, label), {
keyCode: KEY_DOWN,
})
// Wait for the dropdown of options to be drawn.
await findByText(container, choice)
// Select the item we care about.
fireEvent.click(getByText(container, choice))
// Wait for your choice to be set as the input value.
await findByDisplayValue(container, choice)
}
It can be used like this:
it('selects an item', async () => {
const { container } = render(<MyComponent/>)
await selectItem(container, 'My label', 'value')
})
You can try the following to get it working:
Fire focus event on the ReactSelect component .react-select input element.
Fire a mouseDown event on the .react-select__control element
Fire a click on the option element that you want to select
You can add a className and classNamePrefix props with the value of "react-select" in order to specifically select the component you are trying to test.
PS: In case you are still stuck I'd encourage you to take a look at this conversation from where the above answer is borrowed - https://spectrum.chat/react-testing-library/general/testing-react-select~5857bb70-b3b9-41a7-9991-83f782377581

How to Manipulate Dropdown placeholder, onFocus?

Am new to ReactJS. I need to make the "placeholder" which is set to "State" initially to Empty/Null when onClicked or onFocus and then when it's not focused on, it goes back to "State" again. Can someone help me with this, am very new to react so any help will be appreciated.
import React from "react";
import { render } from "react-dom";
import { Container, Button, Modal, Dropdown } from "semantic-ui-react";
const stateOptions = [
{ key: "AL", value: "AL", text: "Alabama" },
{ key: "NY", value: "NY", text: "New York" }
];
const App = () => (
<Dropdown
placeholder="State"
fluid
multiple
search
selection
options={stateOptions}
/>
);
render(<App />, document.getElementById("root"));
From React's perspective, placeholder is a state that needs to be changed according to user's actions (onClick, onBlur)
So create a state to hold placeholder value that need to change.
There are two ways (since v16.8.0 with the introduction of React Hooks).
Using Class Component
class DropDown extends React.Component {
defaultPlaceholderState = "State";
state = { placeholder: this.defaultPlaceholderState };
clearPlaceholder = () => this.setState({ placeholder: "" });
resetPlaceholder = () =>
this.setState({ placeholder: this.defaultPlaceholderState });
render() {
return (
<Dropdown
onClick={this.clearPlaceholder}
onFocus={this.clearPlaceholder}
onBlur={this.resetPlaceholder}
placeholder={this.state.placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
}
}
In the code above, placeholder declared as a state with default value set to this.defaultPlaceholderState.
When a user clicks on the dropdown, onClick clears the placeholder value by setting it to an empty string. Same for onFocus when the Dropdown is on focus.
When a user clicks outside (onBlur), resetPlaceHolder sets the placeholder value to the default this.defaultPlaceholderState.
Using Function Component with useState hook
React v16.8.0 introduces Hooks, which enables Function Components (not a Functional Component, as it refers to Functional Programming) to hold states.
You can use React.useState hook to hold placeholder value.
const DropDownUsingHook = () => {
const defaultPlaceholderState = "State";
const [placeholder, setPlaceholder] = React.useState(defaultPlaceholderState);
const clearPlaceholder = () => setPlaceholder("");
const resetPlaceholder = () => setPlaceholder(defaultPlaceholderState);
return (
<Dropdown
onClick={clearPlaceholder}
onFocus={clearPlaceholder}
onBlur={resetPlaceholder}
placeholder={placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
};
âš  Note: Unlike the Class version, clearPlaceholder, resetPlaceholder methods and placeholder state don't use this. prefix.
The implementation is similar but you use useState hook to declare the state and the setter (setPlaceholder).
Refer to the Hooks documentation, Using State Hook for more info.
You can play around with the working code on CodeSandbox.

Resources