0

Currently experiencing an issue where in my React Native app, on android only, when first entering with expo, the UI is not correct, when doing any change in _layout the UI will snap to place

Screen before fast refresh Screen after fast refresh

When debugging with expo, elements appear in one place altough the UI debugger will show them in another

  "dependencies": {
    "@react-native-async-storage/async-storage": "1.23.1",
    "@react-native-community/netinfo": "11.4.1",
    "@react-navigation/native": "^7.0.0",
    "@rn-primitives/alert-dialog": "^1.1.0",
    "@rn-primitives/avatar": "~1.1.0",
    "@rn-primitives/dropdown-menu": "^1.1.0",
    "@rn-primitives/label": "^1.1.0",
    "@rn-primitives/popover": "^1.1.0",
    "@rn-primitives/portal": "~1.1.0",
    "@rn-primitives/progress": "~1.1.0",
    "@rn-primitives/select": "^1.1.0",
    "@rn-primitives/separator": "^1.1.0",
    "@rn-primitives/slot": "~1.1.0",
    "@rn-primitives/toggle": "^1.1.0",
    "@rn-primitives/toggle-group": "^1.1.0",
    "@rn-primitives/tooltip": "~1.1.0",
    "@rn-primitives/types": "~1.1.0",
    "@shopify/flash-list": "1.7.3",
    "@tanstack/react-query": "^5.69.0",
    "axios": "^1.8.4",
    "axois": "^0.0.1-security",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "expo": "~52.0.46",
    "expo-apple-authentication": "~7.1.3",
    "expo-auth-session": "~6.0.3",
    "expo-clipboard": "~7.0.1",
    "expo-device": "~7.0.3",
    "expo-linking": "~7.0.4",
    "expo-location": "~18.0.10",
    "expo-navigation-bar": "~4.0.9",
    "expo-notifications": "~0.29.14",
    "expo-router": "~4.0.20",
    "expo-secure-store": "~14.0.1",
    "expo-splash-screen": "~0.29.24",
    "expo-status-bar": "~2.0.1",
    "expo-system-ui": "~4.0.9",
    "i18next": "^24.2.3",
    "lottie-react-native": "7.1.0",
    "lucide-react-native": "^0.378.0",
    "nativewind": "^4.1.23",
    "qs": "^6.14.0",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-i18next": "^15.4.1",
    "react-native": "0.76.9",
    "react-native-gesture-handler": "~2.20.2",
    "react-native-reanimated": "~3.16.1",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.4.0",
    "react-native-svg": "15.8.0",
    "react-native-svg-transformer": "^1.5.0",
    "react-native-toast-message": "^2.2.1",
    "react-native-web": "~0.19.13",
    "react-phone-number-input": "^3.4.12",
    "tailwind-merge": "^2.2.1",
    "tailwindcss": "3.3.5",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.24.2",
    "zustand": "^4.4.7",
    "expo-dev-client": "~5.0.20"
  },
  "devDependencies": {
    "@babel/core": "^7.26.0",
    "@types/qs": "^6.9.18",
    "@types/react": "~18.3.12",
    "typescript": "^5.3.3"
  },

My current project folder structure: Folder structure

Root layout:


import "~/global.css";
import {
  DarkTheme,
  DefaultTheme,
  Theme,
  ThemeProvider,
} from "@react-navigation/native";
import { Slot } from "expo-router";
import { StatusBar } from "expo-status-bar";
import * as React from "react";
import { AppState, AppStateStatus, Dimensions, Platform } from "react-native";
import { NAV_THEME } from "~/lib/constants";
import { useColorScheme } from "~/lib/useColorScheme";
import { PortalHost } from "@rn-primitives/portal";
import { setAndroidNavigationBar } from "~/lib/android-navigation-bar";
import AuthContextProvider from "~/features/auth/AuthContextProvider";
import "../i18n";
import NetInfo from "@react-native-community/netinfo";
import {
  onlineManager,
  QueryClient,
  QueryClientProvider,
  focusManager,
} from "@tanstack/react-query";
import {
  configureReanimatedLogger,
  ReanimatedLogLevel,
} from "react-native-reanimated";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import OrderContextProvider from "~/features/order/context/OrderContextProvider";
import Toast from "react-native-toast-message";
import AppSettingsContextProvider from "~/features/appSettings/AppSettingsContextProvider";
import {
  SafeAreaFrameContext,
  SafeAreaInsetsContext,
  SafeAreaView,
  SafeAreaContext,
} from "react-native-safe-area-context";

// This is the default configuration
configureReanimatedLogger({
  level: ReanimatedLogLevel.warn,
  strict: false, // Reanimated runs in strict mode by default
});

onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

function onAppStateChange(status: AppStateStatus) {
  if (Platform.OS !== "web") {
    focusManager.setFocused(status === "active");
  }
}

const LIGHT_THEME: Theme = {
  ...DefaultTheme,
  colors: NAV_THEME.light,
};
const DARK_THEME: Theme = {
  ...DarkTheme,
  colors: NAV_THEME.dark,
};
const queryClient = new QueryClient();

export {
  // Catch any errors thrown by the Layout component.
  ErrorBoundary,
} from "expo-router";

