<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ product hunt - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ product hunt - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 26 Jun 2026 22:48:03 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/product-hunt/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ I left my full-time job one year ago to ride the indie hacker road. This is what I've learned. ]]>
                </title>
                <description>
                    <![CDATA[ By Pierre de Wulf My partner Kevin and I have been working and talking about different side projects/startups for over 5 years. Two years ago we released our first product to the public, but it was one year ago that we decided to go full time on the ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/12-months-3-products-some-mrr-and-one-irrigation-pivot/</link>
                <guid isPermaLink="false">66d4608af855545810e934ad</guid>
                
                    <category>
                        <![CDATA[ Life lessons ]]>
                    </category>
                
                    <category>
                        <![CDATA[ product development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ product hunt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[  Startup Lessons ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 07 Oct 2019 06:00:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/10/1_tiAicAxobXZwIwERPf03tA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Pierre de Wulf</p>
<p>My partner Kevin and I have been working and talking about different side projects/startups for over 5 years. Two years ago we released our first product to the public, but it was one year ago that we decided to go full time on the indie hacker road. In this post, I’m going to explain our journey, our background and how we did it after many failed attempts.</p>
<p>This post is not about some magic product we launched in 2 days while getting 10k signups and reaching $20k MRR in one month while working 4 hours a week in Hawaï. This post is more about the small wins and loses we had during our first year in the Indie Hacker world, and the things we wish we knew before starting.</p>
<p>This post is about 3 products: one irrigation pivot, one startup pivot, and of course, some MRR.</p>
<p><em>(Disclaimer: ScrapingBee was initially launched as ScrapingNinja, but due to some copyright issues we had to quickly rebrand it. We'll talk about it in a future blog post.)</em></p>
<h2 id="heading-background">Background</h2>
<p>It started when we were both employed in different startups as software developers. We had lots of ideas and we loved to build side-projects for fun.</p>
<p>Kevin and I were doing lots of Web Scraping in our jobs. Kevin worked at a Fintech startup called Fiduceo which was acquired by a big French bank, and they were doing bank account aggregation, like <a target="_blank" href="http://mint.com/">Mint.com</a> in the US. He was leading a small team handling the web scraping code and infrastructure.</p>
<p>I worked in the US and then came back to France to work in the biggest French real-estate data provider as a data engineer. Part of my job was to find, gather, extract and load new data sets from the web.</p>
<p>So we both had experience with Web Scraping and data at scale.</p>
<h2 id="heading-our-first-project-shoptolist">Our first project: ShopToList</h2>
<p>One of the first “mini-success” we had was <a target="_blank" href="https://www.shoptolist.com/">Shoptolist.com</a>, a B2C website/browser extension which is a universal wishlist that sends you alerts if it sees any price drop. It was really just a fun side project that was never meant to be more.</p>
<p>It allowed us to try many different things and to discover that acquisition is really, really, really hard. We quickly reached 1k users by just submitting our product on frugal/fashion subreddits. We were very happy about it because it was just an experiment. </p>
<p>Every day we had a script that scraped each product in our database to update its price, and we were sending an email in case of a price drop. The links in the email were affiliate links, so we took a small percentage if the user ended up buying the product.</p>
<p>In theory, this model works great, but in practice here is what happened:</p>
<ul>
<li>Out of 1000 emails sent, about 20–30% were opened</li>
<li>2% click on the product links that were on sale</li>
<li>Out of this 2 %, 5–10% buy the product</li>
</ul>
<p>The percentage we earned was very small, depending on the niche it was 0.5–5%, so this business model only works with millions of users.</p>
<p>And this is where we hit a wall, we did not manage to create sustainable growth. We tested many things, content marketing, affiliation, some paid advertising, but we just did not manage to create growth. And since it was just a little side project that only took us 2 weeks to build, we were ok with that.</p>
<p>For us, this was a very good experience, because this was the first project we really shipped to real users, and we learned a lot.</p>
<p>By digging into the database we noticed that a few users had thousands of products saved inside ShopToList. It seems strange unless they were crazy impulsive buyers, the majority of users had like 20 products saved on average…</p>
<p>So after a little “investigation”, we discovered that these users were E-commerce owners that were “spying” on their competitor…</p>
<h2 id="heading-our-first-pivot-pricingbot">Our first pivot: PricingBot</h2>
<p>We assumed that those people were doing this to receive alerts when their competitor’s products were changing the price. There were many solutions on the web that allowed someone to do this, but ShopToList allowed them to monitor thousands of products for free when other solutions were quite expensive.</p>
<p>We did a small market research and discovered that many tools offered to monitor your competitor’s product, however, all those tools seemed either really difficult to use or really expensive.</p>
<p>Because we felt we could do better, the PricingBot idea was born. I quit my job and we both decided to commit full-time on this. Side project era was over ?.</p>
<p>We made a landing page explaining our value proposition, nothing fancy but something clear and nice enough so people could trust us. We got 60 signups from different E-commerce owners in different niches.</p>
<p>While technically challenging, extracting E-commerce product data was something we knew how to do thanks to ShopToList, so building the MVP was pretty quick.</p>
<p>We launched our beta on ProductHunt on November 2018 and it was a big success, followed by a big crash, the classic <strong>startup trough of sorrow.</strong></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_t_YsErGF5PCdAzpwaQ7Vzw.jpeg" alt="Image" width="600" height="400" loading="lazy">
<em>ProductHunt launch</em></p>
<p>You had to upload a CSV file with your product catalog, and for each product match it with a competitor product URL.nIt’s ok for several dozens of products, but people had often hundreds or thousands of product in their online store.</p>
<p>So with this feedback, we created some integration with popular E-commerce platforms like Shopify and Woocommerce to let people import their catalog in one click.</p>
<p>Our activation <strong>tripled ? ,</strong> we were very happy about how things were going. One thing to note, though, is that until this moment the product was completely free and we did not ask people for money.</p>
<p>At this point in time here are the few numbers we had that made use happy:</p>
<ul>
<li>We managed to have around 200 signups with $0 spent</li>
<li>20 users seemed to use the product and had their account fully set up</li>
</ul>
<p>What could go wrong right?</p>
<p>We decide to close the beta and start asking our user to pay for our software with a classic SaaS model with three plans, $29/$99/$299 per month based on volume.</p>
<p>The first day was magic because literally several seconds after sending the email announcing the end of the beta we got our first customer for a $29 plan ?</p>
<p>We also managed to signup a $299 soon after, but for him, we had to manually set up his account and manually match 1000 products across 10 websites. It was long but we felt it was worth it. We were wrong! Just before renewing he churned telling us PricingBot was very good but not useful enough for him. We were sad and angry, mostly at ourselves, but decided to move forward and continue.</p>
<p>It seemed we were on a good path and that we just needed to go all-in on marketing. And that’s what we did. Content marketing, cold outreach, affiliation, SEO you name it!</p>
<p>But before diving into this, let’s talk again about our activation</p>
<h2 id="heading-mistake-1-bad-metrics-leads-to-bad-conclusion-bad-conclusion-leads-to-bad-decisions-in-yodas-voice">Mistake #1: bad metrics leads to bad conclusion, bad conclusion leads to bad decisions (in Yoda’s voice)</h2>
<p>When we first decided to monitor our activation rate we assumed that one user was activated when he did two things:</p>
<ul>
<li>Add at least one of his product, (or link his store with our built-in integration)</li>
<li>Add at least one of his competitor’s product</li>
</ul>
<p>And so, with that definition, we had around 10% of our users that were “activated”. Considering that at that time most of our users were coming from ProductHunt and that hunters are known to easily signup to products they don’t plan to use and just for the sake of it we were happy with these numbers.</p>
<p>But we were wrong.</p>
<p>This definition meant that someone who owns a Shopify store with 4000 products, and who adds only one competitor’s product, was activated. This was silly. Someone who only adds one competitor’s product out of 4000 of is own catalog won’t use PricingBot to do price monitoring and surely won’t pay for it. We learned this the hard way.</p>
<p>Because soon after we had this first paying customer, nobody followed, literally nobody. At first, we did not understand. Then it was obvious: out of 200 signups, we had 20 active users, out of 20 active users we had 1 paying customers, so the only solutions were to have more signups.</p>
<p>This was another mistake.</p>
<h2 id="heading-mistake-2-thinking-our-only-problem-was-acquisition">Mistake #2: Thinking our only problem was acquisition</h2>
<p>We thought we only needed more users and just went full marketing. Because we did not know the e-commerce community very well we had some trouble starting. But we eventually managed to write some piece of content that was shared on relevant Facebook/Reddit/LinkedIn group that brought in a few leads.</p>
<p>We also did some paid advertising and cold outreach but it failed miserably.</p>
<p>One month later, we needed to see the obvious: we were not on the right path.</p>
<p>Our leads used the product but did not pay, and even if all the leads we brought in paid, it would have not been sustainable.</p>
<p>At this point in time we finally decide to understand better why users don’t use our product more and with feedback request and lots of analytics insight we discover two things:</p>
<ul>
<li>For most of our users, PricingBot was a nice to have, but it was not something worth paying for </li>
<li>Most of our users didn't want to do the setup because it is too tedious, but they didn't want to pay us to do it for them.</li>
</ul>
<p>Next thing we knew we revamped our whole onboarding process and try to automate as much as possible. But it was still not working.</p>
<p>When you want, as an e-commerce owner, to monitor your competitors, you first have to link your products with your competitors - and this was the hard part. This part alone meant approximately 1 hour of work per 100 products you want to match. This was way too much time for an e-commerce owner working alone with a 10k products catalog.</p>
<h2 id="heading-fear-uncertainty-and-doubt">Fear, Uncertainty, and Doubt</h2>
<p>To help you understand how we felt at that point in time let me just recap the timeline:</p>
<ul>
<li>January 2018: ? we launch ShopToList</li>
<li>July 2018: ? I quit my job and we decide to build PricingBot</li>
<li>October 2018: ? After a busy summer and 1 month of code we launch the MVP in beta</li>
<li>January 2019: ? First paying customer</li>
<li>February-March 2019: Acquisition, product dev</li>
</ul>
<p>Back in May 2019 we kind of hit a wall. Nothing we did really worked and it was hard staying motivated. The only silver lining was that we managed to rank well on Google so we had, every day, around 3 new signups without any acquisition.</p>
<p>But we still did not manage to make them pay. And we still did not manage to make them configure their account.</p>
<p>This period of time was hard because it was full of negativity. My cofounder and I both knew that we were not moving forward. While this did not degrade our working relationship, it certainly degraded our working productivity.</p>
<p>We both felt that no matter what we did, we were not able to move any meaningful needle that could have boosted our business.</p>
<p>We improved the product a lot, managed to gather some signups along the way but it was not enough. Here is a look at our revenue.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1__V2G-InXFLPChcA2eF2Wpg.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-one-agricultural-pivot-to-build-one-startup-pivot-to-make">One agricultural pivot to build, one startup pivot to make</h2>
<p>Mid-June 2019 things are not looking good, we only have 3 months to launch a successful business. We both agreed in 2018 that we gave ourselves 1 year to launch something that worked, 1 year to reach “<a target="_blank" href="http://www.paulgraham.com/ramenprofitable.html">ramen profitability</a>” ?.</p>
<p>We had a long talk beginning of June and we both agreed that we needed to step back. We currently had 3 options:</p>
<ol>
<li>Continue with PricingBot hoping that some magic happens and that we cross 4k MRR in 3 months</li>
<li>Leaving the company and start going our own way</li>
<li>Building something else</li>
</ol>
<p>Point 1 was hard because we were both fed up with the product. Everything we did seemed useless and it was not working. Point 2 needed to be addressed, but although it was not a success we felt that working together was working really well (in the human side of things). It would be a pity to give up. </p>
<p>But we chose option 3, and we are both very happy with the outcome of that talk and full of energy. We only needed one thing: to choose what we would build.</p>
<p>We also decide to do something we should have done earlier, we sold ShopToList. The whole deal was done in less than 1 month thanks to <a target="_blank" href="http://1kprojects.com/">1kprojects.com</a> and it brought some welcomed money in our company bank account.</p>
<p>In the same time, my father in law, a farmer in the south of France called because he needed help assembling an irrigation pivot. The heatwave was supposed to be hard in June (and guess what, it was), and it was an urgent job. We both decided that this was a good opportunity to take a break, to think, each on our side, about the future product, and to come back full of ideas and motivation.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_tiAicAxobXZwIwERPf03tA-1.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>It was kind of ironic because this pivot kind of funded our pivot.</p>
<p><em>Disclaimer: If you ever need to buy an irrigation pivot</em> I <em>strongly suggest that you look into Valley pivot (PS: this post was not sponsored by Valley in any way)</em></p>
<h2 id="heading-scrapingbee">ScrapingBee</h2>
<p>Two weeks later, we both found ourselves with a bullet list of product ideas, some good, some bad, some crazy, some boring, some exciting, well, you get the idea. Both lists were diverse. However we quickly agreed on one idea, because it really stood out from the others. Let me explain.</p>
<p>While working on Shoptolist and Pricingbot, and also in our previous work experience, there were three things that we always needed to do for our web scraping infrastructure: </p>
<ul>
<li>Transforming websites into a structured API, </li>
<li>Running headless browsers at scale, </li>
<li>and managing a pool of proxies.</li>
</ul>
<p>When you extract data from lots of different websites, you always have to deal with Javascript-heavy websites / Single Page application, and you don’t really have other choices than running headless browsers to render all this Javascript.</p>
<p>Running a headless browser like Chrome is really painful because the same thing happening on your desktop (high memory usage, poorly coded Single page application eating 100% of your CPU) will happen on your servers. So it is not only painful but very expensive to do this on your own when you don’t know what you are doing.</p>
<p>When doing web scraping at scale, you often have to use proxies for different reasons. The website you are visiting with your bot may show different information based on your location - for example, a price in Euro in the Euro-zone and a price in dollars in the US.</p>
<p>Dealing with proxies is painful too. There are lots of shady companies selling bad quality proxies so you either have to run your own proxies or test dozens of proxy companies to make sure your proxy pool is always up.</p>
<p>We used to solve all those problems using APIs that were either not really efficient or crazy expensive. These are problems that we solve multiple times in our projects so we thought about packaging it into an API and leveraging our experience to make all kinds of web scraping APIs.</p>
<p>We decided this time, to make things right and to try to avoid doing the mistakes we made with PricingBot while creating <a target="_blank" href="https://www.scrapingbee.com/">ScrapingBee</a>.</p>
<h2 id="heading-mistake-avoided-1-creating-a-product-you-wont-use">Mistake avoided #1: creating a product you won’t use</h2>
<p>One of the biggest problems we had with PricingBot was to find where our potential users gathered online. What group did they follow, what blog did they read, what influencers did they listen to. And the reason was simple, having never worked with or in the e-commerce industry except for some freelancing gigs, the whole landscape was unknown to us.</p>
<p>With ScrapingBee we would be our own users and it changed everything. I know this advice is not new, but often this advice is meant to build a better product. And sure, being one of your own users allows you to build a better product.</p>
<p>But for us, the game-changing fact was that being our own users meant that we knew exactly where to find and how to reach potentials leads.</p>
<p>Kevin and I also have our own blogs running for quite a bit of time and I wrote last year a book dedicated to web scraping in Java. This directly translated into 20k monthly visits that we could leverage to promote ScrapingBee.</p>
<p>And it worked. In about 2 months, we reached 150 beta signup, 4 times the amount of beta testers we had for PricingBot.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_s0LCG7DV7ZRrtQL2dN3HsQ.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-mistake-avoided-2-spending-too-much-money">Mistake avoided #2: Spending too much money</h2>
<p>While building PricingBot, we spent a lot of money on useless infrastructure, APIs and software without reaching Product-Market Fit.</p>
<p>We got to get our money back thanks to ShopToList sale and my agricultural skills before we launched ScrapingBee, but this time, we were way more careful about how we spent it.</p>
<p>I know spending several thousand dollars to bootstrap a project is not a lot of money but we weren’t comfortable with spending more. So we decided to be careful with how we would spend it with ScrapingBee.</p>
<p>We basically reduced our costs by only finding deals (❤ AWS Credits) like <a target="_blank" href="https://www.joinsecret.com/">Secret</a> which basically give you 6 months free for lots of SaaS or a huge discount.</p>
<p>We decided to do more with what we had, and so far we don’t regret it.</p>
<p>I’ll talk more about products and tools we used in a future blog post, this one is already long enough.</p>
<h2 id="heading-launch-and-mistake-avoided-3-not-asking-for-money-from-day-one">? Launch ? and mistake avoided #3: not asking for money from day one</h2>
<p>One thing that did not work well with PricingBot is that for months, we built a product that was free to use. I know this is a classic mistake, but this is not the worst part. The worst part is that we knew it was a mistake. In the last 4 years, we’ve read tons of books, interview, blog posts about startup and everyone seems to agree that the sooner you ask for money the better.</p>
<p>But it was easier said than done and we did not dare ask for money while building PricingBo. We just did not think anyone would pay for an unfinished product.</p>
<p>We did for ScrapingBee. The pricing for ScrapingBee is again a classic three plan SaaS based on API call volume/feature starting at $9 / $29 / $99 per month and an Enterprise plan.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_u8GtUK5YwugRzzDPJ11BBA.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We “soft-launched” first to our mailing list and got our first few small paying customers. Again, we had the same experience with PricingBot but this time it was different. With PricingBot, every paying customer we had was really hard to get, we had sent them tons and tons of email and they took a long time to finally pay.</p>
<p>With ScrapingBee it was different. Our first 2 customers had never talked with us before.</p>
<p>We then started to blog and got tons of leads along with a few more paying customers including a big Enterprise plan as you can see in the MRR chart below.</p>
<p>Then it all went quickly, Kevin and I both having blogged about programming, creating insightful content about Web scraping is not a problem for us, and we knew how and where to promote it.</p>
<p>One particular piece of content we wrote, a <a target="_blank" href="https://www.scrapingninja.co/blog/web-scraping-without-getting-blocked">web scraping guide</a> completely exceed our expectations.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_afh6y24VhYabHLd0vFbCbA.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>This post alone meant that in 2 months we had 3 times the traffic we had in one year of PricingBot. This post not only brought traffic but also customers with real $. It also allowed us to signup the first big enterprise plan that allowed us to reach and cross $1000 MRR.</p>
<h2 id="heading-the-future">The future</h2>
<p>Of course, it’s really early to say if <a target="_blank" href="https://www.scrapingninja.co/">ScrapingBee</a> will be a success or not.</p>
<p>This big enterprise customers thanks to the success of our first blog post could only be an outlier phenomenon that won’t reproduce in the future. Maybe it will. But one thing is certain, things are looking way better with ScrapingBee.</p>
<p>We have lots of engagement from our users and leads, the conversion rate from trial to paying customer being close to 5%.</p>
<p>We also love to talk with our potential customers (❤️ Zoom) and we have the feeling that ScrapingBee is really a must-have for them, instead of a “nice to have”. (small tips: we offer 10 000 free API credits for users that accept to have a small 15 minutes talk with us, this already allowed us to have 50 real conversations with real people about ScrapingBee)</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/10/1_qr_1khM2_jK0eooATk8JbQ.png" alt="Image" width="600" height="400" loading="lazy">
<em>In-app message to incentivize users to schedule a call with us.</em></p>
<p>In the months to come a big challenge will be to find profitable and scalable acquisition channels. We hope that content marketing will continue to work and that it will improve our SEO to get organic traffic. Writing a good piece of content may not be enough and we really have to discover other acquisition channels.</p>
<p>The other big challenge is to prioritize features in the API-store. Meaning figuring out what users <strong>need</strong> not blindly implementing what they want, and hopefully, manage to get them to pay before the feature is implemented.</p>
<p>We still don’t know what we want to do with PricingBot, we seriously think about selling it but are a bit afraid of all the paperwork involved (it was much easier with ShopToList because ShopToList did not bring any money in, so no bank account, Stripe account, etc…)</p>
<p>We also still have a lot to learn and a lot to prove before being able to say that we build a sustainable and profitable business but for the first time in our lives, we feel that it can be done, time will tell us if we’re right.</p>
<p>I really hope you liked our little story and that your learned some interesting bits along the way. We plan to do this kind of posts every 3 months at least, please tell me if you'd like to read the next one ;).</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to build a basic version of Product Hunt using React ]]>
                </title>
                <description>
                    <![CDATA[ By Emmanuel Yusufu This example and design shares what I’ve learned from the book Fullstack React. I highly recommend it as a good resource for learning React and it’s ecosystem technologies. check it out here: fullstackreact.com. Imagine that as a d... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-basic-version-of-product-hunt-using-react-f87d016fedae/</link>
                <guid isPermaLink="false">66c34f4d9972b7c5c7624e90</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ product hunt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Sun, 01 Oct 2017 20:16:57 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*OcJzrdQWZSd07hYpHnBf2A.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Emmanuel Yusufu</p>
<p>This example and design shares what I’ve learned from the book <em>Fullstack React</em>. I highly recommend it as a good resource for learning React and it’s ecosystem technologies. check it out here: <a target="_blank" href="https://fullstackreact.com">fullstackreact.com</a>.</p>
<p>Imagine that as a developer, you have been tasked with creating an MVP for a startup product that needs to be demonstrated to potential investors.</p>
<p>The application is a voting application inspired by Product Hunt and Reddit. In the application, products are displayed in a collection. Users can upvote the best products, and the application will automatically sort them according to the number of votes, placing the highest before the lowest.</p>
<p>The features of the app we will be building are very simple:</p>
<ul>
<li>Users can view the existing/displayed products.</li>
<li>Users can upvote products that delight them.</li>
<li>Products are sorted automatically according to vote count.</li>
</ul>
<p><strong>You can <a target="_blank" href="http://reactdemo.emmanuelyusufu.com">view the demo here</a></strong>.</p>
<h4 id="heading-step-1-first-things-first">Step 1: first things first</h4>
<p>Fist of all, head over to Github and download the starter folder I’ve already created with the necessary set up for our application <a target="_blank" href="https://github.com/emmyyusufu/react-product-voting-app-with-bootstrap/tree/starter">here</a>. Copy the <strong>URL</strong> provided by the green clone/download button and run in your preferred path on your command line. You must have git already installed.</p>
<pre><code>git clone URL
</code></pre><p>Once the folder is downloaded, open it in your code editor and observe the folder files and structure. It look like this:</p>
<pre><code>├───src
|    ├───app.js
|    ├───seed.js
|    ├───style.css
└───vendor
    ├───bootstrap<span class="hljs-number">-3.3</span><span class="hljs-number">.7</span>-dist
    ├───font-awesome<span class="hljs-number">-4.7</span><span class="hljs-number">.0</span>
    ├───react.js
    ├───react-dom.js
    └───babel-standalone.js
</code></pre><p><strong>Note:</strong> Your code editor should have a live server. This allows us to serve the files to our browser to view our work. Make sure to install the extension for your preferred code editor.</p>
<p>Under the src folder there are <strong>app.js</strong> and <strong>seed.js</strong> files. The app.js file is where we will write most of the code for our application. The seed.js file already contains the data collection of the products to be displayed.</p>
<p>Our seed.js file contains the following code</p>
<pre><code class="lang-js"><span class="hljs-built_in">window</span>.Seed = (<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateVoteCount</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.floor((<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">50</span>) + <span class="hljs-number">15</span>);
    }

    <span class="hljs-keyword">const</span> products = [
      {
        <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>,
        <span class="hljs-attr">title</span>: <span class="hljs-string">'Yellow Pail'</span>,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'On-demand sand castle construction expertise.'</span>,
        <span class="hljs-attr">url</span>: <span class="hljs-string">'#'</span>,
        <span class="hljs-attr">votes</span>: generateVoteCount(),
        <span class="hljs-attr">submitterAvatarUrl</span>: <span class="hljs-string">'images/avatars/daniel.jpg'</span>,
        <span class="hljs-attr">productImageUrl</span>: <span class="hljs-string">'images/products/image-aqua.png'</span>,
      },
                                ...
    ];

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">products</span>: products };

  }());
</code></pre>
<p>This code creates a function <code>generateVoteCount()</code> which we will explain later and a <code>products</code> array that contains the data of our products. They are wrapped as a self-invoking function, and are attached to the <code>window</code> object of our browser. This way we can access them anywhere we want them.</p>
<p>The <code>Seed</code> function eventually returns an object with a property of products and a value of <code>products</code>. This means that, if we execute <code>Seed.products</code>, we should have every product object returned to us.</p>
<p>The <strong>react.js</strong> file is the code containing the React core itself. Also, <strong>react-dom.js</strong> is the code that helps us render out React components we’ve created in HTML DOM. Finally, <strong>babel-standalone.js</strong> is the Babel code that transpiles the advanced JSX and ES6 code we will be working with into ES5 code (the most common JavaScript specification that most old and current browsers support today).</p>
<h4 id="heading-step-2-create-components">Step 2: create components</h4>
<p>We need to create two React components. We will call the parent component <code>ProductList</code> , and the collection of children components it houses will be <code>Procuct</code> .</p>
<p>Inside the app.js file, create the parent component by doing this:</p>
<pre><code class="lang-jsx"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductList</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
    render() {
        <span class="hljs-keyword">const</span> products = Seed.products.map(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Product</span> 
            <span class="hljs-attr">id</span>=<span class="hljs-string">{product.id}</span>
            <span class="hljs-attr">title</span>=<span class="hljs-string">{product.title}</span>
            <span class="hljs-attr">description</span>=<span class="hljs-string">{product.description}</span>
            <span class="hljs-attr">url</span>=<span class="hljs-string">{product.url}</span>
            <span class="hljs-attr">votes</span>=<span class="hljs-string">{product.votes}</span>
            <span class="hljs-attr">submitterAvatarUrl</span>=<span class="hljs-string">{product.submitterAvatarUrl}</span>
            <span class="hljs-attr">productImageUrl</span>=<span class="hljs-string">{product.productImageUrl}</span>
            /&gt;</span></span>
        ));
        <span class="hljs-keyword">return</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Popular products<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
                {products}
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
        );
    }
}
ReactDOM.render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ProductList</span> /&gt;</span></span>, <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'content'</span>));
</code></pre>
<p>In the parent component, we intend to create a child component based on each object accessible from <code>Seed.products</code> . So we set up some props. Now let’s actually declare the child component still in the same file called <code>Product</code> :</p>
<pre><code class="lang-jsx"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Product</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
    render() {
        <span class="hljs-keyword">return</span> (
          <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'container'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"row"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'col-md-12'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"main"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"image"</span>&gt;</span>  
                    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{this.props.productImageUrl}</span> /&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> 
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'header'</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">i</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'fa fa-2x fa-caret-up'</span> /&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    {this.props.votes}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'description'</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">{this.props.url}</span>&gt;</span>
                        {this.props.title}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{this.props.description}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'extra'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Submitted by:<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                  <span class="hljs-attr">className</span>=<span class="hljs-string">'avatar'</span>
                  <span class="hljs-attr">src</span>=<span class="hljs-string">{this.props.submitterAvatarUrl}</span>
                /&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
        );
      }
}
</code></pre>
<p>We are able to reference <code>React.Component</code> and <code>ReactDOM.render</code> because we have already loaded the react.js and react-dom.js files. They are available for use even though we’re currently in the app.js file. Having created the component, <code>ReactDOM.render(whatComponent, where)</code> renders it to the DOM.</p>
<p>Running your live server, you should have the following screen:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/n8J6LzTA2DTnLI43sXzHnbJJjpDdfd8BVyeJ" alt="Image" width="800" height="382" loading="lazy">
<em>static components</em></p>
<h4 id="heading-step-3-add-interactivity">Step 3: add interactivity</h4>
<p>So far, we have been able to code the components of our app — but they are still static. How can we make them interactive?</p>
<p>In coding React apps, follow this general process:</p>
<ul>
<li>Divide the app UI into components</li>
<li>Build a static version of the app</li>
<li>Determine what data is a state</li>
<li>Determine in what components each piece of the state should live</li>
<li>Hard code initial states</li>
<li>Add inverse data flow from child to parent via props</li>
<li>Add server communication</li>
</ul>
<p>We wont be doing all of the above, but lets get going with <strong>state</strong>. The only piece of data in our app that can be considered stateful or ever-changing is the number of votes. Remember: that is a property in the collection of products in our seed.js file. Votes are in each <code>product</code> object, so it represents our state.</p>
<p>Knowing our state, where do we initialize it? States in React are self-contained in certain components, unlike props that are passed down. The number of votes as a state is owned by <code>&lt;Product /&gt;</code> , but since the collection of products we have are generated from <code>&lt;ProductList /&gt;</code>, we initialize the state there. In <code>&lt;ProductList /&gt;</code>, do this before the <code>render()</code> method:</p>
<pre><code><span class="hljs-keyword">constructor</span>() {
        <span class="hljs-built_in">super</span>();
        <span class="hljs-built_in">this</span>.state = {
            <span class="hljs-attr">products</span>: []
        }
    }
</code></pre><p>When initializing state in a component, we try to define what it should look like while keeping it empty. Our products are an array, so we use an empty array. We initialize it inside <code>constructor() {}</code> , because that's the piece of code that runs when our component is created.</p>
<p>Lets make our component read <code>products</code> from its own state instead of from a file. Add:</p>
<pre><code class="lang-jsx"> componentDidMount() { 
   <span class="hljs-built_in">this</span>.setState({ <span class="hljs-attr">products</span>: Seed.products }) 
 }
</code></pre>
<p>to set the state to use. Also update <code>const products = Seed.products</code> to <code>const products = this.state.products</code>. To make JavaScript sort it according to the highest number of votes, write this instead:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> products = <span class="hljs-built_in">this</span>.state.products.sort((a, b) {
    b.votes - a.votes
});
</code></pre>
<p>The JavaScript <code>sort();</code> uses a <strong>compare function</strong> inside. You could find out about this in a documentation.</p>
<h4 id="heading-step-4-handle-upvoting">Step 4: handle upvoting</h4>
<p>Let’s head over to the hyperlink surrounding the font-awesome, caret-up icon and create a function using onClick.</p>
<pre><code class="lang-jsx">&lt;a onClick={passTheId}&gt;
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">i</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'fa fa-2x fa-caret-up'</span> /&gt;</span></span>
 &lt;/a&gt;
</code></pre>
<p>After we’ve defined the function, lets actually create it. Inside the Product component, create a <code>passTheId();</code> function:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">constructor</span>() {
        <span class="hljs-built_in">super</span>();
        <span class="hljs-built_in">this</span>.passTheId = <span class="hljs-built_in">this</span>.passTheId.bind(<span class="hljs-built_in">this</span>);
    }
    passTheId() {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Id will be passed'</span>);
    }
</code></pre>
<p>We bound the function to the <code>this</code> keyword, because only in-built functions like render() have access to use that word.</p>
<p>Lets create another function in the ProductList component. This one will update the state working with the <code>handleUpVote</code> function of the Product component.</p>
<pre><code class="lang-jsx">handleProductUpVote = <span class="hljs-function">(<span class="hljs-params">productId</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> nextProducts = <span class="hljs-built_in">this</span>.state.products.map(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (product.id === productId) {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">Object</span>.assign({}, product, {
          <span class="hljs-attr">votes</span>: product.votes + <span class="hljs-number">1</span>,
        });
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> product;
      }
    });
    <span class="hljs-built_in">this</span>.setState({
      <span class="hljs-attr">products</span>: nextProducts,
    });
  }
