Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple re-renders on _layouts and pages. #320

Closed
moxen-dev opened this issue Feb 25, 2023 · 56 comments · Fixed by expo/expo#24147
Closed

Multiple re-renders on _layouts and pages. #320

moxen-dev opened this issue Feb 25, 2023 · 56 comments · Fixed by expo/expo#24147
Assignees

Comments

@moxen-dev
Copy link

Summary

Main Issue

I noticed that my screen is triggering the initial useEffect twice. I was suspecting that some upper level component was unmounting and mounting again, but turns out that the _layout.tsx file is rendering more than once.

Details

My currently structure is the following:

app/
    _layout.tsx (<Slot />)
    (auth)
    (app)
        index.tsx (with a redirect to home/index)
        _layout.tsx (<Slot />)
        (tabs)
            _layout.tsx (<Tabs />)
            home/
                _layout.tsx (<Stack />)
                index.tsx            
         lives/
            [liveId].tsx
            _layout.tsx (<Stack />)

Logs

When I reload my app and the router put me at the logged index, these were the renders that happened:
Screenshot 2023-02-24 at 22 48 40

Why app/(app)/_layout.tsx rendered 3 times and app/(app)/home/ndex.tsx rendered twice?

I understand that the first render of the layout is related to the (app)/index that does the redirect. Another is related to the screen I was redirected to. But what about the third time? I also noticed that it is not being remounted because does not trigger the unmount of the useEffect.

Minimal reproducible example

My app/(app)/_layout.tsx for reference:

import { Slot } from "expo-router";
import { useEffect } from "react";

export default function AppLayout() {
  console.log('rendering AppLayout');

  useEffect(() => {
    return () => {
      console.log('AppLayout unmount');
    };
  }, []);
  return (
    <Slot />
  );
};
@EvanBacon EvanBacon self-assigned this Feb 25, 2023
@TowhidKashem
Copy link

TowhidKashem commented Mar 6, 2023

+1 getting this as well. My JS thread goes down to ~30 frames each on the perf monitor each time a new tab screen is clicked. It's pretty unusable compared to the previous version. So I had to remove expo router, sucks cause it was ~3 days of work for me (complex app). Might try again at a later version when it's more stable.

@moxen-dev
Copy link
Author

moxen-dev commented Mar 10, 2023

Hey folks, any news here? My app rely on useEffect to kick of a video streaming and because of that, it is starting the streaming twice leading to some weird behaviour. Any update could help me, even if it is to say that this will not be tackle right now.

I really want to push expo router forward in my app since I am totally into it's benefits.

@moxen-dev
Copy link
Author

I am investigating this deeply and found this old thread that I suspect is related.
react-navigation/react-navigation#476

@mariusklingl
Copy link

Yeah also facing the same problem.... anyone got a workaround on that issue? Same as @TowhidKashem. Put a lot of effort in designing the layout. Need to switch because of performance and data fetching issues.
Just happens if you nest two or more navigators. Doesn't matter if it is Stack or Tab. Always mounts twice.

@marklawlor
Copy link
Contributor

marklawlor commented Mar 22, 2023

@moxen-dev Can you please provide a full reproduction. When I create an app with the information provided the _layout.tsx is only rendered once.. EDIT: I was using index.tsx instead of (app)/index.tsx, which does not seem to do the double render. I'll look into this further

@moxen-dev
Copy link
Author

Feel free to ask me anything @marklawlor.

@moxen-dev
Copy link
Author

@marklawlor any updates here? just a friendly reach out so I can plan accordingly.

@marklawlor
Copy link
Contributor

Its not a simple fix, but I'm working on something. If this issue is causing you problems, you can use API's like memo() to protect your components against re-renders. Its inconvenient, hence why I'm working on a fix, but shouldn't be completely blocking.

@moxen-dev
Copy link
Author

@marklawlor thank you for your answer. Maybe as part of a impostor syndrome, I was still think that the issue could be only on my code base and some mistake I made. So your confirmation helped me. I am already doing react memoization as part of a good practice but thanks for pointing this route (no pun intended).

