From f4180295bf22e32c65f6a7ab7089523cb2de58fb Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 10 Jul 2020 22:33:13 +0200 Subject: [PATCH] feat: add a getComponent prop to lazily specify components --- example/src/Screens/CompatAPI.tsx | 4 +- example/src/index.tsx | 2 +- packages/compat/src/CompatScreen.tsx | 20 ++---- .../src/createCompatNavigatorFactory.tsx | 8 +-- packages/core/src/SceneView.tsx | 13 ++-- packages/core/src/__tests__/index.test.tsx | 64 ++++++++++++++++++- packages/core/src/types.tsx | 12 ++++ packages/core/src/useNavigationBuilder.tsx | 28 +++++++- 8 files changed, 118 insertions(+), 33 deletions(-) diff --git a/example/src/Screens/CompatAPI.tsx b/example/src/Screens/CompatAPI.tsx index c30e5920..5ffc4ae4 100644 --- a/example/src/Screens/CompatAPI.tsx +++ b/example/src/Screens/CompatAPI.tsx @@ -127,8 +127,8 @@ const CompatStack = createCompatStackNavigator< StackNavigationProp >( { - Feed: FeedScreen, - Article: ArticleScreen, + Feed: { getScreen: () => FeedScreen }, + Article: { getScreen: () => ArticleScreen }, }, { navigationOptions: { headerShown: false } } ), diff --git a/example/src/index.tsx b/example/src/index.tsx index 3ef475ea..12602dff 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -362,7 +362,7 @@ export default function App() { SCREENS[name].component} options={{ title: SCREENS[name].title }} /> ) diff --git a/packages/compat/src/CompatScreen.tsx b/packages/compat/src/CompatScreen.tsx index 96f32302..f415fcab 100644 --- a/packages/compat/src/CompatScreen.tsx +++ b/packages/compat/src/CompatScreen.tsx @@ -1,25 +1,17 @@ import * as React from 'react'; -import type { - NavigationProp, - ParamListBase, - RouteProp, -} from '@react-navigation/native'; import ScreenPropsContext from './ScreenPropsContext'; import useCompatNavigation from './useCompatNavigation'; -type Props = { - navigation: NavigationProp; - route: RouteProp; - component: React.ComponentType; +type Props = { + getComponent: () => React.ComponentType; }; -function ScreenComponent( - props: Props -) { +function CompatScreen({ getComponent }: Props) { const navigation = useCompatNavigation(); const screenProps = React.useContext(ScreenPropsContext); + const ScreenComponent = getComponent(); - return ; + return ; } -export default React.memo(ScreenComponent); +export default React.memo(CompatScreen); diff --git a/packages/compat/src/createCompatNavigatorFactory.tsx b/packages/compat/src/createCompatNavigatorFactory.tsx index a8abec7d..24a3f4f5 100644 --- a/packages/compat/src/createCompatNavigatorFactory.tsx +++ b/packages/compat/src/createCompatNavigatorFactory.tsx @@ -142,13 +142,7 @@ export default function createCompatNavigatorFactory< initialParams={{ ...parentRouteParams, ...initialParams }} options={screenOptions} > - {({ navigation, route }) => ( - - )} + {() => } ); }), diff --git a/packages/core/src/SceneView.tsx b/packages/core/src/SceneView.tsx index 126afd09..87e1c53f 100644 --- a/packages/core/src/SceneView.tsx +++ b/packages/core/src/SceneView.tsx @@ -95,19 +95,22 @@ export default function SceneView< ] ); + const ScreenComponent = screen.getComponent + ? screen.getComponent() + : screen.component; + return ( - {'component' in screen && screen.component !== undefined ? ( - - ) : 'children' in screen && screen.children !== undefined ? ( + {ScreenComponent !== undefined ? ( + + ) : screen.children !== undefined ? ( screen.children({ navigation, route }) ) : null} diff --git a/packages/core/src/__tests__/index.test.tsx b/packages/core/src/__tests__/index.test.tsx index 35ef3ece..1ba3e86e 100644 --- a/packages/core/src/__tests__/index.test.tsx +++ b/packages/core/src/__tests__/index.test.tsx @@ -1403,6 +1403,7 @@ it('throws if both children and component are passed', () => { const element = ( + {/* @ts-ignore */} {jest.fn()} @@ -1415,6 +1416,48 @@ it('throws if both children and component are passed', () => { ); }); +it('throws if both children and getComponent are passed', () => { + const TestNavigator = (props: any) => { + useNavigationBuilder(MockRouter, props); + return null; + }; + + const element = ( + + + {/* @ts-ignore */} + + {jest.fn()} + + + + ); + + expect(() => render(element).update(element)).toThrowError( + "Got both 'getComponent' and 'children' props for the screen 'foo'. You must pass only one of them." + ); +}); + +it('throws if both component and getComponent are passed', () => { + const TestNavigator = (props: any) => { + useNavigationBuilder(MockRouter, props); + return null; + }; + + const element = ( + + + {/* @ts-ignore */} + + + + ); + + expect(() => render(element).update(element)).toThrowError( + "Got both 'component' and 'getComponent' props for the screen 'foo'. You must pass only one of them." + ); +}); + it('throws descriptive error for undefined screen component', () => { const TestNavigator = (props: any) => { useNavigationBuilder(MockRouter, props); @@ -1430,7 +1473,7 @@ it('throws descriptive error for undefined screen component', () => { ); expect(() => render(element).update(element)).toThrowError( - "Couldn't find a 'component' or 'children' prop for the screen 'foo'" + "Couldn't find a 'component', 'getComponent' or 'children' prop for the screen 'foo'" ); }); @@ -1453,6 +1496,25 @@ it('throws descriptive error for invalid screen component', () => { ); }); +it('throws descriptive error for invalid getComponent prop', () => { + const TestNavigator = (props: any) => { + useNavigationBuilder(MockRouter, props); + return null; + }; + + const element = ( + + + + + + ); + + expect(() => render(element).update(element)).toThrowError( + "Got an invalid value for 'getComponent' prop for the screen 'foo'. It must be a function returning a React Component." + ); +}); + it('throws descriptive error for invalid children', () => { const TestNavigator = (props: any) => { useNavigationBuilder(MockRouter, props); diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index dac13087..02b2ab41 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -406,6 +406,16 @@ export type RouteConfig< * React component to render for this screen. */ component: React.ComponentType; + getComponent?: never; + children?: never; + } + | { + /** + * Lazily get a React component to render for this screen. + */ + getComponent: () => React.ComponentType; + component?: never; + children?: never; } | { /** @@ -415,6 +425,8 @@ export type RouteConfig< route: RouteProp; navigation: any; }) => React.ReactNode; + component?: never; + getComponent?: never; } ); diff --git a/packages/core/src/useNavigationBuilder.tsx b/packages/core/src/useNavigationBuilder.tsx index b46f95c3..37d774a7 100644 --- a/packages/core/src/useNavigationBuilder.tsx +++ b/packages/core/src/useNavigationBuilder.tsx @@ -102,7 +102,7 @@ const getRouteConfigsFromChildren = < if (process.env.NODE_ENV !== 'production') { configs.forEach((config) => { - const { name, children, component } = config as any; + const { name, children, component, getComponent } = config; if (typeof name !== 'string' || !name) { throw new Error( @@ -112,13 +112,29 @@ const getRouteConfigsFromChildren = < ); } - if (children != null || component !== undefined) { + if ( + children != null || + component !== undefined || + getComponent !== undefined + ) { if (children != null && component !== undefined) { throw new Error( `Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.` ); } + if (children != null && getComponent !== undefined) { + throw new Error( + `Got both 'getComponent' and 'children' props for the screen '${name}'. You must pass only one of them.` + ); + } + + if (component !== undefined && getComponent !== undefined) { + throw new Error( + `Got both 'component' and 'getComponent' props for the screen '${name}'. You must pass only one of them.` + ); + } + if (children != null && typeof children !== 'function') { throw new Error( `Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.` @@ -131,6 +147,12 @@ const getRouteConfigsFromChildren = < ); } + if (getComponent !== undefined && typeof getComponent !== 'function') { + throw new Error( + `Got an invalid value for 'getComponent' prop for the screen '${name}'. It must be a function returning a React Component.` + ); + } + if (typeof component === 'function' && component.name === 'component') { // Inline anonymous functions passed in the `component` prop will have the name of the prop // It's relatively safe to assume that it's not a component since it should also have PascalCase name @@ -141,7 +163,7 @@ const getRouteConfigsFromChildren = < } } else { throw new Error( - `Couldn't find a 'component' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.` + `Couldn't find a 'component', 'getComponent' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.` ); } });