diff --git a/src/__tests__/getNavigation-test.js b/src/__tests__/getNavigation-test.js deleted file mode 100644 index 5acaadb..0000000 --- a/src/__tests__/getNavigation-test.js +++ /dev/null @@ -1,97 +0,0 @@ -import getNavigation from '../getNavigation'; - -test('getNavigation provides default action helpers', () => { - const router = { - getActionCreators: () => ({}), - getStateForAction(action, lastState = {}) { - return lastState; - }, - }; - - const dispatch = jest.fn(); - - const topNav = getNavigation( - router, - {}, - dispatch, - new Set(), - () => ({}), - () => topNav - ); - - topNav.navigate('GreatRoute'); - - expect(dispatch.mock.calls.length).toBe(1); - expect(dispatch.mock.calls[0][0].type).toBe('Navigation/NAVIGATE'); - expect(dispatch.mock.calls[0][0].routeName).toBe('GreatRoute'); -}); - -test('getNavigation provides router action helpers', () => { - const router = { - getActionCreators: () => ({ - foo: bar => ({ type: 'FooBarAction', bar }), - }), - getStateForAction(action, lastState = {}) { - return lastState; - }, - }; - - const dispatch = jest.fn(); - - const topNav = getNavigation( - router, - {}, - dispatch, - new Set(), - () => ({}), - () => topNav - ); - - topNav.foo('Great'); - - expect(dispatch.mock.calls.length).toBe(1); - expect(dispatch.mock.calls[0][0].type).toBe('FooBarAction'); - expect(dispatch.mock.calls[0][0].bar).toBe('Great'); -}); - -test('getNavigation get child navigation with router', () => { - const actionSubscribers = new Set(); - let navigation = null; - - const routerA = { - getActionCreators: () => ({}), - getStateForAction(action, lastState = {}) { - return lastState; - }, - }; - const router = { - childRouters: { - RouteA: routerA, - }, - getActionCreators: () => ({}), - getStateForAction(action, lastState = {}) { - return lastState; - }, - }; - - const initState = { - index: 0, - routes: [ - { key: 'a', routeName: 'RouteA' }, - { key: 'b', routeName: 'RouteB' }, - ], - }; - - const topNav = getNavigation( - router, - initState, - () => {}, - actionSubscribers, - () => ({}), - () => navigation - ); - - const childNavA = topNav.getChildNavigation('a'); - - expect(childNavA.router).toBe(routerA); -}); diff --git a/src/createNavigationContainer.js b/src/createNavigationContainer.js index 8c3db81..5cbbb59 100644 --- a/src/createNavigationContainer.js +++ b/src/createNavigationContainer.js @@ -4,8 +4,8 @@ import { polyfill } from 'react-lifecycles-compat'; import { BackHandler } from './PlatformHelpers'; import NavigationActions from './NavigationActions'; -import getNavigation from './getNavigation'; import invariant from './utils/invariant'; +import getNavigationActionCreators from './routers/getNavigationActionCreators'; import docsUrl from './utils/docsUrl'; function isStateful(props) { @@ -358,24 +358,34 @@ export default function createNavigationContainer(Component) { return false; }; - _getScreenProps = () => this.props.screenProps; - render() { let navigation = this.props.navigation; if (this._isStateful()) { - const navState = this.state.nav; - if (!navState) { + const nav = this.state.nav; + if (!nav) { return this._renderLoading(); } - if (!this._navigation || this._navigation.state !== navState) { - this._navigation = getNavigation( - Component.router, - navState, - this.dispatch, - this._actionEventSubscribers, - this._getScreenProps, - () => this._navigation - ); + if (!this._navigation || this._navigation.state !== nav) { + this._navigation = { + dispatch: this.dispatch, + state: nav, + addListener: (eventName, handler) => { + if (eventName !== 'action') { + return { remove: () => {} }; + } + this._actionEventSubscribers.add(handler); + return { + remove: () => { + this._actionEventSubscribers.delete(handler); + }, + }; + }, + }; + const actionCreators = getNavigationActionCreators(nav); + Object.keys(actionCreators).forEach(actionName => { + this._navigation[actionName] = (...args) => + this.dispatch(actionCreators[actionName](...args)); + }); } navigation = this._navigation; } diff --git a/src/getChildNavigation.js b/src/getChildNavigation.js deleted file mode 100644 index 4a8ee6e..0000000 --- a/src/getChildNavigation.js +++ /dev/null @@ -1,88 +0,0 @@ -import getChildEventSubscriber from './getChildEventSubscriber'; -import getChildRouter from './getChildRouter'; - -const createParamGetter = route => (paramName, defaultValue) => { - const params = route.params; - - if (params && paramName in params) { - return params[paramName]; - } - - return defaultValue; -}; - -function getChildNavigation(navigation, childKey, getCurrentParentNavigation) { - const children = - navigation._childrenNavigation || (navigation._childrenNavigation = {}); - - const route = navigation.state.routes.find(r => r.key === childKey); - - if (children[childKey] && children[childKey].state === route) { - return children[childKey]; - } - - const childRouter = getChildRouter(navigation.router, route.routeName); - - const actionCreators = { - ...navigation.actions, - ...navigation.router.getActionCreators(route, navigation.state.key), - }; - const actionHelpers = {}; - Object.keys(actionCreators).forEach(actionName => { - actionHelpers[actionName] = (...args) => { - const actionCreator = actionCreators[actionName]; - const action = actionCreator(...args); - navigation.dispatch(action); - }; - }); - - if (children[childKey]) { - children[childKey] = { - ...children[childKey], - ...actionHelpers, - state: route, - router: childRouter, - actions: actionCreators, - getParam: createParamGetter(route), - }; - return children[childKey]; - } - - const childSubscriber = getChildEventSubscriber( - navigation.addListener, - childKey - ); - - children[childKey] = { - ...actionHelpers, - - state: route, - router: childRouter, - actions: actionCreators, - getParam: createParamGetter(route), - - getChildNavigation: grandChildKey => - getChildNavigation(children[childKey], grandChildKey, () => - getCurrentParentNavigation().getChildNavigation(childKey) - ), - - isFocused: childKey => { - const currentNavigation = getCurrentParentNavigation(); - const { routes, index } = currentNavigation.state; - if (!currentNavigation.isFocused()) { - return false; - } - if (childKey == null || routes[index].key === childKey) { - return true; - } - return false; - }, - dispatch: navigation.dispatch, - getScreenProps: navigation.getScreenProps, - dangerouslyGetParent: getCurrentParentNavigation, - addListener: childSubscriber.addListener, - }; - return children[childKey]; -} - -export default getChildNavigation; diff --git a/src/getChildRouter.js b/src/getChildRouter.js deleted file mode 100644 index 74bfbc2..0000000 --- a/src/getChildRouter.js +++ /dev/null @@ -1,9 +0,0 @@ -export default function getChildRouter(router, routeName) { - if (router.childRouters && router.childRouters[routeName]) { - return router.childRouters[routeName]; - } - - const Component = router.getComponentForRouteName(routeName); - - return Component.router; -} diff --git a/src/getNavigation.js b/src/getNavigation.js deleted file mode 100644 index 236de18..0000000 --- a/src/getNavigation.js +++ /dev/null @@ -1,54 +0,0 @@ -import getNavigationActionCreators from './routers/getNavigationActionCreators'; -import getChildNavigation from './getChildNavigation'; - -export default function getNavigation( - router, - state, - dispatch, - actionSubscribers, - getScreenProps, - getCurrentNavigation -) { - const actions = router.getActionCreators(state, null); - - const navigation = { - actions, - router, - state, - dispatch, - getScreenProps, - getChildNavigation: childKey => - getChildNavigation(navigation, childKey, getCurrentNavigation), - isFocused: childKey => { - const { routes, index } = getCurrentNavigation().state; - if (childKey == null || routes[index].key === childKey) { - return true; - } - return false; - }, - addListener: (eventName, handler) => { - if (eventName !== 'action') { - return { remove: () => {} }; - } - actionSubscribers.add(handler); - return { - remove: () => { - actionSubscribers.delete(handler); - }, - }; - }, - dangerouslyGetParent: () => null, - }; - - const actionCreators = { - ...getNavigationActionCreators(navigation.state), - ...actions, - }; - - Object.keys(actionCreators).forEach(actionName => { - navigation[actionName] = (...args) => - navigation.dispatch(actionCreators[actionName](...args)); - }); - - return navigation; -} diff --git a/src/navigators/createNavigator.js b/src/navigators/createNavigator.js index 59b23ed..b445350 100644 --- a/src/navigators/createNavigator.js +++ b/src/navigators/createNavigator.js @@ -10,48 +10,122 @@ function createNavigator(NavigatorView, router, navigationConfig) { state = { descriptors: {}, + childEventSubscribers: {}, }; static getDerivedStateFromProps(nextProps, prevState) { - const prevDescriptors = prevState.descriptors; const { navigation, screenProps } = nextProps; const { dispatch, state, addListener } = navigation; const { routes } = state; - const descriptors = {}; + const descriptors = { ...prevState.descriptors }; + const childEventSubscribers = { ...prevState.childEventSubscribers }; routes.forEach(route => { - if ( - prevDescriptors && - prevDescriptors[route.key] && - route === prevDescriptors[route.key].state - ) { - descriptors[route.key] = prevDescriptors[route.key]; - return; + if (!descriptors[route.key] || descriptors[route.key].state !== route) { + const getComponent = () => + router.getComponentForRouteName(route.routeName); + + if (!childEventSubscribers[route.key]) { + childEventSubscribers[route.key] = getChildEventSubscriber( + addListener, + route.key + ); + } + + const actionCreators = { + ...navigation.actions, + ...router.getActionCreators(route, state.key), + }; + const actionHelpers = {}; + Object.keys(actionCreators).forEach(actionName => { + actionHelpers[actionName] = (...args) => { + const actionCreator = actionCreators[actionName]; + const action = actionCreator(...args); + dispatch(action); + }; + }); + const childNavigation = { + ...actionHelpers, + actions: actionCreators, + dispatch, + state: route, + addListener: childEventSubscribers[route.key].addListener, + getParam: (paramName, defaultValue) => { + const params = route.params; + + if (params && paramName in params) { + return params[paramName]; + } + + return defaultValue; + }, + }; + + const options = router.getScreenOptions(childNavigation, screenProps); + descriptors[route.key] = { + key: route.key, + getComponent, + options, + state: route, + navigation: childNavigation, + }; } - const getComponent = () => - router.getComponentForRouteName(route.routeName); - const childNavigation = navigation.getChildNavigation(route.key); - const options = router.getScreenOptions(childNavigation, screenProps); - descriptors[route.key] = { - key: route.key, - getComponent, - options, - state: route, - navigation: childNavigation, - }; }); - return { descriptors }; + return { + descriptors, + childEventSubscribers, + }; } + // Cleanup subscriptions for routes that no longer exist + componentDidUpdate() { + const activeKeys = this.props.navigation.state.routes.map(r => r.key); + let childEventSubscribers = { ...this.state.childEventSubscribers }; + Object.keys(childEventSubscribers).forEach(key => { + if (!activeKeys.includes(key)) { + delete childEventSubscribers[key]; + } + }); + if ( + childEventSubscribers.length !== this.state.childEventSubscribers.length + ) { + this.setState({ childEventSubscribers }); + } + } + + _isRouteFocused = route => { + const { state } = this.props.navigation; + const focusedRoute = state.routes[state.index]; + return route === focusedRoute; + }; + + _dangerouslyGetParent = () => { + return this.props.navigation; + }; + render() { + // Mutation in render 😩 + // The problem: + // - We don't want to re-render each screen every time the parent navigator changes + // - But we need to be able to access the parent navigator from callbacks + // - These functions should only be used within callbacks, but they are passed in props, + // which is what makes this awkward. What's a good way to pass in stuff that we don't + // want people to depend on in render? + let descriptors = { ...this.state.descriptors }; + Object.values(descriptors).forEach(descriptor => { + descriptor.navigation.isFocused = () => + this._isRouteFocused(descriptor.state); + descriptor.navigation.dangerouslyGetParent = this._dangerouslyGetParent; + }); + return ( ); } diff --git a/src/routers/StackRouter.js b/src/routers/StackRouter.js index 872642d..bd48044 100644 --- a/src/routers/StackRouter.js +++ b/src/routers/StackRouter.js @@ -149,8 +149,6 @@ export default (routeConfigs, stackConfig = {}) => { paths.sort((a, b) => b[1].priority - a[1].priority); return { - childRouters, - getComponentForState(state) { const activeChildRoute = state.routes[state.index]; const { routeName } = activeChildRoute; diff --git a/src/routers/SwitchRouter.js b/src/routers/SwitchRouter.js index 7005bde..cd375c1 100644 --- a/src/routers/SwitchRouter.js +++ b/src/routers/SwitchRouter.js @@ -75,8 +75,6 @@ export default (routeConfigs, config = {}) => { } return { - childRouters, - getInitialState() { const routes = order.map(resetChildRoute); return {