I try to validate a form in React-Native (0.69.6) with react-hook-form (7.29.0) and yup (0.32.11 with #hookform/resolvers 2.9.10).
I have this minimal reproducible example that always show isValid as false and errors as an empty object {}:
const schema = yup
.object()
.shape({
name: yup.string().required(),
})
.required();
const {
control,
formState: { errors, isValid },
} = useForm({
resolver: yupResolver(schema),
});
return (
<>
<Text>{JSON.stringify(errors)}</Text>
<Text>{JSON.stringify(isValid)}</Text>
<Controller
control={control}
render={({ field: { onChange, value } }) => (
<Input value={value} onChangeText={onChange} />
)}
name="name"
/>
</>
);
Why this simple validation does not work, what am I missing?
errors and isValid are populated when submitting the form.
In order to see them updated in live as the data changes, just add:
mode: "onChange",
as follow:
const {
control,
handleSubmit,
formState: { errors, isValid },
} = useForm({
mode: "onChange",
resolver: yupResolver(schema),
});
Related
I have a form with ~15 fields where each section is a unique child component. I want to know how to pass data between the parent form and child components(using control because this is react native)
Right now, I see the proper value for testResult in onSubmit logs but data is undefined for some reason. This means my parent form is somehow not picking up the value in the child.
Parent Form:
const Stepper = () => {
const form = useForm({ defaultValues: {
testResult: "",
}
});
const { control, handleSubmit, formState: { errors }, } = form;
const testResult = useWatch({ control, name: "testResult" });
const onSubmit = (data) => {
console.log("watched testResult value: ", testResult);
console.log("form submission data: ", data);
};
return (
<WaterStep form={form} />
<Button onSubmit={handleSubmit(onSubmit())} />
)
}
Child component:
const WaterStep = ({ form }) => {
const { control, formState: { errors }, } = form;
return (
<Controller
name="testResult"
control={control}
rules={{
maxLength: 3,
required: true,
}}
render={({ field: onBlue, onChange, value }) => (
<TextInput
keyboardType="number-pad"
maxLength={3}
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
)}
/>
)}
Here I'm trying the first approach this answer suggests, but I've also tried the second with useFormContext() in child https://stackoverflow.com/a/70603480/8561357
Additionally, must we use control in React Native? The examples that use register appear simpler, but the official docs are limited for React Native and only show use of control
Update: From Abe's answer, you can see that I'm getting undefined because I'm calling onSubmit callback in my submit button. I mistakenly did this because I wasn't seeing any data getting logged when passing onSubmit properly like this handleSubmit(onSubmit). I still think my issue is that my child component's data isn't being tracked properly by the form in parent
The problem is most likely in this line:
<Button onSubmit={handleSubmit(onSubmit())} />
Since you're executing the onSubmit callback, you're not allowing react-hook-forms to pass in the data from the form. Try replacing it with the following
<Button onSubmit={handleSubmit(onSubmit)} />
For anyone still looking for guidance on using react-hook-form with child components, here's what I found out to work well:
Parent Component:
const Stepper = (props) => {
const { ...methods } = useForm({
defaultValues: {
testResult: "",
},
});
const onSubmit = (data) => {
console.log("form submission data: ", data);
};
const onError = (errors, e) => {
return console.log("form submission errors: ", errors);
};
return (
<FormProvider {...methods}>
<WaterStep
name="testResult"
rules={{
maxLength: 3,
required: true,
}}
/>
<Button onSubmit={handleSubmit(onSubmit)} />
)
}
Child:
import { useFormContext, useController } from "react-hook-form";
const WaterStep = (props) => {
const formContext = useFormContext();
const { formState } = formContext;
const { name, label, rules, defaultValue, ...inputProps } = props;
const { field } = useController({ name, rules, defaultValue });
if (!formContext || !name) {
const msg = !formContext
? "Test Input must be wrapped by the FormProvider"
: "Name must be defined";
console.error(msg);
return null;
}
return (
<View>
<Text>
Test Input
{formState.errors.testResult && <Text color="#F01313">*</Text>}
</Text>
<TextInput
style={{
...(formState.errors.phTestResult && {
borderColor: "#f009",
}),
}}
placeholder="Test Value"
keyboardType="number-pad"
maxLength={3}
onBlur={field.onBlur}
onChangeText={field.onChange}
value={field.value}
/>
</View>
);
};
Here's what we're doing:
Define useForm() in parent and de-structure all its methods
Wrap child in <FormProvider> component and pass useForm's methods to this provider
Make sure to define name and rules as props for your child component so it can pass these to useController()
In your child component, define useFormContext() and de-structure your props
Get access to the field methods like onChange, onBlur, value by creating a controller. Pass those de-structured props to useController()
You can go to an arbitrary level of nested child, just wrap parents in a <FormProvider> component and pass formContext as prop.
In Ancestor:
...
const { ...methods } = useForm({
defaultValues: {
testResult: "",
},
});
const onSubmit = (data) => {
console.log("form submission data: ", data);
};
...
<FormProvider {...methods}>
<ChildOne/>
</FormProvider>
In Parent:
const ChecklistSection = (props) => {
const formContext = useFormContext();
const { formState } = formContext;
return (
<FormProvider {...formContext}>
<WaterStep
name="testResult"
rules={{
maxLength: 3,
required: true,
}}
/>
</FormProvider>
)}
Thanks to https://echobind.com/post/react-hook-form-for-react-native (one of the only resources I found on using react-hook-form with nested components in react-native)
....
And a further evaluation of my blank submission data problem, if you missed it:
As Abe pointed out, the reason I didn't see data or errors being logged upon form submission was because onSubmit was not being called. This was because my custom submission button, which I didn't include in my original question for simplicity's sake, had a broken callback for a completion gesture. I thought I solved onSubmit not being called by passing it as a call onSubmit(), but I was going down the wrong track.
Hi I created form builder with formik for react native. I'm using jest for testing, but when I test onSubmit is not calling can anyone please explain how to test.
function FormBuilder({data, onSubmit, initialValues, navigation}) {
const formik = useFormik({
enableReinitialize: true,
initialValues: initialValues,
onSubmit: data => {
onSubmit(data);
},
});
return (
<View>
{data.map((item,index) => {
switch (item.type) {
case 'text':
return (
<TextBox
key={index}
onChangeText={formik.handleChange(item.name)}
onBlur={formik.handleBlur(item.name)}
value={formik.values[item.name]}
label={item.name}
touched={formik.touched}
errors={formik.errors}
required={
!!item.validators &&
item.validators.find(valid => valid.type === 'required')
}
{...item}
/>
);
case 'button':
return (
<CustomButton key={index} testID={item.testID} title=
{item.name} onPress={formik.handleSubmit} />
);
}
})}
</View>
)
}
and I call this component like this in my screen. Can anyone explain how can we write test Integration test for this
<FormBuilder
initialValues={initialValues}
data={[
{
type: 'text',
name: 'whatsAppNumber',
testID: 'input',
},
{type: 'button', name: 'login', testID: 'button'},
]}
onSubmit={submitData}
/>
you can test your code by following method, please use msw for creating dummy server for api call.
import {fireEvent, render,} from '#testing-library/react-native';
describe('Login Screen', () => {
it('should validate form', async () => {
const {getByTestId} = render(<Login />);
const numberField = getByTestId('input');
const button = getByTestId('button');
expect(numberField).toBeDefined();
expect(button).toBeDefined();
fireEvent.changeText(numberField, '9876543215');
fireEvent.press(button)
await waitFor(() =>{
//expect your function to be called
})
});
});
I have a React Admin Edit form with a field which by default doesn't exist in the record. I could manually type it in, save it and it would work. However I wish to retrieve the value of this field programmatically when a user clicks a button then adjust it.
I use a useMutation hook to access a custom API which performs an expensive operation and returns a result for the field. I only want to perform this operation when the user clicks a button.
So inside a Edit form I have this field called key I want to apply the data from this useMutation hook to it.
export const PostEdit = (props) => (
<Edit title={<PostTitle />} {...props}>
<SimpleForm>
<TextInput disabled source="id" />
<RetrieveKey />
<TextInput source="key" />
</SimpleForm>
</Edit>
);
The RetrieveKey button is like this
const RetrieveKey = ({ record }) => {
const [retrieve, { loading }] = useMutation(
{
type: "retrieveKey",
resource: "posts",
payload: { id: record.id }
},
{
onSuccess: ({ data }) => {
if (data) {
// Set the key field here.
} else {
console.log("No Key");
}
},
onFailure: (error) => console.log("Error");
}
);
return (
<Button
onClick={retrieve}
disabled={loading}
label={"Retrieve Key"}
></Button>
);
};
I have looked through the various Form Hooks but can't find anything documented which would let me accomplish this.
Note, I don't want to instantly call the UpdateMethod or this would be trivial and I could use the DataProvider useUpdate to call it. Instead I want to prefill in the form for the user from the responsed key.
A codesandbox is provided here: https://codesandbox.io/s/nifty-firefly-k13g7?file=/src/App.js
I needed to access the underlying React-Final-Form API in order to edit the form.
The React-Admin Docs briefly touch on it here: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
So in this case I needed to use the useForm hook which then allowed me to update the form value.
const RetrieveKey = ({ record }) => {
const form = useForm();
const [retrieve, { loading }] = useMutation(
{
type: "retrieveKey",
resource: "posts",
payload: { id: record.id }
},
{
onSuccess: ({ data }) => {
console.log(form);
if (data) {
form.change("key", data);
} else {
console.log("No Key found");
}
},
onFailure: (error) => console.log("Error");
}
);
return (
<Button
onClick={retrieve}
disabled={loading}
label={"Retrieve Key"}
></Button>
);
};
Receive below errors, when using Datagrid component with custom queries. Below code works with react-admin ver 3.3.1, whereas it doesn't work with ver 3.8.1
TypeError: Cannot read property 'includes' of undefined
Browser's console info: List components must be used inside a <ListContext.Provider>. Relying on props rather than context to get List data and callbacks is deprecated and won't be supported in the next major version of react-admin.
Refer: https://marmelab.com/react-admin/List.html
#Tip: You can use the Datagrid component with custom queries:
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading } from 'react-admin'
const CustomList = () => {
const [page, setPage] = useState(1);
const perPage = 50;
const { data, total, loading, error } = useQuery({
type: 'GET_LIST',
resource: 'posts',
payload: {
pagination: { page, perPage },
sort: { field: 'id', order: 'ASC' },
filter: {},
}
});
if (loading) {
return <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
return (
<>
<Datagrid
data={keyBy(data, 'id')}
ids={data.map(({ id }) => id)}
currentSort={{ field: 'id', order: 'ASC' }}
basePath="/posts" // required only if you set use "rowClick"
rowClick="edit"
>
<TextField source="id" />
<TextField source="name" />
</Datagrid>
<Pagination
page={page}
perPage={perPage}
setPage={setPage}
total={total}
/>
</>
)
} ```
Please help!
Since react-admin 3.7, <Datagrid> and <Pagination> read data from a ListContext, instead of expecting the data to be injected by props. See for instance the updated <Datagrid> docs at https://marmelab.com/react-admin/List.html#the-datagrid-component.
Your code will work if you wrap it in a <ListContextProvider>:
import React, { useState } from 'react';
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading, ListContextProvider } from 'react-admin'
export const CustomList = () => {
const [page, setPage] = useState(1);
const perPage = 50;
const { data, total, loading, error } = useQuery({
type: 'GET_LIST',
resource: 'posts',
payload: {
pagination: { page, perPage },
sort: { field: 'id', order: 'ASC' },
filter: {},
}
});
if (loading) {
return <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
return (
<ListContextProvider value={{
data: keyBy(data, 'id'),
ids: data.map(({ id }) => id),
total,
page,
perPage,
setPage,
currentSort: { field: 'id', order: 'ASC' },
basePath: "/posts",
resource: 'posts',
selectedIds: []
}}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
<Pagination />
</ListContextProvider >
)
}
<ReferenceManyField>, as well as other relationship-related components, also implement a ListContext. That means you can use a <Datagrid> of a <Pagination> inside this component.
https://marmelab.com/react-admin/List.html#uselistcontext
Your code should look like this:
import React, { useState } from 'react';
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading, ListContextProvider } from 'react-admin'
export const CustomList = () => {
return (
<ReferenceManyField reference="Your resource for pull the data" target="linked field">
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</ReferenceManyField>
)
}
I'm rendering a ... which resource is "Users", so always is calling this ReferenceManyField on but sometimes "User" don't have "Name", and I want to display some default if "User" don't have name, I did't find any solution for this problem, sorry. Thank you!
You can replace the contents of the 'record' field before passing it to your components:
<ReferenceManyField reference="Users" target="id" >
<SingleFieldList>
<FormDataConsumer>
{ ({ formData, dispatch, record, ...rest }) => {
const defValues = { Name: 'MyName', Param1: 'Value1', Param2: 'Value2' }
const newRecord = { ...defValues, ...record }
const params = { record: newRecord, ...rest }
return (<TextField source="Name" {...params} />)
}
}
</FormDataConsumer>
</SingleFieldList>
</ReferenceManyField>