React Hook Form with Children in React Native - react-native

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.

Related

Not able to get value from async storage It shows 'name' value to 'loading', However same code work in class components name 'value' change

After using the setItem method value of 'name' variable should change to value entered in the textInput but console.log(item) shows 'loading' After clicking submit button at first time. Which is initial value of the 'name' variable in useState.
Once I entered the submit button again the name variable value change to value entered in textInput.
Expected: When I click submit button at first time, item.name variable should change to textInput value
Using same code in class component work fine.
export default function App() {
const arr =[]
const [text, setText] = useState()
const[item,setItem] = useState([
{name: 'loading'}
])
const storedata= async ()=> {
// setItem({name: text})
arr.push({name: text})
try {
await AsyncStorage.setItem('mykey',JSON.stringify(arr));
console.log(await AsyncStorage.getItem('mykey'))
}
catch (error) {
console.log(error)
}
try{
setItem({item: JSON.parse(await AsyncStorage.getItem('mykey'))})
console.log('printing item',item)
}
catch(e){
console.log(e)
}
}
return (
<View style={styles.container}>
<TextInput onChangeText={(text) => setText(text)}
placeholder='type'
/>
<Button title='change'
onPress={storedata}
/>
</View>
);
}
Output:
Button click first time: printing item [{"name":"loading"}]
Button click second time: printing item {"item":[{"name":"hi"}]}
class App extends React.Component{
arr=[]
id=0
state= {
text: '',
item:[
{name: 'loading'}
]
}
storedata = async () => {
this.arr.push({ name: this.state.text})
await AsyncStorage.setItem('mylist',JSON.stringify(this.arr));
this.setState({
item: JSON.parse(await AsyncStorage.getItem('mylist'))
})
console.log(item)
}
The setState function is an asynchronous function. setState calls are batched to improve the performance of the application. So the results are not reflected immediately. That's why, you are getting the previous value for the item variable.
You can find more info here.

Adjust a field in a SimpleForm's value

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>
);
};

Force external component to re-render with new props

