react-native-screens icon indicating copy to clipboard operation
react-native-screens copied to clipboard

feat(iOS, Tabs): Migrate to new invalidate method

Open t0maboro opened this issue 2 months ago • 6 comments

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

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

t0maboro avatar Oct 31 '25 16:10 t0maboro

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 avatar Nov 04 '25 10:11 t0maboro

@kkafar we have the same case in the stack v4: https://github.com/software-mansion/react-native-screens/pull/3368 , therefore I believe that we should land both, or rather, both should be moved to on hold state

t0maboro avatar Nov 04 '25 10:11 t0maboro

@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.

kkafar avatar Nov 04 '25 11:11 kkafar

@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 avatar Nov 04 '25 11:11 t0maboro

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

t0maboro avatar Nov 04 '25 11:11 t0maboro

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: https://github.com/software-mansion/react-native-screens/pull/3288#discussion_r2460578537

kligarski avatar Nov 05 '25 11:11 kligarski