<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Expo Fire]]></title><description><![CDATA[Expo Fire]]></description><link>https://expofire.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 07 Oct 2026 13:08:57 GMT</lastBuildDate><atom:link href="https://expofire.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Cloud Push Notification Service with Expo]]></title><description><![CDATA[Engaging users with notifications after they install an app is important for increasing app retention, reducing churn rate, and communicating with users for new offers, announcements, and more.
Google offers cloud notification service through Firebas...]]></description><link>https://expofire.hashnode.dev/cloud-push-notification-service-with-expo</link><guid isPermaLink="true">https://expofire.hashnode.dev/cloud-push-notification-service-with-expo</guid><category><![CDATA[React Native]]></category><category><![CDATA[notifications]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[Expo]]></category><dc:creator><![CDATA[Expo Fire]]></dc:creator><pubDate>Sun, 28 May 2023 21:10:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1685308208129/59fe7f77-05a3-464e-a250-a94a0e3a258c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Engaging users with notifications after they install an app is important for increasing app retention, reducing churn rate, and communicating with users for new offers, announcements, and more.</p>
<p>Google offers cloud notification service through Firebase Cloud Messaging(FCM) on Android devices and Apple Push Notification service (APNs) for iOS devices.</p>
<p>While you are building with Expo, you don't have direct access to those tools and infrastructure provided by mobile OS vendors.</p>
<p>In this article, we will build our cloud notification service for the Expo app to be used internally by the marketing or sale team.</p>
<h3 id="heading-subscribe-and-collect-device-notification-push-tokens">Subscribe and collect device notification push tokens.</h3>
<p>To receive notifications, the app must collect a device push notification token which is unique for each device, and save it in the database to be used later.</p>
<p>Expo offers a package to collect those push tokens - <a target="_blank" href="https://docs.expo.dev/versions/latest/sdk/notifications/">https://docs.expo.dev/versions/latest/sdk/notifications/</a></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { useState, useEffect, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { Text, View, Button, Platform } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Device <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-device"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Notifications <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-notifications"</span>;

Notifications.setNotificationHandler({
  <span class="hljs-attr">handleNotification</span>: <span class="hljs-keyword">async</span> () =&gt; ({
    <span class="hljs-attr">shouldShowAlert</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">shouldPlaySound</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">shouldSetBadge</span>: <span class="hljs-literal">false</span>,
  }),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [expoPushToken, setExpoPushToken] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [notification, setNotification] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> notificationListener = useRef();
  <span class="hljs-keyword">const</span> responseListener = useRef();

  useEffect(<span class="hljs-function">() =&gt;</span> {
    registerForPushNotificationsAsync().then(<span class="hljs-function">(<span class="hljs-params">pushToken</span>) =&gt;</span> {
      <span class="hljs-comment">// Save notification push token on your  database to be used later..</span>

      setExpoPushToken(pushToken);
    });

    notificationListener.current =
      Notifications.addNotificationReceivedListener(<span class="hljs-function">(<span class="hljs-params">notification</span>) =&gt;</span> {
        setNotification(notification);
      });

    responseListener.current =
      Notifications.addNotificationResponseReceivedListener(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(response);
      });

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
      Notifications.removeNotificationSubscription(
        notificationListener.current
      );
      Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">View</span>
      <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">flex:</span> <span class="hljs-attr">1</span>,
        <span class="hljs-attr">alignItems:</span> "<span class="hljs-attr">center</span>",
        <span class="hljs-attr">justifyContent:</span> "<span class="hljs-attr">space-around</span>",
      }}
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Text</span>&gt;</span>Your expo push token: {expoPushToken}<span class="hljs-tag">&lt;/<span class="hljs-name">Text</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">View</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">registerForPushNotificationsAsync</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">let</span> token;

  <span class="hljs-keyword">if</span> (Platform.OS === <span class="hljs-string">"android"</span>) {
    <span class="hljs-keyword">await</span> Notifications.setNotificationChannelAsync(<span class="hljs-string">"default"</span>, {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"default"</span>,
      <span class="hljs-attr">importance</span>: Notifications.AndroidImportance.MAX,
      <span class="hljs-attr">vibrationPattern</span>: [<span class="hljs-number">0</span>, <span class="hljs-number">250</span>, <span class="hljs-number">250</span>, <span class="hljs-number">250</span>],
      <span class="hljs-attr">lightColor</span>: <span class="hljs-string">"#FF231F7C"</span>,
    });
  }

  <span class="hljs-keyword">if</span> (Device.isDevice) {
    <span class="hljs-keyword">const</span> { <span class="hljs-attr">status</span>: existingStatus } =
      <span class="hljs-keyword">await</span> Notifications.getPermissionsAsync();
    <span class="hljs-keyword">let</span> finalStatus = existingStatus;
    <span class="hljs-keyword">if</span> (existingStatus !== <span class="hljs-string">"granted"</span>) {
      <span class="hljs-keyword">const</span> { status } = <span class="hljs-keyword">await</span> Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    <span class="hljs-keyword">if</span> (finalStatus !== <span class="hljs-string">"granted"</span>) {
      alert(<span class="hljs-string">"Failed to get push token for push notification!"</span>);
      <span class="hljs-keyword">return</span>;
    }
    token = (<span class="hljs-keyword">await</span> Notifications.getExpoPushTokenAsync()).data;
    <span class="hljs-built_in">console</span>.log(token);
  } <span class="hljs-keyword">else</span> {
    alert(<span class="hljs-string">"Must use physical device for Push Notifications"</span>);
  }

  <span class="hljs-keyword">return</span> token;
}
</code></pre>
<h3 id="heading-push-notification-to-subscribed-users">Push notification to subscribed users</h3>
<p>Once the user subscribed and the app collects a push token, we can send a notification to those users.</p>
<p>Expo offers Expo's Push API service which is built on top of Google and Apple's own notification services.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendNotification</span>(<span class="hljs-params">pushToken, title, body</span>) </span>{
  fetch(<span class="hljs-string">"https://exp.host/--/api/v2/push/send"</span>, {
    <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
      <span class="hljs-attr">to</span>: tokenToSend,
      <span class="hljs-attr">sound</span>: <span class="hljs-string">"default"</span>,
      <span class="hljs-attr">title</span>: title,
      <span class="hljs-attr">body</span>: body,
      <span class="hljs-attr">data</span>: { title, body },
      <span class="hljs-attr">channelId</span>: <span class="hljs-string">"default"</span>,
    }),
  });
}
</code></pre>
<p>We can use <code>sendNotification</code> function to push various types of notifications to target users.</p>
<pre><code class="lang-js"><span class="hljs-comment">// Promotion notifications</span>
sendNotification(
  <span class="hljs-string">"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"</span>,
  <span class="hljs-string">"Black Friday 25% OFF"</span>,
  <span class="hljs-string">"Exclusive for you until Monday"</span>
);

