I am trying to utilize dynamic fields with react-hook-form in react native. I initially tried to manually register everything, but that doesn't work. Then I tried to use the useFieldArray hook, but all the newly created fields don't register correctly. Here is my latest approach:
I have a custom component to mimic the web interface for a react native TextInput and forward the ref.
const Input = forwardRef((props, ref) => {
const {
onAdd,
...inputProps
} = props
return (
<View>
<TextInput
ref={ref}
{...inputProps} />
<Button onPress=(() => onAdd()} />
</View>
)
}
I then use this component according to how the useFieldArray docs show in my form except that I have a custom change handler. I also set the ref explicitly and attempt to register the individual new field.
const Inputs = useRef([])
const { control, register, setValue, handleSubmit, errors } = useForm({
defaultValues: {
test: defaultVals // this is an array of objects {title: '', id: ''}
}
})
{
fields.map((field, idx, arr) => (
<Input
key={field.id}
name={`test[${idx}]`}
defaultValue={field.name}
onChangeText={text => handleInput(text, idx)}
onAdd={() => append({title: '', id: ''})
ref={
e => register({ name: `test[${idx}]`
Inputs.curent[idx] = e
})
}
When I click the button for the input it renders a new input as would be expected. But when I submit the data, I only get the defaultVals values and not the data from the new input, though I do have an object that represents that input in the test array. It seems something is off with the registering of the inputs, but I can't put my finger on it.
How do I properly set up useFieldArray or utilize other ways to have dynamic fields in react native with react-hook-form?
you can try handle this problem like me, use each element with 1 input and field like this
<Controller
control={control}
name="username"
rules={{
required: {value: true, message: '* Please enter your username *'},
minLength: {value: 5, message: '* Account should be 5-20 characters in length! *'},
maxLength: {value: 20, message: '* Account should be 5-20 characters in length! *'},
pattern: {value: new RegExp(/^[a-zA-Z0-9_-]*$/), message: '* Invalid character *'},
}}
render={({onChange, value}) => (
<InputRegular
placeholder="username"
icon={{
default: images.iconUser,
focus: images.iconUser2,
}}
onChangeText={(val: string) => {
onChange(val);
setValue('username', val);
}}
value={value}
/>
)}
/>
handleSubmit with
const {register, errors, setValue, getValues, control, handleSubmit} = useForm<FormData>({
defaultValues: {
username: null,
email: null,
password: null,
rePassword: null,
},
mode: 'all',
});
useEffect(() => {
register('username');
register('email');
register('password');
register('rePassword');
}, [register]);
const onSubmit = async (data: any) => {
console.log('====================================');
console.log('data', data);
console.log('====================================');
if (getValues('username')) {
dispatch(
actionGetRegister({
data: {
username: getValues('username'),
email: getValues('email'),
password: getValues('password'),
secondaryPassword: getValues('secondaryPassword'),
},
}),
);
}
};
>
you can see my post for detail i use react-hook-form for react native
https://medium.com/#hoanghuychh/how-to-use-react-hook-form-with-react-native-integrate-validate-and-more-via-typescript-signup-244b7cce895b
Related
What is causing this error. I am trying to update the username field in the parent state with a reusable function. and getting the following error. It seems as though I am able to update the username value once. But following that, it replaces the state entirely then falls over and throws an error.
const ProfileEdit: React.FC<Props> = ({ setIsModalOpen }) => {
const user = useSelector(state => state.userData.user);
// Create user details
const [userDetails, setUserDetails] = useState({
username: user.username,
firstName: user.userName,
lastName: user.lastName,
gender: user.gender,
dateOfBirth: user.dateOfBirth,
city: user.city
});
// Change specific detail when input changes. This comes from the state value in the child component
const changeVal = (formData, entry, val) => {
setUserDetails((formData[entry] = val));
console.log(userDetails);
};
return (
<Modal animationType="slide">
<Container>
<EditProfileInput
inputTitle="Username"
changeVal={changeVal}
formData={userDetails}
entry="username"
/>
);
};
// Child Component
const EditProfileInput: React.FC<Props> = ({
inputTitle,
changeVal,
formData,
entry
}) => {
return (
<Container>
<InputHeader>{inputTitle}</InputHeader>
<Input>
<InputText
name={inputTitle}
onChangeText={text => changeVal(formData, entry, text)}
/>
</Input>
</Container>
);
};
It seems that your function changeVal breaks the rule of immutability of Redux (see : https://redux.js.org/style-guide/ and https://daveceddia.com/react-redux-immutability-guide/).
I think your issue is related to : Cannot assign to read only property, React native error : Attempted to assign to readonly property
I need to make a large number of inputs and transfer this data to the server, I decided that the best solution would be to write all the options of these inputs into an array of objects, but I ran into the fact that I can’t get all my inputs to work. help me please
const test = [
{id: 1,state: 'city'},
{id: 2,state: 'language'},
{id: 3,state: 'brand'},
{id: 4,state: 'shop'},
]
const Auth = () => {
const [description, setDescription] = useState({city: "", language: "", brand: "", shop: ""});
const handleClick = async (event: any) => {
await store.update(description.city, description.brand);
};
const update = async (e: ChangeEvent<HTMLInputElement>) => {
setDescription({
...description,
city: e.target.value
});
};
return (
<>
{test.map(({ state, id}) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description.city}
onChange={update}
/>
))}
<Button
className={styles.saveButton}
variant="contained"
color="inherit"
id="login"
onClick={handleClick}
>
Save
</Button>
</>
)
}
You send to TextField description.city for every input. The correct props are like so:
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={update}
/>
See the change in the value prop.
Also, you only update city in the update function. You have to make it so that the update function adapts to what values you pass to it. If you pass the city then it should update the city, if the language then the language and so on.
Overall this is not a good way to implement inputs. I just suggest you do them one by one and send to each TextField its corresponding value and a separate setState for each one.
But just for the sake of the example. The way you can do it is by passing the state value to the Update function.
So your function will look like this:
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription((description) => {
...description,
[state]: e.target.value
});
};
Now you just need to make sure that in the TextField component when you call onChange, you pass to it the event e and state which you have received from props.
Note: If you want to use the value of a state variable in the setState itself, pass to it a callback function like I did in the setDescription
if you want to make it dynamic you would have to send the variable to save to your update method and retrieve your value with description[state]
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e)=>update(e, state)}
/>
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription({
...description,
[state]: e.target.value
});
};
I think first and foremost you need your configuration data to try and closely match the elements you're building. So instead of { id, state } use { id, type, name }.
(This may not have a huge effect on your example because you're specifically using a TextField component, but if you were using native HTML controls you could add in different input types like number, email, date etc, and your JSX could deal with it easily.)
Second, as I mentioned in the comments, you don't need for those functions to be async - for example, there's no "after" code in handleClick so there's no need to await anything.
So here's a working example based on your code. Note: I've stripped out the Typescript (because the snippet won't understand the syntax), and the references to the UI components you're using (because I don't know where they're from).
const { useState } = React;
// So, lets pass in out inputs config
function Example({ inputs }) {
// I've called the state "form" here as it's a little
// more meaningful
const [form, setForm] = useState({});
// `handleSave` is no longer `async`, and for the
// purposes of this example just logs the updated
// form state
function handleSave() {
console.log(form);
// store.update(form);
}
// Also no longer `async` `handleChange` destructures
// the name and value from the changed input, and updates
// the form state - a key wrapped with `[]` is a dynamic key
// which means you can use the value of `name` as the key value
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
// In our JSX we destructure out the id, name, and
// type properties from each input object in the config
// and apply them to the various input element properties.
return (
<div>
{inputs.map(input => {
const { id, name, type } = input;
return (
<input
key={id}
type={type}
name={name}
placeholder={name}
value={form[name]}
onChange={handleChange}
/>
);
})}
<button onClick={handleSave}>Save</button>
</div>
);
}
// Our updated config data
const inputs = [
{ id: 1, type: 'text', name: 'city' },
{ id: 2, type: 'text', name: 'language' },
{ id: 3, type: 'text', name: 'brand' },
{ id: 4, type: 'text', name: 'shop' }
];
ReactDOM.render(
<Example inputs={inputs} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Pass both the key that you want to update and the value to the update function:
const update = (key: string, value: string) => {
setDescription({
...description,
[key]: value,
});
};
{test.map(({ state, id }) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
update(state, e.target.value)
}
/>
))}
Issue summary
I get different errors at different times. When I select a suggested option, I get the following error and warning:
Material-UI: The getOptionLabel method of Autocomplete returned undefined instead of a string for 0
Material-UI: The value provided to Autocomplete is invalid. None of the options match with 0
Plus, the option doesn't get selected and the input becomes undefined. However, when trying to choose a value a second time, it gets selected (but still shows the errors).
When I clear the input I get this error:
A component is changing the controlled value state of Autocomplete to be uncontrolled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled Autocomplete element for the lifetime of the component.
The nature of the state is determined during the first render, it's considered controlled if the value is not `undefined`.
Code for the autocomplete component
const AutocompleteUnit = ({control, label, name, ...rest}) => {
return (
<>
<Controller
onChange={([,data]) => data}
name={name}
as={
<Autocomplete
{...rest}
autoHighlight
style={{marginTop: "25px"}}
getOptionLabel={option => option.label}
renderInput={params => (
<TextField
{...params}
label={label}
variant="outlined"
/>
)}
/>
}
control={control}
defaultValue={rest.options[0]}
/>
</>
}
Options
const districtOptions = [
{ value: "ciutatvella", label: "Ciutat Vella" },
{ value: "gracia", label: "Gràcia" },
{ value: "eixample", label: "L'Eixample" },
{ value: "sarria", label: "Sarrià" }
];
Any idea on what's wrong?
just in case some stumbles upon this: you have to use defaultValue of Autocomplete instead of the Controller
Hey just use this package below:
https://www.npmjs.com/package/mui-react-hook-form-plus
import { HookAutoComplete, useHookForm } from 'mui-react-hook-form-plus ';
const Component = () => {
const {registerState, handleSubmit} = useHookForm({
defaultValues: { movie: '' },
});
const onSubmit = (data: typeof defaultValues) => {
// will run if it is valid
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<HookAutoComplete
{...registerState('movie')}
autocompleteProps={{
options: ['Fight Club', 'Top Gun', 'Titanic']
}}
textFieldProps={{
label: 'Movie',
placeholder: 'search...',
}}
/>
<button type='submit'>Submit</button>
</form>
)
}
See Demo
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
If the mutation is successful, I am trying to setAdded to true in the .then of ```submitForm()``. If this is true, I want to show a message from the SuccessfulMessage(). However, when I log the value of added, I keep seeing false.
Since addedis not changed to true. I am unable to see any message when mutation is successful. Why doesn't it change?
export default function AddUserPage() {
const [state, setState] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
phoneNumber:'',
loggedIn: false,
});
const [added, setAdded] = useState(false);
function SuccessMessage(){
if (added)
{
console.log('User Added');
return (
<Typography>
User Added
</Typography>)
}
}
useEffect(() => {
if(added){
SuccessMessage();
}
},[] );
function submitForm(AddUserMutation: any) {
const { firstName, lastName, email, password, phoneNumber } = state;
if (firstName && lastName && email && password && phoneNumber) {
AddUserMutation({
variables: {
firstName: firstName,
lastName: lastName,
email: email,
password: password,
phoneNumber: phoneNumber,
},
}).then(({ data }: any) => {
setAdded(true);
console.log('doing', added);
console.log('ID: ', data.createUser.id);
console.log('doing', added);
})
.catch(console.log)
}
}
return (
<Mutation mutation={AddUserMutation}>
{(AddUserMutation: any) => (
<div>
<PermanentDrawerLeft></PermanentDrawerLeft>
<Formik
initialValues={{ firstName: '', lastName: '', email: '', password: '', phoneNumber: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}
>
{props => {
const {
values: { firstName, lastName, email, password, phoneNumber },
errors,
touched,
handleChange,
isValid,
setFieldTouched
} = props;
const change = (name: string, e: any) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
setState( prevState => ({ ...prevState, [name]: e.target.value }));
};
return (
<div className='main-content'>
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
submitForm(AddUserMutation);SuccessMessage()}}>
<div>
<TextField
variant="outlined"
margin="normal"
id="firstName"
name="firstName"
helperText={touched.firstName ? errors.firstName : ""}
error={touched.firstName && Boolean(errors.firstName)}
label="First Name"
value={firstName}
onChange={change.bind(null, "firstName")}
/>
<TextField
variant="outlined"
margin="normal"
id="email"
name="email"
helperText={touched.email ? errors.email : ""}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, "email")}
/>
<Button
type="submit"
disabled={!isValid || !email || !password}
>
Add User</Button>
</div>
</form>
</div>
)
}}
</Formik>
</div>
)
}
</Mutation>
);
}
Your console.log() directly after calling setAdded will not show true as state updates are async an will only be visible on the next render. Also your SuccessMessage will never be triggered because you did not provide any dependencies for your useEffect(). This means it will only ever be called after mount
You need to add added to the dependency list:
useEffect(() => {
if(added){
SuccessMessage();
}
},[added]);
But actually I don't see any reason to trigger it in a useEffect anyways. Why not just call it in the mutation handler?
Also if you are already using hooks you can use useMutation.
Also you can't return JSX from a handler. It will not do anything. How should react even know where to display your <Typography>User Added</Typography>? You must render everything in the component itself depending on the state.
Sorry, but ...
... it looks like a great example of abusing react ... result of skipping basic tutorials, docs and mixing random oudated examples.
async nature of useState (and setState) - you can't expect updated value;
using useEffect param without knowledge how it affects behaviour;
'useEffect' not required at all;
returning components from event handler instead of [data driven] conditional rendering;
if useMutation used you don't need to pass mutation as param to event handler;
you're using Formik (with fields validation) then checking params (fields) in handler is simply unnecessary;
you can simply use variables: { ...state } if variables props names are matching;
Formik manages values, you don't need to duplicate this using local state - any reason?;
Formik has hooks, too ;) use useFormik();
... event handlers, binding...