<?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[Gan's Notes]]></title><description><![CDATA[My notes on coding and tech.]]></description><link>https://notes.yhg.io</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 10:01:16 GMT</lastBuildDate><atom:link href="https://notes.yhg.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Treasure Hunt 💰💰💰 - a location-based NFT game using thirdweb]]></title><description><![CDATA[In this thirdweb x Hashnode hackathon post I wanna show you how to build a location-based NFT browser game using thirdweb. Think Pokémon GO™️ but instead of Pokémon, you capture NFT in the wild! 

These are the technologies and frameworks being used ...]]></description><link>https://notes.yhg.io/treasure-hunt-a-location-based-nft-game-using-thirdweb</link><guid isPermaLink="true">https://notes.yhg.io/treasure-hunt-a-location-based-nft-game-using-thirdweb</guid><category><![CDATA[thirdweb]]></category><category><![CDATA[thirdweb Hackathon]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[React]]></category><category><![CDATA[Blockchain]]></category><dc:creator><![CDATA[YH Gan]]></dc:creator><pubDate>Tue, 01 Feb 2022 04:18:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1643688846454/wuOF9XyB-.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this <strong>thirdweb x Hashnode hackathon</strong> post I wanna show you how to build a location-based NFT browser game using <strong>thirdweb</strong>. Think Pokémon GO™️ but instead of Pokémon, you capture NFT in the wild! </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643688846454/wuOF9XyB-.png" alt="apps.png" /></p>
<p>These are the technologies and frameworks being used in this game:</p>
<ul>
<li>web3/blockchain integration: <a target="_blank" href="https://thirdweb.com">thirdweb</a>, <a target="_blank" href="https://www.alchemy.com">Alchemy</a> </li>
<li>storage/database: <a target="_blank" href="https://firebase.google.com">Firebase</a></li>
<li>geolocation: <a target="_blank" href="https://developers.google.com/maps/documentation/javascript/overview">Google Maps API</a></li>
<li>server: <a target="_blank" href="https://nodejs.org">Node JS</a></li>
<li>client: <a target="_blank" href="https://reactjs.org">React JS</a></li>
</ul>
<p>Let's Rock-n-code!</p>
<p>(All the codes here are on my <a target="_blank" href="https://github.com/ganyinghung/treasure-hunt">github</a> so feel free to clone it, fork it, and star it!)</p>
<h3 id="heading-the-client">The client</h3>
<p>The easiest way to create a React app is to use, well, <a target="_blank" href="https://create-react-app.dev/">create-react-app</a>. Let's call our game <code>treasure-hunt</code> and use create-react-app to create it:</p>
<pre><code>npx create<span class="hljs-operator">-</span>react<span class="hljs-operator">-</span>app treasure<span class="hljs-operator">-</span>hunt
</code></pre><p>First, we want to build a "main screen" that presents a game intro page, along with a wallet connect button to start. This task has been made <strong>extremely easy</strong> through thirdweb, which <a target="_blank" href="https://thirdweb.com/portal/guides/add-connectwallet-to-your-website">provides React UI components and hooks</a> directly. But before we can use them, we need to <code>npm install</code> them first:</p>
<pre><code>npm install <span class="hljs-operator">-</span><span class="hljs-operator">-</span>save @3rdweb<span class="hljs-operator">/</span>react @3rdweb<span class="hljs-operator">/</span>hooks
</code></pre><p>Open your favourite editor, and edit <code>src/index.js</code>:</p>
<pre><code><span class="hljs-keyword">import</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">ReactDOM</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'react-dom'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">ThirdwebProvider</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@3rdweb/react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">Main</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./Main'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">reportWebVitals</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./reportWebVitals'</span>;

ReactDOM.render(
  <span class="hljs-operator">&lt;</span>React.StrictMode&gt;
    <span class="hljs-operator">&lt;</span>ThirdwebProvider connectors<span class="hljs-operator">=</span>{{ injected: {} }} supportedChainIds<span class="hljs-operator">=</span>{[<span class="hljs-number">80001</span>]}<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span>Main <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>ThirdwebProvider<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>React.StrictMode&gt;,
  document.getElementById(<span class="hljs-string">'root'</span>)
);
reportWebVitals();
</code></pre><p>Note that we wrap the <code>&lt;Main /&gt;</code> screen component with <code>ThirdwebProvider</code>. It accepts a few props: <code>connectors</code> specifies what wallets it is going to support. For now, we focus on <a target="_blank" href="https://metamask.io/">MetaMask</a> and hence we have <code>{ injected: {} }</code> for that. <code>supportedChainIds</code> is the chains that we want to live on. We put in 80001 for Polygon Mumbai testnet.</p>
<p>Now create a new file <code>src/Main.js</code> and edit it:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">useWeb3</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@3rdweb/hooks"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./Main.css'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">App</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./App'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Main</span>(<span class="hljs-params"></span>) </span>{
  const { <span class="hljs-keyword">address</span>, connectWallet } <span class="hljs-operator">=</span> useWeb3();
  <span class="hljs-keyword">return</span> (
    <span class="hljs-keyword">address</span> ? 
    (
      <span class="hljs-operator">&lt;</span>App<span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    )
    :
    (
      <span class="hljs-operator">&lt;</span>div className<span class="hljs-operator">=</span><span class="hljs-string">"Main"</span><span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span>div<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>em<span class="hljs-operator">&gt;</span>Treasure Hunt<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>em<span class="hljs-operator">&gt;</span> <span class="hljs-keyword">is</span> a <span class="hljs-operator">&lt;</span>br<span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>location<span class="hljs-operator">-</span>based NFT game.&lt;br<span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
          To start, connect your MetaMask wallet<span class="hljs-operator">&lt;</span>br<span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span><span class="hljs-operator">&lt;</span>br<span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span>div<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>button onClick<span class="hljs-operator">=</span>{() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> connectWallet(<span class="hljs-string">'injected'</span>)}<span class="hljs-operator">&gt;</span>Connect<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>button<span class="hljs-operator">&gt;</span>
          <span class="hljs-operator">&lt;</span>button<span class="hljs-operator">&gt;</span>Help<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>button<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
      <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>div<span class="hljs-operator">&gt;</span>
    )
  );
}