<span class="hljs-comment">// Event notifications</span>
sendNotification(
  <span class="hljs-string">"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"</span>,
  <span class="hljs-string">"Account upgaraded"</span>,
  <span class="hljs-string">"You are now eligible for premium features.."</span>
);
</code></pre>
<p>You can build your dashboard where the marketing team member writes the notification title and body, and select target users to receive that notification or any other automated notification solution.</p>
<h3 id="heading-further-reading">Further Reading :</h3>
<p>Expo Push Service - <a target="_blank" href="https://docs.expo.dev/push-notifications/sending-notifications/">https://docs.expo.dev/push-notifications/sending-notifications/</a></p>
<p>Google Cloud Messaging (FCM) - <a target="_blank" href="https://firebase.google.com/docs/cloud-messaging/">https://firebase.google.com/docs/cloud-messaging/</a></p>
<p>Apple Push Notification service (APNs - <a target="_blank" href="https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns">https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns</a></p>
]]></content:encoded></item><item><title><![CDATA[Upload Image with Expo and Firebase Cloud Storage]]></title><description><![CDATA[User-uploaded images are common in mobile apps, how can be done with Expo and Firebase cloud storage?  - let's find out the solution.
Request a device media library files  access permission
Both iOS and Android require a user to grant access to the a...]]></description><link>https://expofire.hashnode.dev/upload-image-with-expo-and-firebase-cloud-storage</link><guid isPermaLink="true">https://expofire.hashnode.dev/upload-image-with-expo-and-firebase-cloud-storage</guid><category><![CDATA[React]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Expo Fire]]></dc:creator><pubDate>Fri, 11 Feb 2022 14:34:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644589214757/GcFf_5FhPr.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>User-uploaded images are common in mobile apps, how can be done with Expo and Firebase cloud storage?  - let's find out the solution.</p>
<h3 id="heading-request-a-device-media-library-files-access-permission">Request a device media library files  access permission</h3>
<p>Both iOS and Android require a user to grant access to the app before reading or writing to the media library. </p>
<p>Throughout this article, we will use <code>expo-image-picker</code> to request permission and upload an image from the media library. </p>
<pre><code class="lang-js">
<span class="hljs-comment">// hasMediaLibraryPermissionGranted.js</span>

<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> ImagePicker <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-image-picker"</span>;

