diff --git a/packages/react-navigation/examples/NavigationPlayground/js/App.js b/packages/react-navigation/examples/NavigationPlayground/js/App.js
index 31274260..a517c357 100644
--- a/packages/react-navigation/examples/NavigationPlayground/js/App.js
+++ b/packages/react-navigation/examples/NavigationPlayground/js/App.js
@@ -36,6 +36,7 @@ import StackWithTranslucentHeader from './StackWithTranslucentHeader';
import SimpleTabs from './SimpleTabs';
import SwitchWithStacks from './SwitchWithStacks';
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
+import TabsWithNavigationEvents from './TabsWithNavigationEvents';
import KeyboardHandlingExample from './KeyboardHandlingExample';
const ExampleInfo = {
@@ -126,6 +127,11 @@ const ExampleInfo = {
name: 'withNavigationFocus',
description: 'Receive the focus prop to know when a screen is focused',
},
+ TabsWithNavigationEvents: {
+ name: 'NavigationEvents',
+ description:
+ 'Declarative NavigationEvents component to subscribe to navigation events',
+ },
KeyboardHandlingExample: {
name: 'Keyboard Handling Example',
description:
@@ -166,6 +172,7 @@ const ExampleRoutes = {
path: 'settings',
},
TabsWithNavigationFocus,
+ TabsWithNavigationEvents,
KeyboardHandlingExample,
// This is commented out because it's rarely useful
// InactiveStack,
diff --git a/packages/react-navigation/examples/NavigationPlayground/js/TabsWithNavigationEvents.js b/packages/react-navigation/examples/NavigationPlayground/js/TabsWithNavigationEvents.js
new file mode 100644
index 00000000..9b3af7a9
--- /dev/null
+++ b/packages/react-navigation/examples/NavigationPlayground/js/TabsWithNavigationEvents.js
@@ -0,0 +1,127 @@
+/**
+ * @flow
+ */
+
+import React from 'react';
+import { FlatList, SafeAreaView, StatusBar, Text, View } from 'react-native';
+import { NavigationEvents } from 'react-navigation';
+import { createMaterialBottomTabNavigator } from 'react-navigation-material-bottom-tabs';
+import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
+
+const Event = ({ event }) => (
+
+ {event.type}
+
+ {event.action.type.replace('Navigation/', '')}
+ {event.action.routeName ? '=>' + event.action.routeName : ''}
+
+
+);
+
+const createTabScreen = (name, icon, focusedIcon) => {
+ class TabScreen extends React.Component {
+ static navigationOptions = {
+ tabBarLabel: name,
+ tabBarIcon: ({ tintColor, focused }) => (
+
+ ),
+ };
+
+ state = { eventLog: [] };
+
+ append = navigationEvent => {
+ this.setState(({ eventLog }) => ({
+ eventLog: eventLog.concat(navigationEvent),
+ }));
+ };
+
+ render() {
+ return (
+
+
+ Events for tab {name}
+
+
+
+ `${this.state.eventLog.indexOf(item)}`}
+ renderItem={({ item }) => (
+
+
+
+ )}
+ />
+
+
+
+
+
+
+ );
+ }
+ }
+
+ return TabScreen;
+};
+
+const TabsWithNavigationEvents = createMaterialBottomTabNavigator(
+ {
+ One: {
+ screen: createTabScreen('One', 'numeric-1-box-outline', 'numeric-1-box'),
+ },
+ Two: {
+ screen: createTabScreen('Two', 'numeric-2-box-outline', 'numeric-2-box'),
+ },
+ Three: {
+ screen: createTabScreen(
+ 'Three',
+ 'numeric-3-box-outline',
+ 'numeric-3-box'
+ ),
+ },
+ },
+ {
+ shifting: false,
+ activeTintColor: '#F44336',
+ }
+);
+
+export default TabsWithNavigationEvents;
diff --git a/packages/react-navigation/flow/react-navigation.js b/packages/react-navigation/flow/react-navigation.js
index 77ad9a59..6d58b9b0 100644
--- a/packages/react-navigation/flow/react-navigation.js
+++ b/packages/react-navigation/flow/react-navigation.js
@@ -557,6 +557,21 @@ declare module 'react-navigation' {
navigationOptions?: O,
}>;
+ /**
+ * NavigationEvents component
+ */
+
+ declare type _NavigationEventsProps = {
+ navigation?: NavigationScreenProp,
+ onWillFocus?: NavigationEventCallback,
+ onDidFocus?: NavigationEventCallback,
+ onWillBlur?: NavigationEventCallback,
+ onDidBlur?: NavigationEventCallback,
+ };
+ declare export var NavigationEvents: React$ComponentType<
+ _NavigationEventsProps
+ >;
+
/**
* Navigation container
*/
diff --git a/packages/react-navigation/src/react-navigation.js b/packages/react-navigation/src/react-navigation.js
index 500b4e56..e424d601 100644
--- a/packages/react-navigation/src/react-navigation.js
+++ b/packages/react-navigation/src/react-navigation.js
@@ -156,6 +156,11 @@ module.exports = {
return require('./views/SwitchView/SwitchView').default;
},
+ // NavigationEvents
+ get NavigationEvents() {
+ return require('./views/NavigationEvents').default;
+ },
+
// HOCs
get withNavigation() {
return require('./views/withNavigation').default;
diff --git a/packages/react-navigation/src/react-navigation.web.js b/packages/react-navigation/src/react-navigation.web.js
index 30f0464c..e4f0e22f 100644
--- a/packages/react-navigation/src/react-navigation.web.js
+++ b/packages/react-navigation/src/react-navigation.web.js
@@ -42,6 +42,11 @@ module.exports = {
return require('./routers/SwitchRouter').default;
},
+ // NavigationEvents
+ get NavigationEvents() {
+ return require('./views/NavigationEvents').default;
+ },
+
// HOCs
get withNavigation() {
return require('./views/withNavigation').default;
diff --git a/packages/react-navigation/src/views/NavigationEvents.js b/packages/react-navigation/src/views/NavigationEvents.js
new file mode 100644
index 00000000..84d7e918
--- /dev/null
+++ b/packages/react-navigation/src/views/NavigationEvents.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import withNavigation from './withNavigation';
+
+const EventNameToPropName = {
+ willFocus: 'onWillFocus',
+ didFocus: 'onDidFocus',
+ willBlur: 'onWillBlur',
+ didBlur: 'onDidBlur',
+};
+
+const EventNames = Object.keys(EventNameToPropName);
+
+class NavigationEvents extends React.Component {
+ componentDidMount() {
+ this.subscriptions = {};
+ EventNames.forEach(this.addListener);
+ }
+
+ componentDidUpdate(prevProps) {
+ EventNames.forEach(eventName => {
+ const listenerHasChanged =
+ this.props[EventNameToPropName[eventName]] !==
+ prevProps[EventNameToPropName[eventName]];
+ if (listenerHasChanged) {
+ this.removeListener(eventName);
+ this.addListener(eventName);
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ EventNames.forEach(this.removeListener);
+ }
+
+ addListener = eventName => {
+ const listener = this.props[EventNameToPropName[eventName]];
+ if (listener) {
+ this.subscriptions[eventName] = this.props.navigation.addListener(
+ eventName,
+ listener
+ );
+ }
+ };
+
+ removeListener = eventName => {
+ if (this.subscriptions[eventName]) {
+ this.subscriptions[eventName].remove();
+ this.subscriptions[eventName] = undefined;
+ }
+ };
+
+ render() {
+ return null;
+ }
+}
+
+export default withNavigation(NavigationEvents);
diff --git a/packages/react-navigation/src/views/__tests__/NavigationEvents-test.js b/packages/react-navigation/src/views/__tests__/NavigationEvents-test.js
new file mode 100644
index 00000000..8a4c8968
--- /dev/null
+++ b/packages/react-navigation/src/views/__tests__/NavigationEvents-test.js
@@ -0,0 +1,241 @@
+import React from 'react';
+import { View } from 'react-native';
+import renderer from 'react-test-renderer';
+import NavigationEvents from '../NavigationEvents';
+import { NavigationProvider } from '../NavigationContext';
+
+const createListener = () => payload => {};
+
+// An easy way to create the 4 listeners prop
+const createEventListenersProp = () => ({
+ onWillFocus: createListener(),
+ onDidFocus: createListener(),
+ onWillBlur: createListener(),
+ onDidBlur: createListener(),
+});
+
+const createNavigationAndHelpers = () => {
+ // A little API to spy on subscription remove calls that are performed during the tests
+ const removeCallsAPI = (() => {
+ let removeCalls = [];
+ return {
+ reset: () => {
+ removeCalls = [];
+ },
+ add: (name, handler) => {
+ removeCalls.push({ name, handler });
+ },
+ checkRemoveCalled: count => {
+ expect(removeCalls.length).toBe(count);
+ },
+ checkRemoveCalledWith: (name, handler) => {
+ expect(removeCalls).toContainEqual({ name, handler });
+ },
+ };
+ })();
+
+ const navigation = {
+ addListener: jest.fn((name, handler) => {
+ return {
+ remove: () => removeCallsAPI.add(name, handler),
+ };
+ }),
+ };
+
+ const checkAddListenerCalled = count => {
+ expect(navigation.addListener).toHaveBeenCalledTimes(count);
+ };
+ const checkAddListenerCalledWith = (eventName, handler) => {
+ expect(navigation.addListener).toHaveBeenCalledWith(eventName, handler);
+ };
+ const checkRemoveCalled = count => {
+ removeCallsAPI.checkRemoveCalled(count);
+ };
+ const checkRemoveCalledWith = (eventName, handler) => {
+ removeCallsAPI.checkRemoveCalledWith(eventName, handler);
+ };
+
+ return {
+ navigation,
+ removeCallsAPI,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ checkRemoveCalled,
+ checkRemoveCalledWith,
+ };
+};
+
+// We test 2 distinct ways to provide the navigation to the NavigationEvents (prop/context)
+const NavigationEventsTestComp = ({
+ withContext = true,
+ navigation,
+ ...props
+}) => {
+ if (withContext) {
+ return (
+
+
+
+ );
+ } else {
+ return ;
+ }
+};
+
+describe('NavigationEvents', () => {
+ it('add all listeners with navigation prop', () => {
+ const {
+ navigation,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ } = createNavigationAndHelpers();
+ const eventListenerProps = createEventListenersProp();
+ const component = renderer.create(
+
+ );
+ checkAddListenerCalled(4);
+ checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
+ checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
+ });
+
+ it('add all listeners with navigation context', () => {
+ const {
+ navigation,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ } = createNavigationAndHelpers();
+ const eventListenerProps = createEventListenersProp();
+ const component = renderer.create(
+
+ );
+ checkAddListenerCalled(4);
+ checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
+ checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
+ });
+
+ it('remove all listeners on unmount', () => {
+ const {
+ navigation,
+ checkRemoveCalled,
+ checkRemoveCalledWith,
+ } = createNavigationAndHelpers();
+ const eventListenerProps = createEventListenersProp();
+
+ const component = renderer.create(
+
+ );
+ checkRemoveCalled(0);
+ component.unmount();
+ checkRemoveCalled(4);
+ checkRemoveCalledWith('willBlur', eventListenerProps.onWillBlur);
+ checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
+ });
+
+ it('add a single listener', () => {
+ const {
+ navigation,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ } = createNavigationAndHelpers();
+ const listener = createListener();
+ const component = renderer.create(
+
+ );
+ checkAddListenerCalled(1);
+ checkAddListenerCalledWith('didFocus', listener);
+ });
+
+ it('do not attempt to add/remove stable listeners on update', () => {
+ const {
+ navigation,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ } = createNavigationAndHelpers();
+ const eventListenerProps = createEventListenersProp();
+ const component = renderer.create(
+
+ );
+ component.update(
+
+ );
+ component.update(
+
+ );
+ checkAddListenerCalled(4);
+ checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
+ checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
+ });
+
+ it('add, remove and replace (remove+add) listeners on complex updates', () => {
+ const {
+ navigation,
+ checkAddListenerCalled,
+ checkAddListenerCalledWith,
+ checkRemoveCalled,
+ checkRemoveCalledWith,
+ } = createNavigationAndHelpers();
+ const eventListenerProps = createEventListenersProp();
+
+ const component = renderer.create(
+
+ );
+
+ checkAddListenerCalled(4);
+ checkAddListenerCalledWith('willBlur', eventListenerProps.onWillBlur);
+ checkAddListenerCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkAddListenerCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkAddListenerCalledWith('didFocus', eventListenerProps.onDidFocus);
+ checkRemoveCalled(0);
+
+ const onWillFocus2 = createListener();
+ const onDidFocus2 = createListener();
+
+ component.update(
+
+ );
+ checkAddListenerCalled(6);
+ checkAddListenerCalledWith('willFocus', onWillFocus2);
+ checkAddListenerCalledWith('didFocus', onDidFocus2);
+ checkRemoveCalled(3);
+ checkRemoveCalledWith('didBlur', eventListenerProps.onDidBlur);
+ checkRemoveCalledWith('willFocus', eventListenerProps.onWillFocus);
+ checkRemoveCalledWith('didFocus', eventListenerProps.onDidFocus);
+ });
+});