2

I am making a list of posts in a .map fetching from my Firebase Cloud Firestore. I also have photos connecting to each post, and I fetch them by using the post.title + '.jpg' from Cloud Storage. The post title is fetching great, but when using the fetchImage it is not showing in the post. It is showing in the console.log. I can also access the url from the console, so nothing wrong there.

Any ideas?

{this.state.posts.map((post, i) => {
  let fetchImage
  firebase
  .storage()
  .ref(post.title + '.jpg')
  .getDownloadURL()
  .then((url) => {
    console.log(url)
    fetchImage = url;
  });
  return (
    <Text>{post.title}</Text>
    <Image
      style={styles.image}
      source={{
      uri: fetchImage
      }}
    />
  )}
)}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
vemund
  • 1,667
  • 4
  • 29
  • 43
  • Can you please directly open image in browser and share network call header in response? – A Qadeer Qureshi Oct 16 '18 at 13:18
  • @AQadeerQureshi The image that is loaded in the console.log is this: https://firebasestorage.googleapis.com/v0/b/greencitynorway-a8324.appspot.com/o/Gryr%20lanserer%20to%20nye%20plantebaserte%20nyheter.jpg?alt=media&token=589a975b-e57c-49b1-b021-fcd21cb4210d – vemund Oct 16 '18 at 13:30

4 Answers4

2

The download URL is loaded asynchronously. To see what this means, place a few log statements:

console.log("Before calling getDownloadURL")
firebase
.storage()
.ref(post.title + '.jpg')
.getDownloadURL()
.then((url) => {
  console.log("Got URL")
});
console.log("After calling getDownloadURL")

When you run this code you get:

Before calling getDownloadURL

After calling getDownloadURL

Got URL

This is probably not the order you expected the output in. But it completely explains why your return does not return the download URL: it hasn't been loaded yet.

The then() callback from your call to getDownloadURL runs after you return the component that uses uri: fetchImage. And you can't return a value that hasn't loaded yet.

The common solution in React is to store any data that is asynchronously loaded into the component's state.

this.state.posts.map((post, i) => {
  let fetchImage
  firebase
  .storage()
  .ref(post.title + '.jpg')
  .getDownloadURL()
  .then((url) => {
    let state = {};
    state[post.title+"_url"] = url
    this.setState(state)
  });
})

Since any call to setState() forces the component to rerender itself, the new render will then pick up the updated download URL from the state.

return (
  <Text>{post.title}</Text>
  <Image
    style={styles.image}
    source={{
    uri: this.state[post.title+"_url"]
    }}
  />
Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • This doesn't seem to work. Can I set a state for each item in the list, is that really possible? – vemund Oct 16 '18 at 13:33
  • Woops, I forgot to call `setState`. I just added that. – Frank van Puffelen Oct 16 '18 at 13:53
  • @vemund setState will not be accessible inside then, as it's out of scope. According to closure rules. – A Qadeer Qureshi Oct 17 '18 at 07:08
  • @vemund please follow this example.. and set url in state. I hope it works :) If you still need my help.. i will write complete solution. Happy coding... https://reactjs.org/docs/faq-ajax.html – A Qadeer Qureshi Oct 17 '18 at 07:15
  • Now it works! Thanks! However, what I can say, is that it loads incredibly slow. Might this be because of the file size? – vemund Oct 17 '18 at 07:53
  • Had to remove this again. The handeling of state in the list makes all other Firebase functions very delayed and it gives a lot of warnings. `Unhandled Promise Rejection'. Error: No object exist at the desired reference.` as well as `RCTBridge equired dispatch_sync to load RCTDevLoading View. This may lead to deadlocks.` – vemund Oct 18 '18 at 20:21
1

I suspect that at the moment this function returns Text and Image components the promise which gets the images from the server is not yet resolved -- and thus nothing is displayed and the component doesn't re-render once the promise is done.

The typical way of fetching external resources in a React component is to use the class Component/PureComponent syntax, dispatch a fetch request in componentDidMount hook and save the data returned by the promise in state (using this.setState).

The render function should render based on the values in state -- so that once state is updated with the fetched value, the component will re-render using the fetched data.

1

What is the ReactJS this.props.items.map Property?

This should help you understand the concepts around the usage of the “map” method to traverse and display a list of similar objects representing a component in ReactJS. The title “this.props.items.map” could be any other map method, such as “this.props.profiles.map” which has examples below where profiles or items represent an array. It could be used to create a list, table, etc.

Here are the main points of this article:

  • Map is NOT a feature of ReactJS
  • See a code sample using “map” in the context of this.props.profiles.map

After looking at the tutorial provided on this ReactJS tutorials page where the reference of .map is made to display Comment objects, one may get confused and think that “map” is a ReactJS feature. As a matter of fact, this is a standard JavaScript function which could be called on any array

