react-native-navigation
react-native-navigation copied to clipboard
Updated state not available in navigationButtonPressed method in a functional component
🐛 Bug Report
I am running into a strange problem and it may be related to how Javascript works than an issue in react-native-navigation. I created a functional component which also listens to navigation button pressed. So, I followed the recipe on the documentation website and wrote a component that looks like below:
const AutoSnoozeConfigurationContainer = props => {
const colorScheme = useSelector(state => state.userSettings.colorScheme)
const [enabled, setEnabled] = useState(props.enabled)
const [duration, setDuration] = useState(props.duration)
const [count, setCount] = useState(props.count)
useEffect(() => {
const listener = {
navigationButtonPressed: ({ buttonId }) => {
if (buttonId === 'cancel') {
Navigation.pop(props.componentId)
}
if (buttonId === 'save') {
console.log('onSave', enabled, duration, count)
props.onSave({ enabled, duration, count })
Navigation.pop(props.componentId)
}
}
}
// Register the listener to all events related to our component
const unsubscribe = Navigation.events().registerComponentListener(
listener,
props.componentId
)
return () => {
// Make sure to unregister the listener during cleanup
unsubscribe.remove()
}
}, [])
const autoSnoozeDurations = Constants.AutoSnoozeDurations.map(item => ({
...item,
selected: item.value === duration
}))
const autoSnoozeCounts = Constants.AutoSnoozeCounts.map(item => ({
...item,
selected: item.value === count
}))
console.log('render', enabled, duration, count)
return (
<ScrollView style={styles.sceneContainer}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Text colorScheme={colorScheme} style={styles.settingLabelStyle}>
{props.isGlobal ? I18n.t('autoSnooze') : I18n.t('autoSnoozePerAlarm')}
</Text>
<Switch
value={enabled}
onValueChange={setEnabled}
style={{ marginHorizontal: 10 }}
/>
</View>
<SeparatorWithTitle
title={I18n.t('duration')}
style={{ textAlign: 'center' }}
/>
<ComponentListView
component={AutoSnoozeSetting}
data={autoSnoozeDurations}
otherProps={{
onSelect: setDuration,
autoSnoozeEnabled: enabled
}}
renderSeparator
separatorStyle={{ marginLeft: 10 }}
/>
<SeparatorWithTitle
title={I18n.t('repeat')}
style={{ textAlign: 'center' }}
/>
<ComponentListView
component={AutoSnoozeSetting}
data={autoSnoozeCounts}
otherProps={{
onSelect: setCount,
autoSnoozeEnabled: enabled
}}
renderSeparator
separatorStyle={{ marginLeft: 10 }}
/>
</ScrollView>
)
}
When I hit Save on this component, the onSave method is called with the old state. Even though, the component renders the updated value of duration and count when changed on the screen. But when I Save, the onSave method is called with the initial values of enabled, duration, and count.
I changed it to a class component and it works fine. There is no other change than changing it to a class component that looks like below:
class AutoSnoozeConfigurationContainer extends Component {
constructor(props) {
super(props)
Navigation.events().bindComponent(this)
}
navigationButtonPressed = ({ buttonId }) => {
if (buttonId === 'cancel') {
Navigation.pop(this.props.componentId)
}
if (buttonId === 'save') {
console.tron.log('onSave', this.state)
this.props.onSave(this.state)
Navigation.pop(this.props.componentId)
}
}
state = {
enabled: this.props.enabled,
duration: this.props.duration,
count: this.props.count
}
setEnabled = enabled =>
this.setState({
enabled: enabled
})
setDuration = duration =>
this.setState({
duration: duration
})
setCount = count =>
this.setState({
count: count
})
render() {
const autoSnoozeDurations = Constants.AutoSnoozeDurations.map(item => ({
...item,
selected: item.value === this.state.duration
}))
const autoSnoozeCounts = Constants.AutoSnoozeCounts.map(item => ({
...item,
selected: item.value === this.state.count
}))
console.log('render', this.state)
return (
<ScrollView style={styles.sceneContainer}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Text
colorScheme={this.props.colorScheme}
style={styles.settingLabelStyle}>
{this.props.isGlobal
? I18n.t('autoSnooze')
: I18n.t('autoSnoozePerAlarm')}
</Text>
<Switch
value={this.state.enabled}
onValueChange={this.setEnabled}
style={{ marginHorizontal: 10 }}
/>
</View>
<SeparatorWithTitle
title={I18n.t('duration')}
style={{ textAlign: 'center' }}
/>
<ComponentListView
component={AutoSnoozeSetting}
data={autoSnoozeDurations}
otherProps={{
onSelect: this.setDuration,
autoSnoozeEnabled: this.state.enabled
}}
renderSeparator
separatorStyle={{ marginLeft: 10 }}
/>
<SeparatorWithTitle
title={I18n.t('repeat')}
style={{ textAlign: 'center' }}
/>
<ComponentListView
component={AutoSnoozeSetting}
data={autoSnoozeCounts}
otherProps={{
onSelect: this.setCount,
autoSnoozeEnabled: this.state.enabled
}}
renderSeparator
separatorStyle={{ marginLeft: 10 }}
/>
</ScrollView>
)
}
}
Here the updated state is passed to onSave method. I thought it may have something to do with Javascript closures and I tried a couple of things like moving onSave call out of the listener and use useCallback hook etc. without any luck.
I am not sure if this is a bug or not and if I have a mistake in the code, I would appreciate if somebody can explain the mistake.
Have you read the Contributing Guidelines on issues?
Yes
To Reproduce
Create a screen that is a functional component that updates the state somehow and have a button that invokes a callback with the state values.
Expected behavior
The callback should be called with updated state values.
Actual Behavior
The callback was called with initial state values.
Your Environment
- React Native Navigation version: 6.12.2
- React Native version: 0.64.0
- Platform(s) (iOS, Android, or both?): Both
- Device info (Simulator/Device? OS version? Debug/Release?): All
Reproducible Demo
(Paste the link to an example repo and exact instructions to reproduce the issue.)
This is related to your code. Check these docs: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect.
You need to add enabled, duration, count to your useEffect deps.
useEffect(() => {}, [enabled, duration, count]). So the effect will be triggered with the new values when the dependencies change.
I did try that but that doesn't fix it either. Also, I don't want to rerun the effect meaning attach the event listener again when the value of these states change. It should be the latest value when the onSave button is pressed. I am only attaching the event handler in the useEffect.
I don't want to rerun the effect meaning attach the event listener again when the value of these states change
That's how useEffect works. You'll get stale value if you don't use the correct dependency array.
If you don't want to resubscribe (why?), you can store your value in a ref which doesn't need to be in dependency array, but that's working around how React works.
Seeing the same problem but with useCallback. Call a function from a rightButton -> state not updated