<span class="hljs-keyword">const</span> hasMediaLibraryPermissionGranted = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">let</span> granted =  <span class="hljs-literal">false</span>;

  <span class="hljs-keyword">const</span> permission = <span class="hljs-keyword">await</span> ImagePicker.requestMediaLibraryPermissionsAsync();

  <span class="hljs-keyword">if</span> (!permission.canAskAgain || permission.status === <span class="hljs-string">"denied"</span>) {
    granted = <span class="hljs-literal">false</span>;

  }

  <span class="hljs-keyword">if</span> (permission.granted) {
    granted = <span class="hljs-literal">true</span>;
  }

  <span class="hljs-keyword">return</span> granted;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> hasMediaLibraryPermissionGranted
</code></pre>
<h3 id="heading-upload-image-from-device-media-library">Upload image  from device media library</h3>
<p><code>expo-image-picker</code>  exposes <code>ImagePicker.launchImageLibraryAsync</code> method to select image or video and copy to app cache then return file <strong>Universal Resource Identifier (URI</strong>) path.</p>
<pre><code class="lang-js"><span class="hljs-comment">// uploadImageFromDevice.js</span>

<span class="hljs-keyword">const</span> uploadImageFromDevice = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">let</span> imgURI = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">const</span> storagePermissionGranted = <span class="hljs-keyword">await</span> hasMediaLibraryPermissionGranted();

  <span class="hljs-comment">// Discard execution when  media library permission denied</span>
  <span class="hljs-keyword">if</span> (!storagePermissionGranted) <span class="hljs-keyword">return</span> imgURI;

  <span class="hljs-keyword">let</span> result = <span class="hljs-keyword">await</span> ImagePicker.launchImageLibraryAsync({
    <span class="hljs-attr">mediaTypes</span>: ImagePicker.MediaTypeOptions.Images,
    <span class="hljs-attr">allowsEditing</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">aspect</span>: [<span class="hljs-number">4</span>, <span class="hljs-number">4</span>],
    <span class="hljs-attr">quality</span>: <span class="hljs-number">1</span>,
  });

  <span class="hljs-keyword">if</span> (!result.cancelled) {
    imgURI = result.uri;
  }

  <span class="hljs-keyword">return</span> imgURI;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> uploadImageFromDevice;
</code></pre>
<h3 id="heading-fetch-uploadable-image-binary-data">Fetch uploadable image binary data.</h3>
<p>The URI return by <code>ImagePicker.launchImageLibraryAsync</code>  is a reference to the cached image and doesn't hold the image data.</p>
<p>Most cloud storage providers including Firebase Cloud Storage support a  binary large object (BLOB) for uploading various files types.</p>
<p>Let us fetch  BLOB data from an image URI .</p>
<pre><code class="lang-js">
<span class="hljs-comment">// getBlobFroUri.js</span>

<span class="hljs-keyword">const</span> getBlobFroUri = <span class="hljs-keyword">async</span> (uri) =&gt; {
  <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> xhr = <span class="hljs-keyword">new</span> XMLHttpRequest();
    xhr.onload = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
      resolve(xhr.response);
    };
    xhr.onerror = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">e</span>) </span>{
      reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">TypeError</span>(<span class="hljs-string">"Network request failed"</span>));
    };
    xhr.responseType = <span class="hljs-string">"blob"</span>;
    xhr.open(<span class="hljs-string">"GET"</span>, uri, <span class="hljs-literal">true</span>);
    xhr.send(<span class="hljs-literal">null</span>);
  });

  <span class="hljs-keyword">return</span> blob;
};



<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> getBlobFroUri
</code></pre>
<h3 id="heading-upload-binary-data-to-firebase-cloud-storage">Upload  Binary  data to Firebase Cloud Storage</h3>
<p>Image BLOB data are ready now, Firebase provides a cloud object storage service with the free-tier plan for an early-stage app.</p>
<pre><code class="lang-js">
<span class="hljs-comment">// manageFileUpload.js</span>


<span class="hljs-keyword">import</span> firebase <span class="hljs-keyword">from</span> <span class="hljs-string">"firebase"</span>;

