4

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

}
Dan
  • 1,136
  • 10
  • 24

2 Answers2

8

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]);
Guruparan Giritharan
  • 15,660
  • 4
  • 27
  • 50
  • Works perfectly, thanks! but I would like to know why of this behaviour? – Victor Glez Sep 02 '22 at 11:04
  • 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. – ansonl Nov 20 '22 at 02:15
  • 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. – ansonl Nov 20 '22 at 02:16
4

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

}
Dan
  • 1,136
  • 10
  • 24
  • I believe this answer is much better, since it avoids running the useEffect every time the label changes, nice answer @dan – Ahmed Imam Jan 28 '22 at 18:59