export default Main;
</code></pre><p>The hook <code>useWeb3</code> returns, among other things, <code>connectWallet</code> which is a function to call in order to initialise the wallet connection, and <code>address</code>, if connected, the wallet address.</p>
<p>And that's it! Isn't it very simple and neat? All the cool stuffs are handled by thirdweb. And there are more it can do! Let's move on.</p>
<h3 id="heading-creating-project-on-thirdweb">Creating project on thirdweb</h3>
<p>We'd need to create a project on thirdweb and Firebase before we can use some of their cool features. Let's start with thirdweb. </p>
<p>Go <a target="_blank" href="https://thirdweb.com/portal/learn/projects">here</a> and follow their instructions to create a project. Be sure to pick <strong>Polygon Mumbai</strong> when choosing a network.</p>
<p>Once you have access to the dashboard, click on "Create Project". Again, pick Polygon Mumbai as the network. Give it a name and description: let's simply call it "Treasure Hunt"! Your wallet should prompt you a few times to authorise the transaction of deploying a contract. </p>
<blockquote>
<p>The wallet address you use to connect to thirdweb and create the project is important! Later on, we will need to include its PRIVATE KEY in our server code to mint the NFT for us. Choose it wisely! Avoid the one with lots of fund. Maybe simply create a new one just for this purpose. I know we are on testnet and it's play money but better make it a habit. </p>
</blockquote>
<p>Now you should see your project main screen:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643673755007/Dy08EQt5h.png" alt="Screenshot 2022-02-01 at 8.02.20 AM.png" /></p>
<p>Next, we need to add a module. For our purpose we need <strong>NFT Collection</strong>. Click on "Add Module", select "NFT" and then "NFT Collection". Give it a name (e.g. "Treasure") and symbol (e.g. "TRES") and click "Deploy" to authorise its creation.</p>
<p>That's it. We've finished setting it up on thirdweb. We will need the address next to your NFT collection title later so let's copy it for the moment.</p>
<h3 id="heading-creating-project-on-firebase">Creating project on Firebase</h3>
<p>Next, we need to create a project on Firebase as well. If you haven't heard of Firebase before, it's a service provided by Google that gives you support on things like authentication, NoSQL database (called Firestore), and cloud storage in an absolutely painless way. We will be using Firestore and storage for our purpose.</p>
<p>Go <a target="_blank" href="https://console.firebase.google.com/">here</a> and add a project. Again, give it a name. Then, we need to add an app. In the project main page, choose "Web":</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643678073598/jKdJLYZ4Y.png" alt="Screenshot 2022-02-01 at 9.14.17 AM.png" /></p>
<p>At the end of the set up you should see a listing of config code. Copy this snippet so it'll be handy later on:</p>
<pre><code><span class="hljs-keyword">const</span> firebaseConfig = {
  apiKey: <span class="hljs-string">"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>,
  authDomain: <span class="hljs-string">"xxxxxxxxx.firebaseapp.com"</span>,
  projectId: <span class="hljs-string">"xxxxxxxxx"</span>,
  storageBucket: <span class="hljs-string">"xxxxxxxxxappspot.com"</span>,
  messagingSenderId: <span class="hljs-string">"xxxxxxxxxxxx"</span>,
  appId: <span class="hljs-string">"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>
};
</code></pre><p>... and you're done! Well, not really. Let's add some data for later use. Go to "Firestore Database", click "Create database", and then "Start in test mode" for easier setup at the moment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643676260216/Dd_WEAF5M.png" alt="Screenshot 2022-02-01 at 8.44.01 AM.png" /></p>
<p>Once you're in the Firestore panel, click on "Start collection", and give it a name (e.g. "treasures"). And then we add a new document to it. It will have the following fields:</p>
<ul>
<li><code>name</code>: string (Name of the NFT treasure)</li>
<li><code>description</code>: string (Description)</li>
<li><code>image</code>: string (Image of the NFT treasure. It will be a Firebase storage link. More on this later)</li>
<li><code>location</code>: geopoint (Latitude/Longitude pair of the NFT treasure. More on this later)</li>
<li><code>geohash</code>: string (A <a target="_blank" href="https://www.movable-type.co.uk/scripts/geohash.html">hash value</a> generated by the latitude/longitude. More on this later)</li>
<li><code>minted</code>: boolean (Whether this NFT has been minted or not)</li>
<li><code>owner</code>: string (Owner address of this NFT treasure)</li>
</ul>
<p>For testing purpose, why don't we set up a NFT treasure that is close to where you are? Go to Google Maps, randomly pick on a spot that is quite close to you, and note the number pair at the bottom. This is the latitude (the first number) and longitude (the second number) that we need for our <code>location</code> field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643677409481/XqnRr5x1G.png" alt="Screenshot 2022-02-01 at 9.02.56 AM.png" /></p>
<p>For <code>geohash</code>, go here: https://www.movable-type.co.uk/scripts/geohash.html and enter the latitude/longitude to generate the hash value. For precision, use "8 characters". </p>
<p>For <code>image</code>, we will need another product: Firebase storage. Simply click on "Storage". Before we upload any files, make sure the rule is set for test mode:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643678625134/zuj_OQ9Lp.png" alt="Screenshot 2022-02-01 at 9.23.33 AM.png" /></p>
<p>For easier copy-paste:</p>
<pre><code>rules_version <span class="hljs-operator">=</span> <span class="hljs-string">'2'</span>;
service firebase.storage {
  match <span class="hljs-operator">/</span>b<span class="hljs-operator">/</span>{bucket}<span class="hljs-operator">/</span>o {
    match <span class="hljs-operator">/</span>{allPaths<span class="hljs-operator">=</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>} {
      allow read, write: <span class="hljs-keyword">if</span> 
          request.time <span class="hljs-operator">&lt;</span> timestamp.date(<span class="hljs-number">2022</span>, <span class="hljs-number">2</span>, <span class="hljs-number">24</span>);
    }
  }
}
</code></pre><p>(for the timestamp, simply pick a date in the future)</p>
<p>Then, we go back to "Files", and upload an image for our NFT treasure. Once it is done, click on "File Location" to copy the location of it (it should start with <code>gs://...</code>)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643678994454/e6pg5h4TK.png" alt="Screenshot 2022-02-01 at 9.27.39 AM.png" /></p>
<p>Go back to Firestore Database, and paste the location to the <code>image</code> field.</p>
<p>Now we have one NFT treasure in our database!</p>
<h3 id="heading-creating-project-on-alchemy">Creating project on Alchemy</h3>
<p>Finally, we need one more project for our blockchain integration: <a target="_blank" href="https://alchemy.com">Alchemy</a>. Since all we need from it is a HTTP API key, I am going to point you to <a target="_blank" href="https://docs.alchemy.com/alchemy/introduction/getting-started">this tutorial</a> and leave the rest to you as an exercise. Just note that:</p>
<ol>
<li>We only need the HTTP API key (it should look like <code>https://polygon-mumbai.g.alchemy.com/v2/...</code>), so you can skip steps 2-4 altogether.</li>
<li>We are on Polygon Mumbai testnet.</li>
</ol>
<h3 id="heading-the-app">The App</h3>
<p>We are finally ready to write the App!</p>
<p>Let's do it in this way: simply go to <a target="_blank" href="https://github.com/ganyinghung/treasure-hunt">github</a>, download the code, and I'll explain all the essentials part-by-part.</p>
<p>Once you're ready, open <a target="_blank" href="https://github.com/ganyinghung/treasure-hunt/blob/master/src/App.js"><code>src/App.js</code></a> </p>
<p>Let's briefly talk about the layout of the app. It consists of two parts: at the top, you have the instruction that tells you step-by-step on how to get to a NFT treasure, like a GPS NAT (e.g. "Go straight for 100 m"); at the bottom, you would see a Google map showing where you are:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643681625616/vyTYl8yWd.png" alt="app.png" /></p>
<p>Everything starts with your current location. So one of the <code>useEffect</code> hook is to get load the Google Maps (using <a target="_blank" href="https://www.npmjs.com/package/google-maps-api-loader">this</a> API loader), and then fetch the current location (browser's <code>navigator.geolocation</code>: note that this is when the browser prompt the user that this website is asking your location). We also start an interval that will check and update the user's location every second:</p>
<pre><code>useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    const loader <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Loader({
      apiKey: process.env.REACT_APP_GOOGLE_MAP_API_KEY,
      version: <span class="hljs-string">"weekly"</span>
    });
    loader.load().then((g) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
      google.current <span class="hljs-operator">=</span> g;
      map.current <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> g.maps.Map(document.getElementById(<span class="hljs-string">"map"</span>), {
        center: { lat: <span class="hljs-number">0</span>, lng: <span class="hljs-number">0</span> },
        zoom: <span class="hljs-number">18</span>,
        streetViewControl: <span class="hljs-literal">false</span>,
        fullScreenControl: <span class="hljs-literal">false</span>,
        mapTypeControl: <span class="hljs-literal">false</span>
      });

      <span class="hljs-keyword">if</span> (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition((pos) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
          setOrigin([pos.coords.latitude, pos.coords.longitude]);
          currentPosition.current <span class="hljs-operator">=</span> [pos.coords.latitude, pos.coords.longitude];
        });        
      }
    });
    const interval <span class="hljs-operator">=</span> setInterval(checkingPosition, <span class="hljs-number">1000</span>);
    <span class="hljs-keyword">return</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> clearInterval(interval);
  }, []);