<span class="hljs-keyword">const</span> manageFileUpload = <span class="hljs-keyword">async</span> (
  fileBlob,
  { onStart, onProgress, onComplete, onFail }
) =&gt; {
  <span class="hljs-keyword">const</span> imgName = <span class="hljs-string">"img-"</span> + <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();

  <span class="hljs-keyword">const</span> storageRef = firebase.storage().ref(<span class="hljs-string">`images/<span class="hljs-subst">${imgName}</span>.jpg`</span>);

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"uploading file"</span>, imgName);

  <span class="hljs-comment">// Create file metadata including the content type</span>
  <span class="hljs-keyword">const</span> metadata = {
    <span class="hljs-attr">contentType</span>: <span class="hljs-string">"image/jpeg"</span>,
  };

  <span class="hljs-comment">// Trigger file upload start event</span>
  onStart &amp;&amp; onStart();
  <span class="hljs-keyword">const</span> uploadTask = storageRef.put(fileBlob, metadata);
  <span class="hljs-comment">// Listen for state changes, errors, and completion of the upload.</span>
  uploadTask.on(
    <span class="hljs-string">"state_changed"</span>,
    <span class="hljs-function">(<span class="hljs-params">snapshot</span>) =&gt;</span> {
      <span class="hljs-comment">// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded</span>
      <span class="hljs-keyword">const</span> progress = (snapshot.bytesTransferred / snapshot.totalBytes) * <span class="hljs-number">100</span>;

      <span class="hljs-comment">// Monitor uploading progress</span>
      onProgress &amp;&amp; onProgress(<span class="hljs-built_in">Math</span>.fround(progress).toFixed(<span class="hljs-number">2</span>));
    },
    <span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> {
      <span class="hljs-comment">// Something went wrong - dispatch onFail event with error  response</span>
      onFail &amp;&amp; onFail(error);
    },
    <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Upload completed successfully, now we can get the download URL</span>

      uploadTask.snapshot.ref.getDownloadURL().then(<span class="hljs-function">(<span class="hljs-params">downloadURL</span>) =&gt;</span> {
        <span class="hljs-comment">// dispatch on complete event</span>
        onComplete &amp;&amp; onComplete(downloadURL);

        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"File available at"</span>, downloadURL);
      });
    }
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> manageFileUpload;
</code></pre>
<p>Finally, Connect all pieces together</p>
<pre><code><span class="hljs-comment">//App.js</span>

