diff --git a/.eslintrc.js b/.eslintrc.js
index 444388d492ba45e16c0f5f5af67d7a36afee8a00..94cf8bbc0cfe8fb59003078c21346fce44d57cea 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -225,6 +225,7 @@ module.exports = {
                 "src/components/views/messages/MVoiceMessageBody.tsx",
                 "src/components/views/right_panel/EncryptionPanel.tsx",
                 "src/components/views/rooms/EntityTile.tsx",
+                "src/components/views/rooms/EntityTileRefactored.tsx",
                 "src/components/views/rooms/LinkPreviewGroup.tsx",
                 "src/components/views/rooms/MemberList.tsx",
                 "src/components/views/rooms/MessageComposer.tsx",
diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx
index d3501e7571df0cfb316d71e3e2cf9f9503cfc218..8f1ba7aecfb30b530c9201c1407d27fd1a06e635 100644
--- a/src/components/views/rooms/MemberList.tsx
+++ b/src/components/views/rooms/MemberList.tsx
@@ -327,4 +327,4 @@ export default class RightPanel extends React.Component<Props, IState> {
             </aside>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/components/views/rooms/MemberListNext.tsx b/src/components/views/rooms/MemberListNext.tsx
index 6f0fbe916e09a81e707d6aea103ce08b2032286b..952eef6e62b4dab1f5929b838599eb246a763cc0 100644
--- a/src/components/views/rooms/MemberListNext.tsx
+++ b/src/components/views/rooms/MemberListNext.tsx
@@ -15,11 +15,13 @@ limitations under the License.
 */
 
 import { Form, IconButton, Search, Separator, Text } from "@vector-im/compound-web";
-import { Icon as InviteIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
 import React, { useEffect, useRef, useState } from "react";
-import { Flex } from "../../utils/Flex";
 import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
+import { Icon as InviteIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
+
+import { Flex } from "../../utils/Flex";
 import { useMemberListViewModel } from "../../../view-models/rooms/memberlist/MemberListViewModelNext";
+import MemberTileNext from "./MemberTileNext";
 
 interface IProps {
     roomId: string;
@@ -44,14 +46,14 @@ const MemberListNext: React.FC<IProps> = (props: IProps) => {
         }
     }, [listParent]);
 
-    function rowRenderer({ key, index, style }: ListRowProps) {
+    const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
         const member = viewModel.members[index];
         return (
             <div key={key} style={style}>
-                {member.name}
+                <MemberTileNext member={member} showPresence={false} />
             </div>
         );
-    }
+    };
 
     return (
         <Flex align="stretch" direction="column" className="mx_MemberList_container">
diff --git a/src/components/views/rooms/MemberTileNext.tsx b/src/components/views/rooms/MemberTileNext.tsx
index d498abb279e88b1b6a38c726674c68f91210ad3d..94b64abf61f1063db4e0e54627f9ef205813bcc2 100644
--- a/src/components/views/rooms/MemberTileNext.tsx
+++ b/src/components/views/rooms/MemberTileNext.tsx
@@ -15,143 +15,44 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { useEffect, useState } from "react";
-import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
-import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
-import { CryptoEvent } from "matrix-js-sdk/src/crypto";
-import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
+import React from "react";
 
-import dis from "../../../dispatcher/dispatcher";
 import { _t } from "../../../languageHandler";
-import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { Action } from "../../../dispatcher/actions";
-import { PowerStatus } from "./EntityTile";
 import DisambiguatedProfile from "../messages/DisambiguatedProfile";
 import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
-import { E2EState } from "../../../models/rooms/E2EState";
-import { asyncSome } from "../../../utils/arrays";
-import { getUserDeviceIds } from "../../../utils/crypto/deviceInfo";
 import { RoomMember } from "../../../models/rooms/RoomMember";
 import MemberAvatarNext from "../avatars/MemberAvatarNext";
 import EntityTileRefactored from "./EntityTileRefactored";
+import useMemberTileViewModel, { MemberTileViewModel } from "./MemberTileViewModel";
 
 interface IProps {
     member: RoomMember;
     showPresence?: boolean;
 }
 
-export default function MemberTile(props: IProps): JSX.Element {
-    // const [isRoomEncrypted, setIsRoomEncrypted] = useState(false);
-    const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>();
-
-    useEffect(() => {
-        const cli = MatrixClientPeg.safeGet();
-
-        const updateE2EStatus = async (): Promise<void> => {
-            const { userId } = props.member;
-            const isMe = userId === cli.getUserId();
-            const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId);
-            if (!userTrust?.isCrossSigningVerified()) {
-                setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal);
-                return;
-            }
-
-            const deviceIDs = await getUserDeviceIds(cli, userId);
-            const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => {
-                // For your own devices, we use the stricter check of cross-signing
-                // verification to encourage everyone to trust their own devices via
-                // cross-signing so that other users can then safely trust you.
-                // For other people's devices, the more general verified check that
-                // includes locally verified devices can be used.
-                const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
-                return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
-            });
-            setE2eStatus(anyDeviceUnverified ? E2EState.Warning : E2EState.Verified);
-        };
-
-        const onRoomStateEvents = (ev: MatrixEvent): void => {
-            if (ev.getType() !== EventType.RoomEncryption) return;
-            const { roomId } = props.member;
-            if (ev.getRoomId() !== roomId) return;
-
-            // The room is encrypted now.
-            cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
-            updateE2EStatus();
-        };
-
-        const onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => {
-            if (userId !== props.member.userId) return;
-            updateE2EStatus();
-        };
-
-        const onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
-            if (userId !== props.member.userId) return;
-            updateE2EStatus();
-        };
-
-        const { roomId } = props.member;
-        if (roomId) {
-            const isRoomEncrypted = cli.isRoomEncrypted(roomId);
-            if (isRoomEncrypted) {
-                cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
-                cli.on(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
-                updateE2EStatus();
-            } else {
-                // Listen for room to become encrypted
-                cli.on(RoomStateEvent.Events, onRoomStateEvents);
-            }
-        }
-
-        return () => {
-            if (cli) {
-                cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
-                cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
-                cli.removeListener(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
-            }
-        };
-    }, [props.member]);
+export default function MemberTileView(props: IProps): JSX.Element {
+    const vm = useMemberTileViewModel(props);
+    return <MemberTile vm={vm} />;
+}
 
-    const onClick = (): void => {
-        dis.dispatch({
-            action: Action.ViewUser,
-            member: props.member,
-            push: true,
-        });
-    };
-
-    const getDisplayName = (): string => {
-        return props.member.name;
-    };
+export function MemberTile(props: { vm: MemberTileViewModel }): JSX.Element {
+    const vm = props.vm;
+    const member = vm.member;
 
     const getPowerLabel = (): string => {
         return _t("member_list|power_label", {
-            userName: UserIdentifierCustomisations.getDisplayUserIdentifier(props.member.userId, {
-                roomId: props.member.roomId,
+            userName: UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
+                roomId: member.roomId,
             }),
-            powerLevelNumber: props.member.powerLevel,
+            powerLevelNumber: member.powerLevel,
         }).trim();
     };
 
-    const member = props.member;
-    const name = getDisplayName();
+    const name = vm.name;
 
     const av = <MemberAvatarNext member={member} size="36px" aria-hidden="true" />;
 
-    const powerStatusMap = new Map([
-        [100, PowerStatus.Admin],
-        [50, PowerStatus.Moderator],
-    ]);
-
-    // Find the nearest power level with a badge
-    let powerLevel = props.member.powerLevel;
-    for (const [pl] of powerStatusMap) {
-        if (props.member.powerLevel >= pl) {
-            powerLevel = pl;
-            break;
-        }
-    }
-
-    const powerStatus = powerStatusMap.get(powerLevel);
+    const powerStatus = vm.powerStatus;
 
     const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
 
@@ -167,9 +68,9 @@ export default function MemberTile(props: IProps): JSX.Element {
             name={name}
             nameJSX={nameJSX}
             powerStatus={powerStatus}
-            showPresence={props.showPresence}
-            e2eStatus={e2eStatus}
-            onClick={onClick}
+            showPresence={vm.showPresence}
+            e2eStatus={vm.e2eStatus}
+            onClick={vm.onClick}
         />
     );
 }
diff --git a/src/components/views/rooms/MemberTileViewModel.tsx b/src/components/views/rooms/MemberTileViewModel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..059d38a95cfa03190253ad905647d9c2ef91329e
--- /dev/null
+++ b/src/components/views/rooms/MemberTileViewModel.tsx
@@ -0,0 +1,143 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useEffect, useState } from "react";
+import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
+import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
+import { CryptoEvent } from "matrix-js-sdk/src/crypto";
+import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
+
+import dis from "../../../dispatcher/dispatcher";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { Action } from "../../../dispatcher/actions";
+import { PowerStatus } from "./EntityTile";
+import { E2EState } from "../../../models/rooms/E2EState";
+import { asyncSome } from "../../../utils/arrays";
+import { getUserDeviceIds } from "../../../utils/crypto/deviceInfo";
+import { RoomMember } from "../../../models/rooms/RoomMember";
+
+interface IProps {
+    member: RoomMember;
+    showPresence?: boolean;
+}
+
+export interface MemberTileViewModel extends IProps {
+    e2eStatus?: E2EState;
+    name: string;
+    powerStatus?: PowerStatus;
+    onClick: () => void;
+}
+
+export default function useMemberTileViewModel(props: IProps): MemberTileViewModel {
+    const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>();
+
+    useEffect(() => {
+        const cli = MatrixClientPeg.safeGet();
+
+        const updateE2EStatus = async (): Promise<void> => {
+            const { userId } = props.member;
+            const isMe = userId === cli.getUserId();
+            const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId);
+            if (!userTrust?.isCrossSigningVerified()) {
+                setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal);
+                return;
+            }
+
+            const deviceIDs = await getUserDeviceIds(cli, userId);
+            const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => {
+                // For your own devices, we use the stricter check of cross-signing
+                // verification to encourage everyone to trust their own devices via
+                // cross-signing so that other users can then safely trust you.
+                // For other people's devices, the more general verified check that
+                // includes locally verified devices can be used.
+                const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
+                return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
+            });
+            setE2eStatus(anyDeviceUnverified ? E2EState.Warning : E2EState.Verified);
+        };
+
+        const onRoomStateEvents = (ev: MatrixEvent): void => {
+            if (ev.getType() !== EventType.RoomEncryption) return;
+            const { roomId } = props.member;
+            if (ev.getRoomId() !== roomId) return;
+
+            // The room is encrypted now.
+            cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
+            updateE2EStatus();
+        };
+
+        const onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => {
+            if (userId !== props.member.userId) return;
+            updateE2EStatus();
+        };
+
+        const onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
+            if (userId !== props.member.userId) return;
+            updateE2EStatus();
+        };
+
+        const { roomId } = props.member;
+        if (roomId) {
+            const isRoomEncrypted = cli.isRoomEncrypted(roomId);
+            if (isRoomEncrypted) {
+                cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
+                cli.on(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
+                updateE2EStatus();
+            } else {
+                // Listen for room to become encrypted
+                cli.on(RoomStateEvent.Events, onRoomStateEvents);
+            }
+        }
+
+        return () => {
+            if (cli) {
+                cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
+                cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
+                cli.removeListener(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
+            }
+        };
+    }, [props.member]);
+
+    const onClick = (): void => {
+        dis.dispatch({
+            action: Action.ViewUser,
+            member: props.member,
+            push: true,
+        });
+    };
+
+    const member = props.member;
+    const name = props.member.name;
+
+    const powerStatusMap = new Map([
+        [100, PowerStatus.Admin],
+        [50, PowerStatus.Moderator],
+    ]);
+
+    // Find the nearest power level with a badge
+    let powerLevel = props.member.powerLevel;
+    for (const [pl] of powerStatusMap) {
+        if (props.member.powerLevel >= pl) {
+            powerLevel = pl;
+            break;
+        }
+    }
+
+    const powerStatus = powerStatusMap.get(powerLevel);
+
+    return { powerStatus, member, name, onClick, e2eStatus, showPresence: props.showPresence };
+}
diff --git a/src/view-models/rooms/memberlist/MemberListViewModelNext.ts b/src/view-models/rooms/memberlist/MemberListViewModelNext.ts
index 21ec9315d7dc45ba962c3d04669e242085d34180..055122278e9c6f99f3e31e17e8f33b74ed171bd5 100644
--- a/src/view-models/rooms/memberlist/MemberListViewModelNext.ts
+++ b/src/view-models/rooms/memberlist/MemberListViewModelNext.ts
@@ -168,11 +168,11 @@ export function useMemberListViewModel(roomId: string): MemberListViewModelHook
 }
 
 export class MockMemberListViewModelHook implements MemberListViewModelHook {
-    loading: boolean = false;
-    members: RoomMember[] = [];
-    showInvite: boolean = true;
-    enableInvite: boolean = true;
-    searchQuery?: string | undefined = undefined;
+    public loading: boolean = false;
+    public members: RoomMember[] = [];
+    public showInvite: boolean = true;
+    public enableInvite: boolean = true;
+    public searchQuery?: string | undefined = undefined;
 
     constructor() {
         this.members = [...Array(100).keys()].map((i): RoomMember => {
@@ -190,14 +190,14 @@ export class MockMemberListViewModelHook implements MemberListViewModelHook {
             };
         });
     }
-    setUp(): Promise<void> {
+    public setUp(): Promise<void> {
         throw new Error("Method not implemented.");
     }
-    tearDown(): void {
+    public tearDown(): void {
         throw new Error("Method not implemented.");
     }
 
-    onSearchQueryChanged(query: string): Promise<void> {
+    public onSearchQueryChanged(query: string): Promise<void> {
         throw new Error("Method not implemented.");
     }
 }