</code></pre><p>Once we have the initial location - we store it as the <code>origin</code>, we have another <code>useEffect</code> to query our Firestore database if there're any NFT treasures nearby (with 10 km). This is achieved by using geohash, and we make use of <a target="_blank" href="https://firebase.google.com/docs/firestore/solutions/geoqueries">this</a> <code>geofire</code> package: </p>
<pre><code>useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    (
      async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
        <span class="hljs-keyword">if</span> (origin) {
          <span class="hljs-keyword">try</span> {
            const radius <span class="hljs-operator">=</span> <span class="hljs-number">10000</span>; <span class="hljs-comment">// in meter</span>
            const bounds <span class="hljs-operator">=</span> geofire.geohashQueryBounds([origin[<span class="hljs-number">0</span>], origin[<span class="hljs-number">1</span>]], radius);
            <span class="hljs-keyword">if</span> (bounds.<span class="hljs-built_in">length</span> <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
              const t <span class="hljs-operator">=</span> await Database.retrieveTreasures(bounds[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>], bounds[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]);
              setTreasures(t);
              <span class="hljs-keyword">if</span> (t.<span class="hljs-built_in">length</span> <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
                target.current <span class="hljs-operator">=</span> t[<span class="hljs-number">0</span>];   
                updateInstruction();
              } <span class="hljs-keyword">else</span> {
                setInstruction(<span class="hljs-string">'No treasure found within 10 km'</span>);
              }
            }
          } <span class="hljs-keyword">catch</span> (err) {
            console.error(err);
            setInstruction(<span class="hljs-string">'Error! Please try again later'</span>);
          }
        }
      }
    )();
  }, [origin]);
</code></pre><p>As you can see, we have abstracted all operations with Firestore to a Database object. Let's look at <code>src/Database/index.ts</code>:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">firebaseApp</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./firebase.config'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getFirestore</span>, <span class="hljs-title">collection</span>, <span class="hljs-title">query</span>, <span class="hljs-title">where</span>, <span class="hljs-title">startAt</span>, <span class="hljs-title">endAt</span>, <span class="hljs-title">orderBy</span>, <span class="hljs-title">getDocs</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"firebase/firestore"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getStorage</span>, <span class="hljs-title">ref</span>, <span class="hljs-title">getDownloadURL</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"firebase/storage"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Treasure</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./interfaces'</span>;

