React native: race condition in TextInput - react-native

I encountered an issue with TextInput, which gets input from user, and a button which send the message in TextInput and clear the input.
So overall flow would be:
User type into TextInput
At some point, user presses Button(aka. TouchableOpacity)
Store the text from the TextInput to temporary buffer then clear the TextInput.
Send the text via api.
Code looks like:
{/* Message Input */}
<TextInput
style={styles.messageInput}
multiline
onChangeText={text => {
setMessage(text);
}}
value={message}
/>
{/* Send Button */}
<Button
title={"Send"}
onPress={() => {
const msg = message
onSendMessage(msg);
setMessage("");
}}
disabled={false}
style={styles.sendButton}
/>
And my problem occurs when user type too soon after tapping the send button.
If user decides to type too soon, then TextInput does not get cleared.
I think this is because:
User tap send => enqueues render because message state change by setMessage("") in Button's onPress
User type too soon => enqueues render because message change by onChangeText handler in TextInput. The problem is setMessage from previous state hasn't been really handled yet. Therefore, this render is enqueued with message's previous value (aka. value before message was set to "" ).
I tried Promise, useEffect, and useRef, but nothing really solved this issue. If anyone knows how to overcome with this issue, please let me know. Thank you in advance.

You should use Callback or Promise or Async/Await for this use-case. I suggest you use Promise.
onSendMessage = msg => {
axios
.post("/your-url", {
msg
})
.then(function(response) {
console.log(response);
// ! You can clear the message here !
setMessage("");
// OR
return new Promise(() => resolve(msg));
})
.catch(function(error) {
console.log(error);
});
};
Something like that. The choice of using is yours :)

useState hook is asynchronous, and will not immediately reflect and update but will trigger a re-render. Therefore you shouldn't store this value in a constant like this: const msg = message.
I'd make an async function that sends the input to the api. (Bonus: add an loading state to give the user feedback by disabling the submit button)
const [isLoading, setIsLoading] = useState(false);
onSubmit = async () => {
setIsLoading(true);
const response = await fetch('url/action', settings);
if(response){
setIsLoading(false);
return response.json();
}
}
<TextInput
style={styles.messageInput}
multiline
onChangeText={text => {
setMessage(text);
}}
value={message}
/>
<Button
title={"Send"}
onPress={() => onSubmit()}
disabled={isLoading}
style={styles.sendButton}
/>

Related

Is it possible to wait for a component to render? React Testing Library/Jest