If you have worked on languages such as Python (apply method), or R (lapply method), you've probably used “map” as a method to pass a function with a parameter representing the reference of an object stored in an array. When “map” is called, the function is applied to each of the objects stored in the array. The “map” returns a new array consisting of objects which might be created using objects of the passed array

The general syntax is: array.map(func)

where func should take one parameter.

As mentioned in text above, the return value of array.map is another array.

Code sample using “map” in the context of this.props.profiles.map

In the example below, notice some of the following things:

  • There are two components such as UserProfiles and Profile
  • Profile component is used to represent actual profile comprising of name and country attributes.
  • UserProfiles, as it sounds, is used to represents one or more profile and renders Profile components.
  • Note that UserProfiles is passed a json object such as profilesJson which consists of profiles represented in form of JSON object.
  • render method of UserProfiles displays “allProfiles” variable which is created using “map” method. The “map” method, in turn, returns an array Profile object.

Following is how the below code sample would be displayed on HTML:

<div id="content"></div>
<script type="text/jsx">
var profilesJson = [
{name: "Pete Hunt", country: "USA"},
{name: "Jordan Walke", country: "Australia"}];
var Profile = React.createClass({
render: function(){
          return(
              <div>
<div>Name: {this.props.name}</div>
<div>Country: {this.props.country}</div>
<hr/>
     </div>
);
    }
});
var UserProfiles = React.createClass({
render: function(){
var allProfiles = this.props.profiles.map(function(profile){
return (
<Profile name={profile.name} country={profile.country} />
);
});
return(
<div>{allProfiles}</div>
);
}
});
React.render( <UserProfiles profiles={profilesJson}/>, document.getElementById( "content"));</script>
Kousic
  • 2,651
  • 2
  • 10
  • 23
0

I really don't know why, but I couldn't make it in actions, and had to use a hack setTimeout(...), even if I have thunk in my applyMiddleware on App.tsx level.

I'm sending my full working solution, for feedback, and fully workable code if somebody is looking for! I'm sending the entire file, please ignore the analytics-related, and other, non-related stuff.

action

import analytics from '@react-native-firebase/analytics';
import crashlytics from '@react-native-firebase/crashlytics';
import firestore from '@react-native-firebase/firestore';
import readItemFromStorage from '../../../common/readItemFromStorage';
import storage from "@react-native-firebase/storage";
import writeItemToStorage from '../../../common/writeItemToStorage';
import { Event } from '../../../interfaces/event';
import {
  FIRESTORE_COLLECTION,
  // @ts-ignore
} from '@env';

export const SET_EVENTS = 'SET_EVENTS';

export const fetchUsersEvents: () => (dispatch: any) => void = () => {
  return async (dispatch: any) => {
    let eventsInStorage: Event[] | null | any = await readItemFromStorage();
    let imageUrlPath: string = "";

    await firestore()
      .collection(FIRESTORE_COLLECTION)
      .get()
      .then(querySnapshot => {
        querySnapshot.forEach(async documentSnapshot => {
          await storage().ref(documentSnapshot.data().imageUrl).getDownloadURL()
            .then((imageUrlStorage) => {
              imageUrlPath = imageUrlStorage;

              analytics().logEvent('custom_log', {
                description:
                  '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> then2'
              });
            }).catch((error: unknown) => {
              if (error instanceof Error) {
                imageUrlPath = "";

                analytics().logEvent('custom_log', {
                  description:
                    '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> catch, error: ' +
                    error,
                });
                crashlytics().recordError(error);
              }
            });

          eventsInStorage.push({
            id: documentSnapshot.data().id,
            date: new Date(documentSnapshot.data().date.seconds * 1000),
            description: documentSnapshot.data().description,
            // @ts-ignore
            firestoreDocumentId: documentSnapshot.ref._documentPath._parts[1],
            imageUrl: imageUrlPath,
            isUserEvent: documentSnapshot.data().isUserEvent,
            latitude: documentSnapshot.data().latitude,
            longitude: documentSnapshot.data().longitude,
            title: documentSnapshot.data().title,
          });
        });
        analytics().logEvent('custom_log', {
          description:
            '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then, eventsInStorage: ' +
            eventsInStorage,
        });
      })
      .catch((error: unknown) => {
        if (error instanceof Error) {
          analytics().logEvent('custom_log', {
            description:
              '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> catch, error: ' +
              error,
          });
          crashlytics().recordError(error);
        }
      })
      .finally(() => {
        // TODO: Temporary solution with the "setTimeout", make it properly with async actions (redux-thunk).
        setTimeout(() => {
          dispatch({
            type: SET_EVENTS,
            events: eventsInStorage,
          });
          writeItemToStorage(eventsInStorage);
        }, 1000);
        analytics().logEvent('custom_log', {
          description:
            '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> finally',
        });
      });
  };
};

Later on, I just load them from my state in a different file (screen). However, I couldn't make it happen without setTimeout(...) while fetching data from an action.

Daniel Danielecki
  • 8,508
  • 6
  • 68
  • 94