I have component which display data in a table fetched from Firestore, even though the fetched data is complete, but setting
attribute is most of the time is null when accessing on JSX. I sometimes see glimpse correct output like randomly.
As I know first render histories
going to be null but not on the second render. But it seems even in the second render settings
is null. I am missing something here?
DataTable.tsx
export default function DataTable() {
const [histories, setHistories] = useState<Array<Array<History>>>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
readHistories().then((item) => {
if (item) {
console.log(item);
setHistories(item);
setLoading(false);
}
});
}, [setHistories, setLoading]);
return (
<Table striped highlightOnHover verticalSpacing="md">
<thead>
<tr>
<th>UID</th>
<th>Test 1</th>
<th>Test 1</th>
<th>Test 3</th>
<th>Settings</th>
</tr>
</thead>
<tbody>
{!loading ? (
histories != null ? (
histories.map((item: Array<History>, index: number) => {
return (
<Link key={index} href={`profile/${item[0].uid}`} passHref>
<tr>
<td>
{trimUid(item[0].uid)}
</td>
<td>
<div>
<span>{item[0].score} </span>
<span>{item[0].questionnaireType}</span>
</div>
<div>{convertTimestampToDate(item[0].timestamp.seconds)}</div>
</td>
<td>
<div>
<span>{item[1].score}</span>
<span> {item[1].questionnaireType}</span>
</div>
<div>{convertTimestampToDate(item[1].timestamp.seconds)}</div>
</td>
<td>
<div>
<span>{item[2].score}</span>
<span> {item[2].questionnaireType}</span>
</div>
<div>
{convertTimestampToDate(item[2].timestamp.seconds)}
</div>
</td>
<td>
<div>
<span>Sms:</span>
<span> {displaySettings(item[0].settings, "sms")}</span>
</div>
<div>
<span>Call:</span>
<span> {displaySettings(item[0].settings, "call")}</span>
</div>
</td>
</tr>
</Link>
);
})
) : (
""
)
) : (
<tr>
<td>Loading...</td>
</tr>
)}
</tbody>
</Table>
);
}
...
function displaySettings(settings: any, key: string) {
if (settings != null && key == "sms") {
let onOff = settings.smsOn ? "On" : "Off";
console.log("Sms On : " onOff);
return onOff;
} else console.log("Settings Sms is null");
if (settings != null && key == "call") {
let onOff = settings.callOn ? "On" : "Off";
console.log("Call On : " onOff);
return onOff;
} else console.log("Settings Call is null");
}
Console Output on page load
[Array(3)]
DataTable.tsx?a927:135 Settings Sms is null
DataTable.tsx?a927:141 Settings Call is null
DataTable.tsx?a927:135 Settings Sms is null
DataTable.tsx?a927:141 Settings Call is null
Response Object : [Array(3)] in console output
[
[
{
"uid": "prW2afyS3WVMaUsnKoXBoEyrvLR2",
"timestamp": {
"seconds": 1647168151,
"nanoseconds": 124000000
},
"questionnaireType": "EPDS",
"score": 16,
"settings": {
"callOn": true,
"smsOn": true
}
},
{
"uid": "prW2afyS3WVMaUsnKoXBoEyrvLR2",
"timestamp": {
"seconds": 1647163878,
"nanoseconds": 998000000
},
"questionnaireType": "PHQ9",
"score": 18,
"settings": {
"callOn": true,
"smsOn": true
}
},
{
"uid": "prW2afyS3WVMaUsnKoXBoEyrvLR2",
"timestamp": {
"seconds": 1647162553,
"nanoseconds": 5000000
},
"questionnaireType": "PHQ9",
"score": 17,
"settings": {
"callOn": true,
"smsOn": true
}
}
]
]
History.ts
import { Timestamp } from "firebase/firestore";
import { QuestionnaireType } from "./QuestionnaireType";
import { Settings } from "./Settings";
export class History {
uid: string;
timestamp: Timestamp;
questionnaireType: QuestionnaireType; // enum
score: number;
settings: Settings;
constructor(
uid: string,
timestamp: Timestamp,
questionnaireType: QuestionnaireType,
score: number,
settings: Settings
) {
this.uid = uid;
this.timestamp = timestamp;
this.questionnaireType = questionnaireType;
this.score = score;
this.settings = settings;
}
}
Settings.ts
export class Settings {
smsOn: boolean;
callOn: boolean;
constructor(smsOn: boolean, callOn: boolean) {
this.smsOn = smsOn;
this.callOn = callOn;
}
}
Update : Added readHistory
function
export async function readHistories() {
const q = query(
collectionGroup(db, "History"),
where("score", ">=", 10)
).withConverter(historyConverter);
const querySnapshot = await getDocs(q);
const histories = Array<History>();
querySnapshot.forEach((doc) => {
if (doc.exists()) {
const history: History = doc.data();
history.uid = doc.ref.parent.parent!.id;
histories.push(history);
} else console.log("No documents!");
});
return filterLastMonthHistories(histories);
}
function filterLastMonthHistories(histories: Array<History>) {
// ...
const groupedHistories = groupByUid(recentHistories, "uid");
Object.keys(groupedHistories).forEach(function (key) {
readUser(key).then((user) => {
if (user) {
groupedHistories[key].forEach(function (history: History) {
history.settings = user.settings;
});
}
});
});
return sortByTimestampLimit(groupedHistories);
}
function groupByUid(histories: Array<History>, key: string) {
// ...
}
function sortByTimestampLimit(arr: Object) {
// ...
return sortedRecentHistories;
}
export async function readUser(docId: string) {
const docRef = doc(db, "Users", docId).withConverter(userConverter);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
let user = docSnap.data();
return user;
} else console.log("No such document");
}
CodePudding user response:
Problem was in following piece of code I was making async
request for every key
in groupedHistories
object and setting the History
object settings
property without waiting for all the async
requests to complete.
Object.keys(groupedHistories).forEach(function (key) {
readUser(key).then((user) => {
if (user) {
groupedHistories[key].forEach(function (history: History) {
history.settings = user.settings;
});
}
});
});
Solution was to use Promise.all()
which wait until all promises
get resolved
and assign returned values
to an array
.
async function filterLastMonthHistories(histories: Array<History>) {
// ...
const groupedHistories = groupByUid(recentHistories, "uid");
const uIds = Object.keys(groupedHistories);
const sortedRecentHistories = sortByTimestampLimit(groupedHistories);
const users: Array<User | undefined> = await Promise.all(
uIds.map(function (uid) {
return readUser(uid);
})
);
sortedRecentHistories.forEach(function (histories) {
histories.forEach(function (history) {
users.forEach(function (user) {
if (user != undefined) {
history.settings = user.settings;
}
});
});
});
return sortedRecentHistories;