const Database <span class="hljs-operator">=</span> {
  <span class="hljs-comment">/** Retrieve a list of items */</span>
  retrieveTreasures: async <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">start: <span class="hljs-keyword">string</span>, end: <span class="hljs-keyword">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">Treasure</span>[]&gt; </span>{    
    const k <span class="hljs-operator">=</span> []; 
    const firestore <span class="hljs-operator">=</span> getFirestore(firebaseApp);
    const <span class="hljs-keyword">storage</span> <span class="hljs-operator">=</span> getStorage(firebaseApp);
    const collectionRef <span class="hljs-operator">=</span> collection(firestore, <span class="hljs-string">'treasures'</span>);
    const q <span class="hljs-operator">=</span> query(collectionRef, where(<span class="hljs-string">'minted'</span>, <span class="hljs-string">'=='</span>, <span class="hljs-literal">false</span>), orderBy(<span class="hljs-string">'geohash'</span>), startAt(start), endAt(end));
    const snapshot <span class="hljs-operator">=</span> await getDocs(q);

    <span class="hljs-keyword">for</span> (const doc of snapshot.docs) {
      let data <span class="hljs-operator">=</span> doc.data();
      const storedImage <span class="hljs-operator">=</span> ref(<span class="hljs-keyword">storage</span>, data.image);
      data.imageUrl <span class="hljs-operator">=</span> await getDownloadURL(storedImage);
      k.<span class="hljs-built_in">push</span>(data);
    }

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

export default Database;
</code></pre><p>So we query the database by using the geohash and the condition that <code>minted == false</code>. The config file <code>src/Database/firebase.config.ts</code> contains the config code of Firebase. Remember the config code we've copied in Firebase? You can paste it here - or better yet, paste it in <code>.env</code> environmental variables and reference it here:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">initializeApp</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"firebase/app"</span>;

const firebaseConfig <span class="hljs-operator">=</span> {
  apiKey: process.env.REACT_APP_FIREBASE_APIKEY,
  projectId: process.env.REACT_APP_FIREBASE_PROJECTID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE,
  appId: process.env.REACT_APP_FIREBASE_APPID,
  measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT
};

const firebaseApp <span class="hljs-operator">=</span> initializeApp(firebaseConfig);

export { firebaseApp };
</code></pre><p>Now, go back to <code>src/App.js</code>. If we successfully retrieved a list of treasures that are within 10 km, we use <a target="_blank" href="https://developers.google.com/maps/documentation/javascript/directions">Directions service</a> - which is part of the Google Maps API grand scheme - to get an "instruction" to get to it. The function <code>updateInstruction()</code>  handles it:</p>
<pre><code>const updateInstruction <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (google.current <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> currentPosition.current <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> target.current) {
      let dir <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> google.current.maps.DirectionsService();    
      dir.route({
        travelMode: <span class="hljs-string">'WALKING'</span>,
        origin: <span class="hljs-keyword">new</span> google.current.maps.LatLng(currentPosition.current[<span class="hljs-number">0</span>], currentPosition.current[<span class="hljs-number">1</span>]),
        destination: <span class="hljs-keyword">new</span> google.current.maps.LatLng(target.current.location.latitude, target.current.location.longitude)
      }, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">data, status</span>) </span>{
        <span class="hljs-keyword">if</span> (status <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">'OK'</span>) {
          setInstruction(data.routes[<span class="hljs-number">0</span>].legs[<span class="hljs-number">0</span>].steps[<span class="hljs-number">0</span>].instructions);
          setDistance(data.routes[<span class="hljs-number">0</span>].legs[<span class="hljs-number">0</span>].steps[<span class="hljs-number">0</span>].distance.text);
          setDuration(data.routes[<span class="hljs-number">0</span>].legs[<span class="hljs-number">0</span>].steps[<span class="hljs-number">0</span>].duration.text);
        }
      });  

      const d2 <span class="hljs-operator">=</span> distanceBetween(
        [currentPosition.current[<span class="hljs-number">0</span>], currentPosition.current[<span class="hljs-number">1</span>]],
        [target.current.location.latitude, target.current.location.longitude]
      );
      setTotalDistance(d2);
      <span class="hljs-keyword">if</span> (d2 <span class="hljs-operator">&lt;</span> <span class="hljs-number">5</span>) {
        setArrived(<span class="hljs-literal">true</span>);
      }
    }
  };
</code></pre><p>The important bit is the calculation of <code>d2</code>: the distance between the user's current location and the target's location, using <a target="_blank" href="https://en.wikipedia.org/wiki/Haversine_formula">Haversine formula</a> (implemented in <code>src/distance.ts</code>). If it's less than 5 meters, you set <code>arrived</code> to true - or put it another way, <strong>YOU WIN!</strong> And the user can now claim that NFT treasure!</p>
<pre><code>useEffect(() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (arrived <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> <span class="hljs-keyword">address</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> target.current) {
      fetch(<span class="hljs-string">'/api/receive-treasure/'</span><span class="hljs-operator">+</span>target.current.geohash+<span class="hljs-string">'/'</span><span class="hljs-operator">+</span><span class="hljs-keyword">address</span>)
      .then(resp <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> resp.json())
      .then(data <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
        setInstruction(<span class="hljs-string">'The Treasure NFT is now yours!'</span>)
      })
      .catch(err <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
        console.error(err);
      });
    }
  }, [arrived, <span class="hljs-keyword">address</span>, target.current]);
</code></pre><p>And that leads us to our server-side code.</p>
<h3 id="heading-the-server">The server</h3>
<p>We will use Node JS for our server-side code. They are all under the directory <code>/api</code>. </p>
<p>You may have notice that we haven't minted any NFT up to this moment. In fact, all treasure's data is on Firebase. We only mint the NFT when a player successfully reach the location and hit the claim button, and this is all done by, you know it, thirdweb again!</p>
<p>We need to install the packages first, this time for our server. Open a terminal and <code>cd</code> to the <code>treasure-hunt</code> directory. Then init the api directory by running:</p>
<pre><code>mkdir api
cd api
<span class="hljs-built_in">npm</span> install express dotenv
<span class="hljs-built_in">npm</span> install firebase
<span class="hljs-built_in">npm</span> install @<span class="hljs-number">3</span>rdweb/sdk ethers
</code></pre><p>Now let's look at <code>api/thirdsdk.js</code>, it's our Thirdweb config file:</p>
<pre><code><span class="hljs-keyword">import</span> { <span class="hljs-title">ThirdwebSDK</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@3rdweb/sdk"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">ethers</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"ethers"</span>;

<span class="hljs-keyword">import</span> <span class="hljs-title">dotenv</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"dotenv"</span>;
dotenv.config();

const thirdsdk <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ThirdwebSDK(
  <span class="hljs-keyword">new</span> ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL),
  ),
);

