1

I am trying to read a value from RealtimeDatabase on Firebase and render it in an element, but it keeps returning undefined. I have the following code:

const getStudentName = (studentId) => {
    firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
        return snapshot.val().name; 
    })
}

const StudentName = (studentId) => ( <p>{getStudentName(studentId)}</p> )

I know it's nothing wrong with the database itself or the value I'm finding, because if I do:

const getStudentName = (studentId) => {
    firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
        console.log(snapshot.val().name);
        return "Test"; 
    })
}

I still see a correct name from my database outputted to console as expected, yet "Test" is not returned to the element. However, if I do it like this:

const getStudentName = (studentId) => {
    firebase.database().ref('students').child(studentId).on("value", (snapshot) => {
        console.log(snapshot.val().name);
    })
    return "Test"; 
}

then "Test" is returned to the

element and displayed. I'm very confused, as I don't understand how my console.log() can be reached inside the function but a 'return' statement right after it will not return.

New to React and Firebase, please help! Thank you.

EDIT: I'm sure it's self-explanatory, but you can assume a simple database in the form:

{ "students": [
    "0": { "name": "David" },
    "1": { "name": "Sally" } ]}

If 'studentId' is 0 then 'console.log(snapshot.val().name)' successfully outputs 'David', but 'David' will not return to the

element.

Chris
  • 79
  • 1
  • 7
  • 1
    Your Firebase call is *asynchronous*. You can't return directly from it like you are expecting. However, there are a number of ways to deal with that situation. [How to return the response from an asynchronous call](https://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-asynchronous-call) – jnpdx Feb 07 '22 at 01:11

2 Answers2

3

You can't return something from an asynchronous call like that. If you check in the debugger or add some logging, you'll see that your outer return "Test" runs before the console.log(snapshot.val().name) is ever called.

Instead in React you'll want to use a useState hook (or setState method) to tell React about the new value, so that it can then rerender the UI.

I recommend reading the React documentation on the using the state hook, and the documentation on setState.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you! I've been experimenting the last few days and building my app successfully using this sort of mentality. This exposed a lot of other problems, but in resolving them I've developed a much stronger grasp of React. Thanks again! – Chris Feb 16 '22 at 13:47
  • Good to hear @Chris – Frank van Puffelen Feb 16 '22 at 14:39
2

I'm not sure where you are consuming getStudentName, but your current code makes attaches a real-time listener to that database location. Each time the data at that location updates, your callback function gets invoked. Because of that, returning a value from such a function doesn't make much sense.

If you instead meant to fetch the name from the database just once, you can use the once() method, which returns a Promise containing the value you are looking for.

As another small optimization, if you only need the student's name, consider fetching /students/{studentId}/name instead.

const getStudentName = (studentId) => {
    return firebase.database()
      .ref("students")
      .child(studentId)
      .child("name")
      .once("value")
      .then(nameSnapshot => nameSnapshot.val());
}

With the above code, getStudentName(studentId) now returns a Promise<string | null>, where null would be returned when that student doesn't exist.

getStudentName(studentId)
  .then(studentName => { /* ... do something ... */ })
  .catch(err => { /* ... handle errors ... */ })

If instead you were filling a <Student> component, continuing to use the on snapshot listener may be the better choice:

const Student = (props) => {
  const [studentInfo, setStudentInfo] = useState({ status: "loading", data: null, error: null });

  useEffect(() => {
    // build reference
    const studentDataRef = firebase.database()
      .ref("students")
      .child(props.studentId)
      .child("name");

    // attach listener
    const listener = studentDataRef.on(
      'value',
      (snapshot) => {
        setStudentInfo({
          status: "ready",
          data: snapshot.val(),
          error: null
        });
      },
      (error) => {
        setStudentInfo({
          status: "error",
          data: null,
          error
        });
      }
    );

    // detach listener in unsubscribe callback
    return () => studentDataRef.off(listener);
  }, [props.studentId]); // <- run above code whenever props.studentId changes

  // handle the different states while the data is loading
  switch (studentInfo.status) {
    case "loading":
      return null; // hides component, could also show a placeholder/spinner
    case "error":
      return (
        <div class="error">
          Failed to retrieve data: {studentInfo.error.message}
        </div>
      );
  }

  // render data using studentInfo.data
  return (
    <div id={"student-" + props.studentId}>
      <img src={studentInfo.data.image} />
      <span>{studentInfo.data.name}</span>
    </div>
  );
}

Because of how often you might end up using that above useState/useEffect combo, you could rewrite it into your own useDatabaseData hook.

samthecodingman
  • 23,122
  • 4
  • 30
  • 54