Force external component to re-render with new props - react-native

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

Related

React Hook Form with Children in 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.

React Native : Conditional render() based on AsyncStorage result

Trying to use a AsyncStorage variable to conditionally render content.
My app uses createBottomTabNavigator from react-navigation. I have a tab called Settings that must conditionally render content based on wether a user is logged in or not (checking AsyncStorage). The following code works on first render but another tab can update AsyncStorage value, returning back to Settings tab it still renders initial content.
Which approach can i use to achieve this, i'm also trying to use shouldComponentUpdate but i'm not sure how it works.
import React, {Component} from 'react';
class Settings extends React.Component{
constructor(props){
super(props);
this.state = {
isLoggedIn:false
};
}
//I want to use this method but not sure how.
shouldComponentUpdate(nextProps, nextState){
// return this.state.isLoggedIn != nextState;
}
componentDidMount(){
console.log("componentWillUpdate..");
this.getLocalStorage();
}
getLocalStorage = async () => {
try {
const value = await AsyncStorage.getItem('username');
if(value !== null) {
this.setState({isLoggedIn:true});
}
} catch(e) {
// error reading value
}
}
render() {
if(this.state.isLoggedIn)
{
return(
<View>
<Text style={styles.title_header}>Logged In</Text>
</View>
);
}
else{
return(
<View>
<Text style={styles.title_header}>Logged Out</Text>
</View>
);
}
}
}
export default Settings;
})
Use NavigationEvents. Add event listeners to your Settings components.
onWillFocus - event listener
onDidFocus - event listener
onWillBlur - event listener
onDidBlur - event listener
for example, the following will get fired when the next screen is focused.
focusSubscription = null;
onWillFocus = payload => {
// get values from storage here
};
componentDidMount = () => {
this.focusSubscription = this.props.navigation.addListener(
'willFocus',
this.onWillFocus
);
};
componentWillUnmount = () => {
this.focusSubscription && this.focusSubscription.remove();
this.focusSubscription = null;
};
The problem comes from react-navigation createBottomTabNavigator. On first visit, the component is mounted and so componentDidMount is called and everything is great.
However, when you switch tab, the component is not unmounted, which means that when you come back to the tab there won't be any new call to componentDidMount.
What you should do is add a listener to the willFocus event to know when the user switches back to the tab.
componentDidMount() {
this.listener = this.props.navigation.addListener('willFocus', () => {
AsyncStorage.getItem('username').then((value) => {
if (value !== null) {
this.setState({ isLoggedIn: true });
}
catch(e) {
// error reading value
}
});
});
}
Don't forget to remove the listener when the component is unmounted:
componentWillUnmount() {
this.listener.remove();
}

Adding checked checkboxes to an array and removing the unchecked ones - react native

