diff --git a/spec/bookmarks.js b/spec/bookmarks.js
index ce736458ed8c946587b0ea94986eb34880abcf52..82908e5ee9320c38b701a779c386fa80b9be38fb 100644
--- a/spec/bookmarks.js
+++ b/spec/bookmarks.js
@@ -191,17 +191,18 @@
                     ['rosterGroupsFetched'], {}, async function (done, _converse) {
 
                 await test_utils.waitUntilBookmarksReturned(_converse);
-                const room_jid = 'coven@chat.shakespeare.lit';
+                const muc_jid = 'coven@chat.shakespeare.lit';
                 _converse.bookmarks.create({
-                    'jid': room_jid,
+                    'jid': muc_jid,
                     'autojoin': false,
                     'name':  'The Play',
                     'nick': 'Othello'
                 });
-                const room = await _converse.api.rooms.open(room_jid);
-                spyOn(room, 'join').and.callThrough();
-                await test_utils.getRoomFeatures(_converse, 'coven', 'chat.shakespeare.lit');
-                await u.waitUntil(() => room.join.calls.count());
+                spyOn(_converse.ChatRoom.prototype, 'getAndPersistNickname').and.callThrough();
+                const room_creation_promise = _converse.api.rooms.open(muc_jid);
+                await test_utils.getRoomFeatures(_converse, muc_jid);
+                const room = await room_creation_promise;
+                await u.waitUntil(() => room.getAndPersistNickname.calls.count());
                 expect(room.get('nick')).toBe('Othello');
                 done();
             }));
diff --git a/spec/muc.js b/spec/muc.js
index 3c994ecf52fed14409b0cccd02f3ee1ff6f1cf19..5f8a45fe2a7082d2e0d0f06ed64aa3a58c285d13 100644
--- a/spec/muc.js
+++ b/spec/muc.js
@@ -22,7 +22,11 @@
                     async function (done, _converse) {
 
                 await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
+
+                _converse.connection.IQ_stanzas = [];
                 await test_utils.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo');
+
+                _converse.connection.IQ_stanzas = [];
                 await test_utils.openAndEnterChatRoom(_converse, 'news@montague.lit', 'romeo');
                 expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy();
                 expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy();
@@ -108,6 +112,7 @@
                 await test_utils.openControlBox(_converse);
                 await test_utils.waitForRoster(_converse, 'current');
                 await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length);
+
                 let room = await _converse.api.rooms.open(jid);
                 // Test on groupchat that's not yet open
                 expect(room instanceof Backbone.Model).toBeTruthy();
@@ -267,7 +272,7 @@
                     ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
                     async function (done, _converse) {
 
-                const IQ_stanzas = _converse.connection.IQ_stanzas;
+                let IQ_stanzas = _converse.connection.IQ_stanzas;
                 const muc_jid = 'lounge@montague.lit';
                 await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
 
@@ -291,6 +296,8 @@
                 input.value = 'nicky';
                 view.el.querySelector('input[type=submit]').click();
                 expect(view.model.join).toHaveBeenCalled();
+                _converse.connection.IQ_stanzas = [];
+                await test_utils.getRoomFeatures(_converse, muc_jid);
                 await u.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING);
 
                 // The user has just entered the room (because join was called)
@@ -334,7 +341,9 @@
                  *   <query xmlns="http://jabber.org/protocol/muc#owner"><x xmlns="jabber:x:data" type="submit"/></query>
                  * </iq>
                  */