export default function RootLayout() {
  const hasMounted = React.useRef(false);
  const { colorScheme, isDarkColorScheme } = useColorScheme();
  const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false);
  useIsomorphicLayoutEffect(() => {
    if (hasMounted.current) {
      return;
    }

    if (Platform.OS === "web") {
      // Adds the background color to the html element to prevent white background on overscroll.
      document.documentElement.classList.add("bg-background");
    }
    setAndroidNavigationBar(colorScheme);
    setIsColorSchemeLoaded(true);
    hasMounted.current = true;
  }, []);

  React.useEffect(() => {
    const subscription = AppState.addEventListener("change", onAppStateChange);

    return () => subscription.remove();
  }, []);

  if (!isColorSchemeLoaded || !hasMounted.current) {
    return null;
  }
  return (
    <GestureHandlerRootView>
      <QueryClientProvider client={queryClient}>
        <AppSettingsContextProvider>
          <AuthContextProvider>
            <ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
              <OrderContextProvider>
                <Slot />
                <Toast />
                <StatusBar style={isDarkColorScheme ? "light" : "dark"} />
                <PortalHost />
              </OrderContextProvider>
            </ThemeProvider>
          </AuthContextProvider>
        </AppSettingsContextProvider>
      </QueryClientProvider>
    </GestureHandlerRootView>
  );
}

const useIsomorphicLayoutEffect =
  Platform.OS === "web" && typeof window === "undefined"
    ? React.useEffect
    : React.useLayoutEffect;

First child of root layout

import { Stack } from "expo-router";
import usePushNotifications from "~/shared/hooks/usePushNotifications";

export default function RootLayout() {
  usePushNotifications();
  return (
    <Stack initialRouteName="(auth)" screenOptions={{ headerShown: false }}>
      {/* Initial route name auth else tabs */}
      <Stack.Screen name="(auth)" />
      <Stack.Screen name="(tabs)" />
      <Stack.Screen
        name="(order)"
        options={{
          presentation: "modal", // Enables modal behavior
          animation: "fade_from_bottom", // Smooth transition
          gestureEnabled: true, // Enables swipe gestures
        }}
      />
    </Stack>
  );
}

Auth layout(example issue screen parent layout)


import { Stack, useRouter } from "expo-router";
import { Pressable } from "react-native";
import { ArrowLeft } from "lucide-react-native";
import GuestRoute from "~/features/auth/components/GuestRoute";

export default function RootLayout() {
  const { back } = useRouter();

  return (
    <GuestRoute>
      <Stack
        initialRouteName="login"
        screenOptions={{
          headerLeft: ({ canGoBack }) =>
            canGoBack && (
              <Pressable onPress={back}>
                <ArrowLeft className="text-foreground" />
              </Pressable>
            ),
          title: "",
        }}
      >
        <Stack.Screen name="index" />
        <Stack.Screen name="login" />
        <Stack.Screen name="register" />
      </Stack>
    </GuestRoute>
  );
}

Login screen

import { AppleAuthenticationButtonType } from "expo-apple-authentication";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform, Pressable, SafeAreaView, View } from "react-native";
import { emailLoginSchema, TEmailLogin } from "~/features/auth/auth.types";
import AppleAuthButton from "~/features/auth/components/AppleAuthButton";
import useAuth from "~/features/auth/hooks/useAuth";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Separator } from "~/components/ui/separator";
import { Text } from "~/components/ui/text";
import useForm from "~/shared/hooks/useForm";
import GuestButton from "~/features/auth/components/GuestButton";

export default function LoginScreen() {
  const { t } = useTranslation();
  const { navigate } = useRouter();
  const { form, errors, handleChange, submit } = useForm<TEmailLogin>(
    undefined,
    emailLoginSchema
  );
  const { handleLogin, isPendingLogin } = useAuth();
  const handleSubmit = () => {
    submit(handleLogin);
  };

  return (
    <SafeAreaView className="bg-secondary/30 flex-1">
      <View className="flex-1  p-6">
        <Text className="text-4xl font-semibold mb-12">
          {t("auth.greetingLogin")}
        </Text>
        <View className="flex-1 gap-y-4">
          <Input
            label={t("auth.email")}
            keyboardType="email-address"
            autoComplete="email"
            autoCapitalize="none"
            placeholder={t("auth.enterEmail")}
            value={form.email}
            onChangeText={(val) => handleChange("email", val)}
            error={errors.email}
          />
          <Input
            label={t("auth.password")}
            placeholder={t("auth.enterPassword")}
            secureTextEntry
            value={form.password}
            onChangeText={(val) => handleChange("password", val)}
            error={errors.password}
          />
          <Pressable className="ml-auto">
            <Text className="text-right text-muted-foreground">
              {t("auth.forgotPass")}
            </Text>
          </Pressable>
          <Button
            size="lg"
            onPress={handleSubmit}
            disabled={!(form.email && form.password) || isPendingLogin}
          >
            <Text>{t("auth.login")}</Text>
          </Button>
          <View className="flex flex-row items-center mt-6 mb-2">
            <Separator className="flex-1" />
            <Text className="text-muted-foreground px-4">
              {t("auth.otherLogin")}
            </Text>
            <Separator className="flex-1" />
          </View>
          {Platform.select({
            ios: (
              <AppleAuthButton type={AppleAuthenticationButtonType.SIGN_IN} />
            ),
            android: null,
            // <GoogleAuthButton />
          })}
          <View className="flex flex-row gap-x-2 justify-center mt-auto mb-2 items-center">
            <Text>{t("auth.noAccount")}</Text>
            <Pressable onPress={() => navigate("/register")}>
              <Text className="font-bold text-blue-500">
                {t("auth.registerNow")}
              </Text>
            </Pressable>
          </View>
          <GuestButton />
        </View>
      </View>
    </SafeAreaView>
  );
}

I tried returning the screen directly, skipping the rendering of navigator to see if it would work if no Navigator container, and turns out it works perfectly without a navigator, so my current guess is that Stack is causing some issues regarding it's wrapper

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.