<span class="hljs-keyword">import</span> <span class="hljs-operator">*</span> <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">React</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {
  <span class="hljs-title">Text</span>,
  <span class="hljs-title">View</span>,
  <span class="hljs-title">StyleSheet</span>,
  <span class="hljs-title">Image</span>,
  <span class="hljs-title">Platform</span>,
  <span class="hljs-title">useWindowDimensions</span>,
} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">Constants</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"expo-constants"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">firebase</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./config/firebase"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">AntDesign</span>, <span class="hljs-title">Feather</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">uplodImageFromDevice</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./uploadImageFromDevice"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">getBlobFromUri</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./getBlobFromUri"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">manageFileUpload</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./manageFileUpload"</span>;
export default <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  const [imgURI, setImageURI] <span class="hljs-operator">=</span> React.useState(null);

  const [isUploading, setIsUploading] <span class="hljs-operator">=</span> React.useState(<span class="hljs-literal">false</span>);
  const [progress, setProgress] <span class="hljs-operator">=</span> React.useState(<span class="hljs-number">0</span>);
  const [remoteURL, setRemoteURL] <span class="hljs-operator">=</span> React.useState(<span class="hljs-string">""</span>);

  const [<span class="hljs-function"><span class="hljs-keyword">error</span>, <span class="hljs-title">setError</span>] = <span class="hljs-title">React</span>.<span class="hljs-title">useState</span>(<span class="hljs-params">null</span>)</span>;
  const { width } <span class="hljs-operator">=</span> useWindowDimensions();

  const handleLocalImageUpload <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    const fileURI <span class="hljs-operator">=</span> await uplodImageFromDevice();

    <span class="hljs-keyword">if</span> (fileURI) {
      setImageURI(fileURI);
    }
  };

  const onStart <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    setIsUploading(<span class="hljs-literal">true</span>);
  };

  const onProgress <span class="hljs-operator">=</span> (progress) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    setProgress(progress);
  };
  const onComplete <span class="hljs-operator">=</span> (fileUrl) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    setRemoteURL(fileUrl);
    setIsUploading(<span class="hljs-literal">false</span>);
    setImageURI(null);
  };

  const onFail <span class="hljs-operator">=</span> (<span class="hljs-function"><span class="hljs-keyword">error</span>) =&gt; </span>{
    setError(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
    setIsUploading(<span class="hljs-literal">false</span>);
  };
  const handleCloudImageUpload <span class="hljs-operator">=</span> async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>imgURI) <span class="hljs-keyword">return</span>;

    let fileToUpload <span class="hljs-operator">=</span> null;

    const blob <span class="hljs-operator">=</span> await getBlobFromUri(imgURI);

    await manageFileUpload(blob, { onStart, onProgress, onComplete, onFail });
  };

  <span class="hljs-keyword">return</span> (
    <span class="hljs-operator">&lt;</span>View style<span class="hljs-operator">=</span>{styles.container}<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span>Text style<span class="hljs-operator">=</span>{styles.heading}<span class="hljs-operator">&gt;</span>Attach and upload image<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Text<span class="hljs-operator">&gt;</span>
      {Boolean(imgURI) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> (
        <span class="hljs-operator">&lt;</span>View<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>Image
            source<span class="hljs-operator">=</span>{{ uri: imgURI }}
            resizeMode<span class="hljs-operator">=</span><span class="hljs-string">"contain"</span>
            style<span class="hljs-operator">=</span>{{ width, height: width }}
          <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>View<span class="hljs-operator">&gt;</span>
      )}

      {<span class="hljs-operator">!</span>isUploading <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> (
        <span class="hljs-operator">&lt;</span>View style<span class="hljs-operator">=</span>{styles.row}<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>AntDesign
            name<span class="hljs-operator">=</span><span class="hljs-string">"addfile"</span>
            size<span class="hljs-operator">=</span>{<span class="hljs-number">36</span>}
            color<span class="hljs-operator">=</span>{imgURI ? <span class="hljs-string">"green"</span> : <span class="hljs-string">"black"</span>}
            onPress<span class="hljs-operator">=</span>{handleLocalImageUpload}
          <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>

          {Boolean(imgURI) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> (
            <span class="hljs-operator">&lt;</span>Feather
              name<span class="hljs-operator">=</span><span class="hljs-string">"upload-cloud"</span>
              size<span class="hljs-operator">=</span>{<span class="hljs-number">36</span>}
              color<span class="hljs-operator">=</span><span class="hljs-string">"black"</span>
              onPress<span class="hljs-operator">=</span>{handleCloudImageUpload}
            <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
          )}
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>View<span class="hljs-operator">&gt;</span>
      )}

      {isUploading <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> (
        <span class="hljs-operator">&lt;</span>View style<span class="hljs-operator">=</span>{styles.uploadProgressContainer}<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>Text<span class="hljs-operator">&gt;</span> Upload {progress} of <span class="hljs-number">100</span><span class="hljs-operator">%</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Text<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>View<span class="hljs-operator">&gt;</span>
      )}

      {Boolean(remoteURL) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> (
        <span class="hljs-operator">&lt;</span>View style<span class="hljs-operator">=</span>{{ paddingVertical: <span class="hljs-number">20</span> }}<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>Text<span class="hljs-operator">&gt;</span>
            Image has been uploaded at
            <span class="hljs-operator">&lt;</span>Text style<span class="hljs-operator">=</span>{{ color: <span class="hljs-string">"blue"</span> }}<span class="hljs-operator">&gt;</span> {remoteURL} <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Text<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>Text<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>View<span class="hljs-operator">&gt;</span>
      )}
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>View<span class="hljs-operator">&gt;</span>
  );
}
</code></pre><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://snack.expo.io/@expofire/upload-image-with-expo-and-firebase-cloud-storage">https://snack.expo.io/@expofire/upload-image-with-expo-and-firebase-cloud-storage</a></div>
<blockquote>
<p>Stuck with code and need 1:1 assistance. Ping <a target="_blank" href="mailto:emmbyiringiro@gmail.com">me</a> </p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Build Infinite Scroll with Expo and Firebase]]></title><description><![CDATA[According to the latest statistics by  statista , the average time  spent on social media is 145 minutes, or 2 hours and 25 minutes every day. Infinite scrolling  is key factor to make users stay longer on social apps  which result in increase revenu...]]></description><link>https://expofire.hashnode.dev/build-infinite-scroll-with-expo-and-firebase</link><guid isPermaLink="true">https://expofire.hashnode.dev/build-infinite-scroll-with-expo-and-firebase</guid><category><![CDATA[Firebase]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Expo]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Expo Fire]]></dc:creator><pubDate>Sun, 17 Oct 2021 20:39:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1633621722314/z9dhkW-_W.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>According to the latest statistics by  <a target="_blank" href="https://www.statista.com/statistics/433871/daily-social-media-usage-worldwide/">statista</a> , the average time  spent on social media is 145 minutes, or 2 hours and 25 minutes every day. Infinite scrolling  is key factor to make users stay longer on social apps  which result in increase revenue and users retention.</p>
<h3 id="what-is-infinite-scroll">What Is Infinite Scroll?</h3>
<p>A design technique where, as the user scrolls down a page, more content automatically and continuously loads at the bottom, eliminating the user's need to click to the next page. The idea behind infinite scroll is that it allows people to enjoy a frictionless scrolling experience.</p>
<p>In this tutorial we will implement this design pattern with Firebase's  <a target="_blank" href="https://firebase.google.com/docs/firestore">Firestore</a>  database and  <a target="_blank" href="http://expo.io/">Expo</a> . </p>
<p>Despite relational databases like PostgreSQL, MySQL and others.  Firestore is a <a target="_blank" href="https://firebase.google.com/docs/firestore/data-model">document </a> database and saves data in JSON-like format.</p>
<p>Firestore <strong>collection</strong> contains <strong>documents</strong>, the same <strong>SQL table</strong> contain<strong> records</strong>. </p>
<pre><code><span class="hljs-comment">/**
* Firestore collection which documents data structure
*/</span>
{
  <span class="hljs-attr">"xyrt023"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"xyrt02"</span>,
    <span class="hljs-attr">"fullName"</span>: <span class="hljs-string">"Leonard M. Adleman"</span>,
    <span class="hljs-attr">"knownFor"</span>: <span class="hljs-string">"Computational Complexity Theory, Cryptography"</span>,
    <span class="hljs-attr">"bio"</span>: <span class="hljs-string">"Adleman was born in San Francisco..."</span>,
    <span class="hljs-attr">"avatar"</span>: <span class="hljs-string">"https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/leonard-adleman"</span>
  },
  <span class="hljs-attr">"y7rt0bb"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"y7rt0bb"</span>,
    <span class="hljs-attr">"fullName"</span>: <span class="hljs-string">" Frances E. Allen"</span>,
    <span class="hljs-attr">"knownFor"</span>: <span class="hljs-string">"Compilers, Program optimization, Parallel computing"</span>,
    <span class="hljs-attr">"bio"</span>: <span class="hljs-string">"Allen was born in the town of Peru...."</span>,
    <span class="hljs-attr">"avatar"</span>: <span class="hljs-string">"https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/frances-allen"</span>
  },
  <span class="hljs-attr">"qoft080"</span>: {
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"qoft080"</span>,
    <span class="hljs-attr">"fullName"</span>: <span class="hljs-string">" Timothy J. Berners-Lee"</span>,
    <span class="hljs-attr">"knownFor"</span>: <span class="hljs-string">"Network design, World Wide Web, HTTP"</span>,
    <span class="hljs-attr">"bio"</span>: <span class="hljs-string">"Berners-Lee was born in London in ...."</span>,
    <span class="hljs-attr">"avatar"</span>: <span class="hljs-string">"https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/timothy-berners-lee-1"</span>
  }
}
</code></pre><p>With that knowledge, It's time to build a simple mobile app listing the most influential computer scientists.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1632499556601/sGCOZSOoM.jpeg" alt="Infinity Scroll Mockup.jpg" /></p>
<p>Here the final app</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://snack.expo.dev/@expofire/infinity-scroll-with-firebase">https://snack.expo.dev/@expofire/infinity-scroll-with-firebase</a></div>
<h3 id="batching-stream-of-content">Batching Stream of Content</h3>
<p>Continuously stream content require fetching data as multiple batches with limited size. Ideally, each content batch has at least 10 items</p>
<p>When the app is initialized, we will fetch the initial batch includes 10 documents, and save the last document ID from the initial batch to use it as the starting point for the next batch and recursively for all next batches.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1633621802882/9jV8rh5h3.png" alt="Batching- Infinity Scroll.png" /></p>
<p>To make our life easier, Let write a function with the following responsibilities:</p>
<ol>
<li><p>When the last document ID is not provided, it starts from the first document in the collection, otherwise starts after the last document from the previous batch.</p>
</li>
<li><p>For each batch, the function will return an object contains  :</p>
</li>
</ol>
<p> <strong> docs </strong> : array of documents in current batch.</p>
<p><strong>lastDocId</strong>  : last  document ID from previous  batch to be used  as starting point for next  batch.</p>
<p><strong>status</strong> :  asynchronous loading status which should be <strong>UNDETERMINED</strong>,  <strong>PENDING</strong>, 
                                  <strong> SUCCEEDED</strong> or  <strong>FAILED</strong> .</p>