-                const iq = IQ_stanzas.filter(s => s.querySelector(`query[xmlns="${Strophe.NS.MUC_OWNER}"]`)).pop();
+                const selector = `query[xmlns="${Strophe.NS.MUC_OWNER}"]`;
+                IQ_stanzas = _converse.connection.IQ_stanzas;
+                const iq = await u.waitUntil(() => IQ_stanzas.filter(s => s.querySelector(selector)).pop());
                 expect(Strophe.serialize(iq)).toBe(
                     `<iq id="${iq.getAttribute('id')}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
                         `<query xmlns="http://jabber.org/protocol/muc#owner"><x type="submit" xmlns="jabber:x:data"/>`+
@@ -405,10 +414,8 @@
                             Strophe.NS.SID
                         ];
                         const nick = 'romeo';
-                        const room = Strophe.getNodeFromJid(muc_jid);
-                        const server = Strophe.getDomainFromJid(muc_jid);
                         await _converse.api.rooms.open(muc_jid);
-                        await test_utils.getRoomFeatures(_converse, room, server, features);
+                        await test_utils.getRoomFeatures(_converse, muc_jid, features);
                         await test_utils.waitForReservedNick(_converse, muc_jid, nick);
                         test_utils.receiveOwnMUCPresence(_converse, muc_jid, nick);
                         const view = _converse.chatboxviews.get(muc_jid);
@@ -532,11 +539,9 @@
                     async function (done, _converse) {
 
                 const muc_jid = 'coven@chat.shakespeare.lit';
-                const room = Strophe.getNodeFromJid(muc_jid);
-                const server = Strophe.getDomainFromJid(muc_jid);
                 const nick = 'romeo';
                 await _converse.api.rooms.open(muc_jid);
-                await test_utils.getRoomFeatures(_converse, room, server);
+                await test_utils.getRoomFeatures(_converse, muc_jid);
                 await test_utils.waitForReservedNick(_converse, muc_jid, nick);
 
                 const view = _converse.chatboxviews.get(muc_jid);
@@ -577,8 +582,9 @@
                     ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
                     async function (done, _converse) {
 
+                const muc_jid = 'coven@chat.shakespeare.lit';
                 await test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
-                await test_utils.getRoomFeatures(_converse, 'coven', 'chat.shakespeare.lit');
+                await test_utils.getRoomFeatures(_converse, muc_jid);
 
                 const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
                 const chat_content = view.el.querySelector('.chat-content');
@@ -1633,13 +1639,8 @@
                 expect(view.model.occupants.length).toBe(9);
                 expect(view.model.occupants.filter(o => o.isMember()).length).toBe(8);
 
-
+                view.model.rejoin();
                 // Test that members aren't removed when we reconnect
-                // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres
-                spyOn(view.model, 'removeNonMembers').and.callThrough();
-                view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
-                view.model.enterRoom();
-                expect(view.model.removeNonMembers).toHaveBeenCalled();
                 expect(view.model.occupants.length).toBe(8);
                 expect(occupants.querySelectorAll('li').length).toBe(8);
                 done();
@@ -1856,8 +1857,6 @@
                         .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
                 _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
 
-                const view = _converse.chatboxviews.get('lounge@montague.lit');
-                spyOn(view.model, 'join').and.callThrough();
 
                 /* <iq from='hag66@shakespeare.lit/pda'
                  *     id='getnick1'
@@ -1890,6 +1889,7 @@
                  *     </query>
                  * </iq>
                  */
+                const view = _converse.chatboxviews.get('lounge@montague.lit');
                 stanza = $iq({
                     'type': 'result',
                     'id': iq.getAttribute('id'),
@@ -1899,8 +1899,6 @@
                 .c('identity', {'category': 'conference', 'name': 'thirdwitch', 'type': 'text'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                expect(view.model.join).toHaveBeenCalled();
-
                 // The user has just entered the groupchat (because join was called)
                 // and receives their own presence from the server.
                 // See example 24:
@@ -2203,7 +2201,7 @@
                     async function (done, _converse) {
 
                 const muc_jid = 'coven@chat.shakespeare.lit';
-                await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo');
+                await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
                 const view = _converse.chatboxviews.get(muc_jid);
                 expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
                 await test_utils.sendMessage(view, 'hello world');
@@ -2238,8 +2236,9 @@
                 sent_stanzas = _converse.connection.sent_stanzas;
                 const index = sent_stanzas.length -1;
 
+                _converse.connection.IQ_stanzas = [];
                 _converse.connection._dataRecv(test_utils.createRequest(result));
-                await test_utils.getRoomFeatures(_converse, 'coven', 'chat.shakespeare.lit');
+                await test_utils.getRoomFeatures(_converse, muc_jid);
 
                 const pres = await u.waitUntil(
                     () => sent_stanzas.slice(index).filter(s => s.nodeName === 'presence').pop());
@@ -4320,9 +4319,9 @@
                 const sent_IQs = _converse.connection.IQ_stanzas;
                 const muc_jid = 'coven@chat.shakespeare.lit';
 
-                await _converse.api.rooms.open(muc_jid, {'nick': 'romeo'});
+                const room_creation_promise = _converse.api.rooms.open(muc_jid, {'nick': 'romeo'});
 
-                // Check that the groupchat queried for the feautures.
+                // Check that the groupchat queried for the features.
                 let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`)).pop());
                 expect(Strophe.serialize(stanza)).toBe(
                     `<iq from="romeo@montague.lit/orchard" id="${stanza.getAttribute("id")}" to="${muc_jid}" type="get" xmlns="jabber:client">`+
@@ -4351,6 +4350,8 @@
                 await u.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING));
                 expect(view.model.features.get('membersonly')).toBeTruthy();
 
