From c0045d82b3a80413cb228c1eaa0aaef7c8140567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Osadnik?= Date: Wed, 21 Aug 2019 12:33:29 +0100 Subject: [PATCH] feat: add dangerouslyGetParent (#62) --- packages/core/src/__tests__/index.test.tsx | 64 +++++++++++++++- .../core/src/__tests__/useNavigation.test.tsx | 74 +++++++++++++++++++ packages/core/src/types.tsx | 8 ++ packages/core/src/useNavigationCache.tsx | 7 +- 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/index.test.tsx b/packages/core/src/__tests__/index.test.tsx index 1a5b6039..d9ec1a2b 100644 --- a/packages/core/src/__tests__/index.test.tsx +++ b/packages/core/src/__tests__/index.test.tsx @@ -421,6 +421,68 @@ it('updates route params with setParams', () => { }); }); +it('updates route params with setParams applied to parent', () => { + const TestNavigator = (props: any) => { + const { state, descriptors } = useNavigationBuilder(MockRouter, props); + + return descriptors[state.routes[state.index].key].render(); + }; + + let setParams: (params: object) => void = () => undefined; + + const FooScreen = (props: any) => { + const parent = props.navigation.dangerouslyGetParent(); + if (parent) { + setParams = parent.setParams; + } + + return null; + }; + + const onStateChange = jest.fn(); + + render( + + + + {() => ( + + + + )} + + + + + ); + + act(() => setParams({ username: 'alice' })); + + expect(onStateChange).toBeCalledTimes(1); + expect(onStateChange).lastCalledWith({ + index: 0, + key: '0', + routeNames: ['foo', 'bar'], + routes: [ + { key: 'foo', name: 'foo', params: { username: 'alice' } }, + { key: 'bar', name: 'bar' }, + ], + }); + + act(() => setParams({ age: 25 })); + + expect(onStateChange).toBeCalledTimes(2); + expect(onStateChange).lastCalledWith({ + index: 0, + key: '0', + routeNames: ['foo', 'bar'], + routes: [ + { key: 'foo', name: 'foo', params: { username: 'alice', age: 25 } }, + { key: 'bar', name: 'bar' }, + ], + }); +}); + it('handles change in route names', () => { const TestNavigator = (props: any): any => { useNavigationBuilder(MockRouter, props); @@ -491,7 +553,7 @@ it('throws if navigator is not inside a container', () => { ); }); -it('throws if muliple navigators rendered under one container', () => { +it('throws if multiple navigators rendered under one container', () => { const TestNavigator = (props: any) => { useNavigationBuilder(MockRouter, props); return null; diff --git a/packages/core/src/__tests__/useNavigation.test.tsx b/packages/core/src/__tests__/useNavigation.test.tsx index e7bc664b..3321c9ab 100644 --- a/packages/core/src/__tests__/useNavigation.test.tsx +++ b/packages/core/src/__tests__/useNavigation.test.tsx @@ -32,6 +32,80 @@ it('gets navigation prop from context', () => { ); }); +it("gets navigation's parent from context", () => { + expect.assertions(1); + + const TestNavigator = (props: any): any => { + const { state, descriptors } = useNavigationBuilder(MockRouter, props); + + return state.routes.map(route => descriptors[route.key].render()); + }; + + const Test = () => { + const navigation = useNavigation(); + + expect(navigation.dangerouslyGetParent()).toBeDefined(); + + return null; + }; + + render( + + + + {() => ( + + + + )} + + + + ); +}); + +it("gets navigation's parent's parent from context", () => { + expect.assertions(2); + + const TestNavigator = (props: any): any => { + const { state, descriptors } = useNavigationBuilder(MockRouter, props); + + return state.routes.map(route => descriptors[route.key].render()); + }; + + const Test = () => { + const navigation = useNavigation(); + const parent = navigation.dangerouslyGetParent(); + + expect(parent).toBeDefined(); + if (parent !== undefined) { + expect(parent.navigate).toBeDefined(); + } + + return null; + }; + + render( + + + + {() => ( + + + {() => ( + + + + )} + + + )} + + + + ); +}); + it('throws if called outside a navigation context', () => { expect.assertions(1); diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index b947ed1e..fc4b9661 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -369,6 +369,14 @@ export type NavigationProp< * It can be useful to decide whether to display a back button in a stack. */ isFirstRouteInParent(): boolean; + /** + * Returns the parent navigator, if any. Reason why the function is called + * dangerouslyGetParent is to warn developers against overusing it to eg. get parent + * of parent and other hard-to-follow patterns. + */ + dangerouslyGetParent(): + | NavigationProp + | undefined; } & EventConsumer & PrivateValueStore; diff --git a/packages/core/src/useNavigationCache.tsx b/packages/core/src/useNavigationCache.tsx index 2c46f2e4..a3e7101b 100644 --- a/packages/core/src/useNavigationCache.tsx +++ b/packages/core/src/useNavigationCache.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import * as BaseActions from './BaseActions'; import { NavigationEventEmitter } from './useEventEmitter'; +import NavigationContext from './NavigationContext'; + import { NavigationAction, NavigationHelpers, @@ -52,6 +54,8 @@ export default function useNavigationCache< ...BaseActions, }; + const parentNavigation = React.useContext(NavigationContext); + cache.current = state.routes.reduce>( (acc, route, index) => { const previous = cache.current[route.key]; @@ -87,6 +91,7 @@ export default function useNavigationCache< ...rest, ...helpers, ...emitter.create(route.key), + dangerouslyGetParent: () => parentNavigation, dispatch, setOptions: (options: object) => setOptions(o => ({ @@ -100,7 +105,7 @@ export default function useNavigationCache< return false; } - // If the current screen is focused, we also need to check if parent navigtor is focused + // If the current screen is focused, we also need to check if parent navigator is focused // This makes sure that we return the focus state in the whole tree, not just this navigator return navigation ? navigation.isFocused() : true; },