While you can use the <Tabs>
component from antd
to achieve what you want, your current code has some bugs regarding how it handles user state and the asynchronous calls.
Taking a look at these lines:
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve, reject) => {
database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => {
resolve(snapshot.val());
});
});
};
Here there are two main problems:
- You make use of
auth().currentUser
but this isn't guaranteed to contain the user object that you expect as it may still be resolving with the server (where it will be null
) or the user may be signed out (also null
).
- You incorrectly chain the promise by wrapping it in a Promise constructor (known as the Promise constructor anti-pattern) where the errors of the original promise will never reach the
reject
handler leading to crashes.
To fix the user state problem, you should make use of onAuthStateChanged
and look out for when the user signs in/out/etc.
function useCurrentUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
// returns [firebase.auth.User | null, boolean]
return [user || null, userLoading];
}
// in your component
const [user, userLoading] = useCurrentUser();
To fix the PCAPs, you'd use:
const homeidProvider = () => {
return database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const roomListProvider = () => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
Because these functions don't depend on state changes, you should place them outside your component and pass the relevant arguments into them.
Next, these lines should be inside a useEffect
call where error handling and unmounting the component should be handled as appropriate:
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
should be swapped out with:
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
return;
}
let disposed = false;
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
You should also track the status of your component:
Status |
Meaning |
"loading" |
data is loading |
"error" |
something went wrong |
"signed-out" |
no user logged in |
"ready" |
data is ready for display |
Rolling this together:
import { Tabs, Spin, Alert, Card } from 'antd';
const { TabPane } = Tabs;
const { Meta } = Card;
function useUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
return [user || null, userLoading];
}
const getUserHomeId = (uid) => {
return database()
.ref(`/USERS/${uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const getHomeRoomData = (homeId) => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
const RoomView = () => {
const [homeId, setHomeId] = useState(0);
const [status, setStatus] = useState("loading")
const [roomList, setRoomList] = useState([]);
const [user, userLoading] = useUser();
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
setStatus("signed-out");
return;
}
let disposed = false;
setStatus("loading");
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
setStatus("ready");
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
setStatus("error");
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
switch (status) {
case "loading":
return <Spin tip="Loading rooms..." />
case "error":
return <Alert
message="Error"
description="An unknown error has occurred"
type="error"
/>
case "signed-out":
return <Alert
message="Error"
description="User is signed out"
type="error"
/>
}
if (roomList.length === 0) {
return <Alert
message="No rooms found"
description="You haven't created any rooms yet"
type="info"
/>
}
return (
<Tabs defaultActiveKey={roomList[0].key}>
{
roomList.map(room => {
let tabContent;
if (room.apps.length == 0) {
tabContent = "No apps found in this room";
} else {
tabContent = room.apps.map(app => {
<Card style={{ width: 300, marginTop: 16 }} key={app.key}>
<Meta
avatar={
<Avatar src="https://via.placeholder.com/300x300?text=Icon" />
}
title={app.name}
description={app.description}
/>
</Card>
});
}
return <TabPane tab={room.title} key={room.key}>
{tabContent}
</TabPane>
})
}
</Tabs>
);
};