export default thirdsdk;
</code></pre><p><strong>VERY IMPORTANT</strong>: why do we need our private key here? Because the server is going to mint the NFT, and transfer it to the user, on behalf of us. <strong>BUT WE NEVER STORE THE PRIVATE KEY DIRECTLY IN CODE</strong>. Instead, we ALWAYS put it in our <code>.env</code> and reference it there. Last but not least, check your <code>.gitignore</code> and make sure it includes <code>.env</code>. There are bots that scan github looking for private key leakage, and remember: not your key, not your wallet! Another tip: use a dedicated development-only key!</p>
<p>Also see that <code>ALCHEMY_API_URL</code>? That's where you paste your Alchemy HTTP API. Again, better do it on <code>.env</code>.</p>
<p>The gem is in <code>api/server.js</code>:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">thirdsdk</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./thirdsdk.js'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">firebaseApp</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'./firebase.config.js'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getFirestore</span>, <span class="hljs-title">collection</span>, <span class="hljs-title">query</span>, <span class="hljs-title">where</span>, <span class="hljs-title">limit</span>, <span class="hljs-title">getDocs</span>, <span class="hljs-title">setDoc</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"firebase/firestore"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">getStorage</span>, <span class="hljs-title">ref</span>, <span class="hljs-title">getDownloadURL</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"firebase/storage"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">express</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">fs</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">https</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'https'</span>;

const app <span class="hljs-operator">=</span> express.Router(); 

app.get(<span class="hljs-string">'/api/test'</span>, (req, res) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  res.<span class="hljs-built_in">send</span>({ hello: <span class="hljs-string">'WORLD!'</span> });
});

app.get(<span class="hljs-string">'/api/receive-treasure/:geohash/:addr'</span>, async (req, res) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  const firestore <span class="hljs-operator">=</span> getFirestore(firebaseApp);
  const <span class="hljs-keyword">storage</span> <span class="hljs-operator">=</span> getStorage(firebaseApp);

  const collectionRef <span class="hljs-operator">=</span> collection(firestore, <span class="hljs-string">'treasures'</span>);
  const q <span class="hljs-operator">=</span> query(
    collectionRef, 
    where(<span class="hljs-string">'geohash'</span>, <span class="hljs-string">'=='</span>, req.params.geohash), 
    where(<span class="hljs-string">'minted'</span>, <span class="hljs-string">'=='</span>, <span class="hljs-literal">false</span>), 
    limit(<span class="hljs-number">1</span>)
  );

  <span class="hljs-keyword">try</span> {
    const snapshot <span class="hljs-operator">=</span> await getDocs(q);

    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>snapshot.empty) {
      const doc <span class="hljs-operator">=</span> snapshot.docs[<span class="hljs-number">0</span>];
      const data <span class="hljs-operator">=</span> doc.data();

      const storedImage <span class="hljs-operator">=</span> ref(<span class="hljs-keyword">storage</span>, data.image);
      const url <span class="hljs-operator">=</span> await getDownloadURL(storedImage);

      const nftModule <span class="hljs-operator">=</span> thirdsdk.getNFTModule(process.env.NFT_MODULE);
      const nft <span class="hljs-operator">=</span> await nftModule.mintTo(req.params.addr, {
        name: <span class="hljs-string">'Treasure: '</span> <span class="hljs-operator">+</span> data.<span class="hljs-built_in">name</span>,
        description: data.description,
        image: url,
        properties: { 
          latitude: data.location.latitude,
          longitude: data.location.longitude
        }
      });
      <span class="hljs-comment">// link to opensea: https://testnets.opensea.io/assets/mumbai/{process.env.NFT_MODULE}/{nft.id}</span>
      <span class="hljs-keyword">if</span> (nft) {
        data.minted <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        data.owner <span class="hljs-operator">=</span> req.params.addr;
        await setDoc(doc.ref, data);
        res.<span class="hljs-built_in">send</span>(JSON.stringify(nft));
      }    
    } <span class="hljs-keyword">else</span> {
      res.sendStatus(<span class="hljs-number">404</span>);
    }
  } <span class="hljs-keyword">catch</span> (err) {
    console.error(err);
    res.sendStatus(<span class="hljs-number">500</span>);
  }
});

const http <span class="hljs-operator">=</span> express();
http.use(<span class="hljs-string">'/'</span>, app);
const port <span class="hljs-operator">=</span> <span class="hljs-number">5000</span>; 
<span class="hljs-keyword">if</span> (process.env.NODE_ENV <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">'production'</span>) {
  <span class="hljs-comment">// use SSL in production</span>
  const httpsServer <span class="hljs-operator">=</span> https.createServer({
    cert: fs.readFileSync(process.env.SSL_CERT),
    ca: fs.readFileSync(process.env.SSL_CA),
    key: fs.readFileSync(process.env.SSL_KEY)
  }, http); 
  httpsServer.listen(port, (err) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (err)
      <span class="hljs-keyword">throw</span> err;
    console.log(<span class="hljs-string">'Listening on '</span><span class="hljs-operator">+</span>port);
  });
} <span class="hljs-keyword">else</span> {
  http.listen(port, (err) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    <span class="hljs-keyword">if</span> (err)
      <span class="hljs-keyword">throw</span> err;
    console.log(<span class="hljs-string">'Listening on '</span><span class="hljs-operator">+</span>port);
  });
}
</code></pre><p>There's actually just one API endpoint: <code>/api/receive-treasure</code>. (Well, two if you count the testing one.) It first retrieves the necessary details from Firestore, then loads Thirdweb's "NFT Module" in order to mint and transfer the NFT to the user's address.</p>
<p>To load the NFT module, we need a parameter for <code>thirdsdk.getNFTModule(NFTModuleAddress)</code>. What's that? Well, it's the address next to your NFT collection that you've created in your Thirdweb project:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643685357295/49tkqu0lbl.png" alt="Screenshot 2022-02-01 at 11.14.38 AM.png" /></p>
<p>Simply copy-paste it there. Or better yet, paste it to <code>.env</code>, like I did!</p>
<p>Finally, if the NFT is minted and transferred successfully, we update our record on Firestore, and return to user.</p>
<p><strong>Another great thing about thirdweb</strong>: remember for the NFT image we put it on Firebase storage? In fact, when we mint the NFT using that link, thirdweb actually helps to download it from Firebase, then upload it to <a target="_blank" href="https://ipfs.io">IPFS</a>, and use this reference instead for the NFT - all done automatically and behind the scene!</p>
<h3 id="heading-more-treasures">More treasures!</h3>
<p>Back to <code>src/App.js</code>. Once the player has successfully claimed a NFT treasure, or when he/she simply wants to try another one, we can allow them to pick another treasure to play (from the array <code>treasures</code> which contains all treasures within 10 km):</p>
<pre><code>const pickAnotherTreasure <span class="hljs-operator">=</span> () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    let k;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> treasures.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
      <span class="hljs-keyword">if</span> (target.current.geohash <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> treasures[i].geohash) {
        k <span class="hljs-operator">=</span> (i <span class="hljs-operator">+</span> <span class="hljs-number">1</span>) <span class="hljs-operator">%</span> treasures.<span class="hljs-built_in">length</span>;  <span class="hljs-comment">// just move on to the next one</span>
        <span class="hljs-keyword">break</span>;
      }
    }
    target.current <span class="hljs-operator">=</span> treasures[k];
    updateInstruction();
  };
