feat: return a NavigationContent component from useNavigationBuilder

This commit is contained in:
Satyajit Sahoo
2021-05-10 00:12:49 +02:00
parent a6e498170f
commit 1179d56c50
14 changed files with 246 additions and 198 deletions

View File

@@ -70,7 +70,12 @@ function BottomTabNavigator({
);
}
const { state, descriptors, navigation } = useNavigationBuilder<
const {
state,
descriptors,
navigation,
NavigationContent,
} = useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
@@ -85,13 +90,15 @@ function BottomTabNavigator({
});
return (
<BottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
sceneContainerStyle={sceneContainerStyle}
/>
<NavigationContent>
<BottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
sceneContainerStyle={sceneContainerStyle}
/>
</NavigationContent>
);
}

View File

@@ -1,8 +1,7 @@
import * as React from 'react';
import { StyleSheet, Platform } from 'react-native';
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
import {
NavigationHelpersContext,
import type {
ParamListBase,
TabNavigationState,
} from '@react-navigation/native';
@@ -89,72 +88,70 @@ export default function BottomTabView(props: Props) {
const { routes } = state;
return (
<NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat>
<MaybeScreenContainer
enabled={detachInactiveScreens}
style={styles.container}
>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { lazy = true, unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
<SafeAreaProviderCompat>
<MaybeScreenContainer
enabled={detachInactiveScreens}
style={styles.container}
>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { lazy = true, unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
if (unmountOnBlur && !isFocused) {
return null;
}
if (unmountOnBlur && !isFocused) {
return null;
}
if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a lazy screen if we've never navigated to it
return null;
}
if (lazy && !loaded.includes(route.key) && !isFocused) {
// Don't render a lazy screen if we've never navigated to it
return null;
}
const {
header = ({ layout, options }: BottomTabHeaderProps) => (
<Header
{...options}
layout={layout}
title={getHeaderTitle(options, route.name)}
/>
),
} = descriptor.options;
const {
header = ({ layout, options }: BottomTabHeaderProps) => (
<Header
{...options}
layout={layout}
title={getHeaderTitle(options, route.name)}
/>
),
} = descriptor.options;
return (
<MaybeScreen
key={route.key}
style={StyleSheet.absoluteFill}
visible={isFocused}
enabled={detachInactiveScreens}
>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
<Screen
focused={isFocused}
route={descriptor.route}
navigation={descriptor.navigation}
headerShown={descriptor.options.headerShown}
headerStatusBarHeight={
descriptor.options.headerStatusBarHeight
}
header={header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
options: descriptor.options,
})}
style={sceneContainerStyle}
>
{descriptor.render()}
</Screen>
</BottomTabBarHeightContext.Provider>
</MaybeScreen>
);
})}
</MaybeScreenContainer>
<BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
{renderTabBar()}
</BottomTabBarHeightCallbackContext.Provider>
</SafeAreaProviderCompat>
</NavigationHelpersContext.Provider>
return (
<MaybeScreen
key={route.key}
style={StyleSheet.absoluteFill}
visible={isFocused}
enabled={detachInactiveScreens}
>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
<Screen
focused={isFocused}
route={descriptor.route}
navigation={descriptor.navigation}
headerShown={descriptor.options.headerShown}
headerStatusBarHeight={
descriptor.options.headerStatusBarHeight
}
header={header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
options: descriptor.options,
})}
style={sceneContainerStyle}
>
{descriptor.render()}
</Screen>
</BottomTabBarHeightContext.Provider>
</MaybeScreen>
);
})}
</MaybeScreenContainer>
<BottomTabBarHeightCallbackContext.Provider value={setTabBarHeight}>
{renderTabBar()}
</BottomTabBarHeightCallbackContext.Provider>
</SafeAreaProviderCompat>
);
}

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
export default function useComponent<
T extends React.ComponentType<any>,
P extends {}
>(Component: T, props: P) {
const propsRef = React.useRef<P | null>(props);
// Normally refs shouldn't be mutated in render
// But we return a component which will be rendered
// So it's just for immediate consumption
propsRef.current = props;
React.useEffect(() => {
propsRef.current = null;
});
return React.useRef((rest: Omit<React.ComponentProps<T>, keyof P>) => {
const props = propsRef.current;
if (props === null) {
throw new Error(
'The returned component must be rendered in the same render phase as the hook.'
);
}
// @ts-expect-error: the props should be fine here
return <Component {...props} {...rest} />;
}).current;
}

View File

