Skip to content

Conversation

@t0maboro
Copy link
Contributor

@t0maboro t0maboro commented Oct 31, 2025

Description

This PR updates the logic responsible for triggering the invalidate callback. We're now aligning the logic to use RCTComponentViewProtocol callback, when available.

Depending on the React Native architecture and version, the invalidate mechanism behaves differently:

  • Paper - the invalidate flow continues to use the RCTInvalidating protocol.
  • Fabric, RN < 0.82.0 - views implement RNSViewControllerInvalidating.
  • Fabric, RN starting from 0.82.0 - the recommended way to handle invalidation is through the callback provided by RCTComponentViewProtocol. This PR enables usage of that callback.

Changes

  • added RNSReactNativeVersionUtils - for runtime checks as the commit with the new method in protocol was CP to 0.82 release
  • added a common code for invalidation and extracted all 3 paths described above

Test code and steps to reproduce

Verified that breakpoints in invalidateImpl are hit when expected from the expected paths for:

  1. Paper
  2. Fabric 0.82.0-rc.4 (withour invalidate callback)
  3. Fabric 0.82.1 (with invalidate callback)
import React, { createContext, useContext, useState } from 'react';
import { enableFreeze } from 'react-native-screens';
import { View, Button, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import {
  NativeStackNavigationProp,
  createNativeStackNavigator
} from '@react-navigation/native-stack';
import {
  BottomTabsContainer,
  type TabConfiguration,
} from '../../shared/gamma/containers/bottom-tabs/BottomTabsContainer';
import ConfigWrapperContext, {
  type Configuration,
  DEFAULT_GLOBAL_CONFIGURATION,
} from '../../shared/gamma/containers/bottom-tabs/ConfigWrapperContext';

enableFreeze(true);

type TabToggleContextType = {
  toggleTabs: () => void;
};
const TabToggleContext = createContext<TabToggleContextType>({
  toggleTabs: () => {},
});
export const useTabToggle = () => useContext(TabToggleContext);

function Tab1() {
  const { toggleTabs } = useTabToggle();
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Tab 1</Text>
      <Button title="Toggle Tab 4" onPress={toggleTabs} />
    </View>
  );
}
function Tab2() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Tab 2</Text>
    </View>
  );
}
function Tab3() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Tab 3</Text>
    </View>
  );
}
function Tab4() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Tab 4</Text>
    </View>
  );
}

const ALL_TABS: TabConfiguration[] = [
  {
    tabScreenProps: {
      tabKey: 'Tab1',
      title: 'Tab1',
      icon: {
        ios: { type: 'sfSymbol', name: 'house.fill' },
        android: { type: 'imageSource', imageSource: require('../../../assets/variableIcons/icon_fill.png') }
      },
    },
    component: Tab1,
  },
  {
    tabScreenProps: {
      tabKey: 'Tab2',
      title: 'Tab2',
      icon: {
        ios: { type: 'sfSymbol', name: 'phone.fill' },
        android: { type: 'drawableResource', name: 'sym_call_missed' }
      },
    },
    component: Tab2,
  },
  {
    tabScreenProps: {
      tabKey: 'Tab3',
      title: 'Tab3',
      icon: {
        shared: {
          type: 'imageSource',
          imageSource: require('../../../assets/variableIcons/icon.png'),
        }
      },
    },
    component: Tab3,
  },
  {
    tabScreenProps: {
      tabKey: 'Tab4',
      title: 'Tab4',
      icon: {
        ios: { type: 'sfSymbol', name: 'rectangle.stack' },
        android: { type: 'drawableResource', name: 'custom_home_icon' }
      },
    },
    component: Tab4,
  },
];

function App({navigation}) {
  const [config, setConfig] = useState<Configuration>(DEFAULT_GLOBAL_CONFIGURATION);
  const [showAllTabs, setShowAllTabs] = useState<boolean>(true);

  const toggleTabs = () => {
    setShowAllTabs((prev) => !prev);
  };

  const tabsToRender = showAllTabs ? ALL_TABS : ALL_TABS.slice(0, 3);

  return (
    <ConfigWrapperContext.Provider value={{ config, setConfig }}>
      <TabToggleContext.Provider value={{ toggleTabs }}>
        <Button onPress={() => navigation.goBack()} title='Go back' />
        <BottomTabsContainer tabConfigs={tabsToRender} />
      </TabToggleContext.Provider>
    </ConfigWrapperContext.Provider>
  );
}

type RouteParamList = {
  Auth: undefined;
  Tabs: undefined;
};

type NavigationProp = NativeStackNavigationProp<RouteParamList>;
const Stack = createNativeStackNavigator<RouteParamList>();

function Auth({ navigation }: { navigation: NavigationProp }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Login Screen</Text>
      <Button title="Go to Tabs" onPress={() => navigation.push('Tabs')} />
    </View>
  );
}

export default function WrappedApp() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Auth" component={Auth} />
        <Stack.Screen name="Tabs" component={App} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

@t0maboro t0maboro force-pushed the @t0maboro/tabs-invalidation branch from 33dd2e4 to f060ee3 Compare November 3, 2025 12:07
@t0maboro t0maboro marked this pull request as ready for review November 3, 2025 12:30
Copy link
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I'm not a big fan of dispatching invalidation after 1 tick but if it's necessary then it is what it is I guess.

Co-authored-by: Krzysztof Ligarski <[email protected]>
@t0maboro t0maboro requested a review from kligarski November 4, 2025 08:54
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we complicate things too much here.

If the old solution is reliable (it is, right?) and the new one is only a refactor using new APIs & cleaning up the code, we defeat the purpose by introducing both & increasing complexity of the code even more. I am against landing this change before RN 0.82 becomes our minimal supported react native version. What do you think?

@t0maboro
Copy link
Contributor Author

t0maboro commented Nov 4, 2025

I think that we complicate things too much here.

If the old solution is reliable (it is, right?) and the new one is only a refactor using new APIs & cleaning up the code, we defeat the purpose by introducing both & increasing complexity of the code even more. I am against landing this change before RN 0.82 becomes our minimal supported react native version. What do you think?

The old solution works reliably and I agree that this PR adds additional complexity. We can wait until we support 0.82 as a minimal version, because these changes I'm introducing here are needed for backward compatibility atm
If you're okay with the current approaches to scanning the list of mutations (here and in stack v4), I'm also okay with that

@t0maboro
Copy link
Contributor Author

t0maboro commented Nov 4, 2025

@kkafar we have the same case in the stack v4: #3368 , therefore I believe that we should land both, or rather, both should be moved to on hold state

@kkafar
Copy link
Member

kkafar commented Nov 4, 2025

@t0maboro Let's move both to on-hold state. I think that we defeat the purpose currently by introducing two separate mechanism depending on version. Let's wait few weeks.

@t0maboro
Copy link
Contributor Author

t0maboro commented Nov 4, 2025

@t0maboro Let's move both to on-hold state. I think that we defeat the purpose currently by introducing two separate mechanism depending on version. Let's wait few weeks.

ack

@t0maboro
Copy link
Contributor Author

t0maboro commented Nov 4, 2025

Switching to draft until we drop support for RN versions prior to 0.82

@t0maboro t0maboro marked this pull request as draft November 4, 2025 11:20
@kligarski
Copy link
Contributor

Switching to draft until we drop support for RN versions prior to 0.82

When this is the case, we should also handle bottomAccessory. More context: #3288 (comment)

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 this pull request may close these issues.

4 participants