</code></pre><h3 id="heading-8-bit-craziness">8-bit craziness!</h3>
<p>We will make use of <a target="_blank" href="https://nostalgic-css.github.io/NES.css">NES.css</a> for all the retro style touch!</p>
<h3 id="heading-final-words">Final words</h3>
<p>That's it! We have implemented a location-based NFT game using thirdweb and Firebase!</p>
<p>Like I said, I haven't gone through all the details in the code, but all the essential ideas have been covered. You can always grab the full code in github: https://github.com/ganyinghung/treasure-hunt</p>
<p>I have also put the build here so you can play the game yourself (still on Polygon Mumbai): https://yhg.io/treasure-hunt</p>
<p>(<strong>Note</strong>: I haven't put in many geo locations though, so quite unlikely there is one within 50 km radius of your location. But if you want to test it out, do tell me and I'll create a spot close to you!)</p>
<p>I want to emphasise that there are many improvement and potential to this whole project:</p>
<ul>
<li>The server is too easy to be cheated. We should remove target location from the client and demand more proof before the server mint the NFT.</li>
<li>Firebase and thirdweb project consoles are great and handy, but having a GM admin system too would be good.</li>
<li>Apart from NFT, there can be other different types of treasures. </li>
<li><strong>The NFT treasure can link to real world economy!</strong> For example, a cafe shop can have a spot in the game and issue cash coupons in terms of NFT treasure. Or for a tourism site to issue NFT or tokens for visitors to collect.</li>
</ul>
<p>The biggest hiccup, however, is that wallets on mobile phone generally do not support location access. So at the meantime you can only play it on, e.g. laptop. But given the huge potential of integration between geolocation and web3, I am sure wallets in the future will support it.</p>
]]></content:encoded></item><item><title><![CDATA[Passing Data Between Components using props, useReducer, and useContext]]></title><description><![CDATA[The good thing about React (or any other good SDKs, or toolkits, or programming languages for that matter) is that you can get the same result using different methods. In this post, I would like to demonstrate how you can pass data between different ...]]></description><link>https://notes.yhg.io/passing-data-between-components-using-props-usereducer-and-usecontext</link><guid isPermaLink="true">https://notes.yhg.io/passing-data-between-components-using-props-usereducer-and-usecontext</guid><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[2Articles1Week]]></category><dc:creator><![CDATA[YH Gan]]></dc:creator><pubDate>Wed, 12 May 2021 13:00:57 GMT</pubDate><content:encoded><![CDATA[<p>The good thing about React (or any other good SDKs, or toolkits, or programming languages for that matter) is that you can get the same result using different methods. In this post, I would like to demonstrate how you can pass data between different components using different methods. We will introduce the concept of <em>reducer</em>, <em>context</em>, and how to use them through the <code>useReducer</code> and <code>useContext</code> hooks.</p>
<p>Imagine a typical app which has a top toolbar and a bottom view, which shows a list of items:</p>
<pre><code><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Main</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">TopBar</span> /&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">ItemListView</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Main</span>&gt;</span></span>
   );
}
</code></pre><p>It is very common that the <code>ItemListView</code> has to communication with the <code>TopBar</code>. For example, when you click and select an item, the <code>TopBar</code> shows some action buttons for you to do with it. How can you implement this?</p>
<h3 id="method-1-through-props">Method 1: Through props</h3>
<p>The most straightforward way is to use <em>props</em>. We can write a handler function and pass it to <code>ItemListView</code>, which, when called, will change a local state variable, which is passed to <code>TopBar</code>. Like this:</p>
<pre><code><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">const</span> [ selection, setSelection ] = useState(<span class="hljs-number">0</span>);
   <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Main</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">TopBar</span> <span class="hljs-attr">selection</span>=<span class="hljs-string">{selection}</span> /&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">ItemListView</span> <span class="hljs-attr">onSelectionChanged</span>=<span class="hljs-string">{setSelection}</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Main</span>&gt;</span></span>
   );
}
</code></pre><p>Somewhere in <code>ItemListView</code>, the handler is called when needed to:</p>
<pre><code><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ItemListView = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
   <span class="hljs-comment">// ....</span>
   <span class="hljs-keyword">const</span> itemClicked = <span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
      <span class="hljs-comment">//...</span>
      props.onSelectionChanged(item.id);
   }
   <span class="hljs-comment">// ...</span>
}
</code></pre><p>This method is easy and straightforward. However, if you have many data that need to be communicated, you may get into this:</p>
<pre><code><span class="hljs-tag">&lt;<span class="hljs-name">ItemListView</span>
   <span class="hljs-attr">onSelectionChanged</span>=<span class="hljs-string">{setSelection}</span>
   <span class="hljs-attr">onItemChanged</span>=<span class="hljs-string">{handleItemChanged}</span>
   <span class="hljs-attr">onSomeEvent1</span>=<span class="hljs-string">{handleEvent1}</span>
   <span class="hljs-attr">onSomeEvent2</span>=<span class="hljs-string">{handleEvent2}</span>
   <span class="hljs-attr">...</span>