@@ -14,6 +14,7 @@ import {
} from '@react-navigation/routers';
import NavigationStateContext from './NavigationStateContext';
import NavigationRouteContext from './NavigationRouteContext';
import NavigationHelpersContext from './NavigationHelpersContext';
import Group from './Group';
import Screen from './Screen';
import useEventEmitter from './useEventEmitter';
@@ -29,6 +30,7 @@ import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import useComponent from './useComponent';
import isArrayEqual from './isArrayEqual';
import {
DefaultNavigatorOptions,
@@ -586,9 +588,14 @@ export default function useNavigationBuilder<
descriptors,
});
const NavigationContent = useComponent(NavigationHelpersContext.Provider, {
value: navigation,
});
return {
state,
navigation,
descriptors,
NavigationContent,
};
}

View File

@@ -67,7 +67,12 @@ function DrawerNavigator({
);
}
const { state, descriptors, navigation } = useNavigationBuilder<
const {
state,
descriptors,
navigation,
NavigationContent,
} = useNavigationBuilder<
DrawerNavigationState<ParamListBase>,
DrawerRouterOptions,
DrawerActionHelpers<ParamListBase>,
@@ -83,12 +88,14 @@ function DrawerNavigator({
});
return (
<DrawerView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
<NavigationContent>
<DrawerView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
</NavigationContent>
);
}

View File