<p><strong>error</strong> :   returned by Firestore when something went wrong.</p>
<pre><code class="lang-js">
<span class="hljs-keyword">import</span> firebase <span class="hljs-keyword">from</span> <span class="hljs-string">"firebase"</span>;

<span class="hljs-keyword">const</span> collection = firebase.firestore().collection(<span class="hljs-string">"[COLLECTION_NAME_HERE]"</span>);

<span class="hljs-comment">/**
 * Utilities function to extract documents in snapshots
 */</span>

<span class="hljs-keyword">const</span> extractSnapshots = <span class="hljs-function">(<span class="hljs-params">snapshots</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> extracts = [];
  snapshots.forEach(<span class="hljs-function">(<span class="hljs-params">documentSnapshot</span>) =&gt;</span> {
    extracts.push(documentSnapshot.data());
  });
  <span class="hljs-keyword">return</span> extracts;
};

<span class="hljs-comment">/**
 * Retrieve documents in batches of specified limit.
 * when last document  ID  provided, fetch documents after that
 * document (pagination query fetching)
 * @param {String} options.lastDocId -  ID of last document in previous batch
 * @param {Number} options.limit -  limit of documents per batch
 *
 * @returns - promise which will resolve into  object contains `docs`,`lastDoc`,`status`,`error`
 *
 */</span>