/&gt;</span>
</code></pre><p>You can write another function to consolidate them, with complicated state logic and management. But why bother? You can always use...</p>
<h3 id="method-2-usereducer">Method 2: useReducer</h3>
<p><em>Redux</em>, and its functional component hook counterpart <em>useReducer</em>, is the perfect tool for such complex state management across different components. Let's see how we can use useReducer here. </p>
<p>(I once heard a speaker said: "...and then we use useState state management..." and I was surprised he didn't stutter.)</p>
<p>Simply put, a <em>reducer</em> is merely a function which takes two arguments: the current "state" object and an "action" object, and returns a new state object. When you feed a reducer to <code>useReducer()</code>, it returns an array, similar to <code>useState()</code>:</p>
<pre><code><span class="hljs-keyword">const</span> [ state, dispatch ] = useReducer(reducer);
</code></pre><p>Where <code>state</code> is the state object for you to consume, and <code>dispatch</code> a function for you to dispatch actions to the reducer.</p>
<p>Let's use our app again as an example. Suppose when we click an item, the <code>TopBar</code> goes into "select mode", such that there will be some UI changes, and at the same time store the selected item id. We can define our reducer like this:</p>
<pre><code>const reducer = <span class="hljs-function"><span class="hljs-params">(state, action)</span> =&gt;</span> {
   <span class="hljs-keyword">switch</span> (action.type) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">'TURN_ON_SELECT_MODE'</span>:
         <span class="hljs-keyword">return</span> {...state, selectMode: <span class="hljs-literal">true</span>};
      <span class="hljs-keyword">case</span> <span class="hljs-string">'TURN_OFF_SELECT_MODE'</span>:
         <span class="hljs-keyword">return</span> {...state, selectMode: <span class="hljs-literal">false</span>};
      <span class="hljs-keyword">case</span> <span class="hljs-string">'SET_ITEM'</span>:
         <span class="hljs-keyword">return</span> {...state, selectedItem: action.value};
   }
}
</code></pre><p>Note:</p>
<ul>
<li>It is a common practice that the <code>action</code> object has two properties: <code>type</code> and <code>value</code>, although it doesn't have to be.</li>
<li>You cannot change the state directly. State change is achieved by returning a <em>new</em> state object. Also, when you return, you must return the <em>entire</em> object, not just the value. Therefore, we use the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spread syntax ...</a> to merge the new value with the existing ones, and return the new state object.</li>
</ul>
<p>It's time to use it in our app:</p>
<pre><code><span class="hljs-comment">// App.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">const</span> [ state, dispatch ] = useReducer(reducer);
   <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Main</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">TopBar</span> <span class="hljs-attr">selectMode</span>=<span class="hljs-string">{state.selectMode}</span> /&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">ItemListView</span> <span class="hljs-attr">dispatch</span>=<span class="hljs-string">{dispatch}</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Main</span>&gt;</span></span>
   );
}