@@ -9,7 +9,6 @@ import {
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import Animated from 'react-native-reanimated';
import {
NavigationHelpersContext,
DrawerNavigationState,
DrawerActions,
useTheme,
@@ -296,13 +295,11 @@ function DrawerViewBase({
export default function DrawerView({ navigation, ...rest }: Props) {
return (
<NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat>
<GestureHandlerWrapper style={styles.content}>
<DrawerViewBase navigation={navigation} {...rest} />
</GestureHandlerWrapper>
</SafeAreaProviderCompat>
</NavigationHelpersContext.Provider>
<SafeAreaProviderCompat>
<GestureHandlerWrapper style={styles.content}>
<DrawerViewBase navigation={navigation} {...rest} />
</GestureHandlerWrapper>
</SafeAreaProviderCompat>
);
}

View File

@@ -28,7 +28,12 @@ function MaterialBottomTabNavigator({
screenOptions,
...rest
}: Props) {
const { state, descriptors, navigation } = useNavigationBuilder<
const {
state,
descriptors,
navigation,
NavigationContent,
} = useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
@@ -42,12 +47,14 @@ function MaterialBottomTabNavigator({
});
return (
<MaterialBottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
/>
<NavigationContent>
<MaterialBottomTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
/>
</NavigationContent>
);
}

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { Text, StyleSheet, Platform } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import {
NavigationHelpersContext,
Route,
TabNavigationState,
TabActions,
@@ -75,7 +74,7 @@ try {
};
}
function MaterialBottomTabViewInner({
export default function MaterialBottomTabView({
state,
navigation,
descriptors,
@@ -192,14 +191,6 @@ function MaterialBottomTabViewInner({
);
}
export default function MaterialBottomTabView(props: Props) {
return (
<NavigationHelpersContext.Provider value={props.navigation}>
<MaterialBottomTabViewInner {...props} />
</NavigationHelpersContext.Provider>
);
}
const styles = StyleSheet.create({
icon: {
backgroundColor: 'transparent',

View File

@@ -71,7 +71,12 @@ function MaterialTopTabNavigator({
);
}
const { state, descriptors, navigation } = useNavigationBuilder<
const {
state,
descriptors,
navigation,
NavigationContent,
} = useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
@@ -85,12 +90,14 @@ function MaterialTopTabNavigator({
});
return (
<MaterialTopTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
/>
<NavigationContent>
<MaterialTopTabView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
/>
</NavigationContent>
);
}

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import { TabView, SceneRendererProps } from 'react-native-tab-view';
import {
NavigationHelpersContext,
TabNavigationState,
TabActions,
ParamListBase,
@@ -43,29 +42,27 @@ export default function MaterialTopTabView({
};
return (
<NavigationHelpersContext.Provider value={navigation}>
<TabView<Route<string>>
{...rest}
onIndexChange={(index) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={renderTabBar}
renderLazyPlaceholder={({ route }) =>
descriptors[route.key].options.lazyPlaceholder?.() ?? null
}
lazy={({ route }) => descriptors[route.key].options.lazy === true}
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
/>
</NavigationHelpersContext.Provider>
<TabView<Route<string>>
{...rest}
onIndexChange={(index) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={renderTabBar}
renderLazyPlaceholder={({ route }) =>
descriptors[route.key].options.lazyPlaceholder?.() ?? null
}
lazy={({ route }) => descriptors[route.key].options.lazy === true}
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
/>
);
}

View File

@@ -4,7 +4,6 @@ import {
createNavigatorFactory,
StackRouter,
TabRouter,
NavigationHelpersContext,
createNavigationContainerRef,
ParamListBase,
} from '@react-navigation/core';
@@ -22,36 +21,36 @@ it('integrates with the history API', () => {
jest.useFakeTimers();
const createStackNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
const { state, descriptors, NavigationContent } = useNavigationBuilder(
StackRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
<NavigationContent>
{state.routes.map((route, i) => (
<div key={route.key} aria-current={state.index === i || undefined}>
{descriptors[route.key].render()}
</div>
))}
</NavigationHelpersContext.Provider>
</NavigationContent>
);
});
const createTabNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
const { state, descriptors, NavigationContent } = useNavigationBuilder(
TabRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
<NavigationContent>
{state.routes.map((route, i) => (
<div key={route.key} aria-current={state.index === i || undefined}>
{descriptors[route.key].render()}
</div>
))}
</NavigationHelpersContext.Provider>
</NavigationContent>
);
});

View File

@@ -4,7 +4,6 @@ import {
createNavigatorFactory,
StackRouter,
TabRouter,
NavigationHelpersContext,
NavigatorScreenParams,
} from '@react-navigation/core';
import { renderToString } from 'react-dom/server';
@@ -23,17 +22,17 @@ jest.mock('../useLinking', () => require('../useLinking.tsx').default);
it('renders correct state with location', () => {
const createStackNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
const { state, descriptors, NavigationContent } = useNavigationBuilder(
StackRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
<NavigationContent>
{state.routes.map((route) => (
<div key={route.key}>{descriptors[route.key].render()}</div>
))}
</NavigationHelpersContext.Provider>
</NavigationContent>
);
});
@@ -116,17 +115,17 @@ it('renders correct state with location', () => {
it('gets the current options', () => {
const createTabNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
const { state, descriptors, NavigationContent } = useNavigationBuilder(
TabRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
<NavigationContent>
{state.routes.map((route) => (
<div key={route.key}>{descriptors[route.key].render()}</div>
))}
</NavigationHelpersContext.Provider>
</NavigationContent>
);
});

View File

@@ -51,7 +51,12 @@ function StackNavigator({
`Stack Navigator: 'headerMode' is moved to 'options'. Moved it to 'screenOptions' to keep current behavior.`
);
const { state, descriptors, navigation } = useNavigationBuilder<
const {
state,
descriptors,
navigation,
NavigationContent,
} = useNavigationBuilder<
StackNavigationState<ParamListBase>,
StackRouterOptions,
StackActionHelpers<ParamListBase>,
@@ -94,12 +99,14 @@ function StackNavigator({
);
return (
<StackView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
<NavigationContent>
<StackView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
</NavigationContent>
);
}

View File

@@ -5,7 +5,6 @@ import {
EdgeInsets,
} from 'react-native-safe-area-context';
import {
NavigationHelpersContext,
StackActions,
StackNavigationState,
Route,
@@ -418,7 +417,6 @@ export default class StackView extends React.Component<Props, State> {
render() {
const {
state,
navigation,
keyboardHandlingEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
descriptors: _,
@@ -433,45 +431,43 @@ export default class StackView extends React.Component<Props, State> {
} = this.state;
return (
<NavigationHelpersContext.Provider value={navigation}>
<GestureHandlerWrapper style={styles.container}>
<SafeAreaProviderCompat>
<SafeAreaInsetsContext.Consumer>
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<HeaderShownContext.Consumer>
{(isParentHeaderShown) => (
<CardStack
insets={insets as EdgeInsets}
isParentHeaderShown={isParentHeaderShown}
getPreviousRoute={this.getPreviousRoute}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}
onGestureEnd={this.handleGestureEnd}
onGestureCancel={this.handleGestureCancel}
{...rest}
{...props}
/>
)}
</HeaderShownContext.Consumer>
)}
</KeyboardManager>
)}
</SafeAreaInsetsContext.Consumer>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
</NavigationHelpersContext.Provider>
<GestureHandlerWrapper style={styles.container}>
<SafeAreaProviderCompat>
<SafeAreaInsetsContext.Consumer>
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<HeaderShownContext.Consumer>
{(isParentHeaderShown) => (
<CardStack
insets={insets as EdgeInsets}
isParentHeaderShown={isParentHeaderShown}
getPreviousRoute={this.getPreviousRoute}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}
onGestureEnd={this.handleGestureEnd}
onGestureCancel={this.handleGestureCancel}
{...rest}
{...props}
/>
)}
</HeaderShownContext.Consumer>
)}
</KeyboardManager>
)}
</SafeAreaInsetsContext.Consumer>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
);
}
}