What I need to do is - add/remove the name of each checkbox(which are checked/unchecked by the user) in an array and send to the server. I am stuck in the following code. Any help is appreciated. Thankyou
class App extends Component<Props> {
render() {
return (
<View style={{ padding: 15 }}>
{
response.map(
item => {
return (
<CheckBoxItem label={item.name} />
);
}
)
}
</View>
);
}
}
CheckBoxItem.js
class CheckBoxItem extends Component<Props> {
state = {
check: false,
problemTypeArray: [],
}
changeArray = (label) => {
let array = [...this.state.problemTypeArray, label];
let index = array.indexOf(label);
console.log('array', array);//returns array with length 1 all the time
}
render() {
return (
<View>
<CheckBox value={this.state.check} onValueChange={(checkBoolean) => { this.setState({ check: checkBoolean }); this.changeArray(this.props.label); }} />
<MyText>{this.props.label}</MyText>
</View>
);
}
}
export default CheckBoxItem;
The real trick to this is to maintain a list of the selected items in the parent component. Each CheckBoxItem can control its own state but you will need to pass a value back to the parent component each time it is checked/unchecked.
As you haven't shown where your CheckBox component has come from, I will show you how to do it using the react-native-elements CheckBox. The principles can then be applied to your own CheckBox.
Here is the App.js
import CheckBoxItem from './CheckBoxItem'
export default class App extends React.Component {
// set some initial values in state
state = {
response: [
{
name:'first'
},
{
name:'second'
},
{
name:'third'
},
{
name:'fourth'
},
{
name:'fifth'
},
{
name:'sixth'
},
],
selectedBoxes: [] // this array will hold the names of the items that were selected
}
onUpdate = (name) => {
this.setState(previous => {
let selectedBoxes = previous.selectedBoxes;
let index = selectedBoxes.indexOf(name) // check to see if the name is already stored in the array
if (index === -1) {
selectedBoxes.push(name) // if it isn't stored add it to the array
} else {
selectedBoxes.splice(index, 1) // if it is stored then remove it from the array
}
return { selectedBoxes }; // save the new selectedBoxes value in state
}, () => console.log(this.state.selectedBoxes)); // check that it has been saved correctly by using the callback function of state
}
render() {
const { response } = this.state;
return (
<View style={styles.container}>
{
response.map(item => <CheckBoxItem label={item.name} onUpdate={this.onUpdate.bind(this,item.name)}/>)
}
</View>
);
}
}
Here is the CheckBoxItem
import { CheckBox } from 'react-native-elements'
class CheckBoxItem extends Component<Props> {
state = {
check: false, // by default lets start unchecked
}
onValueChange = () => {
// toggle the state of the checkbox
this.setState(previous => {
return { check: !previous.check }
}, () => this.props.onUpdate());
// once the state has been updated call the onUpdate function
// which will update the selectedBoxes array in the parent componetn
}
render() {
return (
<View>
<CheckBox
title={this.props.label}
checked={this.state.check}
onPress={this.onValueChange}
/>
</View>
);
}
}
export default CheckBoxItem;
Explanation
When a CheckBoxItem is created two things are passed to it. One is a label and the second is an onUpdate function. The onUpdate function links the CheckBoxItem back to the parent component so that it can manipulate the state in the parent.
The onUpdate function takes the previous value of the state, before this update is applied, and it looks to see if the name of the checkbox exists in the selectedBoxes array. If it exists in the selectedBoxes array it is removed, otherwise it is added.
Now there exists an array in the parent component that you can access that contains all that items that have been selected.
Snack
Want to try out my code? Well I have created a snack that shows it working https://snack.expo.io/#andypandy/checkboxes
Setting state
You may have noticed that I am doing some unusual things with setState. I am making sure that setState gets called properly by using the previous value of the state and then applying my updates to that. I am also using the fact that setState takes a callback to perform actions after the state has been updated. If you would like to read more here are some great articles on setState.
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6

How can I compare two variable in onPress?

I am trying to create a changing pin screen and i was failed in comparing two variable that getting from the user (new pin and comfirm pin). The error show me that "this.state.newpin" is an undefined object.
class SettingScreen extends Component {
state = {
oldpin: '000000',
newpin: '',
secpin: ''
}
onPressButton(){
if( this.state.newpin == this.state.secpin){
ToastAndroid.show("Password Changed", ToastAndroid.SHORT);
this.setState({ oldpin : this.state.newpin})
}
else {
ToastAndroid.show("Password Unmatched", ToastAndroid.SHORT);
}
}
handleNewPin = (text) => {
this.setState({ newpin: text })
}
handleSecPin = (text) => {
this.setState({ secpin: text })
}
...
<TextInput onChangeText = {this.handleNewPin} />
<TextInput onChangeText = {this.handleSecPin} />
<TouchableOpacity onPress={this.onPressButton}>
<Text> Change Password </Text>
</TouchableOpacity>
I can get the output for "this.state.newpin" and "this.state.secpin" from user.
I just failed in the comparing statement ( OnPressButton()).
I am new in React-Native.
Sorry for any inconvenience.
you just need to bind your onPressButton()func. in the constructor with this. and move your state to constructor like this;
class SettingScreen extends Component {
constructor(props) {
super(props);
this.state = {
oldpin: '000000',
newpin: '',
secpin: ''
};
this.onPressButton = this.onPressButton.bind(this);
}
}

React Native setState does not refresh render

I try to get which is not active (in term of NativeBase.io - https://docs.nativebase.io/Components.html#button-def-headref, which simply means that it has no background color) and after I click it, it becomes active (it has a background color).
I define button like this:
<Button active={this.state.selected} onPress={() => this.select()} first>
<Text>Puppies</Text>
</Button>
selected variable in my state is by default false. When I run the application, it works correctly.
The select() method is implemented:
select() {
this.setState({ selected: true })
}
I expect that after I click on the button, it should change its background but it isn't. I check the value of this.state.selected and it changes appropriately. What I'm doing wrong?
export default class MyComponent extends Component {
state = {
selected: false
}
handlePress = () => {
const { selected } = this.state;
this.setState({
selected: !selected,
})
}
render() {
const { selected } = this.state;
return (
<Button active={selected} onPress={this.handlePress} first>
<Text>Puppies</Text>
</Button>
);
}
}