<span class="hljs-comment">// ItemListView.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ItemListView = <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
   <span class="hljs-comment">// ....</span>
   <span class="hljs-keyword">const</span> itemClicked = <span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
      <span class="hljs-comment">//...</span>
      props.dispatch({ <span class="hljs-attr">type</span>: <span class="hljs-string">'TURN_ON_SELECT_MODE'</span> });
      props.dispatch({ <span class="hljs-attr">type</span>: <span class="hljs-string">'SET_ITEM'</span>, <span class="hljs-attr">value</span>: item.id });
   }
   <span class="hljs-comment">// ...</span>
}
</code></pre><p>The good thing about useReducer (and Redux) is that it packs state values/object and action together, so you do not need 10x props for different values and handlers. Also, since a state value can only be modified through action, it will never go into some incoherent state, which makes debugging state-related issues much, much easier.</p>
<p>But what if <code>ItemListView</code> consists of many many child components, where some, if not most, of them also need access to <code>dispatch</code>? We can certainly pass it down through props, but it's tedious. Hm... I wonder if there's some kind of <em>global</em> variables...</p>
<h3 id="method-3-usecontext">Method 3: useContext</h3>
<p>Well, turns out there is! But it's in the form of <em>context</em>.</p>
<p>None other than <a target="_blank" href="https://reactjs.org/docs/context.html">the official React site</a> summarizes the use of context better:</p>
<blockquote>
<p>Context provides a way to pass data through the component tree without having to pass props down manually at every level.</p>
</blockquote>
<p>Think of it like a magic, hyper-dimensional secret knapsack that you can put things (variables) in, and when needed, you use the hook <code>useContext</code> to retrieve the things back. Difficult to understand? Let's see it in action!</p>
<p>First of all, we define the context. Let's call it <code>TopBarContext</code>:</p>
<pre><code><span class="hljs-comment">// TopBarContext.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> TopBarContext = React.createContext(<span class="hljs-keyword">null</span>);
</code></pre><p>Then, we wrap our whole app, or the highest node possible that will use this context, with the context's <code>Provider</code>. Also, in the process, we state the "things" we want to store, which in this case is the <code>dispatch</code> function:</p>
<pre><code><span class="hljs-comment">// App.js</span>
<span class="hljs-keyword">import</span> { TopBarContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'./TopBarContext'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">const</span> [ state, dispatch ] = useReducer(reducer);
   <span class="hljs-keyword">return</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TopBarContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{dispatch}</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">Main</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">TopBar</span> <span class="hljs-attr">selectMode</span>=<span class="hljs-string">{state.selectMode}</span> /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ItemListView</span> /&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">Main</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">TopBarContext.Provider</span>&gt;</span></span>
   );
}
</code></pre><p>That's it! Note that we don't need to pass <code>dispatch</code> to <code>ItemListView</code> through props now. </p>
<p>In anywhere (anywhere under <code>TopBarContext.Provider</code>, that is) you want to use the <code>dispatch</code>, you use the <code>useContext</code> hook to retrieve it back:</p>
<pre><code><span class="hljs-comment">// ItemListView.js</span>
<span class="hljs-keyword">import</span> { TopBarContext } <span class="hljs-keyword">from</span> <span class="hljs-string">'./TopBarContext'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ItemListView = <span class="hljs-function">() =&gt;</span> {
   <span class="hljs-comment">// ....</span>
   <span class="hljs-keyword">const</span> dispatch = useContext(TopBarContext);
   <span class="hljs-keyword">const</span> itemClicked = <span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
      <span class="hljs-comment">//...</span>
      dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'TURN_ON_SELECT_MODE'</span> });
      dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'SET_ITEM'</span>, value: item.id });
   }
   <span class="hljs-comment">// ...</span>
}
</code></pre><h3 id="which-method-should-i-use">Which method should I use?</h3>
<p>Each method has its pros and cons. Obviously, using props is the most straightforward, but can get tedious very quickly. Using context is the ideal way if your app is huge, and the value you wish to share is quite fundamental and universal (normally we see examples like theme, authentication status etc.) But it could be an overkill and lead to a difficult-to-understand codes. Reducer is somewhere in between... So, it really depends on the situation. Like I said at the beginning, it's a good thing to have different ways to do thing.</p>
<p>Hopefully this post can give you a basic understanding on how to use the props, reducer, and context to pass data between React components. If you want to dig deeper, these are some of the best tutorials on the topics:</p>
<ul>
<li><a target="_blank" href="https://www.robinwieruch.de/javascript-reducer">reducer</a> and <a target="_blank" href="https://www.robinwieruch.de/react-usereducer-hook">useReduce</a></li>
<li><a target="_blank" href="https://daveceddia.com/usecontext-hook/">useContext</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Multi-selectable FlatList]]></title><description><![CDATA[Hi, this is my first post on hashnode. In this post I want to talk about how I implement a multi-selectable FlatList. 
It is about React Native, a way to write cross-platform mobile app using the React JS UI library. I assume you have knowledge on bo...]]></description><link>https://notes.yhg.io/multi-selectable-flatlist</link><guid isPermaLink="true">https://notes.yhg.io/multi-selectable-flatlist</guid><category><![CDATA[React]]></category><category><![CDATA[React Native]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[2Articles1Week]]></category><dc:creator><![CDATA[YH Gan]]></dc:creator><pubDate>Thu, 06 May 2021 20:03:51 GMT</pubDate><content:encoded><![CDATA[<p>Hi, this is my first post on hashnode. In this post I want to talk about how I implement a <strong>multi-selectable <code>FlatList</code></strong>. </p>
<p>It is about <a target="_blank" href="https://reactnative.dev">React Native</a>, a way to write cross-platform mobile app using the <a target="_blank" href="https://reactjs.org">React JS</a> UI library. I assume you have knowledge on both of them. If not, see <a target="_blank" href="https://reactjs.org/tutorial/tutorial.html">here</a> and <a target="_blank" href="https://reactnative.dev/docs/getting-started">here</a> for a great introduction.</p>
<h3 id="lets-begin">Let's Begin</h3>
<p>First let's look at <code>FlatList</code>. <code>FlatList</code> is <a target="_blank" href="https://reactnative.dev/docs/using-a-listview">a component in React Native</a>  which provides an easy, performant way to implement a scrollable list, something you've seen a lot in mobile apps. A <code>FlatList</code> requires, essentially, only two props:</p>
<ol>
<li><code>data</code>: an array of items, and</li>
<li><code>renderItem</code>: a function that takes an item and returns a React element, ie. a "renderer".</li>
</ol>
<p>Let's take a look at this simple example:</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.io/@yhgan/simple-flatlist">https://snack.expo.io/@yhgan/simple-flatlist</a></div>
<p>Easy, right? The good thing about <code>FlatList</code> is that it comes with a lot of features out of the box, such as scrolling, multiple columns, infinite scrolling, and even pull to refresh. However, one thing it doesn't support is selection. So we would have to implement our owns.</p>
<p>In most cases, pressing an item goes to a "detailed view" of that item, and long-pressing it to select it. We will adapt this UI norm here. To support pressing/long-pressing, we use a React Native component called <a target="_blank" href="https://reactnative.dev/docs/handling-touches#touchables"><code>TouchableWithoutFeedback</code></a>. We will use it to wrap our item view, and use its <code>onLongPress</code> prop to handle the long-pressing. We will also add a state variable to store the selected item's ID:</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.io/@yhgan/simple-selectable-flatlist-not-working">https://snack.expo.io/@yhgan/simple-selectable-flatlist-not-working</a></div>
<p>It looks good, but it doesn't work! Why? Because we need to tell our <code>FlatList</code> that something has changed and a re-render is needed. We use its <code>extraData</code> prop to do so, by setting it to our state variable:</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.io/@yhgan/simple-selectable-flatlist-working">https://snack.expo.io/@yhgan/simple-selectable-flatlist-working</a></div>
<p>Horray! It works! We have our selectable <code>FlatList</code>!</p>
<h3 id="multiple-selection">Multiple Selection</h3>
<p>Now we move further on, to implement a multi-selectable <code>FlatList</code>.</p>
<p>It basically has the same idea, but instead of a single state value, we use an <em>array</em>. Moreover, we cannot simply just set/resetting it, because we also need to handle the case when we want to deselect an item. Therefore, the long-press handler will now be a "toggle" function: if the item is not in our array, we push it in; otherwise, we splice it out.</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.io/@yhgan/simple-multi-selectable-flatlist">https://snack.expo.io/@yhgan/simple-multi-selectable-flatlist</a></div>
<p>It works pretty well, isn't it? With just a little effort, we have our multi-selectable <code>FlatList</code>.</p>
<h3 id="scalability">Scalability</h3>
<p>Finally, we want to see how scalable it is. Instead of 3 items, we increased it to, say, 50000 items:</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.io/@yhgan/simple-multi-selectable-flatlist-50000-items">https://snack.expo.io/@yhgan/simple-multi-selectable-flatlist-50000-items</a></div>
<p>(If you have the <a target="_blank" href="https://expo.io">Expo app</a>, feel free to test it on your device too.)</p>
<p>Turns out it still runs pretty smoothly, thanks to the excellent implementation of <code>FlatList</code>. Of course it also depends on how complicated your item component is. In fact, they have a <a target="_blank" href="https://reactnative.dev/docs/optimizing-flatlist-configuration">whole section</a> on how you can optimize  <code>FlatList</code>. But in our case, it is good to know our way of handling multiple selection won't hurt its performance.</p>
<p>Thank you so much for reading! I hope your find my very first post on hashnode useful.</p>
]]></content:encoded></item></channel></rss>