I have a component. It has a button. Upon pressing the button, I am changing the style of the button text (color) using setState function. When I am testing the changed component, the test is failing because the change happens asynchronously. I want to do something as is given here (https://testing-library.com/docs/dom-testing-library/api-async/)
const button = screen.getByRole('button', { name: 'Click Me' })
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')
But rather than waiting for the text to change. I want to wait for the text color to change. Thanks
This is the code for my button
<Button onPress = {() => {this.setState({state : 1});}}>
<Text style = {style}>Button Text</Text>
</Button>
So when this button is pressed. state is set to 1. And in render :
if(this.state.state === 1) style = style1
else style = style2;
But it can be seen from logs that render is called after the test checks for the styles. So How can I wait for the render to complete before checking if the font color has been changed?
Here is the testing code
test('The button text style changes after press', () => {
const {getByText} = render(<Component/>);
fireEvent.press(getByText('button'));
expect(getByText('button')).toHaveStyle({
color : '#ffffff'
});
})
It looks like you have a custom button, not a native button. I'm guessing your component is something like this:
import React from "react";
import {Text, TouchableOpacity} from "react-native";
const Button = ({pressHandler, children}) => (
<TouchableOpacity onPress={pressHandler}>
{children}
</TouchableOpacity>
);
const ColorChangingButton = ({text}) => {
const [color, setColor] = React.useState("red");
const toggleColor = () => setTimeout(() =>
setColor(color === "green" ? "red" : "green"), 1000
);
return (
<Button pressHandler={toggleColor}>
<Text style={{color}}>{text}</Text>
</Button>
);
};
export default ColorChangingButton;
If so, you can test it with waitFor as described here:
import React from "react";
import {
fireEvent,
render,
waitFor,
} from "#testing-library/react-native";
import ColorChangingButton from "../src/components/ColorChangingButton";
it("should change the button's text color", async () => {
const text = "foobar";
const {getByText} = render(<ColorChangingButton text={text} />);
fireEvent.press(getByText(text));
await waitFor(() => {
expect(getByText(text)).toHaveStyle({color: "green"});
});
});
For a native button which has rigid semantics for changing colors and doesn't accept children, instead using title="foo", a call to debug() shows that it expands to a few nested elements. You can use
const text = within(getByRole("button")).getByText(/./);
expect(text).toHaveStyle({color: "green"});
inside the waitFor callback to dip into the button's text child and wait for it to have the desired color.
I used the same packages/versions for this post as shown in React Testing Library: Test if Elements have been mapped/rendered.
You can try
<Text style = {this.state.state === 1 ? style1 : style2}>Button Text</Text>
This will consequently lead to the style being defined all time. So you don't have to wait for the setState to complete.
Edit
You can use the callback provided by setState function to perform your tests for styles.
this.setState({
state : 1
} , () => {
//this is called only after the state is changed
//perform your test here
})

Trouble updating Picker component from Async Storage in React Native

I'm using a Picker component to let the user set a value for how frequently they want to be reminded about something. In the code below, I'm saving the result in the component's state as well as saving it to the device with Async Storage:
const [frequency, setFrequency] = useState('year');
...
<Picker
selectedValue={frequency}
style={styles.picker}
itemStyle={styles.pickerItem}
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
})();
setFrequency(itemValue);
}}
mode={'dropdown'}
prompt={'Reminder every:'}
>
<Picker.Item label="Never" value="never" />
<Picker.Item label="Day" value="day" />
<Picker.Item label="Week" value="week" />
<Picker.Item label="Year" value="year" />
etc...
</Picker>
I'd also like to have the component grab the saved data and set that as the state when first rendering.
useEffect(() => {
const fetchFrequency = async () => {
let storedFrequency = await AsyncStorage.getItem(`#circle_${circle.name}_frequency`);
if (storedFrequency != null) {
setFrequency(storedFrequency);
};
}
fetchFrequency();
}, []);
Based on the limited amount I know about Async Storage, this seems to make sense. I think it's
awaiting the result of grabbing the value from storage
setting the state
rendering the component (this could be happening before setting state as well, but I figure it would render again when the state changes)
updating both storage and state when the user chooses a new option
However, this doesn't work. If I navigate away and then back to the page, the state has been reset.
UPDATE:
If I console.log the itemValue in the onValueChange async function this is what I get:
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
console.log(itemValue)
})();
setFrequency(itemValue);
}}
When changing the value to 'never', it prints
never
never
When I navigate away and then come back, without even touching the compnent it prints out:
week
week
never
never
year
year
or
year
never
year
year
or some other long string of values which shows that there's a feedback loop going on somewhere.
Your expression AsyncStorage.setItem() is not firing because you forget to invoke Self-Invoking Functions inside the callback function of onValueChange.
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
})(); // I will invoke myself
setFrequency(itemValue);
}}
UPDATED (following the updated question):
I didn't spot any more bugs on your given snippet code and I don't know what's going on with your full source code. Anyway, I have created a super simple working snippet code following by the code in your question, so you can just copy into your project.
import React, {useState, useEffect} from 'react';
import {Picker, AsyncStorage} from 'react-native';
export default function App() {
const [frequency, setFrequency] = useState('year');
useEffect(() => {
const fetchFrequency = async () => {
let storedFrequency = await AsyncStorage.getItem('#circle_circle_name_frequency');
if (storedFrequency != null) {
setFrequency(storedFrequency);
}
};
fetchFrequency();
}, []);
return (
<Picker
selectedValue={frequency}
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem('#circle_circle_name_frequency', itemValue);
})();
setFrequency(itemValue);
}}
mode={'dropdown'}
prompt={'Reminder every:'}>
<Picker.Item label="Never" value="never" />
<Picker.Item label="Day" value="day" />
<Picker.Item label="Week" value="week" />
<Picker.Item label="Year" value="year" />
</Picker>
);
}
Hope this can help!
PS: I see you put expo tag on your question and I just wanna remind that, if you preview the project on the web browser, your storedFrequency inside useEffect will always be null because the browser doesn't support AsyncStorage.
It looks like the problem was an issue with the Picker itself and how it calls onValueChange every render rather than only when changed. I found a temporary solution in this thread for until it gets fixed: https://github.com/lawnstarter/react-native-picker-select/issues/112#issuecomment-634038287

