feat(iOS, Tabs): Migrate to new invalidate method
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
RCTInvalidatingprotocol. - 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:
- Paper
- Fabric 0.82.0-rc.4 (withour invalidate callback)
- 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
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
@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 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 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
Switching to draft until we drop support for RN versions prior to 0.82
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