diff --git src/wp-includes/class-wp-customize-nav-menus.php src/wp-includes/class-wp-customize-nav-menus.php
index 1197dc1..c85a9eb 100644
--- src/wp-includes/class-wp-customize-nav-menus.php
+++ src/wp-includes/class-wp-customize-nav-menus.php
@@ -822,6 +822,15 @@ final class WP_Customize_Nav_Menus {
 	//
 
 	/**
+	 * Nav menu args used for each instance, keyed by the args HMAC.
+	 *
+	 * @since 4.3.0
+	 * @access public
+	 * @var array
+	 */
+	public $preview_nav_menu_instance_args = array();
+
+	/**
 	 * Filter arguments for dynamic nav_menu selective refresh partials.
 	 *
 	 * @since 4.5.0
@@ -862,6 +871,7 @@ final class WP_Customize_Nav_Menus {
 		add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
 		add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
 		add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
+		add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
 	}
 
 	/**
@@ -881,7 +891,7 @@ final class WP_Customize_Nav_Menus {
 		 * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
 		 * selective refreshed if...
 		 */
-		$can_selective_refresh = (
+		$can_partial_refresh = (
 			// ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
 			! empty( $args['echo'] )
 			&&
@@ -904,13 +914,16 @@ final class WP_Customize_Nav_Menus {
 				( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
 			)
 		);
-
-		if ( ! $can_selective_refresh ) {
-			return $args;
-		}
+		$args['can_partial_refresh'] = $can_partial_refresh;
 
 		$exported_args = $args;
 
+		// Empty out args which may not be JSON-serializable.
+		if ( ! $can_partial_refresh ) {
+			$exported_args['fallback_cb'] = '';
+			$exported_args['walker'] = '';
+		}
+
 		/*
 		 * Replace object menu arg with a term_id menu arg, as this exports better
 		 * to JS and is easier to compare hashes.
@@ -923,7 +936,7 @@ final class WP_Customize_Nav_Menus {
 		$exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
 
 		$args['customize_preview_nav_menus_args'] = $exported_args;
-
+		$this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
 		return $args;
 	}
 
@@ -942,7 +955,7 @@ final class WP_Customize_Nav_Menus {
 	 * @return null
 	 */
 	public function filter_wp_nav_menu( $nav_menu_content, $args ) {
-		if ( ! empty( $args->customize_preview_nav_menus_args ) ) {
+		if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
 			$attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
 			$attributes .= ' data-customize-partial-type="nav_menu_instance"';
 			$attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
@@ -987,11 +1000,15 @@ final class WP_Customize_Nav_Menus {
 	 * Exports data from PHP to JS.
 	 *
 	 * @since 4.3.0
-	 * @deprecated 4.5.0 Obsolete
 	 * @access public
 	 */
 	public function export_preview_data() {
-		_deprecated_function( __METHOD__, '4.5.0' );
+
+		// Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
+		$exports = array(
+			'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
+		);
+		printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
 	}
 
 	/**
diff --git src/wp-includes/js/customize-preview-nav-menus.js src/wp-includes/js/customize-preview-nav-menus.js
index 1ba3c87..464f3ac 100644
--- src/wp-includes/js/customize-preview-nav-menus.js
+++ src/wp-includes/js/customize-preview-nav-menus.js
@@ -1,7 +1,15 @@
+/* global _wpCustomizePreviewNavMenusExports */
 wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( $, _, wp, api ) {
 	'use strict';
 
-	var self = {};
+	var self = {
+		data: {
+			navMenuInstanceArgs: {}
+		}
+	};
+	if ( 'undefined' !== typeof _wpCustomizePreviewNavMenusExports ) {
+		_.extend( self.data, _wpCustomizePreviewNavMenusExports );
+	}
 
 	/**
 	 * Initialize nav menus preview.
@@ -10,7 +18,15 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
 		var self = this;
 
 		if ( api.selectiveRefresh ) {
-			self.watchNavMenuLocationChanges();
+			api.each( function( setting ) {
+				self.bindSettingListener( setting );
+			} );
+			api.bind( 'add', function( setting ) {
+				self.bindSettingListener( setting, { fire: true } );
+			} );
+			api.bind( 'remove', function( setting ) {
+				self.unbindSettingListener( setting );
+			} );
 		}
 
 		api.preview.bind( 'active', function() {
@@ -128,6 +144,31 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
 			},
 
 			/**
+			 * Make sure that partial fallback behavior is invoked if there is no associated menu.
+			 *
+			 * @since 4.5.0
+			 *
+			 * @returns {Promise}
+			 */
+			refresh: function() {
+				var partial = this, menuId, deferred = $.Deferred();
+
+				// Make sure the fallback behavior is invoked when
+				if ( _.isNumber( partial.params.navMenuArgs.menu ) ) {
+					menuId = partial.params.navMenuArgs.menu;
+				} else if ( partial.params.navMenuArgs.theme_location && api.has( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ) ) {
+					menuId = api( 'nav_menu_locations[' + partial.params.navMenuArgs.theme_location + ']' ).get();
+				}
+				if ( ! menuId ) {
+					partial.fallback();
+					deferred.reject();
+					return deferred.promise();
+				}
+
+				return api.selectiveRefresh.Partial.prototype.refresh.call( partial );
+			},
+
+			/**
 			 * Render content.
 			 *
 			 * @inheritdoc
@@ -135,6 +176,12 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
 			 */
 			renderContent: function( placement ) {
 				var partial = this, previousContainer = placement.container;
+
+				// Do fallback behavior to refresh preview if menu is now empty.
+				if ( '' === placement.addedContent ) {
+					placement.partial.fallback();
+				}
+
 				if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
 
 					// Trigger deprecated event.
@@ -152,33 +199,148 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
 		api.selectiveRefresh.partialConstructor.nav_menu_instance = self.NavMenuInstancePartial;
 
 		/**
-		 * Watch for changes to nav_menu_locations[] settings.
+		 * Request full refresh if there are nav menu instances that lack partials which also match the supplied args.
 		 *
-		 * Refresh partials associated with the given nav_menu_locations[] setting,
-		 * or request an entire preview refresh if there are no containers in the
-		 * document for a partial associated with the theme location.
+		 * @param {object} navMenuInstanceArgs
+		 */
+		self.handleUnplacedNavMenuInstances = function( navMenuInstanceArgs ) {
+			var unplacedNavMenuInstances;
+			unplacedNavMenuInstances = _.filter( _.values( self.data.navMenuInstanceArgs ), function( args ) {
+				return ! api.selectiveRefresh.partial.has( 'nav_menu_instance[' + args.args_hmac + ']' );
+			} );
+			if ( _.findWhere( unplacedNavMenuInstances, navMenuInstanceArgs ) ) {
+				api.selectiveRefresh.requestFullRefresh();
+				return true;
+			}
+			return false;
+		};
+
+		/**
+		 * Add change listener for a nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
 		 *
 		 * @since 4.5.0
+		 *
+		 * @param {wp.customize.Value} setting
+		 * @param {object}             options
+		 * @param {boolean}            options.fire Whether to invoke the callback after binding.
+		 *                                          This is used when a dynamic setting is added.
+		 * @return {boolean} Whether the setting was bound.
 		 */
-		self.watchNavMenuLocationChanges = function() {
-			api.bind( 'change', function( setting ) {
-				var themeLocation, themeLocationPartialFound = false, matches = setting.id.match( /^nav_menu_locations\[(.+)]$/ );
-				if ( ! matches ) {
-					return;
+		self.bindSettingListener = function( setting, options ) {
+			var matches;
+			options = options || {};
+
+			matches = setting.id.match( /^nav_menu\[(-?\d+)]$/ );
+			if ( matches ) {
+				setting._navMenuId = parseInt( matches[1], 10 );
+				setting.bind( this.onChangeNavMenuSetting );
+				if ( options.fire ) {
+					this.onChangeNavMenuSetting.call( setting, setting(), false );
 				}
-				themeLocation = matches[1];
-				api.selectiveRefresh.partial.each( function( partial ) {
-					if ( partial.extended( self.NavMenuInstancePartial ) && partial.params.navMenuArgs.theme_location === themeLocation ) {
-						partial.refresh();
-						themeLocationPartialFound = true;
-					}
-				} );
+				return true;
+			}
+
+			matches = setting.id.match( /^nav_menu_item\[(-?\d+)]$/ );
+			if ( matches ) {
+				setting._navMenuItemId = parseInt( matches[1], 10 );
+				setting.bind( this.onChangeNavMenuItemSetting );
+				if ( options.fire ) {
+					this.onChangeNavMenuItemSetting.call( setting, setting(), false );
+				}
+				return true;
+			}
+
+			matches = setting.id.match( /^nav_menu_locations\[(.+?)]/ );
+			if ( matches ) {
+				setting._navMenuThemeLocation = matches[1];
+				setting.bind( this.onChangeNavMenuLocationsSetting );
+				if ( options.fire ) {
+					this.onChangeNavMenuLocationsSetting.call( setting, setting(), false );
+				}
+				return true;
+			}
 
-				if ( ! themeLocationPartialFound ) {
-					api.selectiveRefresh.requestFullRefresh();
+			return false;
+		};
+
+		/**
+		 * Remove change listeners for nav_menu[], nav_menu_item[], or nav_menu_locations[] setting.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {wp.customize.Value} setting
+		 */
+		self.unbindSettingListener = function( setting ) {
+			setting.unbind( this.onChangeNavMenuSetting );
+			setting.unbind( this.onChangeNavMenuItemSetting );
+			setting.unbind( this.onChangeNavMenuLocationsSetting );
+		};
+
+		/**
+		 * Handle change for nav_menu[] setting for nav menu instances lacking partials.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @this {wp.customize.Value}
+		 */
+		self.onChangeNavMenuSetting = function() {
+			var setting = this;
+
+			self.handleUnplacedNavMenuInstances( {
+				menu: setting._navMenuId
+			} );
+
+			// Ensure all nav menu instances with a theme_location assigned to this menu are handled.
+			api.each( function( otherSetting ) {
+				if ( ! otherSetting._navMenuThemeLocation ) {
+					return;
+				}
+				if ( setting._navMenuId === otherSetting() ) {
+					self.handleUnplacedNavMenuInstances( {
+						theme_location: otherSetting._navMenuThemeLocation
+					} );
 				}
 			} );
 		};
+
+		/**
+		 * Handle change for nav_menu_item[] setting for nav menu instances lacking partials.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @param {object} newItem New value for nav_menu_item[] setting.
+		 * @param {object} oldItem Old value for nav_menu_item[] setting.
+		 * @this {wp.customize.Value}
+		 */
+		self.onChangeNavMenuItemSetting = function( newItem, oldItem ) {
+			var item = newItem || oldItem, navMenuSetting;
+			navMenuSetting = api( 'nav_menu[' + String( item.nav_menu_term_id ) + ']' );
+			if ( navMenuSetting ) {
+				self.onChangeNavMenuSetting.call( navMenuSetting );
+			}
+		};
+
+		/**
+		 * Handle change for nav_menu_locations[] setting for nav menu instances lacking partials.
+		 *
+		 * @since 4.5.0
+		 *
+		 * @this {wp.customize.Value}
+		 */
+		self.onChangeNavMenuLocationsSetting = function() {
+			var setting = this, hasNavMenuInstance;
+			self.handleUnplacedNavMenuInstances( {
+				theme_location: setting._navMenuThemeLocation
+			} );
+
+			// If there are no wp_nav_menu() instances that refer to the theme location, do full refresh.
+			hasNavMenuInstance = !! _.findWhere( _.values( self.data.navMenuInstanceArgs ), {
+				theme_location: setting._navMenuThemeLocation
+			} );
+			if ( ! hasNavMenuInstance ) {
+				api.selectiveRefresh.requestFullRefresh();
+			}
+		};
 	}
 
 	/**
diff --git tests/phpunit/tests/customize/nav-menus.php tests/phpunit/tests/customize/nav-menus.php
index fd380bc..f5d56b6 100644
--- tests/phpunit/tests/customize/nav-menus.php
+++ tests/phpunit/tests/customize/nav-menus.php
@@ -617,15 +617,17 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 	function test_filter_wp_nav_menu_args() {
 		do_action( 'customize_register', $this->wp_customize );
 		$menus = $this->wp_customize->nav_menus;
+		$menu_id = wp_create_nav_menu( 'Foo' );
 
 		$results = $menus->filter_wp_nav_menu_args( array(
 			'echo'            => true,
 			'fallback_cb'     => 'wp_page_menu',
 			'walker'          => '',
-			'menu'            => wp_create_nav_menu( 'Foo' ),
+			'menu'            => $menu_id,
 			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
 		$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
+		$this->assertTrue( $results['can_partial_refresh'] );
 
 		$results = $menus->filter_wp_nav_menu_args( array(
 			'echo'            => false,
@@ -633,7 +635,8 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 			'walker'          => new Walker_Nav_Menu(),
 			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
-		$this->assertArrayNotHasKey( 'customize_preview_nav_menus_args', $results );
+		$this->assertFalse( $results['can_partial_refresh'] );
+		$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
 		$this->assertEquals( 'wp_page_menu', $results['fallback_cb'] );
 
 		$nav_menu_term = get_term( wp_create_nav_menu( 'Bar' ) );
@@ -644,8 +647,39 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 			'menu'            => $nav_menu_term,
 			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
 		) );
+		$this->assertTrue( $results['can_partial_refresh'] );
 		$this->assertArrayHasKey( 'customize_preview_nav_menus_args', $results );
 		$this->assertEquals( $nav_menu_term->term_id, $results['customize_preview_nav_menus_args']['menu'] );
+
+		$results = $menus->filter_wp_nav_menu_args( array(
+			'echo'            => true,
+			'fallback_cb'     => 'wp_page_menu',
+			'walker'          => '',
+			'menu'            => $menu_id,
+			'container'       => 'div',
+			'items_wrap'      => '%3$s',
+		) );
+		$this->assertTrue( $results['can_partial_refresh'] );
+
+		$results = $menus->filter_wp_nav_menu_args( array(
+			'echo'            => true,
+			'fallback_cb'     => 'wp_page_menu',
+			'walker'          => '',
+			'menu'            => $menu_id,
+			'container'       => false,
+			'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
+		) );
+		$this->assertTrue( $results['can_partial_refresh'] );
+
+		$results = $menus->filter_wp_nav_menu_args( array(
+			'echo'            => true,
+			'fallback_cb'     => 'wp_page_menu',
+			'walker'          => '',
+			'menu'            => $menu_id,
+			'container'       => false,
+			'items_wrap'      => '%3$s',
+		) );
+		$this->assertFalse( $results['can_partial_refresh'] );
 	}
 
 	/**
@@ -691,6 +725,20 @@ class Test_WP_Customize_Nav_Menus extends WP_UnitTestCase {
 	}
 
 	/**
+	 * Test WP_Customize_Nav_Menus::export_preview_data() method.
+	 *
+	 * @see WP_Customize_Nav_Menus::export_preview_data()
+	 */
+	function test_export_preview_data() {
+		ob_start();
+		$this->wp_customize->nav_menus->export_preview_data();
+		$html = ob_get_clean();
+		$this->assertTrue( (bool) preg_match( '/_wpCustomizePreviewNavMenusExports = ({.+})/s', $html, $matches ) );
+		$exported_data = json_decode( $matches[1], true );
+		$this->assertArrayHasKey( 'navMenuInstanceArgs', $exported_data );
+	}
+
+	/**
 	 * Test WP_Customize_Nav_Menus::render_nav_menu_partial() method.
 	 *
 	 * @see WP_Customize_Nav_Menus::render_nav_menu_partial()