React Native Screen refresh from nested component screen

I have a screen, PDP, that screen contains a component, TopNews. I want TopNews onclick to redraw PDP (it passes in an Article ID which the PDP uses to retrieve the article). The diagram below shows the flow
The code I have to support this inside TopNews is;
<TouchableOpacity onPress={() => navigation.navigate('Pdp', {articleid: item.id})}>
The challenge is the TouchableOpacity event triggers, but the page doesn't refresh the PDP. I don't want to refresh only the PDP as I may include the TopNews component in other screens outside of the PDP, its just in this case its inside of the screen it needs to call.
There are more then one way to do it.
You can create a state for the article id and allow the TopNews to set this state:
const PDP = () => {
const [ id, setId ] = React.useState(0);
return (
<div>
<ArticleComponent articleId={id} />
<TopNews idSetter={setId} />
</div>
);
}
const ArticleComponent = ({ articleId }) => {
return /* something that renders the article by the id */ ;
}
const TopNews = ({ idSetter }) => {
return (
<TouchableOpacity
onPress={() => {
idSetter(item.id);
navigation.navigate('Pdp')
}}>
);
}
Another more elegant way, is to use react contexts. You can create a context provider in the PDP component and change the context data in any PDP's children component.
I found the answer, instead of using navigation.navigate, use navigation.push instead;
<TouchableOpacity onPress={() => navigation.push('Pdp', {articleid: item.id})}>
This works perfectly :)

How to initiate props when rendering Component?

I have a Login Component where I want the user to choose Service from a Service Catalogue. The Picker gets and sets values to redux:
<Picker
selectedValue={this.props.service.id}
onValueChange={itemValue => this.props.setServiceType(itemValue)}>
{service_catalogue.map(service =>
<Picker.Item key={service.id} label={service.id} value={service.id} />
)}
</Picker>
But I don't know how to properly set the initial value. I set the default value in componentDidMount (the first item in the Catalogue), but I think componentDidMount is trigged on update? Is there a lifecycle function that is only triggered on rendering Component in React Native?
componentDidMount() {
this.props.setServiceType(service_catalogue[0].id)
}
So the problem that I'm facing is that even though the user might choose "Instructor" the service becomes service_catalogue[0] = "Cleaner". And if I don't setServiceType on componentDidMount no Picker appears, as this.props.service.id doesn't exist.
You can set default value for the props on the following way:
...
YourComponent.propTypes = {
myState: PropTypes.string,
}
YourComponent.defaultProps = {
myState: 'default value',
};
const mapStateToProps = state = ({
myState: state.myState,
});
export default connect(mapStateToProps)(YourComponent);
More info: https://reactjs.org/docs/typechecking-with-proptypes.html#default-prop-values

How do I get the value in TextInput when onBlur is called?

In React Native, I want to pass the value of the TextInput in the onBlur event handler.
onBlur={(e) => this.validateText(e.target.value)}
e.target.value works for plain React. But, in react-native, e.target.value is undefined. What is the structure of event args available in React Native?
You should use the 'onEndEditing' method instead of the 'onBlur'
onEndEditing?: function Callback that is called when text input ends.
onBlur is a component function where onEndEditing is specific for TextInput
onEndEditing
This approach works for both multiline and single line.
<TextInput
onEndEditing={(e: any) =>
{
this.setState({textValue: e.nativeEvent.text})
}
}/>
In React Native, you can get the value of the TextInput from e.nativeEvent.text.
Unfortunately, this doesn't work for multiline={true}. One hack around this is to maintain a ref to your TextInput and access the text value through the _lastNativeText property of the component. For example (assuming you've assigned your TextInput component a ref of "textInput"):
onBlur={() => console.log(this.refs.textInput._lastNativeText)}
Simple solution:
This way onBlur your state's email will always have the last value changed by the user.
validate = () => {
const { email } = this.state
console.log('Validating Email Here!', email)
}
<TextInput
style={styles.input}
placeholder='E-mail'
onChangeText={email => this.setState({email})}
onBlur={e => this.validate()}
/>