+                await room_creation_promise;
+
                 await test_utils.createContacts(_converse, 'current');
 
                 let sent_stanza, sent_id;
diff --git a/spec/roomslist.js b/spec/roomslist.js
index 55b258c66c0d3ee23c66c8ef2aad13ea109faf18..590fe9f494df20109984e0455336caca0e232e16 100644
--- a/spec/roomslist.js
+++ b/spec/roomslist.js
@@ -279,7 +279,7 @@
             await test_utils.openControlBox(_converse);
             const room_jid = 'kitchen@conference.shakespeare.lit';
             await u.waitUntil(() => _converse.rooms_list_view !== undefined, 500);
-            await test_utils.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'romeo');
+            await test_utils.openAndEnterChatRoom(_converse, room_jid, 'romeo');
             const view = _converse.chatboxviews.get(room_jid);
             view.model.set({'minimized': true});
             const nick = mock.chatroom_names[0];
diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js
index 2e55a8f386bc6f8fc7cdb6a9089cd2ea5262de51..cd4c597dcae35c0c3102349a96d36edf6aa4ae8c 100644
--- a/src/converse-muc-views.js
+++ b/src/converse-muc-views.js
@@ -1649,12 +1649,11 @@ converse.plugins.add('converse-muc-views', {
                 });
                 const switch_el = container.querySelector('a.switch-chat');
                 if (switch_el) {
-                    switch_el.addEventListener('click', ev => {
+                    switch_el.addEventListener('click', async ev => {
                         ev.preventDefault();
-                        this.model.save('jid', moved_jid);
-                        container.innerHTML = '';
-                        this.showSpinner();
-                        this.model.enterRoom();
+                        const room = await _converse.api.rooms.get(moved_jid, null, true);
+                        room.maybeShow(true);
+                        this.model.destroy();
                     });
                 }
                 u.showElement(container);
diff --git a/src/headless/converse-muc.js b/src/headless/converse-muc.js
index 6d162ddaf2461454a451335a6e83780eae959657..0f0756285b3bc0eaa3be2cdebadf563c0a18a742 100644
--- a/src/headless/converse-muc.js
+++ b/src/headless/converse-muc.js
@@ -387,10 +387,13 @@ converse.plugins.add('converse-muc', {
                 this.on('change:connection_status', this.onConnectionStatusChanged, this);
 
                 this.initMessages();
+                this.initOccupants();
                 this.registerHandlers();
 
-                await this.initOccupants();
-                this.enterRoom();
+                const restored = await this.restoreFromCache()
+                if (!restored) {
+                    this.join();
+                }
                 this.initialized.resolve();
             },
 
@@ -402,25 +405,85 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            async enterRoom () {
-                const conn_status = this.get('connection_status');
-                if (conn_status !==  converse.ROOMSTATUS.ENTERED) {
-                    // We're not restoring a room from cache, so let's clear the potentially stale cache.
-                    this.removeNonMembers();
-                    await this.refreshRoomFeatures();
-                    if (_converse.muc_show_logs_before_join) {
-                        await this.fetchMessages();
-                    } else if (_converse.clear_messages_on_reconnection) {
-                        await this.clearMessages();
-                    }
-                    this.join();
-                } else if (!(await this.rejoinIfNecessary())) {
+            /**
+             * Checks whether we're still joined and if so, restores the MUC state from cache.
+             * @private
+             * @method _converse.ChatRoom#restoreFromCache
+             * @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`.
+             */
+            async restoreFromCache () {
+                if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED && await this.isJoined()) {
                     // We've restored the room from cache and we're still joined.
                     await new Promise(resolve => this.features.fetch({'success': resolve, 'error': resolve}));
+                    await this.fetchOccupants();
                     await this.fetchMessages();
+                    return true;
+                } else {
+                    await this.clearCache();
+                    return false;
                 }
             },
 
+            /**
+             * Join the MUC
+             * @private
+             * @method _converse.ChatRoom#join
+             * @param { String } nick - The user's nickname
+             * @param { String } [password] - Optional password, if required by the groupchat.
+             */
+            async join (nick, password) {
+                if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
+                    // We have restored a groupchat from session storage,
+                    // so we don't send out a presence stanza again.
+                    return this;
+                }
+                await this.refreshRoomFeatures();
+                nick = await this.getAndPersistNickname(nick);
+                if (!nick) {
+                    u.safeSave(this, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
+                    if (_converse.muc_show_logs_before_join) {
+                        await this.fetchMessages();
+                    }
+                    return this;
+                }
+                const stanza = $pres({
+                    'from': _converse.connection.jid,
+                    'to': this.getRoomJIDAndNick()
+                }).c("x", {'xmlns': Strophe.NS.MUC})
+                  .c("history", {'maxstanzas': this.features.get('mam_enabled') ? 0 : _converse.muc_history_max_stanzas}).up();
+
+                if (password) {
+                    stanza.cnode(Strophe.xmlElement("password", [], password));
+                }
+                this.save('connection_status', converse.ROOMSTATUS.CONNECTING);
+                _converse.api.send(stanza);
+                return this;
+            },
+
+            async clearCache () {
+                this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
+                if (this.occupants.length) {
+                    // Remove non-members when reconnecting
+                    this.occupants.filter(o => !o.isMember()).forEach(o => o.destroy());
+                } else {
+                    // Looks like we haven't restored occupants from cache, so we clear it.
+                    this.occupants.clearSession();
+                }
+                if (_converse.clear_messages_on_reconnection) {
+                    await this.clearMessages();
+                }
+            },
+
+            /**
+             * Clear stale cache and re-join a MUC we've been in before.
+             * @private
+             * @method _converse.ChatRoom#rejoin
+             */
+            rejoin () {
+                this.clearCache();
+                return this.join();
+            },
+
             async onConnectionStatusChanged () {
                 if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
                     if (_converse.muc_fetch_members) {
@@ -442,17 +505,9 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            removeNonMembers () {
-                const non_members = this.occupants.filter(o => !o.isMember());
-                if (non_members.length) {
-                    non_members.forEach(o => o.destroy());
-                }
-            },
-
             async onReconnection () {
-                this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
                 this.registerHandlers();
-                await this.enterRoom();
+                await this.rejoin();
                 this.announceReconnection();
             },
 
@@ -468,7 +523,10 @@ converse.plugins.add('converse-muc', {
                 this.occupants = new _converse.ChatRoomOccupants();
                 const id = `converse.occupants-${_converse.bare_jid}${this.get('jid')}`;
                 this.occupants.browserStorage = _converse.createStore(id, 'session');
-                this.occupants.chatroom  = this;
+                this.occupants.chatroom = this;
+            },
+
+            fetchOccupants () {
                 this.occupants.fetched = new Promise(resolve => {
                     this.occupants.fetch({
                         'add': true,
@@ -564,38 +622,6 @@ converse.plugins.add('converse-muc', {
                 }
             },
 
-            /**
-             * Join the groupchat.
-             * @private
-             * @method _converse.ChatRoom#join
-             * @param { String } nick - The user's nickname
-             * @param { String } [password] - Optional password, if required by the groupchat.
-             */
-            async join (nick, password) {
-                if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
-                    // We have restored a groupchat from session storage,
-                    // so we don't send out a presence stanza again.
-                    return this;
-                }
-                nick = await this.getAndPersistNickname(nick);
-                if (!nick) {
-                    u.safeSave(this, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
-                    return this;
-                }
-                const stanza = $pres({
-                    'from': _converse.connection.jid,
-                    'to': this.getRoomJIDAndNick()
-                }).c("x", {'xmlns': Strophe.NS.MUC})
-                  .c("history", {'maxstanzas': this.features.get('mam_enabled') ? 0 : _converse.muc_history_max_stanzas}).up();
-
-                if (password) {
-                    stanza.cnode(Strophe.xmlElement("password", [], password));
-                }
-                this.save('connection_status', converse.ROOMSTATUS.CONNECTING);
-                _converse.api.send(stanza);
-                return this;
-            },
-
             /**
              * Sends a message stanza to the XMPP server and expects a reflection
              * or error message within a specific timeout period.
@@ -1262,7 +1288,7 @@ converse.plugins.add('converse-muc', {
                     _converse.getDefaultMUCNickname();
 
                 if (nick) {
-                    this.save({'nick': nick}, {'silent': true});
+                    this.save({nick}, {'silent': true});
                 }
                 return nick;
             },
@@ -1581,10 +1607,8 @@ converse.plugins.add('converse-muc', {
              * @method _converse.ChatRoom#rejoinIfNecessary
              */
             async rejoinIfNecessary () {
-                const is_joined = await this.isJoined();
-                if (!is_joined) {
-                    this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
-                    this.enterRoom();
+                if (! await this.isJoined()) {
+                    this.rejoin();
                     return true;
                 }
             },
@@ -2231,7 +2255,7 @@ converse.plugins.add('converse-muc', {
             if (result === true) {
                 const chatroom = await openChatRoom(room_jid, {'password': x_el.getAttribute('password') });
                 if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
-                    _converse.chatboxes.get(room_jid).join();
+                    _converse.chatboxes.get(room_jid).rejoin();
                 }
             }
         };
diff --git a/tests/utils.js b/tests/utils.js
index 4540b85e5b41b7eaffd4bcacfe759751cebef2d2..848db6d3f4cf2fb0408d3e74dbf33c6c8b6b8de7 100644
--- a/tests/utils.js
+++ b/tests/utils.js
@@ -145,18 +145,15 @@
         return model;
     };
 
-    utils.getRoomFeatures = async function (_converse, room, server, features=[]) {
-        const muc_jid = `${room}@${server}`.toLowerCase();
+    utils.getRoomFeatures = async function (_converse, muc_jid, features=[]) {
+        const room = Strophe.getNodeFromJid(muc_jid);
+        muc_jid = muc_jid.toLowerCase();
         const stanzas = _converse.connection.IQ_stanzas;
-        // XXX How necessary is this?
-        const index = stanzas.length-2;
-        const stanza = await u.waitUntil(() => _.filter(
-            stanzas.slice(index),
+        const stanza = await u.waitUntil(() => stanzas.filter(
             iq => iq.querySelector(
                 `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
-            )).pop());
-
-
+            )).pop()
+        );
         const features_stanza = $iq({
             'from': muc_jid,
             'id': stanza.getAttribute('id'),
@@ -183,7 +180,6 @@
 
 
     utils.waitForReservedNick = async function (_converse, muc_jid, nick) {
-        const view = _converse.chatboxviews.get(muc_jid);
         const stanzas = _converse.connection.IQ_stanzas;
         const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`;
         const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop());
@@ -196,7 +192,7 @@
         const stanza = $iq({
             'type': 'result',
             'id': IQ_id,
-            'from': view.model.get('jid'),
+            'from': muc_jid,
             'to': _converse.connection.jid
         }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'});
         if (nick) {
@@ -204,7 +200,7 @@
         }
         _converse.connection._dataRecv(utils.createRequest(stanza));
         if (nick) {
-            return u.waitUntil(() => view.model.get('nick'));
+            return u.waitUntil(() => nick);
         }
     };
 
@@ -296,15 +292,15 @@
 
     utils.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) {
         muc_jid = muc_jid.toLowerCase();
-        const room = Strophe.getNodeFromJid(muc_jid);
-        const server = Strophe.getDomainFromJid(muc_jid);
-        await _converse.api.rooms.open(muc_jid);
-        await utils.getRoomFeatures(_converse, room, server, features);
+        const room_creation_promise = _converse.api.rooms.open(muc_jid);
+        await utils.getRoomFeatures(_converse, muc_jid, features);
         await utils.waitForReservedNick(_converse, muc_jid, nick);
         // The user has just entered the room (because join was called)
         // and receives their own presence from the server.
         // See example 24: https://xmpp.org/extensions/xep-0045.html#enter-pres
         utils.receiveOwnMUCPresence(_converse, muc_jid, nick);
+
+        await room_creation_promise;
         const view = _converse.chatboxviews.get(muc_jid);
         await u.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.ENTERED));
         if (_converse.muc_fetch_members) {