<span class="hljs-keyword">const</span> getDocs = <span class="hljs-keyword">async</span> ({ lastDocId, limit = <span class="hljs-number">10</span> }) =&gt; {
  <span class="hljs-keyword">let</span> docs = []; <span class="hljs-comment">// Array of docs in current bath</span>
  <span class="hljs-keyword">let</span> newLastDocId = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Last document ID in this batch</span>
  <span class="hljs-keyword">let</span> error = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> batch;

  <span class="hljs-comment">/***
   *  Fetching  documents is asynchronous operation,  It's good practice to
   *  to monitor each status of operation. Status should be UNDETERMINED, PENDING, SUCCEEDED
   *  or FAILED.
   */</span>
  <span class="hljs-keyword">let</span> status = <span class="hljs-string">"undetermined"</span>;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">/***
     * In case lastDocId provided, start after that document, otherwise
     * start on first document.
     */</span>

    <span class="hljs-keyword">if</span> (lastDocId) {
      <span class="hljs-keyword">const</span> lastDoc = <span class="hljs-keyword">await</span> collection.doc(lastDocId).get();

      <span class="hljs-comment">/**
       *  Read more about Firestore paginated query here
       *  https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query
       */</span>
      batch = collection
        .orderBy(<span class="hljs-string">"createdAt"</span>, <span class="hljs-string">"desc"</span>)
        .startAfter(lastDoc)
        .limit(limit);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-comment">/**
       *  The {lastDocId} not provided. Start on first document in collection
       */</span>
      batch = collection.orderBy(<span class="hljs-string">"createdAt"</span>, <span class="hljs-string">"desc"</span>).limit(limit);
    }

    status = <span class="hljs-string">"pending"</span>;
    <span class="hljs-keyword">const</span> snapshots = <span class="hljs-keyword">await</span> batch.get();

    <span class="hljs-comment">/**
     *  For current batch, keep lastDocId to be used in next batch
     *  as starting point.
     */</span>

    newLastDocId =
      snapshots.docs[snapshots.docs.length - <span class="hljs-number">1</span>]?.data()?.id || <span class="hljs-literal">null</span>;

    docs = extractSnapshots(snapshots);
    status = <span class="hljs-string">"succeeded"</span>;

    <span class="hljs-keyword">return</span> {
      status,
      error,
      docs,
      <span class="hljs-attr">lastDocId</span>: newLastDocId,
    };
  } <span class="hljs-keyword">catch</span> (error) {
    status = <span class="hljs-string">"failed"</span>;
    <span class="hljs-keyword">return</span> {
      status,
      <span class="hljs-attr">error</span>: error,
      docs,
      <span class="hljs-attr">lastDocId</span>: newLastDocId,
    };
  }
};
</code></pre>
<h3 id="fetch-initial-batch">Fetch Initial Batch</h3>
<p>When app initialized or main component mounted, by using <code>useEffect</code> hook, we fetch initial batch documents and save last document ID for this batch to be used as the start point for next batch.</p>
<pre><code class="lang-js"><span class="hljs-comment">/** Fetch initial batch docs and save last document ID */</span>
<span class="hljs-keyword">const</span> getInitialData = <span class="hljs-keyword">async</span> () =&gt; {
  setData({ <span class="hljs-attr">initialBatchStatus</span>: <span class="hljs-string">"pending"</span>, <span class="hljs-attr">error</span>: <span class="hljs-literal">null</span> });
  <span class="hljs-keyword">const</span> {
    docs,
    error,
    lastDocId,
    <span class="hljs-attr">status</span>: initialBatchStatus,
  } = <span class="hljs-keyword">await</span> getDocs({ <span class="hljs-attr">limit</span>: <span class="hljs-number">10</span> });

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">return</span> setData({ initialBatchStatus, error });
  }
  <span class="hljs-keyword">return</span> setData({ initialBatchStatus, docs, lastDocId });
};

useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// Load initial batch documents when main component mounted.</span>
  getInitialData();
}, []);
</code></pre>
<h3 id="fetch-next-batches">Fetch next batches</h3>
<p>Before we proceed with fetching the next batch, let us examine how to render the content.
We use 2 components. </p>
<ol>
<li><p><code>&lt;ListItem&gt;</code> : Re-usable component to render document information, in our context, it's information for each scientist.</p>
</li>
<li><p><code>&lt;List&gt;</code> : By using React Native built-in  <a target="_blank" href="https://reactnative.dev/docs/flatlist">FlatList</a>. It renders the list of <code>&lt;ListItem/&gt;</code> components.</p>
</li>
</ol>
<p>Interesting things here are props provided by FlatList, which help us to determine how far user reach scrolling content then the app can fetch the next batch. Those props are  <a target="_blank" href="https://reactnative.dev/docs/flatlist#onendreachedthreshold"> onEndReachedThreshold</a>   and <a target="_blank" href="https://reactnative.dev/docs/flatlist#onendreached">onEndReached</a>.</p>
<p> <code>onEndReachThreshold</code> set to <code>0.5</code> which translate to the half of scrollable height, it simply means that whole scrollable height equal <code>1</code>. You can set to any value you want in range between 0 to 1.</p>
<p>When user scroll  until half of content, this indicate that, she has interest to view more content and FlatList fires <code>onEndReached</code> event which trigger function to fetch next batch of documents then append new fetched documents to existing ones.</p>
<pre><code><span class="hljs-comment">/*
 * Fetch next batch of documents start from {lastDocId}
 */</span>
  <span class="hljs-keyword">const</span> getNextData = async () =&gt; {
    <span class="hljs-comment">// Discard next API call when there's pending request</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">data</span>.nextBatchStatus === <span class="hljs-string">"pending"</span> || !<span class="hljs-keyword">data</span>.lastDocId) <span class="hljs-keyword">return</span>;

    setData({ ...<span class="hljs-keyword">data</span>, nextBatchStatus: <span class="hljs-string">"pending"</span>, error: <span class="hljs-literal">null</span> });
    <span class="hljs-keyword">const</span> {
      docs,
      error,
      lastDocId,
      status: nextBatchStatus,
    } = await getDocs({ limit: <span class="hljs-number">3</span>, lastDocId: <span class="hljs-keyword">data</span>.lastDocId });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> setData({ nextBatchStatus, error });
    }

    <span class="hljs-keyword">const</span> newDocs = [...<span class="hljs-keyword">data</span>.docs].concat(docs);
    <span class="hljs-keyword">return</span> setData({ ...<span class="hljs-keyword">data</span>, nextBatchStatus, docs: newDocs, lastDocId });
  };
</code></pre><p>Fetching documents is an asynchronous operation that should take a while depending on user device network speed or server availability, the app will show the Activity Indicator component when the request is pending by listening to <code>nextBatchStatus</code> when equal to <code>pending</code>.</p>
<h3 id="debouncing-server-calls">Debouncing Server Calls</h3>
<p>Debounce is fancy way to say that we want to trigger a function, but only once per use case.</p>
<p>Let's say that we want to show suggestions for a search query, but only after a visitor has finished typing it.</p>
<p>Or we want to save changes on a form, but only when the user is not actively working on those changes, as every "save" costs us a database read.</p>
<p>When user scroll and reach threshold we trigger new documents fetch, but when user is scrolling quickly we don't have to trigger more unnecessary requests. </p>
<p>By debouncing the <code>getNextData</code> function, we can delay it for a certain period like <code>1000</code> ms and save database cost while optimizing the app for performance.</p>
<p>Here simple debounce function</p>
<pre><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">debounce</span>(<span class="hljs-params">func, timeout = <span class="hljs-number">300</span></span>)</span>{
  <span class="hljs-keyword">let</span> timer;
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">...args</span>) =&gt;</span> {
    <span class="hljs-built_in">clearTimeout</span>(timer);
    timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> { func.apply(<span class="hljs-built_in">this</span>, args); }, timeout);
  };
}
</code></pre><p>Here Expo snack for whole app </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://snack.expo.dev/@expofire/infinity-scroll-with-firebase">https://snack.expo.dev/@expofire/infinity-scroll-with-firebase</a></div>
<h3 id="further-reading">Further Reading</h3>
<p> <a target="_blank" href="https://firebase.google.com/docs/firestore/">Firebase Firestore</a> </p>
<p> <a target="_blank" href="https://reactnative.dev/docs/flatlist">React Native FlatList</a> </p>
<p> <a target="_blank" href="https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query"> Firestore paginate query</a> </p>
]]></content:encoded></item></channel></rss>