@SavageWilliam
Copy link

Can someone explain why you need index.tsx (with a redirect to home/index).. I'm also havign trouble where the app entry is / which corresponds to an index file.

I thought the point of:

export const unstable_settings = {
  initialRouteName: 'welcome',
}

would mean that the entry route is /welcome and not /?

@leandromatos
Copy link

Its not a simple fix, but I'm working on something. If this issue is causing you problems, you can use API's like memo() to protect your components against re-renders. Its inconvenient, hence why I'm working on a fix, but shouldn't be completely blocking.

Unfortunately, memo() didn't work for me, @marklawlor.

I have three simple screens, using only a <Text/> to identify each screen and a console.log to confirm that every navigate re-render all screens.

In a large application, the performance will be very poor.

@minuz
Copy link

minuz commented Apr 20, 2023

Hi @marklawlor ! Firstly, thanks for looking into the issue and trying to fix it.
I'm in the process of building a new app and thought I was going mad with the re-renders that is obviously causing some weird behaviours on my app. Don't want to put more pressure on you, but do you have a rough ETA for the fix?
Also, if you need any help, I'd be happy to assist in any way I can.

@athenawisdoms
Copy link

Also though I was going crazy when every screen in the stack is re-rendering after the user navigates to a new screen. This led to a strange behavior where every previous screens that re-rendered are still trying to read the search params that may no longer be passed on to the newer screens.

@AmeerDlshad
Copy link

I have 2-3 ways to fix that issue if anyone is still having this issue.

@topzdev
Copy link

topzdev commented Jun 5, 2023

I have 2-3 ways to fix that issue if anyone is still having this issue.

I'm still having this issue, can you share your fixes regarding this issue?

@marklawlor
Copy link
Contributor

If you are experiencing this issue you can also the next tag which is published on npm which includes various fixes.

@AmeerDlshad
Copy link

AmeerDlshad commented Jun 5, 2023

The easiest way to fix this would be just change the folder name of app/(tabs) to app/tabs by doing this everything will work as you expect, but now anywhere you've used router like router.push("home") you need to change it back to router.push("tabs/home") basically now the 'tabs' shows in the url. If you don't like this approach there is another way.

you are most likely using a Stack at the root that has the Tabs inside it that looks like this:

return <Stack>   
      <Stack.Screen name="login" />  
      <Stack.Screen name="(tabs)" />  
       </Stack>

You can change it to this:

return <Tabs tabBar={() => <></>} backBehavior="none">
      <Tabs.Screen name="login" options={{ unmountOnBlur: true }} />
      <Tabs.Screen name="(tabs)" options={{ unmountOnBlur: true }} />
    </Tabs>

the only issue with this one is now router.repalce() functions just like router.push() it means if the user goes from the login screen to the (tabs)/home screen and presses the back button it goes back to the login screen, to fix this we can use backBehavior="none" now the back button doesn't work between the root screens but they work just fine inside the (tabs) or any other nested screen you have that's not in the root layout.

Another way would be to have one big Tabs in the root like this in the app/_layout.tsx:

return <Tabs>
      <Tabs.Screen
        name="login"
        options={{ unmountOnBlur: true, href: null }}
      /> {/* Stack Screen */}
      <Tabs.Screen name="home" /> {/* Tab Screen */}
      <Tabs.Screen name="account" /> {/* Tab Screen */}
    </Tabs>

add options={{ unmountOnBlur: true, href: null }} to every stack screen and write the Tab screens in the same way you were writing before, href:null would make it so the login screen doesn't show on the TabBar, but remember now you need to copy your Tab screens to the root folder like this:
app/(tabs)/home to app/home
app/(tabs)/account to app/account

The main cause of this multiple render bug is when you navigate form a root Stack screen to a screen that uses group syntax (tabs)/home, and/or you are using useSearchParams() hook.