</code></pre>
<p>States in React should be treated as immutable. That is, they should not be modified directly. The above function will do that using JavaScript’s <code>Object.assign();</code>by creating a seemingly new array called <code>nextProducts</code> . This is similar to the existing state, but has a change in the number of votes. <code>nextProducts</code>is then set as the new state. It seems weird to do things this way, but this is what the React team recommends to improve performance.</p>
<p>We want to pass the ID of the product from the child <code>Product</code> component to the parent <code>ProductList</code> component, so lets make <code>handleProductUpVote</code> available to the child as props:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> productComponents = products.map(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> (
      <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Product</span>
        <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>'<span class="hljs-attr">product-</span>' + <span class="hljs-attr">product.id</span>}
        <span class="hljs-attr">id</span>=<span class="hljs-string">{product.id}</span>
        <span class="hljs-attr">title</span>=<span class="hljs-string">{product.title}</span>
        <span class="hljs-attr">description</span>=<span class="hljs-string">{product.description}</span>
        <span class="hljs-attr">url</span>=<span class="hljs-string">{product.url}</span>
        <span class="hljs-attr">votes</span>=<span class="hljs-string">{product.votes}</span>
        <span class="hljs-attr">submitterAvatarUrl</span>=<span class="hljs-string">{product.submitterAvatarUrl}</span>
        <span class="hljs-attr">productImageUrl</span>=<span class="hljs-string">{product.productImageUrl}</span>
        <span class="hljs-attr">onVote</span>=<span class="hljs-string">{this.handleProductUpVote}</span>
      /&gt;</span></span>
    ));
</code></pre>
<p>We added <code>onVote={this.handleProductUpVote}</code>. So at the child level, we can access it through <code>this.props</code></p>
<pre><code class="lang-jsx">passTheId() {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Id will be passed'</span>);
        <span class="hljs-built_in">this</span>.props.onVote(<span class="hljs-built_in">this</span>.props.id)
    }
</code></pre>
<p>Your entire <code>app.js</code> file should look like this:</p>
<pre><code class="lang-jsx"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProductList</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
    state = {
        <span class="hljs-attr">products</span>: [],
      };
      componentDidMount() {
        <span class="hljs-built_in">this</span>.setState({ <span class="hljs-attr">products</span>: Seed.products });
      }
      handleProductUpVote = <span class="hljs-function">(<span class="hljs-params">productId</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> nextProducts = <span class="hljs-built_in">this</span>.state.products.map(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> {
          <span class="hljs-keyword">if</span> (product.id === productId) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">Object</span>.assign({}, product, {
              <span class="hljs-attr">votes</span>: product.votes + <span class="hljs-number">1</span>,
            });
          } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span> product;
          }
        });
        <span class="hljs-built_in">this</span>.setState({
          <span class="hljs-attr">products</span>: nextProducts,
        });
      }
    render() {
        <span class="hljs-keyword">const</span> products = <span class="hljs-built_in">this</span>.state.products.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> (
            b.votes - a.votes
        ));
        <span class="hljs-keyword">const</span> productComponents = products.map(<span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Product</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>'<span class="hljs-attr">product-</span>' + <span class="hljs-attr">product.id</span>}
              <span class="hljs-attr">id</span>=<span class="hljs-string">{product.id}</span>
              <span class="hljs-attr">title</span>=<span class="hljs-string">{product.title}</span>
              <span class="hljs-attr">description</span>=<span class="hljs-string">{product.description}</span>
              <span class="hljs-attr">url</span>=<span class="hljs-string">{product.url}</span>
              <span class="hljs-attr">votes</span>=<span class="hljs-string">{product.votes}</span>
              <span class="hljs-attr">submitterAvatarUrl</span>=<span class="hljs-string">{product.submitterAvatarUrl}</span>
              <span class="hljs-attr">productImageUrl</span>=<span class="hljs-string">{product.productImageUrl}</span>
              <span class="hljs-attr">onVote</span>=<span class="hljs-string">{this.handleProductUpVote}</span>
            /&gt;</span></span>
          ));
        <span class="hljs-keyword">return</span> (
            <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Popular products<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
                {productComponents}
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
        );
    }
}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Product</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">React</span>.<span class="hljs-title">Component</span> </span>{
    <span class="hljs-keyword">constructor</span>() {
        <span class="hljs-built_in">super</span>();
        <span class="hljs-built_in">this</span>.passTheId = <span class="hljs-built_in">this</span>.passTheId.bind(<span class="hljs-built_in">this</span>);
    }
    passTheId() {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Id will be passed'</span>);
        <span class="hljs-built_in">this</span>.props.onVote(<span class="hljs-built_in">this</span>.props.id);
    }
    render() {
        <span class="hljs-keyword">return</span> (
          <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'container'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"row"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'col-md-12'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"main"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"image"</span>&gt;</span>  
                    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{this.props.productImageUrl}</span> /&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> 
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'header'</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{this.passTheId}</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">i</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'fa fa-2x fa-caret-up'</span> /&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    {this.props.votes}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'description'</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">{this.props.url}</span>&gt;</span>
                        {this.props.title}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
                        {this.props.description}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'extra'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Submitted by:<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                  <span class="hljs-attr">className</span>=<span class="hljs-string">'avatar'</span>
                  <span class="hljs-attr">src</span>=<span class="hljs-string">{this.props.submitterAvatarUrl}</span>
                /&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
        );
      }
}
ReactDOM.render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ProductList</span> /&gt;</span></span>, <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'content'</span>));
</code></pre>
<p>Refresh your browser and you should see the working app. <a target="_blank" href="http://reactdemo.emmanuelyusufu.com"><strong>View demo</strong></a>.</p>
<p>Feel free to share, comment or ask questions. For the final code, visit this <a target="_blank" href="https://github.com/emmyyusufu/react-product-voting-app-with-bootstrap">github link</a> and clone to your computer.</p>
<p>If you enjoyed this article, give me some claps so more people see it. Thank you for reading.</p>
<p>You can read more of my writing on my blog: <a target="_blank" href="http://stellarcode.co/build-a-product-hunt-inspired-app-with-react-2/">Stellar Code</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
