react-native-screens: headerRight randomly position the component incorrectly

I was testing the createNativeStackNavigation and ended up having this random issue where the component rendered by headerRight is being positioned incorrectly sometimes. There’s nothing special in my code, I am just rendering a <Button> from react-native.

Here’s the source code: https://github.com/lnmunhoz/react-native-experiments/blob/master/react-navigation-examples/examples/NativeNavigation.tsx.

Kapture 2020-03-21 at 20 56 50

Update

The issue also happens when the headerLargeTitle is false.

image

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 8
  • Comments: 77 (24 by maintainers)

Commits related to this issue

Most upvoted comments

I have a workaround if it’s a blocker for anyone here https://github.com/react-navigation/react-navigation/issues/6746#issuecomment-583897436:

As a workaround I show the Icon after a few ms:

const [shouldRenderIcon, setShouldRenderIcon] = useState(false)

useEffect(() => {
const nbr = setTimeout(() => setShouldRenderIcon(true), 200)
return () => clearTimeout(nbr)
}, [])

return shouldRenderIcon ? <MyIcon /> : null

Still happening on 3.25.0. In my case it happens every time the width of the right component changes due to a button appearing/disappearing. The workaround for me was keeping the width consistent by setting opacity to 0/1.

It seems there’s been a bit of a regression here. I am seeing the header title being randomly positioned to the right when using NativeStack from react-navigation. Opening the view debugger in Xcode shows the width as 0, which would explain why it’s “to the right”, it’s centered with the width being 0 and shows the overflow.

Screen Shot 2022-08-05 at 12 27 48 PM

$  grep react-native-screens package.json     
    "react-native-screens": "^3.13.1",
$  grep react-native package.json 
...
    "react-native": "0.67.4",
...
$  grep react-navigation package.json
    "@react-navigation/bottom-tabs": "^6.3.1",
    "@react-navigation/elements": "^1.3.3",
    "@react-navigation/native": "^6.0.10",
    "@react-navigation/native-stack": "^6.6.1",

It’s been years and this still hasn’t been resolved… I found this issue after having trouble with the expo-router implementation and tweaked some of the above advice…

Here’s the workaround:

const [loaded, setLoaded] = useState(false)

useLayoutEffect(() => {
  setLoaded(true)
}, [])

...

headerRight: loaded ? <CustomComponent /> : undefined

this work for me:

useLayoutEffect(() => {
      navigation.setOptions({
        ...
        },
      })
    }, [navigation])

Using a custom compontent in headerCenter is affected by the same bug/problem…

I’m also getting this issue on Expo Router ~3.4.7 (SDK 50.0.0). It went away for me when moving more logic about the header into the layout instead of setting it on mount in the useEffect hook. Like this:

// app/(app)/_layout.tsx
<Stack.Screen
    name="settings"
    options={{
    ...
    title: "",
    headerLeft: () => (
        <View>
            ...
        </View>
        ),
    }}
/>
// app/(app)/settings.tsx
useEffect(() => {
    navigation.setOptions({
      headerRight: () => <HeaderRight onPress={() => saveMutation.mutate()} loading={saveMutation.isLoading} />,
    });
  }, [saveMutation]);

Might work for some of you!

Hi @tboba! Unfortunately I couldn’t reproduce it in a deterministic way, and I ended up by making a custom header component, that way I never noticed the issue again!

@WoLewicki It’s on my todo list. Hopefully by Monday next week. Including the other modal bug, if I can reproduce everything.

Closed https://github.com/software-mansion/react-native-screens/issues/571 for this as seems to be related. I shouldn’t be the one to dictate priority but this seems really important, it’s really not ideal to have production apps experience this.

Also I have a feeling mentioning expo is not relevant, I experience this consistently without.

Hey @WoLewicki, do you want me to setup a sample project, showing this issue? Here’s some screenshots demonstrating the problem:

This first one, is just adding a 44x44 Touchable button as the headerRight - you can see it’s quite far to the left. Screenshot 2020-04-30 at 17 45 37

If I inspect the element, you can see it’s 44x44: Screenshot 2020-04-30 at 17 51 30

If I add a marginRight: -20, it does “fix” the alignment, but not the issue itself. Screenshot 2020-04-30 at 17 46 22

Now if I inspect the element, you can see it’s only 24px wide, so almost half of the button is cropped and not easy to tap. Screenshot 2020-04-30 at 17 46 44

It feels like this extra 20px on the right side added by the native wrapper is causing the problem - if that space would be removed entirely the problem would be solved. The back button works this way if you inspect it:

Screenshot 2020-04-30 at 17 54 17

I cloned your project and also made a bare react-native project. The issue exists only while opening it through Expo Client. This means that the version of RNScreens in the current Expo Client is having the bug since it has RNScreens in version 2.0.0-alpha.12. Can you repro it that way to prove if it is right?

@trajano this should do the thing 👍 thanks!

@elliotfleming no need to do that, just put navigation.setOptions inside uselayouteffect

Having the same issue on iPhone.

I only see it happening on release mode and it doesn’t happen always or in all the screens.

Worked it around by setting headerRight as an empty Text on the screen definition.

const ScreenComponent = ({ navigation }) => {
  useEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <TouchableOpacity>
          <Text>Button</Text>
        </TouchableOpacity> 
      )
  }, []);

  return <View>...</View>;
}