Hope that helps.

@orbiteleven
Copy link

@marklawlor are you expecting this to be fixed with v2? Are there related issues/PRs to follow?

@makirby
Copy link

makirby commented Jul 3, 2023

Having this issue when using screens with a _layout file. When the l remove the _layout file, no more double useEffect calls.

@steve228uk
Copy link

steve228uk commented Jul 3, 2023

This seems to be fixed on the next branch. Thanks @marklawlor

Edit: Scratch that, same issue if I use a Redirect.

@RohovDmytro
Copy link

Using latest expo-router (beta-10) and expo beta 4. The issue persists.

Having 2-3 mounts. The structure is very similar as in the described issue.

Seems escalaty to mee! :)

@Dundyne
Copy link

Dundyne commented Jul 5, 2023

I'm also experiencing a bunch of unnecessary rerenders every time a screen loads. Spent a bunch of time moving over from reactnav, shame to need to undo it...

@devoren
Copy link

devoren commented Jul 6, 2023

same issue on sdk 49 stable

@EvanBacon
Copy link
Contributor

Worth clarifying that this doesn't happen on all layout routes, can be triggered by a number of reasonable causes like using useGlobalSearchParams, and is other times related to how the link is sent (which can be circumvented by using useNavigation as-is).

@devoren
Copy link

devoren commented Aug 24, 2023

In my case i have auth stack and root stack, and if push to auth/login from root stack screen it rerender 2 times, and if i push to auth it does not rerender 2 times
Authlayout:

export default function AuthLayout() {
	return (
		<Stack screenOptions={{ headerShown: false }}>
			<Stack.Screen name="login" />
			<Stack.Screen name="signup" />
			<Stack.Screen name="reset-password" />
			<Stack.Screen name="verification" />
		</Stack>
	);
}

For example i push to auth from products/[id]
(root layout)
image

UPD:
Yes, it's _layout's fault, if I remove it, the screen is mounted once

@claudesortwell
Copy link

claudesortwell commented Aug 27, 2023

If anyone’s willing to provide me with a minimal repo for reproduction, I’m happy to look into and try fix this issue.

Edit: seems like this person has a similar issue and has a repo I can look at #838

@EvanBacon
Copy link
Contributor

There appear to be some unrelated issues linked in here. I'll assume #838 is an accurate representation of this issue as it has a reproducible demo linked which generally aligns with the comments I'm reading in here. When it's fixed in expo/expo#24147, I'll close this issue. If people have further issues afterwards, please open new issues with reproducible demos.

EvanBacon added a commit to expo/expo that referenced this issue Aug 28, 2023
# Why

Prevent double renders by cloning state to avoid leaking state between
functions.

- fix expo/router#838 
- fix expo/router#733
- fix expo/router#320
- The issue expo/router#320 has multiple
different things linked, but the original case appears to be fixed.
- possibly also addresses expo/router#847

<!--
Please describe the motivation for this PR, and link to relevant GitHub
issues, forums posts, or feature requests.
-->

# How

- Prevent mutating the input state to avoid invalidating the nested
state.

# Test Plan

- The testing library doesn't seem to support this case. @marklawlor has
been tasked with ensuring the original branch can detect the error
https://github.com/expo/expo/compare/%40evanbacon/router/fix-838
Just in case the testing library isn't fixed, I ran locally with:

- `app/_layout.js`, `app/(a)/_layout.js`, `app/b/_layout.js`
```js
import { Slot } from "expo-router";
export default function RootLayout() {
  return <Slot />
}
```
- `app/(a)/index.js`
```js
import { router, useNavigation } from "expo-router";
import {  View } from "react-native";

export default function App() {
  // const navigation = useNavigation();

  setTimeout(() => {
    router.push("/b");
    // navigation.push("b");
  });
  return (
    <View />
  );
}
```
- `app/b/index.js`
```js
import { usePathname } from 'expo-router';
import { Text, View } from 'react-native';

let i = 0;
export default function Page() {
  const path = usePathname();
  i++;
  return (
    <View style={{ flex: 1, backgroundColor: i > 1 ? "red" : "white" }}>
      <Text>Path: {path}</Text>
    </View>
  );
}
```