I am trying to set up e-mail sign-up. I have a screen with a TextInput, which I want to reuse. I have an EmailConnector from which I navigate to the TextInputScreen. This TextInputScreen containt a TextInputComponent. Here, the user enters his email. If the email is invalid, I throw an error and try to update the error message in my TextInputScreen.
The problem that I am facing right now is that the error message from my TextInputComponent is not updated when an error is thrown.
The flow is this:
The user taps on "Sign-up with email" in a separate screen -> openEmailScreen is called
The user inputs an email and taps on the keyboard "Done" button -> inputReceived is called
If the email is invalid -> an error is thrown in inputReceived and the error message is displayed in TextInputViewComponent
Refreshing the errror message in step #3 is NOT working at the moment and I can't figure out how to get it working.
This is my EmailConnector:
export default class EmailConnector {
static keyboardTypes = {
email: 'email-address',
default: 'default',
};
static openEmailScreen = async navigation => {
navigation.navigate('TextInputScreen', {
placeholder: strings.onboarding.email_flow.email_placeholder,
keyboardType: this.keyboardTypes.email,
onKeyboardPressed: () => this.inputReceived(),
errorMessage: 'placeholder message',
})
}
//method called when the "Done" button from the keyboard is pressed
static inputReceived = () => {
try {
const email = new SignUpUserBuilder().setEmail('testexample.com').build();//used to validate the email
}
catch(error) {
console.log(error);
****//HERE I need to figure out a way to change props.errorMessage and force TextInputViewComponent to rerender****
<TextInputViewComponent errorMessage = 'Invalid email'/>;
const viewComponent = new TextInputViewComponent();
viewComponent.forceUpdate();
}
}
}
This is my TextInputScreen:
class TextInputScreen extends Component {
render() {
return (
<View style={styles.rootView}>
<TextInputViewComponent
placeholder={this.props.route.params.placeholder}
keyboardType={this.props.route.params.keyboardType}
onKeyboardPressed={this.props.route.params.onKeyboardPressed}
errorMessage={this.props.route.params.errorMessage}
/>
</View>
)
}
}
export default TextInputScreen;
And this is my TextInputViewComponent:
class TextInputViewComponent extends Component {
constructor(props) {
super(props);
this.state = {
shouldRefreshComponent: false
}
}
refreshComponent = () => {
this.setState({
shouldRefreshComponent: true
})
}
render() {
return (
<View>
<TextInput
placeholder={this.props.placeholder}
placeholderTextColor={colors.placeholder}
keyboardType={this.props.keyboardType}
style={styles.textInput}
onSubmitEditing= {() => this.props.onKeyboardPressed()}
/>
<Text
style={{fontSize: 18, color: colors.white}}
ref={Text => { this._textError = Text}}>
{this.props.errorMessage}
</Text>
</View>
)
}
}
export default TextInputViewComponent;
From the inputReceived method, I have tried calling forceUpdate and setState for the TextInputViewComponent, but in both cases I get the message: "Can't call forceUpdate/setState on a component that is not yet mounted"
Any help will be greatly appreciated!
In general terms if I want a parent component to mutate it's data or update when a child component changes I pass the child a function to its props.
For example
class Parent {
state = {
value: 1
}
updateValue() {
this.setState({value: 2})
}
render() (
<Child
updateValue={this.updateValue}
/>
)
That way I can call the function inside the child to update the parent's state.
class Child {
updateParent() {
this.props.updateValue()
}
}

Picker onValueChange() called twice

I want to support localization using react-18next.
This component shows a Picker, sets a LocalStorage key (the selected language) and change the app language.
I noticed the onValueChange method is called twice. The first call (using a proper selection tap action on a Picker item) have the correct parameter (the language I have chosen), the second call have the value of the first Picker item, whenever the values is (!).
Example: if I select the English Picker item, I see the Picker switch to English (first _changeLanguage() call), and then again to "Device" (second _changeLanguage() call). I'm sure it's an async ops problem, not sure where..
#translate(['settings', 'common'], { wait: true })
export default class Settings extends Component {
state = {};
constructor(props) {
super(props);
}
componentWillMount() {
this.getLang();
}
async _changeLanguage(ln) {
const { t, i18n, navigation } = this.props;
console.warn("_changeLanguage: ",ln)
await this.promisedSetState({lang:ln})
if(ln=="device") {
console.warn("removing lang setting")
await AsyncStorage.removeItem('#App:lang');
} else {
console.warn("lang setting: ", ln)
await AsyncStorage.setItem('#App:lang', ln);
i18n.changeLanguage(ln)
}
};
//get Language from AsyncStorage, if it has been previously set
async getLang() {
const value = await AsyncStorage.getItem('#App:lang');
console.warn("getLangfrom asyncstorage:", value)
if(value) await this.promisedSetState ({lang:value})
}
promisedSetState = (newState) => {
return new Promise((resolve) => {
this.setState(newState, () => {
resolve()
});
});
};
render() {
const { t, i18n, navigation } = this.props;
const { navigate } = navigation;
return (<View>
<Picker
selectedValue={this.state.lang}
onValueChange={(itemValue, itemIndex) =>this._changeLanguage(itemValue) }>
<Picker.Item color="#666" label="detected from device" value="device" />
<Picker.Item label="English" value="en" />
<Picker.Item label="Deutsch" value="it" />
</Picker>
</View>);
}
}
The code is based on the react-i18next Expo example
https://github.com/i18next/react-i18next/tree/master/example/v9.x.x/reactnative-expo
I'm not sure if this belongs here but if anyone is encountering the problem of onValueChange firing twice and is using react-native-picker-select instead of the default Picker then this solution may work for you.
I ended up using the itemKey property instead of value and setting the key of each item to be the same as its value. Note the example item would end up shaped like so: { key: 'value1', value: 'value1', label: 'Your Label' } where the key and value are identical.
Original Code:
<RNPickerSelect
style={styles.picker}
value={value}
onValueChange={this.onValueChange}
placeholder={placeholder}
items={options}
/>
Fixed Code:
<RNPickerSelect
style={styles.picker}
itemKey={value}
onValueChange={this.onValueChange}
placeholder={placeholder}
items={options}
/>
I got my inspiration from this: GitHub issue.
Searching around the react-native Picker seems bugged.
This quickfix solved my problem https://github.com/facebook/react-native/issues/13351#issuecomment-450281257

react-native reseting TextInput after ClearTextOnFocus

I am building a form in React-Native and have set ClearTextOnFocus to true as it is easier to handle dynamic formating for editing.
I am trying to add a reset function by setting all local state to the redux store, but if the user has not typed anything in a selected TextInput, the local state has not changed, and react native does not re-render the TextInput; leaving it blank.
Anyone have any thoughts on how I can unclear the TextInput or force React to re-render. Code is a work in progress, but here are the relevant bits.
Thanks
class GoalScreen extends Component {
componentWillMount = () => this.setPropsToState();
onReset = () => {
this.setPropsToState();
}
onChange = text => this.setState({ [text.field]: text.input });
setPropsToState = () => {
const { name } = this.props.goal;
this.setState({ name });
};
render() {
const { name } = this.state;
return (
<View style={styles.screenContainer}>
<Text style={styles.text}> Name </Text>
<TextInput
placeholder="a brand new bag"
keyboardType="default"
autoCorrect={false}
style={styles.inputField}
clearTextOnFocus
onChangeText={text => this.onChange({ input: text, field: 'rate' })}
value={name}
/>
</View>
}
}
So, I'm not using Redux, and my use case might be a bit different than yours, but I thought my solution might still be relevant here, if only to confirm that (after hours of wrangling with this) it appears that passing true to the clearTextOnFocus prop prevents further updates to a TextInput component.
I tried every conceivable workaround (like setNativeProps(), forceUpdate()) but nothing worked, so I ended up having to basically write my own logic for clearing and resetting the input text.
This component should 1) clear input text on focus and then 2) reset it to its previous value if the user hasn't pressed a key:
class ResettableInput extends Component {
state = {
Current: this.props.value,
Previous: ""
};
KeyPressed = false;
//cache current input value for later revert if necessary, and clear input
onFocus = () => {
this.setState({ Previous: this.state.Current, Current: "" });
};
//record whether key was pressed so input value can be reverted if necessary
onKeyPress = () => {
this.KeyPressed = true;
};
onChangeText = text => {
this.setState({ Current: text });
};
//if no key was pressed, revert input to previous value
onBlur = () => {
if (!this.KeyPressed) {
this.setState({ Current: this.state.Previous, Previous: "" });
}
};
render = () => {
return (
<TextInput
onChangeText={this.onChangeText}
value={this.state.Current}
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyPress={this.onKeyPress}
/>
);
};
}