const App = () => (
  <Stack.Navigator>
    <Stack.Screen
      name="ScreenName"
      component={ScreenComponent}
       options={{
         headerRight: () => <Text> </Text>,
       }}
     />
  </Stack.Navigator>
);

package.json

{
  "name": "App",
  "version": "2.0.0",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint .",
    "postinstall": "patch-package && npx jetify",
    "relay": "relay-compiler --src ./src --schema ./schema.graphql",
    "update_schema": "node scripts/update_schema.js"
  },
  "dependencies": {
    "@expo/react-native-action-sheet": "^3.8.0",
    "@invertase/react-native-apple-authentication": "^1.1.2",
    "@react-native-community/async-storage": "^1.11.0",
    "@react-native-community/blur": "^3.6.0",
    "@react-native-community/cookies": "^4.0.0",
    "@react-native-community/hooks": "^2.6.0",
    "@react-native-community/masked-view": "^0.1.10",
    "@react-native-community/netinfo": "^5.9.5",
    "@react-native-firebase/app": "^8.2.0",
    "@react-native-firebase/messaging": "^7.5.0",
    "@react-navigation/bottom-tabs": "^5.9.2",
    "@react-navigation/drawer": "^5.9.3",
    "@react-navigation/native": "^5.7.6",
    "@react-navigation/stack": "^5.9.3",
    "@rnhooks/async-storage": "^0.0.1",
    "@rnhooks/keyboard": "^0.0.3",
    "axios": "^0.19.2",
    "babel-relay-plugin": "^0.11.0",
    "cookie": "^0.4.1",
    "formik": "^2.1.5",
    "jetifier": "^1.6.6",
    "lodash": "^4.17.20",
    "moment": "^2.29.0",
    "patch-package": "^6.2.2",
    "postinstall-postinstall": "^2.1.0",
    "react": "16.13.1",
    "react-native": "0.63.3",
    "react-native-background-fetch": "^3.1.0",
    "react-native-config": "^1.3.3",
    "react-native-device-info": "^5.6.3",
    "react-native-elements": "^2.3.2",
    "react-native-geocoding": "^0.4.0",
    "react-native-geolocation-service": "^5.0.0",
    "react-native-gesture-handler": "^1.8.0",
    "react-native-gifted-chat": "^0.16.3",
    "react-native-google-fit": "^0.12.2",
    "react-native-image-crop-picker": "^0.35.0",
    "react-native-inappbrowser-reborn": "^3.4.0",
    "react-native-intercom": "^17.0.0",
    "react-native-localize": "^1.4.1",
    "react-native-offline": "^5.7.0",
    "react-native-permissions": "^2.1.5",
    "react-native-raw-bottom-sheet": "^2.2.0",
    "react-native-reanimated": "^1.13.1",
    "react-native-safe-area-context": "^3.1.7",
    "react-native-screens": "^2.11.0",
    "react-native-side-menu": "^1.1.3",
    "react-native-vector-icons": "^7.0.0",
    "react-native-version-number": "^0.3.6",
    "react-native-webview": "^10.3.2",
    "react-relay": "^10.0.1",
    "reanimated-bottom-sheet": "^1.0.0-alpha.22",
    "relay-runtime": "^10.0.1",
    "rn-apple-healthkit": "^0.8.0",
    "rollbar-react-native": "^0.9.1",
    "swr": "^0.2.3",
    "url": "^0.11.0",
    "yup": "^0.29.3"
  },
  "devDependencies": {
    "@babel/core": "^7.10.4",
    "@babel/runtime": "^7.10.4",
    "@react-native-community/eslint-config": "^2.0.0",
    "babel-jest": "^26.1.0",
    "babel-plugin-relay": "^10.0.1",
    "eslint": "^7.4.0",
    "graphql": "^15.3.0",
    "jest": "^26.1.0",
    "metro-react-native-babel-preset": "^0.60.0",
    "react-test-renderer": "16.13.1",
    "relay-compiler": "^10.0.1",
    "relay-config": "^10.0.1"
  },
  "jest": {
    "preset": "react-native"
  }
}

@zibs can you post this comment in #528 to keep the discussion there? Also, you can try to move all the code inside this added method to a new method, and then call it in RNSScreen viewWillAppear, maybe it will fix the issue for when coming back from another screen. Please comment if you need more help with applying this.

@WoLewicki This 100% fixes my issue!

My replication is very straight forward, just do something along the lines of this:

 <Stack.Screen ...  
   initialParams={{
     screenOptions: {
         headerCenter: ()=>    <ReactNative.Text>Loading</ReactNative.Text>,
     }
  }}
/>

Then in the screen:

    useLayoutEffect(() => {
      if (route?.params?.screenOptions) {
        navigation.setOptions(route.params.screenOptions);
      }
    }, [navigation, route]);

Before image

After image

This has been on my todo for a few days, I’ll prioritise testing this in the evening @WoLewicki, thanks for letting me know again.

@WoLewicki I’ve been testing #600 in my app (where I could occasionally reproduce the issue) and haven’t seen it since I installed your PR. Haven’t seen any sort of regression either.

So, from my perspective, it looks good!

#600 should be ready now. Can you check if it does not destroy any of your behavior? @kyle-ssg @lnmunhoz