<!--
Please describe how you tested this change and how a reviewer could
reproduce your test, especially if this PR does not include automated
tests! If possible, please also provide terminal output and/or
screenshots demonstrating your test/reproduction.
-->

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [ ] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).

---------

Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com>
@EvanBacon
Copy link
Contributor

A fix has been published in expo-router@2.0.3 (2.0.3). If this didn't fix your particular issue, please open a new issue with a minimum reproducible issue. Sorry for any inconvenience this may have caused.

@Seimodei
Copy link

Can confirm that the new fix on expo-router version 2.0.4 solves the issue.

@idrakimuhamad
Copy link

What if I can't upgrade to v 2? I have some other sdk 49 bugs that limiting my base from upgrade to sdk 49 currently.

@Seimodei
Copy link

@idrakimuhamad Can't say for sure. As it seems the bug exists on earlier versions of expo-router. So you might have to look for another work around in terms of upgrading to the latest expo sdk.

@EvanBacon
Copy link
Contributor

@idrakimuhamad you can try pulling this patch into your local copy of expo-router 7b6032d and running npx patch-package expo-router.

@idrakimuhamad
Copy link

@idrakimuhamad you can try pulling this patch into your local copy of expo-router 7b6032d and running npx patch-package expo-router.

Sweet. Thanks!

@hellotingwai
Copy link

hellotingwai commented Sep 19, 2023

i don't know if it's a good solution. i used useEffect in the _layout.js and now it runs once.

import { Stack } from "expo-router";
import { Text } from "react-native";
import React, { useEffect, useState } from "react";

export default function Layout() {
  const [loading, setLoading] = useState('true')

  useEffect(()=>{
      setLoading(false);
  },[]);

  if(loading){
    return <Text>Loading...</Text>
  }else{
    return <Stack name="home" screenOptions={{ headerShown : false}} />;
  }
}

@markymc
Copy link

markymc commented Sep 22, 2023

A fix has been published in expo-router@2.0.3 (2.0.3). If this didn't fix your particular issue, please open a new issue with a minimum reproducible issue. Sorry for any inconvenience this may have caused.

Thanks! I updated to 2.0.8 and the double mount on tab switch seems to have stopped!

@HerpGuy88
Copy link

This bug has re-emerged in 3.4.6.
So far, that's the 2nd bug from a previous 2.0. version that had been fixed that's re-emerged in v3.

@augustosnk12
Copy link

It stills happens when I "minimize" the app and open it again. My "home" page re-render as much as I do this.

I'm using 3.4.5 expo router version.

@augustosnk12
Copy link

In some part of my app I replaced the push by navigation and it does not happen anymore

@PopBot
Copy link

PopBot commented Mar 6, 2024

I too am having the same issue as of March 2024. The frames do not properly mount on time, so each item in the stack for me must be wrapped by a useeffect, causing multiple renders.

@marklawlor
Copy link
Contributor

This repo (and v2) is in maintenance mode and will only receive updates for critical issues.

If you are experience this issue on v3, please create a new issue with a full reproduction on the https://github.com/expo/expo repo

@PopBot
Copy link

PopBot commented Mar 18, 2024

Gotcha, thanks @marklawlor!

@lukaskf
Copy link

lukaskf commented Apr 3, 2024

If someone finds this thread, maybe this will help.

I was having re-render issues on SDK 50 and using v3. Then fixed by

Replacing buttons that had
router.push("route-name")
with
<Link href="/route-name">

@pfcodes
Copy link

pfcodes commented Apr 12, 2024

Happening on SDK 50 and v3.4.8

@erbud
Copy link

erbud commented Apr 19, 2024

I keep reproducing the issue with Expo SDK 50.0.13 and Expo Router 3.4.8 when using useLocalSearchParams() on Android compilation …

