[SOLVED] All attributes of response object is not null but some object attriubes shown as null

Issue

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>&nbsp;{item[1].questionnaireType}</span>
                      </div>
                      <div>{convertTimestampToDate(item[1].timestamp.seconds)}</div>
                    </td>
                    <td>
                      <div>
                        <span>{item[2].score}</span>
                        <span>&nbsp;{item[2].questionnaireType}</span>
                      </div>
                      <div>
                        {convertTimestampToDate(item[2].timestamp.seconds)}
                      </div>
                    </td>
                    <td>
                      <div>
                        <span>Sms:</span>
                        <span>&nbsp;{displaySettings(item[0].settings, "sms")}</span>
                      </div>
                      <div>
                        <span>Call:</span>
                        <span>&nbsp;{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");
}

Solution

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;

Answered By – user158

Answer Checked By – Candace Johnson (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *