React Navigation 5 headerRight button function called doesn't get updated states
Asked Answered
D

2

5

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.

What changes need to be made to the navigation headerRight button to fix this issue?

Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.

import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';

export default function EditScreen({navigation}){
  const [label, setLabel] = useState('');

  useLayoutEffect(() => {
      navigation.setOptions({
        headerRight: () => (
          <TouchableWithoutFeedback onPress={submit}>
            <Text>Save</Text>
          </TouchableWithoutFeedback>
        ),
      });
    }, [navigation]);

  const submit = () => {
    //label doesn't return the updated state here
    const data = {label: label}
    fetch(....)
  }

  return(
    <View>
      <TextInput onChangeText={(text) => setLabel(text) } value={label} />  
    </View>
  )

}
Didache answered 7/6, 2020 at 23:40 Comment(0)
D
9

Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes

  React.useLayoutEffect(() => {
      navigation.setOptions({
        headerRight: () => (
          <TouchableWithoutFeedback onPress={submit}>
            <Text>Save</Text>
          </TouchableWithoutFeedback>
        ),
      });
    }, [navigation,label]);
Drawplate answered 8/6, 2020 at 3:4 Comment(3)
Works perfectly, thanks! but I would like to know why of this behaviour?Jeanene
I assume headerRight exists in the StackNavigation component and not the current focus component so it references the "state context" at the time it is created. When the state in the current component is updated headerRight continues pointing to the old "state context" in memory while the current component points to a new "state context". useEffect that updates headerRight has a dependency of navigation which doesn't change when the state context is changed so the state variable itself needs to be added to signal a rerender headerRight to point to current state context.Wheal
I confirmed this behavior by having two buttons, one in the header and one in the view that increment the state and the one in the header keeps referencing the old state.Wheal
D
5

Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.

To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.

export default function EditScreen({navigation}){
  const [label, setLabel] = useState('');
  const [saving, setSaving] = useState(false);

  useLayoutEffect(() => {
      navigation.setOptions({
        headerRight: () => (
          <TouchableWithoutFeedback onPress={() => setSaving(true)}>
            <Text>Done</Text>
          </TouchableWithoutFeedback>
        ),
      });
    }, [navigation]);

    useEffect(() => {
      // Check if saving to avoid calling submit on screen unmounting
      if(saving){
        submit()
      }
    }, [saving]);

    const submit = () => {
      const data = {label: label}
      fetch(....)
    }

    return(
      <View>
        <TextInput onChangeText={(text) => setLabel(text) } value={label} />  
      </View>
    )

}
Didache answered 8/6, 2020 at 4:7 Comment(2)
I believe this answer is much better, since it avoids running the useEffect every time the label changes, nice answer @danDesdee
Good solution. Remember to add setSaving(false) to the first line of submit function, if we want to call it again for any reason (like validations for example).Amalita

© 2022 - 2024 — McMap. All rights reserved.