@barrownicholas
Copy link

barrownicholas commented May 28, 2024

For better visibility and tracking, and since this seems to be a new iteration of a once-fixed issue, I re-opened a new issue with a new, minimal reproducable example: expo/expo#29163

Edit 1: as @lukaskf points out, this might be an issue with router.push (but his fix of using Link did not work for me)

@Streudal
Copy link

Streudal commented Sep 4, 2024

If someone finds this thread, maybe this will help.

I was having re-render issues on SDK 50 and using v3. Then fixed by

Replacing buttons that had router.push("route-name") with <Link href="/route-name">

This worked for me!

The imperative way of routing in expo-router is very powerful but may cause unwanted behavior if you don't fully understand it. My main issue was using router.push in most places but this would cause screens to stack as fast as I could push the button.

I think it's because the Link component, provided by expo-router, uses the navigation.navigate() method from react-navigation under the hood (so the peeps above saying that if you use router.navigation() or the useNavigation() hook may be correct for your use case because it does the same thing). Also, I don't have nested _layout routes but worth trying out.

Here are the docs that explains this: https://docs.expo.dev/router/navigating-pages/

Also, using router.navigate() was also an option.

@HelgiMagic
Copy link

HelgiMagic commented Oct 8, 2024

use router.navigate() or <Link href=""> instead of router.push

@zzorba
Copy link

zzorba commented Oct 10, 2024

For anyone else hitting this, in my situation it turned out that my pages were setup like this:

// file room/[id].tsx
export default function RoomScreen() {
  return (
    <>
      <Stack.Screen
        options={{
          title: 'Room',
          presentation: 'modal',
          animation: 'slide_from_right',
          gestureEnabled: true,
          gestureDirection: 'horizontal',
          fullScreenGestureEnabled: true,
        }}
      />
      <Room />
    </>
  );
}

Removing the <Stack.Screen> (so the content only contains <Room />) fixed the issue of multiple mounts of the Room component on every push.

DavidAmyot pushed a commit to Villeco-inc/expo-router that referenced this issue Oct 16, 2024
# Why

Prevent double renders by cloning state to avoid leaking state between
functions.

- fix expo/router#838 
- fix expo/router#733
- fix expo/router#320
- The issue expo/router#320 has multiple
different things linked, but the original case appears to be fixed.
- possibly also addresses expo/router#847

<!--
Please describe the motivation for this PR, and link to relevant GitHub
issues, forums posts, or feature requests.
-->

# How

- Prevent mutating the input state to avoid invalidating the nested
state.

# Test Plan

- The testing library doesn't seem to support this case. @marklawlor has
been tasked with ensuring the original branch can detect the error
https://github.com/expo/expo/compare/%40evanbacon/router/fix-838
Just in case the testing library isn't fixed, I ran locally with:

- `app/_layout.js`, `app/(a)/_layout.js`, `app/b/_layout.js`
```js
import { Slot } from "expo-router";
export default function RootLayout() {
  return <Slot />
}
```
- `app/(a)/index.js`
```js
import { router, useNavigation } from "expo-router";
import {  View } from "react-native";

export default function App() {
  // const navigation = useNavigation();

  setTimeout(() => {
    router.push("/b");
    // navigation.push("b");
  });
  return (
    <View />
  );
}
```
- `app/b/index.js`
```js
import { usePathname } from 'expo-router';
import { Text, View } from 'react-native';

let i = 0;
export default function Page() {
  const path = usePathname();
  i++;
  return (
    <View style={{ flex: 1, backgroundColor: i > 1 ? "red" : "white" }}>
      <Text>Path: {path}</Text>
    </View>
  );
}
```



<!--
Please describe how you tested this change and how a reviewer could
reproduce your test, especially if this PR does not include automated
tests! If possible, please also provide terminal output and/or
screenshots demonstrating your test/reproduction.
-->

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [ ] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).

---------

Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.