Revert "Refactor, move child navigation to navigation prop (#4425)"

This reverts commit ba62509ff44236efe026040479b28c4c52205332.
This commit is contained in:
Brent Vatne 2018-06-06 13:19:08 -07:00
parent 395abe5200
commit 6a946d6ab7
8 changed files with 120 additions and 288 deletions

View File

@ -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);
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 (
<NavigatorView
{...this.props}
screenProps={this.props.screenProps}
navigation={this.props.navigation}
navigationConfig={navigationConfig}
descriptors={this.state.descriptors}
descriptors={descriptors}
/>
);
}

View File

@ -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;

View File

@ -75,8 +75,6 @@ export default (routeConfigs, config = {}) => {
}
return {
childRouters,
getInitialState() {
const routes = order.map(resetChildRoute);
return {