<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Max Rozen</title>
        <link>https://maxrozen.com</link>
        <description>Learn React with Max Rozen</description>
        <lastBuildDate>Fri, 02 Jan 2026 11:14:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>React Router</generator>
        <language>en</language>
        <image>
            <title>Max Rozen</title>
            <url>https://maxrozen.com/images/logo.svg</url>
            <link>https://maxrozen.com</link>
        </image>
        <copyright>All rights reserved 2026, Max Rozen</copyright>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 25: you can just build things]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-25-you-can-just-build-things</link>
            <guid>https://maxrozen.com/onlineornot-diaries-25-you-can-just-build-things</guid>
            <pubDate>Sun, 17 Aug 2025 15:10:00 GMT</pubDate>
            <description><![CDATA[You *can* just build things.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-25-you-can-just-build-things">clicking here</a>.)</div><p>I know, it sounds cliché at this point, but you can just build things.</p>
<p>What do I mean by just build things? I mean, don't obsess over the right language, framework, tools, or approach to use.</p>
<p>Just build something, and ship it.</p>
<p>In the absolute worst case, you get it wrong. Guess what? you can just fix it.</p>
<p>I'm writing this article after realizing it has been four and a half years that I've been working on a product folks told me "won't work at scale", and "can't be viable". They were mostly wrong, and the things they were right about? I fixed.</p>
<h2>The right programming language to use</h2>
<p>A painfully common question I see is "what programming language should I use in <code>$INSERT_YEAR_HERE</code> to build my side project?"</p>
<p>The correct answer is: the one you already know. If you already know PHP, just use PHP. Same goes for jQuery, Java, Node, etc.</p>
<p>(Of course there are caveats. If all you know is VB6, and you want to build modern web apps, you might want an egghead.io subscription first.)</p>
<p>I'm a strong believer that getting things done quickly and well is the only thing that matters. If you're spending half of your project's time learning the language, or figuring out if how you use it is the "language-way" of doing things, when you could be building, you're wasting your time. If your goal is to learn the new programming language, do it intentionally. Trying to build a business at the same time will only distract you.</p>
<p>I'm speaking from experience here.</p>
<p>I've known since about 2016 that I wanted to build a business on the Internet (for context, I started my professional career building with AngularJS 1.5 and Django, and shortly afterwards pivoted to React and Node.js). My first month or so of trying to build an Internet business was just to figure out which language/framework to use. I ended up using what I used at work (React + Node.js), but that was a frustrating month of indecision.</p>
<h2>Building it "correctly"</h2>
<p>I'm writing this for folks with little-to-no users. Obviously, at web scale, getting it wrong <em>hurts</em>. Like, "a multiple-year project to handle the rewrite, migration, and user messaging" levels of pain.</p>
<p>The great thing about wanting to carve out a small portion of the Internet to run a bootstrapped business on, is that our projects aren't <em>that</em> hard to fix if we screw things up.</p>
<p>I'd argue at small scale, there's rarely a "right" way to build things. Just get things out there, and see if people will use them.</p>
<p>I started off <a href="https://onlineornot.com/">OnlineOrNot</a> on AWS Lambda because I knew it would cost me nothing if I got no users. I got users, enough to keep my AWS Lambda functions active 100% of the time. It started to cost me a shit ton of money more than if I just used a VPS. If I used a VPS from the start, I doubt I'd have shipped.</p>
<p>So I <a href="https://onlineornot.com/on-moving-million-uptime-checks-onto-fly-io">rewrote to use a VPS</a>. It took about 10 hours. It didn't work for my use case. I undid the change, and eventually <a href="https://onlineornot.com/how-onlineornot-halved-aws-bill">moved to Cloudflare Workers</a>.</p>
<p>More recently, I noticed OnlineOrNot's main dashboard was taking longer several seconds to load (almost at one billion uptime checks complete at time of writing). After spending a bit of time optimizing the query and indexes, I got the query down to 500ms.</p>
<p>Was using postgres for clearly analytical (OLAP) workloads right? Clearly not, but it did the job until things started to get slow, and since moving to Clickhouse, that query now runs in 50ms.</p>
<h2>In short</h2>
<p>Your technical approach doesn't have to be perfect the first go.</p>
<p>Just make sure it works, and iterate.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Four years of running a SaaS in a competitive market]]></title>
            <link>https://maxrozen.com/on-four-years-running-saas-competitive-market</link>
            <guid>https://maxrozen.com/on-four-years-running-saas-competitive-market</guid>
            <pubDate>Fri, 04 Apr 2025 07:10:00 GMT</pubDate>
            <description><![CDATA[Looking back on the last four years, what worked, what didn't.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/on-four-years-running-saas-competitive-market">clicking here</a>.)</div><p>When I played around with the technology that would eventually become <a href="https://onlineornot.com">OnlineOrNot</a> back in 2021, a quick search showed me that there were 200 listed alternatives to the tool I wanted to replace. I thought most of them sucked.</p>
<p>There are even more today.</p>
<p>Over the years, many more started and shut down a few months later (it turns out running a business in a highly competitive space doesn't make you rich overnight). Others raised VC money or were acquired by a private equity firm, and started their descent into <a href="https://en.wikipedia.org/wiki/Enshittification">enshittification</a>.</p>
<p>I started work on OnlineOrNot because I wanted to build software that didn't have to get worse for its users in order to maximize shareholder value. In short, it's a self-funded, sustainable business that I intend to run for decades.</p>
<p>It's default-alive, as I keep a full time job so that I can build the business the way I want.</p>
<p>I've published one of these articles every year since starting, and there were things that I "learned" in the first year, that turned out to be total bullshit by the third year. So this article doesn't just look at the last 12 months, but across the entire lifetime of this business.</p>
<p><strong>Table of contents:</strong></p>
<ul>
<li><a href="#principles-that-havent-changed">Principles that haven't changed</a>
<ul>
<li><a href="#two-hours-a-day-every-workday">Two hours a day, every workday.</a></li>
<li><a href="#no-other-side-projects">No other side projects.</a></li>
<li><a href="#solve-customer-pain">Solve customer pain.</a></li>
<li><a href="#be-ruthlessly-iterative">Be ruthlessly iterative.</a></li>
</ul>
</li>
<li><a href="#lessons">Lessons</a>
<ul>
<li><a href="#read-a-few-books-and-start-building">Read a few books, and start building</a></li>
<li><a href="#solve-pain-dont-try-to-sell-a-subscription">Solve pain, don't try to sell a subscription</a></li>
<li><a href="#ship-small-ship-often">Ship small, ship often</a></li>
<li><a href="#ship-first-worry-about-scale-later">Ship first, worry about scale later</a></li>
<li><a href="#have-an-early-access-program">Have an early-access program</a></li>
<li><a href="#build-a-free-trial-as-soon-as-possible">Build a free trial as soon as possible</a></li>
<li><a href="#docs-are-the-product">Docs <em>are</em> the product</a></li>
<li><a href="#build-for-mobile">Build for mobile</a></li>
<li><a href="#ask-people-how-they-found-you">Ask people how they found you</a></li>
<li><a href="#i-dont-use-invasive-analytics">I don't use invasive analytics</a></li>
<li><a href="#talk-to-potential-customers-even-if-you-think-you-have-nothing-for-them">Talk to potential customers, even if you think you have nothing for them</a></li>
<li><a href="#you-dont-get-to-spend-as-much-time-solving-the-problem-as-you-think">You don't get to spend as much time solving the problem as you think</a></li>
<li><a href="#pricing-is-hard-to-get-right">Pricing is hard to get right</a></li>
<li><a href="#dont-tunnel-vision-your-mrr">Don't tunnel-vision your MRR</a></li>
<li><a href="#never-give-away-unlimited-anything">Never give away "unlimited" anything</a></li>
<li><a href="#rate-limit-your-paid-resources">Rate limit your paid resources</a></li>
<li><a href="#stop-trying-to-explain-everything-on-one-page">Stop trying to explain everything on one page</a></li>
<li><a href="#its-hard-to-bring-in-more-traffic-easy-to-change-what-your-current-traffic-does">It's hard to bring in more traffic, easy to change what your current traffic does</a></li>
<li><a href="#competitors-dont-really-matter">Competitors don't really matter</a></li>
</ul>
</li>
</ul>
<h2>Principles that haven't changed</h2>
<p>While I've learned a lot about running a business over the years, there are some unchanging principles:</p>
<h3>Two hours a day, every workday.</h3>
<p>Years before I even started OnlineOrNot, I started hacking on my own projects in the two hours I have before starting my workday. Having a consistent block of time for myself has let me publish hundreds of articles, a book, dozens of software projects, and more.</p>
<p>Since starting, I worked out that the amount of time per day isn't as important as putting <em>any effort</em> in, every day, for years.</p>
<blockquote>
<p>But how do you find time before your workday?</p>
</blockquote>
<p>I started waking up two hours earlier, and adjusted the rest of my day accordingly.</p>
<h3>No other side projects.</h3>
<blockquote>
<p>the person who chases two rabbits catches neither</p>
</blockquote>
<p>Of course, there are exceptional folks out there chasing a dozen rabbits and managing to catch some of them - I know myself well enough to realize that I'm not that person.</p>
<p>Building out the marketing and sales processes to take a business from $0 to $500 MRR is challenging enough. I don't see a point in repeating the hardest part of starting a business over and over again, instead of doubling down on what's already working (this is also a lesson for picking a marketing approach, more on that later).</p>
<h3>Solve customer pain.</h3>
<p>When a user signs up for OnlineOrNot, I have an automated email going out asking what brought them to sign up today. I explicitly tell them I read and reply to every email. This is the main source of my insight for building product.</p>
<p>I ask my customers what isn't working, and I make it work.</p>
<h3>Be ruthlessly iterative.</h3>
<p>If I can't get a piece of work released in two hours, I cut the scope down to something achievable, and iterate on that.</p>
<p>It's worth noting that this is the ideal, and I don't always succeed at cutting down scope to exactly two hours. Over time I've realised that with how my brain works, I enjoy shipping early versions as quickly as possible, and building out the functionality once it's deployed (behind a feature flag).</p>
<p>Waiting until the entire feature is built before deploying completely saps my motivation in comparison, and I find myself easily distracted when building this way.</p>
<h2>Lessons</h2>
<h3>Read a few books, and start building</h3>
<p>I read dozens of business books when I wanted to get started, mainly from not wanting to repeat mistakes that others have made.</p>
<p>Sometimes though, you need to make mistakes for yourself.</p>
<p>As an example, it took me getting on the front page of Hacker News, having 6000 people visit my landing page, a few hundred people attempt to sign-up, and only single-digits making it through to the app for me to realise something might be wrong.</p>
<p>I had something like a 75% drop-off rate on my sign-up form alone. I got it down to 50% just by adding an extra OAuth login provider.</p>
<p>If I had to do it all again, I'd start with only three books:</p>
<ul>
<li>The Mom Test by Rob Fitzpatrick</li>
<li>Deploy Empathy by Michele Hansen</li>
<li>Badass: Making Users Awesome by Kathy Sierra</li>
</ul>
<p>and if you need specific detail on building and running a SaaS, The SaaS Playbook by Rob Walling.</p>
<h3>Solve pain, don't try to sell a subscription</h3>
<p>The product's goal is to solve your customer's pain, rather than sell subscriptions to your SaaS.</p>
<p>This is a mindset shift from "I'm just going to keep building features, they'll come eventually!" to "I should be helping my users solve this annoying problem in their job"</p>
<p>Building a SaaS is just one of the ways you could be solving their problem. There are other ways you could help, like recording screencasts, writing docs, articles, books, running workshops, providing code samples, etc.</p>
<h3>Ship small, ship often</h3>
<p>People will suggest you should build particular features to improve your product.</p>
<p>They'll never use those features.</p>
<p>They're probably just trying to be helpful, and saw a similar feature in another product. Because you're new to running a SaaS, you'll be excited that people are actually talking to you, and rush out to build that feature for them.</p>
<p>I'm not going to tell you not to build the feature (that's the advice I was given, and I built unused features anyway). You should ask how they would use the feature, ask other customers how they deal with the problem, build the smallest possible version of that feature, and see how the rest of your customers use it. You don't want to be building snowflake features only one person uses.</p>
<p>It stings a lot less to remove a feature no one wanted after spending a few hours on it, rather than a few months.</p>
<h3>Ship first, worry about scale later</h3>
<p>In the first iteration of OnlineOrNot, I didn't optimise the architecture at all.</p>
<p>There was actually a bug that limited the total number of uptime checks the system could handle to around 100. I also didn't have proper error screens, so when users ran into that issue, all they saw on the screen was:</p>
<blockquote>
<p>Error!</p>
</blockquote>
<p>Not a great look.</p>
<p>At the same time, I prefer being embarrassed by incomplete UI than building things people don't need. There was never a guarantee that OnlineOrNot would attract thousands of users, it could have ended up as another SaaS I built only for myself.</p>
<p>My immediate solution was to upgrade my database to a higher tier, increasing the number of uptime checks that could connect to the database, and in the meanwhile I got to work on rearchitecting the product.</p>
<p>A few hours later, I had a solution in place that could handle millions of checks per week on the smallest AWS database, and made that error screen look a bit more professional.</p>
<h3>Have an early-access program</h3>
<p>Shipping early is incredibly useful. Early on in your product's development, almost all users that sign up are expecting rough edges (especially if they've seen your unpolished landing page).</p>
<p>As time goes on, you're going to get folks expecting a mature product, so you can't just keep shipping imperfect features to your entire userbase.</p>
<p>My solution for this was to add a checkbox in each user's account with the label "Join the Early Access Program". Folks that opt in get to see OnlineOrNot's latest features before they're ready, in exchange for patience and feedback.</p>
<h3>Build a free trial as soon as possible</h3>
<p>The common wisdom these days is to not even bother with a free tier - it's too difficult to get right. When I started though, a free tier was a great way to attract people and get them talking about your product.</p>
<p>The thing is, you still need a way to let them sample "the good stuff", especially if the free tier is significantly less useful than your paid tiers.</p>
<p>It took me 11 months to realise I should build an onboarding flow that ended with asking if folks wanted a free trial. Particularly, it asked:</p>
<blockquote>
<p>Do you want to start a free trial?</p>
</blockquote>
<p>What it was actually saying was:</p>
<blockquote>
<p>Do you want to experience OnlineOrNot's best features for 14 days before deciding if it's worth your time, or spend months with a product that has the good stuff disabled, unsure if it'll actually solve your problems?</p>
</blockquote>
<p>I eventually decided to experiment with defaulting all users to a free trial first, so that everyone could experience the <em>entire product</em> first. This one experiment more than doubled OnlineOrNot's monthly growth rate.</p>
<p>It turned out that starting the business relationship with "this is a paid service, you'll need to add payment details to continue getting the good stuff" helps the business significantly more than "this is a free service, if you use it a lot, you might have to pay for it".</p>
<h3>Docs <em>are</em> the product</h3>
<p>Back when I started, folks used to say that "developers don't read documentation".</p>
<p>This turned out to be bullshit.</p>
<p>Some of the early customers in my ideal customer profile (ICP) came in praising OnlineOrNot's documentation, and I doubled down on it since. I even went as far as <a href="https://onlineornot.com/built-my-http-docs-from-scratch">building my API docs from scratch</a> to have full control over the user experience.</p>
<p>Back when I had product analytics, I noticed people would struggle to do something in OnlineOrNot's UI, get frustrated, check the docs, and one of two things would happen:</p>
<ol>
<li>They would find the exact feature they're looking for in the sidebar, and keep using the product for a long time</li>
<li>They would not find what they were looking for, not create any checks, and just churn</li>
</ol>
<p>In short, successful use of the docs drove retention.</p>
<h3>Build for mobile</h3>
<p>Contrary to popular belief (for B2B SaaS), folks actually work from their phones, and I think the rate is increasing.</p>
<p>Something like 50% of users start their journey to my product on mobile. They would quickly create an account, add a few pages to monitor, then eventually get on their laptop/desktop to review their checks from time to time.</p>
<p>For the first 6 months I didn't support mobile well, and folks that signed up on their phone churned rapidly. I eventually took the time to build responsive views for mobile, and new mobile users stuck around.</p>
<h3>Ask people how they found you</h3>
<p>One of the most valuable code changes I made halfway through the first year was asking people as they signed up: "How did you find out about OnlineOrNot?"</p>
<p>You need to know where your users are finding you.</p>
<p>There are dozens of marketing channels you could be using to attract potential customers. You only have a fixed amount of attention, so if you find a channel that's working more than others, you need to focus that attention on that channel until you notice diminishing returns.</p>
<h3>I don't use invasive analytics</h3>
<p>When I started, I integrated with standard SaaS product analytics software that most big SaaS products use. They tend to have features like session recording, where you can see exactly where their mouse moves in your product, and funnel tracking for working out how many users make it the whole way through from landing page to using your product.</p>
<p>This turns out to be useful in bigger companies. You have stakeholders to align with your vision of how the product should be built, it's easy to point to some data and show that version A resulted in sign-up uplift over version B.</p>
<p>The thing is, most products don't get enough users through the funnel to prove that the result you see is actually because something is better, rather than random chance.</p>
<p>As a solo founder with only two hours to spare every morning, I just didn't have the time to go through all that data and try to convince myself something. Instead I have an "inner-circle" of users that I DM for a vibe-check on features and problems in the product, and build things by taste.</p>
<h3>Talk to potential customers, even if you think you have nothing for them</h3>
<p>I was contacted by a CTO early on, asking if OnlineOrNot supported some particular feature.</p>
<p>My normal reaction would've been to just say "sorry, no" and leave it at that. Out of curiosity I started asking what pain they're trying to solve, I also asked the inner-circle users if they ran into this pain too, and what they'd like to achieve with the feature, and I told this CTO how I figured I would build it.</p>
<p>They signed up to a paid plan the next day, they've been customers ever since, and the feature gets used by other customers too.</p>
<h3>You don't get to spend as much time solving the problem as you think</h3>
<p>Of all the time I spent programming in the last four years, significantly less than half went to actually solving the problem I wanted to solve (knowing if a site is down, and alerting folks when that happens). The majority went to building a SaaS platform around that problem.</p>
<p>SaaS platform things you didn't even realise you'd need, like multiple types of authentication and user management, trials, onboarding, recurring database jobs, team management and invoice management, lifecycle emails, and more.</p>
<p>A lot of folks outsource this type of work (and I do! If Stripe didn't exist, I probably wouldn't be selling a service nor using subscription-based billing), but there's always stuff you don't feel comfortable outsourcing, or that you handle differently, so you need to build it yourself.</p>
<h3>Pricing is hard to get right</h3>
<p>Price too high, and you'll either have churn from folks who expect your app does everything or completely kill your sign-up rate. Too low, and you'll have customers that demand you rewrite your app just because they gave you $9.</p>
<p>Refund the difficult customers, raise your prices, and move on. Be prepared to experiment a lot with pricing, especially early on and as you build functionality.</p>
<h3>Don't tunnel-vision your MRR</h3>
<p>Tracking your MRR is a crap way to measure how you're doing as a business.</p>
<p>Things you did weeks (if not months) ago will affect your MRR today, so you won't really know if pricing changes work until you've already got a decent number of customers going through different stages of their customer journey.</p>
<p>Depending on your product, it could take up to 60 days for a user to go from signing up for the first time, to entering their credit card details.</p>
<p>Find another success metric to figure out if people are actually using your product, and whether it's bringing them value. Things like number of images generated, or number of form completions, for example.</p>
<h3>Never give away "unlimited" anything</h3>
<p>There will always, <strong>always</strong> be a whale customer for whom an unlimited amount of your value metric for $250/mo will be the deal of a lifetime. Generally speaking, never offer unlimited anything, especially if it costs you additional money for each thing created.</p>
<p>Lifetime deals fall under this advice too.</p>
<p>You aren't "finding users" for your product, you're finding people that expect you to build exactly the feature they want from you, years down the line, for that $100 they gave you once. Of which you likely only saw 30%, if you used a third-party to run the lifetime deal on their marketplace.</p>
<h3>Rate limit your paid resources</h3>
<p>If you call any sort of paid API (whether it's AI, sending SMS/email etc) as part of your service, you're going to want to rate limit calls to that service.</p>
<blockquote>
<p>But my users are paying me for this service, shouldn't they be able to use it as much as they want?</p>
</blockquote>
<p>Of course there are exceptions (and it depends on your Terms of Service), such as if an extremely large company starts using your service, but generally speaking, this will save you from a large unexpected bill at the end of the month, or being labelled a spammer by your vendors.</p>
<p>If someone genuinely needs to use your service at an extremely high rate, they'll get in touch.</p>
<p>This particular insight comes from the time OnlineOrNot sent thousands of SMSes to a web agency when the one server holding hundreds of their WordPress websites started running out of RAM.</p>
<h3>Stop trying to explain everything on one page</h3>
<blockquote>
<p>If you try to be everything to everyone, you'll end up being nothing to no one</p>
</blockquote>
<p>I think this applies particularly well to copywriting for landing pages.</p>
<p>As I built additional features into OnlineOrNot, I would try add additional sections to my main landing page, and it ended up an incoherent mess, diluting the overall message. I would have folks emailing me to ask if I supported sending alerts to Slack, when it was the second feature I built for OnlineOrNot.</p>
<p>Instead, by breaking up each feature into its own landing page:</p>
<ul>
<li><a href="https://onlineornot.com/">main landing page</a></li>
<li><a href="https://onlineornot.com/website-monitoring">uptime monitoring</a></li>
<li><a href="https://onlineornot.com/api-monitoring">api monitoring</a></li>
<li><a href="https://onlineornot.com/status-pages">status pages</a></li>
<li><a href="https://onlineornot.com/cron-job-monitoring">cron job monitoring</a></li>
</ul>
<p>I can take the space to explain each feature, without diluting the message.</p>
<h3>It's hard to bring in more traffic, easy to change what your current traffic does</h3>
<p>Getting noticed on the internet is a long, slow game.</p>
<p>Eventually over months (if not years), if you're consistent at quality content marketing, the number of readers on your articles will grow from 1-2 a day, to a few hundred per day.</p>
<p>Increasing the number of people landing on your site isn't particularly easy.</p>
<p>On the other hand, what people do once they land on your site is entirely within your influence, and something you can change today (such as adding an additional OAuth login provider to your sign-up form, that I mentioned earlier).</p>
<h3>Competitors don't really matter</h3>
<p>You might have noticed I haven't mentioned anything about competitors here, despite operating in a highly competitive market.</p>
<p>The truth is I don't think they change much.</p>
<p>Sure, there are more "table-stakes" features that customers need before they'll even consider using you, but the real competitor is a lack of awareness of your product, more than anything.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 24]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-24</link>
            <guid>https://maxrozen.com/onlineornot-diaries-24</guid>
            <pubDate>Fri, 24 Jan 2025 07:10:00 GMT</pubDate>
            <description><![CDATA[Looking over last year, and a first incident for 2025.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-24">clicking here</a>.)</div><p>It's Friday morning, the days are getting longer, and the weather better here in Toulouse. Let's go into some thoughts from the past few weeks.</p>
<h2>About last year</h2>
<p>I wrote up my product-focused <a href="https://onlineornot.com/2024">2024 wrap up</a> for OnlineOrNot a few weeks back and I'm still planning on writing a more business-focused "what I learned from four years of running a SaaS" to mark OnlineOrNot's fourth birthday.</p>
<p>A good summary of 2024 for me is: I spent way too much time worrying about feature gaps in the product (the <a href="https://onlineornot.com/status-pages">status page product</a> had <strong>many</strong>), and not enough time marketing.</p>
<p>For example, I built out ten changelog-worthy features, and took the time to add none of them to my landing page. Useful for existing customers (since they become aware of these features through the changelog + product update newsletter), but from the perspective of interested prospects visiting the landing page, the feature gap is still there.</p>
<p>So the goal of 2025 is to take a more balanced approach between marketing and coding. Instead of rushing off to the next feature, take the time to update the landing page as well as the docs, changelog, and product newsletter.</p>
<h2>First incident of the year</h2>
<p>Last week OnlineOrNot had its first incident of the year: uptime checks were moving slower through the pipeline than usual, meaning checks that should run every 30 seconds were running every 5 minutes or so. I spent the end of last year tweaking the system to ensure this would never happen, but I missed a spot.</p>
<p>The postmortem probably says it best:</p>
<blockquote>
<p>The root cause was a loss of connectivity between our Cloudflare Workers service, and the AWS database that stores metadata about uptime checks. There was a replica system ready to run in AWS, however due to a misconfiguration it could not start automatically.</p>
</blockquote>
<blockquote>
<p>The misconfiguration has been fixed, and OnlineOrNot will run regular (monthly) drills to ensure the fallback system functions as expected.</p>
</blockquote>
<p>I was also more than an hour away from my laptop when my PagerDuty alert fired, so that'll also teach me to bring my laptop with me. That's something folks don't realize about starting a SaaS that other businesses rely on - you're <strong>always</strong> on-call (until you hire folks to help):</p>
<p><img src="/assets/onlineornot-diaries-24/pagerduty.png" alt="PagerDuty - OnlineOrNot"></p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 23]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-23</link>
            <guid>https://maxrozen.com/onlineornot-diaries-23</guid>
            <pubDate>Sat, 14 Dec 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[Working with big systems all day can slow you down.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-23">clicking here</a>.)</div><p>Developers will literally spend weeks designing and building a perfect system instead of just solving for the problem they have today.</p>
<p>Normally I would say: it is me, I am developers.</p>
<p>But then I wrote out the problem to a friend on Whatsapp and realized I was just procrastinating.</p>
<h2>I built for the problem I have today</h2>
<p>An extremely common pattern for me (that even after almost four years, is hard to recognize and stop): building overly complex systems as a means of procrastination.</p>
<p>For example:</p>
<p>I recently had a problem where I wanted to display the limits a user is subject to (for example, if they're on a free trial, don't give folks as many SMSes as a paid account, to avoid abuse).</p>
<p>I started planning to build a complicated entitlements system based on a key-value store, that I could update at any time, for any user. Why? Because I've worked on systems with tens of thousands of daily active users as part of my day job for years now, and that's the first thing that comes to mind.</p>
<p>That's wasn't the problem I had to solve <strong>today</strong> though.</p>
<p>I already had functioning means of limiting what types of users could do what. The problem I had to solve <strong>today</strong> was to display that limit to the user.</p>
<p>I was chatting through the complicated system I planned on building with a mate over WhatsApp when I realized I was just procrastinating. I didn't need a fancy entitlements system (yet).</p>
<p>A single variable in a shared package would do the job.</p>
<p>Will it scale? No.</p>
<p>Will updating it be a pain in the ass? You bet.</p>
<p>Is it <strong>enough</strong> for the problem I have right now? Yes.</p>
<p>So I'm pretty thankful I have folks to brain-dump this type of thing to - it's difficult to notice when you're making the problem harder than it needs to be (even when you only have <a href="/indiehacking-3-year-review#how-i-indiehack">two hours a day</a> to focus on the problem).</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 22]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-22</link>
            <guid>https://maxrozen.com/onlineornot-diaries-22</guid>
            <pubDate>Fri, 22 Nov 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[Feels like I've already said everything I had to say]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-22">clicking here</a>.)</div><p>Only three months since the last diary. Slightly better than last time.</p>
<p>Here's how running a SaaS has been going over the last three months.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#i-fixed-four-year-old-code">I fixed four-year-old code</a></li>
<li><a href="#i-realized-aws-lambda-is-too-expensive">I realized AWS Lambda is too expensive.</a></li>
</ul>
<h2>I fixed four-year-old code</h2>
<p>Some code issues only show up when you operate at scale (another reason to just  ship). In my case, OnlineOrNot's dashboard is full of <a href="https://stackoverflow.com/a/97253">N+1 queries</a> thanks to my liberal use of GraphQL without thinking about it too much.</p>
<p>There's enough data in the system now that inefficient queries and poor architecture decisions make for a worse user experience, so I've been taking the time to fix things.</p>
<p>For example: it got to a point where my own personal view of OnlineOrNot was taking 10 seconds to load 10 uptime checks (this screen):</p>
<p><img src="/assets/onlineornot-diaries-22/onlineornot-dashboard.png" alt="OnlineOrNot dashboard"></p>
<p>The fix was remarkably simple: instead of using sub-resolvers (and individual query per field with a squiggly bracket) for queries like:</p>
<pre><code class="language-graphql">checks {
  id
  name
  url
  data {
    timestamp
    result
    ...
  }
}
</code></pre>
<p>I started fetching <em>all</em> of the data for <code>checks</code> in the root resolver, and it now takes 300ms instead of 10+ seconds.</p>
<p>Makes me wish I just wrote the whole thing using REST APIs to begin with (likely something I'll migrate to over time).</p>
<h2>I realized AWS Lambda is too expensive.</h2>
<p>OnlineOrNot is in the enviable position of being default-alive - the minimum cost of operating the business if usage goes to zero is about $30/mo (a small but reliable Postgres DB running in AWS RDS).</p>
<p>As a result, I've never really worried about the business's profitability - as long as more money came in than I spent per month, it was fine. Then I started paying attention to folks on social media bragging that they were running their extremely large businesses on a $5/mo VPS (nevermind the cost of the DevOps person on 24/7 on-call in the background ensuring it never catches fire).</p>
<p>I realised I was paying way too much for serverless compute, because 99.99% of my AWS Lambda execution time was spent waiting for IO (loading websites to see if they're online, running SQL queries).</p>
<p>On Cloudflare Workers, <strong>that's free</strong>.</p>
<p>So long story short, I <a href="https://onlineornot.com/how-onlineornot-halved-aws-bill">rewrote OnlineOrNot to run on Cloudflare Workers</a>, and suddenly I have breathing room for a marketing budget, or even employing folks.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 21]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-21</link>
            <guid>https://maxrozen.com/onlineornot-diaries-21</guid>
            <pubDate>Fri, 30 Aug 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[I was young, and needed to ship...]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-21">clicking here</a>.)</div><p>I can't believe it's been more than four months since my last diary. For once, it wasn't because I fell down a rabbit hole of building a huge feature.</p>
<p>Truth is, I've been meaning to write sooner, and actually drafted several versions, but couldn't find the energy to hit "Publish". I had to prioritise myself and my family before OnlineOrNot for a bit, but I'm bouncing back.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#targeting-enterprise">Targeting Enterprise</a>
<ul>
<li><a href="#the-status-page-mvp-wasnt-viable">The Status Page MVP, wasn't viable</a></li>
<li><a href="#sometimes-you-just-need-to-ship">Sometimes you just need to ship</a></li>
</ul>
</li>
<li><a href="#at-the-same-time-you-need-to-market">At the same time, you <em>need</em> to market</a></li>
<li><a href="#solving-problems-is-seo">Solving problems <em>is</em> SEO</a></li>
</ul>
<h2>Targeting Enterprise</h2>
<p>Last time I wrote to you, I was working on enterprise deals for OnlineOrNot. They fell through.</p>
<p>I <em>did</em> learn things along the way, which was the main reason that I bothered in the first place.</p>
<h3>The Status Page MVP, wasn't viable</h3>
<p>It wasn't viable for OnlineOrNot's ideal customer profile.</p>
<p>They expected more features, that did more things. I wrote about this at the end of last year, so it wasn't a total surprise. I am currently building those features, so we'll get there.</p>
<h3>Sometimes you just need to ship</h3>
<p>I used to be a devoted follower of <a href="https://codingweekmarketingweek.com/">marketing week, coding week</a>. It's cool and all, but sometimes you just need to focus on shipping. It's easier to keep building a feature continuously than to take a break after each week for marketing.</p>
<p>Not in total isolation of course - as you release, you announce each feature to your mailing list/Discord and get feedback along the way.</p>
<h2>At the same time, you <em>need</em> to market</h2>
<p>Sometimes you learn the same lesson that you already wrote about, but in a different way. It feels strange to write the same things over and over, and I imagine it's not amazing to read them over and over either.</p>
<p>That being said, I'm beginning to realise that starting a business of any sort, is a search for marketing channels that work, first and foremost.</p>
<p>You might be in the business of "building good ships, at a loss if we must, but always good ships", but unless someone somehow finds out about your good ships, your business is dead.</p>
<p>The reason I can ignore marketing OnlineOrNot for a few months is because I didn't ignore it in the past, and that content still brings in users consistently.</p>
<h2>Solving problems <em>is</em> SEO</h2>
<p>So, <em>long ago</em>, before OnlineOrNot, I wrote a blog about React as a side project.</p>
<p>It kicked ass in SEO, because people were searching for how to solve their problems, and my articles would fix their problems. That's the gist of how to succeed at SEO.</p>
<p>Somewhere along the way, I convinced myself that if I went <em>really really wide</em> and blogged about barely irrelevant topics like <a href="https://onlineornot.com/ways-to-improve-page-speed">Ten Ways to Improve WordPress Page Speed</a>, I'd catch folks early in their business, and they'd consider using OnlineOrNot. After a few articles that failed to bring in consistent traffic, I gave up and told myself "content doesn't work".</p>
<p>It turns out content <em>does</em> work.</p>
<p>I've started investing in content that makes sense for OnlineOrNot (like <a href="https://onlineornot.com/how-check-website-online">how to check if a website is online</a>), and it's been leading to new customers. Not an "I can retire tomorrow" number of customers, but I get as many monthly trials (even during the summer holidays!) that I did during months where I would be actively trying to insert OnlineOrNot into as many online conversations as I could. I've since stopped doing that, and am focusing purely on building features, and we're still growing.</p>
<p>I still don't do nearly enough content marketing. It's bought me some time to build features, but I should be writing at least one article per week on top of features, so that the usefulness compounds.</p>
<p>When it works, like on <a href="https://maxrozen.com/">MaxRozen.com</a>, it really feels like you're building trust at scale.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 20]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-20</link>
            <guid>https://maxrozen.com/onlineornot-diaries-20</guid>
            <pubDate>Fri, 12 Apr 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[Dipping my toe in enterprise sales]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-20">clicking here</a>.)</div><p>It's finally sunny, and a Friday morning here in Toulouse, time to discuss what I've been doing over the last 4 weeks or so.</p>
<p>I started by taking more demo calls for OnlineOrNot's enterprise plan, reflected on why I didn't do that sooner, and ranted about optimizing for engagement.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#dipping-my-toe-into-enterprise-sales">Dipping my toe into enterprise sales</a></li>
<li><a href="#i-shouldve-done-that-years-ago">I should've done that years ago</a></li>
<li><a href="#how-giving-up-sugar-surge-marketing-is-going">How giving up "sugar-surge marketing" is going</a>
<ul>
<li><a href="#a-quick-rant-on-engagement-games">A quick rant on engagement games</a></li>
</ul>
</li>
</ul>
<h2>Dipping my toe into enterprise sales</h2>
<p>A few diaries ago, I updated my pricing page to have an enterprise plan. I had no idea what people would want, so the copywriting was pretty simple:</p>
<p>I added a card to my pricing page with the content: "Have needs that don't neatly fit into our plans? I can help.", with a call-to-action of "Let's talk".</p>
<p>A few people have reached out, I had a quick chat with some folks to learn what they were looking for in an enterprise version of OnlineOrNot (nothing salesy on my end - I just wanted to learn), and I got absolutely slapped down by their requirements. OnlineOrNot fulfilled maybe half of what was being asked.</p>
<p>One particularly helpful person (that I'm eternally grateful for) walked me through their procurement spreadsheet, as well as letting me know what similar solutions are charging, along with the "must-have", "should-have", "nice-to-have" status of each feature, so I now have more information about what customers expect from OnlineOrNot than ever before. I'm particularly excited for the roadmap.</p>
<h2>I should've done that years ago</h2>
<p>I left those quick demo meetings feeling amazing, like I knew exactly what customers expected, and I'd be able to build those features pretty quickly too. At the same time, I was wondering "why on earth didn't I do that sooner?!" - the answer is simple:</p>
<p>I wasn't ready.</p>
<p>For those of you that don't know me in real life, I'm a massive introvert. Unless we've already talked in real life, I'll probably have a notebook of things to talk about if we end up doing a phone call together.</p>
<p>These demo calls were nothing like that. I just showed up to the call, and asked "Hey, how can I make the next 20 minutes useful?", and chatted about what their business does, what kind of monitoring needs they have, and how they want to display their uptime to their customers.</p>
<p>Frankly, I'm surprised I pulled it off, but I guess when you think about uptime monitoring for at least two hours a day for three years like I have, it starts to become natural, and you don't need a notebook of questions to keep the conversation going.</p>
<h2>How giving up "sugar-surge marketing" is going</h2>
<p>As I wrote in the <a href="/onlineornot-diaries-19#sugar-surge-marketing">previous diary</a>, I'm no longer playing engagement games on Twitter, and writing articles solely so they rank on Hacker News.</p>
<p>It's not like I'm not good at it - I've written articles that reached #1 on Hacker News several times now. I'm not going to lie, it feels AMAZING when it happens. 40k+ people on your site, discussing your product, potentially sending business your way.</p>
<p>The trouble is, most of that traffic is irrelevant at best, distracting and a waste of time at worst. Hacker News are not your customers. Maybe 0.01% of them kinda-sorta fit your Ideal Customer Profile, but you now have to work out if a helpful-looking comment fits the profile of the type of customer you want to serve.</p>
<p>So instead, I've been writing helpful content for software teams. Traffic is down, way down. Sign-ups are surprisingly stable, and conversions from trial-to-paid are increasing.</p>
<p>Now, customer feedback comes directly to me via email, and I can immediately see in their email signature that they're title X at software company Y, and that the thing they're asking for will likely make OnlineOrNot better for folks like them.</p>
<h3>A quick rant on engagement games</h3>
<p>Somewhat related to sugar-surge marketing: asking extremely basic questions for the sake of generating engagement does not do anything for your business, unless you're in the business of selling "how to quit your job and make 500k!" courses for $500 a pop.</p>
<p>You might get some new followers out of it, but these are not the people that'll stay on your mailing list for years. Folks that stick around, get value from what you're building, and give you feedback as you build do not come from engagement games.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 19]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-19</link>
            <guid>https://maxrozen.com/onlineornot-diaries-19</guid>
            <pubDate>Fri, 08 Mar 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[Refactoring the business entity, thoughts on marketing and building]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-19">clicking here</a>.)</div><p>It's a rainy Friday morning here in Toulouse, and I had thoughts about the last few weeks.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#sugar-surge-marketing">Sugar-surge marketing</a></li>
<li><a href="#am-i-just-screwing-around">Am I just screwing around?</a></li>
<li><a href="#refactoring-the-business-entity">Refactoring the business entity</a></li>
</ul>
<h2>Sugar-surge marketing</h2>
<p>It's been a while since I wrote a diary, hopefully the article describing my <a href="/lessons-from-my-third-year-running-a-saas">third year of running OnlineOrNot</a> kept you entertained in the meanwhile.</p>
<p>I'm not going to lie, articles like that are a successful part of my marketing. Between reddit, Hacker News, and various newsletters that syndicated the content (much love!), tens of thousands of people have read that article, a few thousand searched OnlineOrNot, a few hundred tried it out, some have even become paying customers.</p>
<p>It's a complete sugar surge, in that the folks coming in aren't my ideal customer (members of software teams) so my conversion rates are less than half what they normally are, but it is nice to have folks go through the funnel at scale, as it lets me see what works on my landing pages, and what doesn't.</p>
<p>What I actually <em>should</em> be doing, and have started doing, is working out what "jobs" folks hire OnlineOrNot to do, and create consistent content (as in, one article per week) for <em>that</em>.</p>
<p>What's been stopping me until now is that getting tens of thousands of random people reading your content <em>feels good</em>. My more-relevant (for my target audience) content gets maybe a few hundred reads in comparison, but converts at a much higher rate.</p>
<p>Side note: founder psychology is fun!</p>
<h2>Am I just screwing around?</h2>
<p>Looking back on last year, I wasted a <em>shit ton</em> of time. I spent months on a monolith rewrite that didn't pan out, I spent weeks writing and testing ads for a product that didn't meet the bar for "viable" in "minimally viable product", and more.</p>
<p>Apart from promising to complete MVPs (and asking customers where the gaps are) before marketing them, my new tactic for this year is asking myself "am I just screwing around here?" when picking up a new task off my backlog.</p>
<p>Things that don't immediately fix a customer pain, help folks find OnlineOrNot, or explain a feature to users go to the bottom of the backlog now. It's been working out, customers have been noticing:</p>
<p><img src="/assets/onlineornot-diaries-19/insane-velocity.jpeg" alt="Insane velocity"></p>
<h2>Refactoring the business entity</h2>
<p>Since writing the third year of OnlineOrNot article, I've even had inbound interest from folks to learn more about our enterprise plan for the first time in a very long time. Filling out vendor risk assessment forms made me realize running OnlineOrNot as a micro-entrepreneur here in France wasn’t going to cut it in the long term (the equivalent of a <a href="https://www.gov.uk/set-up-sole-trader">sole trader</a> in UK/Australia).</p>
<p>The benefits of doing so are huge, and I'm kind of annoyed with myself for not doing it sooner. One of those <a href="/lessons-from-my-third-year-running-a-saas#i-stopped-being-afraid-of-success">afraid of success</a> things I guess.</p>
<p>How huge, you ask? I currently pay around 44% tax on <strong>revenue</strong> for OnlineOrNot. As a business, it goes down to 25% tax on <strong>profit</strong>. I don't plan on paying myself, and I intend to re-invest 100% of everything it makes back into the business, so it'll be a nice boost to the marketing budget.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Lessons from my third year running a SaaS]]></title>
            <link>https://maxrozen.com/lessons-from-my-third-year-running-a-saas</link>
            <guid>https://maxrozen.com/lessons-from-my-third-year-running-a-saas</guid>
            <pubDate>Fri, 23 Feb 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[I've run this business for three years now, and I'm only just now starting to *get it*.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/lessons-from-my-third-year-running-a-saas">clicking here</a>.)</div><p>It all started three years ago when I shipped a minimum viable product (MVP) in a week, at least, I thought it was minimally viable.</p>
<p><a href="https://onlineornot.com/">OnlineOrNot</a> started as a toy - I had just learned how to use Next.js, and was wondering if it would be possible to build a SaaS app into my marketing website (it was, and honestly the benefit wasn't that huge compared to having them separate).</p>
<p>It wasn't until a few dozen releases later that folks started paying me for the product. That gave me the motivation to keep shipping. Feature after feature (released after 2 hours of work and iterated upon afterwards), the product became minimally-viable for more and more people.</p>
<p>Fast forward to now, I have customers, and have been growing at a steady rate while building the features customers ask for, but the product is no-where near complete, but I'm getting there.</p>
<p><strong>Previous editions of this article:</strong></p>
<ul>
<li><a href="https://onlineornot.com/lessons-from-two-years-of-saas-operation">What I learned running a SaaS for a second year</a></li>
<li><a href="https://onlineornot.com/what-learned-running-saas-for-year">What I learned running a SaaS for a year</a></li>
</ul>
<p><strong>Table of contents:</strong></p>
<ul>
<li><a href="#i-stopped-being-afraid-of-success">I stopped being afraid of success.</a></li>
<li><a href="#i-ripped-out-my-product-analytics">I ripped out my product analytics</a>
<ul>
<li><a href="#how-it-works">How it works</a></li>
</ul>
</li>
<li><a href="#i-experimented-with-onboarding-and-it-massively-paid-off">I experimented with onboarding, and it massively paid off</a></li>
<li><a href="#i-stopped-trying-to-explain-everything-on-one-page">I stopped trying to explain everything on one page</a></li>
<li><a href="#i-came-crawling-back-to-aws">I came crawling back to AWS.</a></li>
<li><a href="#i-started-to-understand-my-product-philosophy">I started to understand my product philosophy</a></li>
<li><a href="#to-wrap-this-all-up">To wrap this all up...</a></li>
</ul>
<h2>I stopped being afraid of success.</h2>
<p>I originally doubted that being afraid of success is even a thing. I'd tell my self "of course I'm doing the best I can", while self-sabotaging my business in a dozen little ways that would compound into enormous effects.</p>
<p>Before this year, I would ignore and avoid:</p>
<ul>
<li>building tiny "table stakes" SaaS features (team management, on-call integrations, audit logs, etc)</li>
<li>writing landing pages to explain the product</li>
<li>making business decisions in general</li>
</ul>
<p>At some point this year I had a realization that I was burning a lot of first impressions on incomplete software. My article writing was attracting curious people to try out OnlineOrNot, but I was losing them on missing features (and in a segment as competitive as mine, folks aren't often patient for incomplete software).</p>
<p>So I've calmed down a bit on the content marketing and immediately releasing software too early. I still release fixes and product improvements in 2 hour increments before work each day, but for completely new things, I wait until they're "ready" before promising something and under-delivering. I read somewhere that this is how 37signals built <a href="https://www.hey.com/calendar/">HEY Calendar</a> - while they do 6-week cycles for improvements, for new products they run as many cycles as it takes.</p>
<h2>I ripped out my product analytics</h2>
<p>It turns out that as a small independent business I spend a good deal of my time actually talking to customers to figure out how it's going, learning what they expected when they signed up and what features are missing, rather than digging through analytics.</p>
<p>While I had analytics in the product, I wasn't using the data. So I ripped it out.</p>
<p>Me and other small business owners around me call it vibes-driven product management. It's not for everyone, but in my (obviously biased) opinion, I think I have good enough taste to pull this off.</p>
<p>That isn't to say I have <em>no data</em>. I still have anonymous cookie-less analytics of how many pageviews I get.</p>
<h3>How it works</h3>
<p>For the last three years, I've asked every single person that signed up to OnlineOrNot:</p>
<blockquote>
<p>What's the one thing you were hoping OnlineOrNot could do for you?</p>
</blockquote>
<p>Some folks respond, and those conversations have driven almost every feature and improvement that I've built since. While I have my own ideas and backlog, some of these conversations will make me realize I was thinking the wrong way about a feature, and that a drastically simpler implementation is possible.</p>
<h2>I experimented with onboarding, and it massively paid off</h2>
<p>OnlineOrNot's onboarding used to present users with a crucial question at the end:</p>
<blockquote>
<p>Do you want to start a free trial?</p>
</blockquote>
<p>Phrased like that it probably doesn't sound very crucial. What it really meant was:</p>
<blockquote>
<p>Do you want to play around with OnlineOrNot's best features for 14 days before deciding if it's worth your time, or spend months with a product that has the good stuff disabled?</p>
</blockquote>
<p>People <em>did</em> end up upgrading their subscription eventually from using the free tier exclusively, but it took significantly longer than folks that use the free trial version (it's a lot harder to see the value with half the features disabled).</p>
<p>So around May 2023 I decided to experiment with defaulting all users to a free trial first. This one experiment <strong>more than doubled</strong> OnlineOrNot's monthly growth rate.</p>
<p>It turns out starting the business relationship with "this is a paid service, you'll need to add payment details to continue getting the good stuff" helps the business significantly more than "this is a free service, if you use it a lot, you might have to pay for it".</p>
<h2>I stopped trying to explain everything on one page</h2>
<p>There's a saying that goes something like:</p>
<blockquote>
<p>if you try to be everything to everyone, you'll end up being nothing to no one</p>
</blockquote>
<p>I think it applies well to copywriting for landing pages.</p>
<p>As I built additional features into OnlineOrNot, I would try add additional sections to my main landing page, and it ended up an incoherent mess, diluting the overall message. I would have folks emailing me to ask if I supported sending alerts to Slack, when it was the second feature I built for OnlineOrNot.</p>
<p>Instead, by breaking up each feature into its own landing page (<a href="https://onlineornot.com/">main landing page</a>, <a href="https://onlineornot.com/website-monitoring">uptime monitoring</a>, <a href="https://onlineornot.com/api-monitoring">api monitoring</a>, <a href="https://onlineornot.com/status-pages">status pages</a>, <a href="https://onlineornot.com/cron-job-monitoring">cron job monitoring</a>), I can take the space to explain each feature, without diluting the message.</p>
<h2>I came crawling back to AWS.</h2>
<p><img src="/assets/onlineornot-diaries-6/fly-to-aws.png" alt="A graph showing a drop off in checks to fly.io and an increase in checks to AWS"></p>
<p>I started OnlineOrNot on AWS Lambda, because I was never sure I would ever attract a customer. So if it had zero usage, it would cost me almost nothing per month to run.</p>
<p>I began evaluating other solutions after a particularly viral article sent hundreds of users to OnlineOrNot. My user base spiked, and so my AWS bill also spiked. I started by looking at fly.io, and moved my workload over. I liked it so much I even <a href="https://onlineornot.com/on-moving-million-uptime-checks-onto-fly-io">wrote about it</a>.</p>
<p>Fly.io was an amazing user experience and a joy to use (especially the us-east region) and I still run a non-critical service there, but trying to run an uptime monitoring service on a platform that would regularly have network connection issues was a non-starter, and was beginning to hurt OnlineOrNot's reputation. It might make sense for running a web service, where a region's requests can failover to replicas, but it just didn't work for me.</p>
<p>I found myself needing to run each check several times in my VM, as well as backup checks on AWS AND Cloudflare to actually be sure my user's URL is actually down, rather than it being an issue with the VM I'm checking from. At that point I questioned why I even used fly.</p>
<p>I also tried running regular, boring VMs from Hetzner and DigitalOcean by refactoring my Lambda functions into a monolith (side note: vendor lock-in is a myth).</p>
<p>As a result of running OnlineOrNot in a single, ridiculously over-provisioned (yet significantly cheaper than AWS) server, uptime checks were randomly timing out, overall latency increased significantly, and made the overall system less reliable. Adding additional monitoring especially for VPSes would cost roughly how much I'd save by moving off AWS Lambda.</p>
<p>I spent a week tweaking code, increasing VPS size several times, trying different VPS providers, before eventually giving up, and deciding that paying a premium for AWS Lambda is worth it for the type of lifestyle business I want to run. Trade-offs and all that.</p>
<h2>I started to understand my product philosophy</h2>
<p>I read a quote this year from <a href="https://jmduke.com/">Justin Duke</a>'s <a href="https://twitter.com/jmduke/status/1687977979752636417?s=20">mood board for his software business</a> that resonated well with me and what I'm trying to do with OnlineOrNot:</p>
<blockquote>
<p>We shall build good ships here
at a profit if we can
at a loss if we must
but always good ships</p>
</blockquote>
<p>The business is bootstrapped and sustainable (it's default-alive), so I'm not too worried about profit. I'll still be here doing this in 10 years time. This year I realized there's nothing left to focus on except quality.</p>
<p>This year:</p>
<ul>
<li>I <a href="/onlineornot-diaries-10#optimizing-an-expensive-query">optimized my most frequently run SQL query</a>, going from running for 5000ms every 15 seconds to 500ms</li>
<li>I <a href="https://onlineornot.com/built-my-http-docs-from-scratch">handrolled my own API documentation</a> so that I could have full control of the product</li>
<li>I <a href="/2023-focus-single-product-pays-off#running-the-business-long-term-starts-paying-off">fixed bugs that took 15 months to surface</a> - a fun part of operating at scale is "one-in-a-million" bugs happen several times a day, though sometimes it takes a <em>bit</em> longer</li>
<li>I finished two separate MVPs (<a href="/onlineornot-diaries-17#cleaned-up-tech-debt-and-realized-i-have-a-long-way-to-go">status pages</a> and <a href="/onlineornot-diaries-15#on-actually-finishing-mvps">cron job monitoring</a>) that I started, so that I have a solid foundation to build a business on top of</li>
<li>I <a href="/onlineornot-diaries-16#i-started-sending-javascript-and-no-one-noticed">started shipping JavaScript, and no one noticed</a> - I started by not shipping JavaScript in the Status Page product as a mark of differentiation, but in the end it was just getting in the way of releasing features customers wanted</li>
</ul>
<h2>To wrap this all up...</h2>
<p>Thanks for reading this, it's time to get back to shipping.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 18]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-18</link>
            <guid>https://maxrozen.com/onlineornot-diaries-18</guid>
            <pubDate>Mon, 15 Jan 2024 07:10:00 GMT</pubDate>
            <description><![CDATA[Starting the year by cleaning up even more]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-18">clicking here</a>.)</div><p>It's a rainy Monday morning here in Toulouse, time for another diary.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#2023-what-a-year">2023, what a year</a></li>
<li><a href="#a-fresh-start-more-shop-cleaning">A fresh start, more shop cleaning</a></li>
<li><a href="#whats-next">What's next?</a></li>
</ul>
<h2>2023, what a year</h2>
<p>Over the Christmas/New Year break I wrote a year-in-review article for how <a href="/2023-focus-single-product-pays-off">all of my projects went in 2023</a>. The gist of it was: I focused on only OnlineOrNot for a whole year, and the business grew twice as fast as I expected, and the article seemed to resonate with a lot of people, getting about 20k unique views.</p>
<p>In that article I promised a proper year in review article for just OnlineOrNot on its birthday (February 25). I'm pretty thankful I've been writing these diaries on a regular basis since OnlineOrNot's last birthday, here's hoping it makes it easier to remember just what I did over a whole year.</p>
<h2>A fresh start, more shop cleaning</h2>
<p>As I mention in <a href="/onlineornot-diaries-17#cleaned-up-tech-debt-and-realized-i-have-a-long-way-to-go">my last diary</a>, I ended the year feeling inspired for 2024, ready to improve OnlineOrNot and the Status Pages feature to a point where bigger tech companies (I'm thinking a few hundred employees) feel more comfortable using it.</p>
<p>A big part of that is cleaning up the API and the tooling around it, and building new API endpoints. I originally shipped OnlineOrNot's API solely as a means of building a <a href="https://github.com/OnlineOrNot/onlineornot">CLI</a>. The docs were <em>hand-written</em>, and lovingly placed alongside my <a href="https://onlineornot.com/docs/welcome">other hand-written docs</a>. Side note: I'm starting to notice this is a recurring theme - I'll build something quickly, and have to come back to clean it up later. That's probably preferable to taking months to ship anything.</p>
<p>Unfortunately, this made updating the API a bit of a pain in the ass. Updating the API meant first writing code, and remembering to context-switch into "content-mode" and deciding where the new API endpoint docs should live, actually writing docs from scratch, and deploying the content.</p>
<p>To start the year, I've been cleaning up my mess, and automating this process away:</p>
<ul>
<li>I migrated the API to <a href="https://hono.dev/">HonoJS</a>, used <a href="https://hono.dev/guides/validation#with-zod">Zod</a> for API validation, and a <a href="https://hono.dev/snippets/zod-openapi">neat middleware</a> for converting that validation into an OpenAPI spec</li>
<li>I built a pipeline to convert that OpenAPI spec to markdown, and that markdown to HTML (effectively building my own OpenAPI to Docs generator from scratch), so that <a href="https://developers.onlineornot.com/">OnlineOrNot's API documentation</a> gets generated as I write my code</li>
<li>I <a href="https://onlineornot.com/built-my-http-docs-from-scratch">wrote about the process</a></li>
</ul>
<p>I could have used a third-party to handle generating my HTTP API docs, but one of my strong convictions is that docs are as much a part of your product as the rest of your UI, and I want to be able to easily respond to feedback and change anything as needed.</p>
<h2>What's next?</h2>
<p>In the coming weeks, I'll be:</p>
<ul>
<li>automatically publishing OnlineOrNot's OpenAPI schema to GitHub
<ul>
<li>so that it's easier to generate tooling off a specific version of the API without having to worry about it spontaneously updating</li>
</ul>
</li>
<li>building API endpoints for status pages
<ul>
<li>starting with status pages themselves, followed by creating incidents for status pages</li>
</ul>
</li>
</ul>
<p>In short, I'm trying to make OnlineOrNot fully compatible with being managed via an Infrastructure-as-Code (IaC) solution like <a href="https://www.terraform.io/">Terraform</a>, and I figure Status Pages are good place to start.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[2023: Focusing on a single product pays off]]></title>
            <link>https://maxrozen.com/2023-focus-single-product-pays-off</link>
            <guid>https://maxrozen.com/2023-focus-single-product-pays-off</guid>
            <pubDate>Sat, 23 Dec 2023 05:52:00 GMT</pubDate>
            <description><![CDATA[The first year where I managed to keep my focus entirely on a single project.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/2023-focus-single-product-pays-off">clicking here</a>.)</div><p>At the end of each year I summarize how the year went, reflecting on how I progressed towards my goal of being self-sufficient as a web developer, here are the previous editions of this article:</p>
<ul>
<li>(2022) <a href="/2022-just-keep-shipping">Just keep shipping</a></li>
<li>(2021) <a href="/2021-strangers-paid-my-macbook">Strangers from the internet paid for my MacBook Air</a></li>
<li>(2020) <a href="/indiehacking-3-year-review">Indiehacking: a review of my 3rd year</a></li>
<li>(2019) <a href="/2019/12/29/2019-further-reflections-trying-to-start-an-internet-business/">Further reflections on trying to start an internet business</a></li>
<li>(2018) <a href="/2018/12/31/2018-review-starting-an-internet-business/">Reflections on trying to start an internet business</a></li>
</ul>
<p><a href="https://rozenmd.com/">ROZENMD</a> (my business here in France that runs OnlineOrNot, and sells my products) continued to grow in 2023, approximately twice as fast as I expected at the start of the year.</p>
<p>I have two theories as to how this happened:</p>
<ol>
<li>On the <a href="https://onlineornot.com/">OnlineOrNot</a> side, what I learn about running the business compounds. Fixing something that wasn't working results in a slight increase in adoption, improving <em>that</em> fix increases adoption even more, and so on.</li>
<li>My book about useEffect seems to make even more money the longer it exists. This is likely partly <a href="https://en.wikipedia.org/wiki/Lindy_effect">lindy effect</a>, and partly from more and more folks entering web development and using React.</li>
</ol>
<p>As I approach the point where my business could feasibly pay my salary (still a few years away), I don't think I see myself quitting full-time work - I work better knowing I have two hours to ship something each morning.</p>
<p><strong>Table of Contents</strong></p>
<ul>
<li><a href="#one-big-bet">One big bet</a>
<ul>
<li><a href="#running-the-business-long-term-starts-paying-off">Running the business long-term starts paying off</a></li>
<li><a href="#dear-diary">Dear diary</a></li>
</ul>
</li>
<li><a href="#i-changed-teams-at-work">I changed teams at work</a></li>
<li><a href="#i-built-the-boring-enterprise-laptop-of-my-dreams">I built the boring enterprise laptop of my dreams</a></li>
</ul>
<h2>One big bet</h2>
<p>At the start of 2023, it was trendy to make many "small bets" when starting an internet business.</p>
<p>Popularized by folks like Pieter Levels and Jon Yongfook, and then turned into a paid product/community itself by others, the idea is to keep throwing products at the wall, until you see what sticks.</p>
<p>Folks will point to investment portfolio management theory, saying if 5 or so investments is enough diversification to smooth out risk/reward, surely the same idea follows for building products.</p>
<p>The trouble I find with that argument is the amount of effort to get from $0 -> $500 MRR is <em>significantly</em> greater than $500 -> $1000 and above. Having to repeat the most difficult part of starting a project, where you need to figure out the means of attracting the right type of users (and figure out who <em>they</em> are), and the features they need to solve a problem <em>several times</em> (and you're going for diversification, so the products can't be related) sounds a bit suboptimal to me, compared to doubling down on what works.</p>
<p>I also don't buy the idea that individual makers <strong>need</strong> diversification. Avoid platform risk (where you build on top of someone else's platform, like Twitter), don't work on things you're not personally interested in, and you're <em>well</em> on your way to reducing the greatest risks to your business running long term.</p>
<h3>Running the business long-term starts paying off</h3>
<p>This year I started having wins that were only possible from sticking around long enough:</p>
<ul>
<li>I had folks that rejected my pitch for OnlineOrNot pre-launch (back when OnlineOrNot was literally just a single input form that sent only email alerts) sign up and become customers of OnlineOrNot. Turns out they got fed up with their existing service, remembered my pitch, and checked out what I built since.</li>
<li>I also had some of the earliest free-tier users of OnlineOrNot subscribe and becoming paying customers.
<ul>
<li>This one is kind of my fault though, as early users of OnlineOrNot never got a free trial of the high-value features, it's a lot harder to win them over compared to folks that started on a free trial.</li>
</ul>
</li>
<li>I fixed bugs this year that took <em>15 months</em> to start impacting people</li>
</ul>
<p>A lot more happened this year, though I'm saving that for an article I'm publishing in February 2024, in celebration of OnlineOrNot's 3rd anniversary since launching publicly on Twitter. You can get the article directly in your email by <a href="#signup">subscribing below</a>, if you haven't already.</p>
<h3>Dear diary</h3>
<p>I've always had folks interested in how I build OnlineOrNot (particularly on the business/marketing side), though I felt that running a newsletter when you only make yearly updates didn't make a lot of sense.</p>
<p>So I started writing more frequently, and dedicated <a href="/articles?q=diaries">part of this blog</a> to diary articles. They don't get tons of views, but I write them primarily as a means of reflecting on what I said I'd do, what I did, and what I learned along the way.</p>
<h2>I changed teams at work</h2>
<p>It'd be weird (to me) to do a year-in-review post without discussing my full-time job at Cloudflare: without it, I would simply not have the ability to pursue a slow-yet-steady growth project that requires patience like OnlineOrNot.</p>
<p>At the start of the year, a partner team started integrating their early-alpha product into wrangler (the command-line tool for building <a href="https://workers.cloudflare.com/">Cloudflare Workers</a>), I helped out by fixing bugs and contributing ideas for a better user experience, and long story short, I ended up being one of the founding engineers of <a href="https://developers.cloudflare.com/d1/">D1</a>.</p>
<p>Considering my product and writing skills (see the last five years in review), I reckon it's a pretty good fit!</p>
<h2>I built the boring enterprise laptop of my dreams</h2>
<p>So I didn't <em>just</em> work on software this year, I also had a couple of cute hardware projects like this where I upgraded a laptop over a few hours, or put together an AirGradient sensor.</p>
<p>I'm actually writing this article from that same laptop - at the start of the year, I grew a bit fed-up with Apple, and wanted to build a cheap enterprise-looking laptop no one would want to steal when traveling (it's one asset tag sticker away from looking like my wife's old work laptop).</p>
<p>So I bought a cheap Lenovo T480 off a refurbisher, bought a bunch of parts to upgrade it, put it all together, and wrote about the project:</p>
<ul>
<li><a href="/replacing-my-macbook-m1-with-thinkpad-t480">On replacing my MacBook Air M1 with a Thinkpad T480</a></li>
<li><a href="/getting-your-own-good-enough-laptop-for-under-500">Getting your own good enough laptop for under $500</a></li>
</ul>
<p>The first article ended up getting translated and posted to <a href="https://www.golem.de/news/diy-mein-thinkpad-t480-ist-so-gut-wie-ein-macbook-air-m1-2308-176569.html">golem.de</a>, which was pretty cool, and I had a stray mention to running OnlineOrNot's dev server in the article, so that accidentally drew thousands of people to Google what OnlineOrNot is.</p>
<p>The first article also drew quite a bit of anger from random strangers online, and I found it pretty funny. As though I was telling people they had to go throw out their Apple laptops immediately because you can get a good-enough laptop for under $500.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Starting a Cloudflare Worker from scratch]]></title>
            <link>https://maxrozen.com/starting-a-cloudflare-worker-from-scratch</link>
            <guid>https://maxrozen.com/starting-a-cloudflare-worker-from-scratch</guid>
            <pubDate>Fri, 22 Dec 2023 05:52:00 GMT</pubDate>
            <description><![CDATA[We don't need no fancy tooling!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/starting-a-cloudflare-worker-from-scratch">clicking here</a>.)</div><p>While tooling for web development is getting more and more complex, with <a href="https://stackoverflow.com/a/76870223">CLIs for your CLIs</a> becoming a thing, sometimes it's worth taking a step back, and realizing that doing things from scratch isn't <em>so</em> hard.</p>
<p>In this article, we're going to create and deploy a Cloudflare Worker without a CLI.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#steps-to-hello-world">Steps to "Hello World"</a></li>
<li><a href="#summary">Summary</a></li>
</ul>
<h2>Steps to "Hello World"</h2>
<ol>
<li>Make a new directory, and cd into it</li>
</ol>
<pre><code class="language-bash">mkdir my-worker/
cd my-worker/
</code></pre>
<ol start="2">
<li>Initialize the npm project</li>
</ol>
<pre><code class="language-bash">npm init -y
</code></pre>
<ol start="3">
<li>Install some packages</li>
</ol>
<pre><code class="language-bash">npm install -D typescript wrangler @cloudflare/workers-types
</code></pre>
<ol start="4">
<li>Create a <code>tsconfig.json</code> file</li>
</ol>
<pre><code class="language-bash">touch tsconfig.json
</code></pre>
<p>After running the above command, paste the following into <code>tsconfig.json</code>:</p>
<pre><code class="language-json">//tsconfig.json
{
  "compilerOptions": {
    "noEmit": true,
    "module": "esnext",
    "target": "esnext",
    "lib": ["esnext"],
    "strict": true,
    "moduleResolution": "node",
    "types": ["@cloudflare/workers-types"]
  }
}
</code></pre>
<ol start="5">
<li>Make a <code>src/</code> directory, create an index.ts file inside</li>
</ol>
<pre><code class="language-bash">mkdir src/
touch src/index.ts
</code></pre>
<p>After running the above command, paste the following into <code>index.ts</code>:</p>
<pre><code class="language-ts">// index.ts
export default {
  fetch: () => {
    return new Response('Hello World!');
  },
};
</code></pre>
<ol start="6">
<li>Create a <code>wrangler.toml</code> file</li>
</ol>
<pre><code class="language-bash">touch wrangler.toml
</code></pre>
<p>After running the above command, paste the following into <code>wrangler.toml</code>:</p>
<pre><code class="language-toml"># wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2023-12-22"
</code></pre>
<ol start="7">
<li>Login to wrangler</li>
</ol>
<pre><code class="language-bash">npx wrangler login
</code></pre>
<ol start="8">
<li>Deploy your Worker</li>
</ol>
<pre><code class="language-bash">npx wrangler deploy
</code></pre>
<p>You can now visit the URL that wrangler outputs, and you should see "Hello World" in your browser.</p>
<ol start="9">
<li>(optionally) Install vitest, add a test script</li>
</ol>
<pre><code class="language-bash">npm install vitest -D
touch src/index.test.ts
</code></pre>
<p>After running the above commands, paste the following into <code>src/index.test.ts</code>:</p>
<pre><code class="language-ts">import { unstable_dev } from 'wrangler';
import type { UnstableDevWorker } from 'wrangler';
import { describe, expect, it, beforeAll, afterAll } from 'vitest';

describe('Worker', () => {
  let worker: UnstableDevWorker;

  beforeAll(async () => {
    worker = await unstable_dev('src/index.ts', {
      experimental: { disableExperimentalWarning: true },
    });
  });

  afterAll(async () => {
    await worker.stop();
  });

  it('should return Hello World', async () => {
    const resp = await worker.fetch();
    if (resp) {
      const text = await resp.text();
      expect(text).toMatchInlineSnapshot(`"Hello World!"`);
    }
  });
});
</code></pre>
<p>Finally, in <code>package.json</code> replace the <code>test</code> script with:</p>
<pre><code class="language-json">"test": "vitest"
</code></pre>
<h2>Summary</h2>
<p>There you have it, a Cloudflare Worker from scratch.</p>
<p>While it's not strictly <em>better</em> than just running</p>
<pre><code class="language-bash">npx wrangler@2 init my-worker2 -y
</code></pre>
<p>(which is the exact CLI command you'd run to get the project above)</p>
<p>At least this way you understand what's happening before reaching for tooling to automate it.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 17]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-17</link>
            <guid>https://maxrozen.com/onlineornot-diaries-17</guid>
            <pubDate>Sun, 17 Dec 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[On learning screencasting, cleaning up tech debt, and focus]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-17">clicking here</a>.)</div><p>It's a foggy Sunday morning here in Toulouse, time to close out the year.</p>
<p>In the last two weeks I:</p>
<ul>
<li><a href="#learned-screencasting">Learned screencasting</a></li>
<li><a href="#cleaned-up-tech-debt-and-realized-i-have-a-long-way-to-go">Cleaned up tech-debt and realized I have a long way to go</a></li>
<li><a href="#had-thoughts-on-focus">Had thoughts on focus</a></li>
</ul>
<h2>Learned screencasting</h2>
<p>As I mentioned in <a href="/onlineornot-diaries-16#started-a-youtube-channel">the last diary</a>, I started a <a href="https://www.youtube.com/@OnlineOrNot">YouTube channel</a> for OnlineOrNot, and I'm still learning. In fact, in the last two weeks I learned enough from the <a href="https://howtoegghead.com/instructor/">egghead instructor guide</a> that I felt the need to re-record some of the original videos I released.</p>
<p>To start off with, I was having issues with:</p>
<ul>
<li>un-normalized audio (one video would be significantly quieter than another)</li>
<li>weird output size (I think I was recording in 4k)</li>
<li>my pronunciation was off</li>
</ul>
<p>Though there are some meta-skills that only apply to YouTube that I refuse to adopt:</p>
<p><img src="/assets/onlineornot-diaries-17/on-youtube-thumbnails.png" alt="I refuse to do the whole &#x22;open mouth pointing at a thing&#x22; youtube thumbnail thing"></p>
<p>I'm treating YouTube like I treat other social media: it's just a place to distribute my content, not the source/single means of hearing from me.</p>
<p>This is why I try get folks to subscribe to my email newsletters instead.</p>
<h2>Cleaned up tech-debt and realized I have a long way to go</h2>
<p>I continued the theme of finishing my MVPs:</p>
<ul>
<li>Since Status Pages now use JavaScript, I was able to finally make the graphs interactive (you can mouse-over the data, and inspect each individual data point)</li>
<li>The emails that Status Page subscribers get now actually tell subscribers what went wrong (before it would just say there was a new incident), without having to visit the status page</li>
<li>I took a few shortcuts while building the UI for creating status pages, incidents/components/subscribers, and in certain cases it would take ages to load, so I cleaned up those shortcuts</li>
</ul>
<p>With Status Pages more-or-less at a point where I'm satisfied with what I've built, I took the time to research how folks buy status page software, and actually got really lucky in finding a public procurement report where a CTO outlined every decision they took to pick a vendor.</p>
<p>It made me realize OnlineOrNot's Status Pages have a really long way to go to support big software teams that already use other software as part of their ops stack.</p>
<p>The OnlineOrNot Status Pages MVP is in-fact, minimal. You can't customize the look-and-feel, there's no API for it, so no terraform support, and almost no integrations. Despite this, I somehow have paying customers that use <em>just</em> status pages.</p>
<p>So in short, I'm feeling inspired for 2024. OnlineOrNot's Status Pages will meet folks where they already are, and integrate with software that customers already use.</p>
<h2>Had thoughts on focus</h2>
<p>In <a href="/2022-just-keep-shipping#on-success">last year's review post</a>, I wrote about how OnlineOrNot's success comes from showing up, doing the work and releasing it. Particularly having a single-minded focus on OnlineOrNot, rather than a portfolio of several distracting small bets.</p>
<p>The neat "hack" keeping me focused on "just" OnlineOrNot is that it's now 3 separate products with a variety of interesting tech powering it, so I never get bored:</p>
<ul>
<li>All parts of the product involve a Next.js web app and a Node.js API server talking to a plain old Postgres DB</li>
<li>Uptime monitoring involves various microservices in a Highly Available configuration on AWS</li>
<li>Status Pages involves using the latest frontend tech with Remix</li>
<li>Heartbeat monitoring involves mixing microservices with my API</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 16]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-16</link>
            <guid>https://maxrozen.com/onlineornot-diaries-16</guid>
            <pubDate>Wed, 29 Nov 2023 17:10:00 GMT</pubDate>
            <description><![CDATA[On content marketing, starting a YouTube channel, and improving status pages]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-16">clicking here</a>.)</div><p>Going too long between these diaries is a sure sign I've fallen down a rabbit-hole (this is when a developer is so focused on solving a problem, they keep digging without thinking to look back and evaluate whether the problem is worth fixing in the first place).</p>
<p>So it's a Wednesday morning here in Toulouse, and it's the first time in two months where I'm not thinking "oh man, I just need to fix this one thing and it'll be good", time to go over what's new.</p>
<p>To be fair to myself, I did get a <strong>lot</strong> done:</p>
<ul>
<li><a href="#wrote-cron-jobs-the-guide">Wrote cron-jobs: the guide</a></li>
<li><a href="#started-a-youtube-channel">Started a YouTube channel</a></li>
<li><a href="#focused-on-improving-status-pages">Focused on improving Status Pages</a>
<ul>
<li><a href="#i-started-sending-javascript-and-no-one-noticed">I started sending JavaScript, and no one noticed</a></li>
<li><a href="#completely-on-cloudflare-workers">Completely on Cloudflare Workers</a></li>
<li><a href="#dedicated-staging-environment">Dedicated staging environment</a></li>
</ul>
</li>
</ul>
<h2>Wrote cron-jobs: the guide</h2>
<p>I wrote the last diary <em>totally sure</em> I would spend the following weeks writing article after article for OnlineOrNot's cron job monitoring feature. I wrote a couple of small articles and realized "this should really just be a single guide", and so I combined them into <a href="https://onlineornot.com/cron-jobs">Cron Jobs: the guide</a>, and built a visual editor for cron jobs (for example: <a href="https://onlineornot.com/cron-jobs/crontab/every-5-minutes">run cron every five minutes</a>).</p>
<p>This hasn't taken off like I imagined it would (content marketing in the devops space is ridiculously competitive), but if it helps folks that <em>do</em> find it, I'm happy.</p>
<h2>Started a YouTube channel</h2>
<p>When I first started OnlineOrNot, I made a couple of YouTube videos showing how to use it. The videos had a few hundred views and were popular with new customers, but got out of date relatively quickly as I continued to build OnlineOrNot over the years.</p>
<p>Now that the interface has more-or-less stabilized, I started making YouTube videos again, essentially turning OnlineOrNot's documentation into a YouTube channel: <a href="https://www.youtube.com/@OnlineOrNot">@OnlineOrNot</a>.</p>
<p>Some folks tend to prefer videos over written documentation, so my prediction is that this will help onboard folks (and act as a kind of "demo" for folks that are reluctant to meet with me to see what the product can do).</p>
<h2>Focused on improving Status Pages</h2>
<p>Speaking of OnlineOrNot's Cron Job Monitoring and my mission to <a href="/onlineornot-diaries-15#on-actually-finishing-mvps">actually finish my MVPs</a> from the last diary, I wasn't particularly proud of OnlineOrNot's Status Pages until recently.</p>
<p>I built the feature in a rush a year ago, and was <em>so sure</em> it would start bringing in additional customers for OnlineOrNot.</p>
<p>It kind of did, but folks tended to use features as things to check off in a checklist when evaluating OnlineOrNot (in other words "Oh, you don't have feature X? You must not be as good as <code>OTHER_PRODUCT</code> that <em>does</em> have feature X...").</p>
<p>This kind of demotivated me at the time, and made it difficult to actually invest time in improving the feature when what most folks cared about was:</p>
<ul>
<li>Can you monitor our service?</li>
<li>Can you stay online while the rest of the Internet is crumbling?</li>
<li>Will your business stick around for 5-10 years?</li>
</ul>
<p>In the last year I've focused on addressing these points, and the answers are: "yes", "yes, OnlineOrNot is highly available", and "I plan on doing this for at least the next 10 years, if not longer".</p>
<p>So in the last two months, I worked on improving Status Pages, and making it easier to add new features:</p>
<h3>I started sending JavaScript, and no one noticed</h3>
<p>Status Pages used to be small (around 13KB), but they weren't necessarily fast. It still took a few hundred milliseconds to query a database, and convert that result into HTML.</p>
<p>I managed this by completely disabling JavaScript in our React framework (Remix). While a fun thing to brag about, this actually slowed down my ability to release new features at a high quality. Charting libraries that support being server-side rendered are surprisingly rare (and buggy). Tooltips required a workaround that added hundreds of DOM nodes to the page, slowing down the app.</p>
<p>Recently I snapped, and started sending JavaScript to users. This means users download approximately 265KB when they first load an OnlineOrNot Status Page (a 20x increase). No customers have complained, instead, folks are happy about new features being shipped.</p>
<h3>Completely on Cloudflare Workers</h3>
<p>OnlineOrNot's Status Pages are built on <a href="https://pages.cloudflare.com/">Cloudflare Pages</a>, but the API was a highly available service running in several nodes across the USA.</p>
<p>I recently moved the API entirely into a Cloudflare Worker using a mix of <a href="https://developers.cloudflare.com/hyperdrive/">Hyperdrive</a>, and <a href="https://hono.dev/">Hono</a>.</p>
<p>This has let me enable caching, is a significantly better developer experience (I can build and deploy the API in 1.9 seconds now, instead of 5+ minutes), and lets me dogfood products from my day job.</p>
<h3>Dedicated staging environment</h3>
<p>Status Pages work by having a Cloudflare Worker intercept all requests coming in, and routing them (with a special header encoding which status page to load) to a web app. Testing changes end-to-end locally with this setup was extremely difficult, the best I could do was unit test my Worker, and a few integration tests for the web app loading status pages.</p>
<p>I managed to get around this recently by building a copy of OnlineOrNot Status Pages' production environment: I now have a separate dedicated domain which has a Worker routing to a Cloudflare Pages app, and that talks to its own API.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 15]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-15</link>
            <guid>https://maxrozen.com/onlineornot-diaries-15</guid>
            <pubDate>Mon, 11 Sep 2023 17:10:00 GMT</pubDate>
            <description><![CDATA[Heading back into the arena. We are so back.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-15">clicking here</a>.)</div><p>Long time no diary! Between late July to early September, I took the longest break from regularly working on OnlineOrNot since starting the project in February 2021.</p>
<p>In that time I visited:</p>
<ul>
<li>Paris</li>
<li>Florence</li>
<li>Rome</li>
<li>Biarritz</li>
<li>San Sebastian</li>
<li>Toulon</li>
</ul>
<p>I came back pretty eager to get back to regularly shipping features and marketing projects for OnlineOrNot, so here we are.</p>
<p>While I was away I read Yvon Chouinard's <a href="https://www.amazon.com/Let-People-Surfing-Education-Businessman/dp/0143037838">Let My People Go Surfing</a>, and it reminded me of why I started building OnlineOrNot in the first place: to build higher quality software than the established products.</p>
<p>In short, with OnlineOrNot, I want to build high-quality software. At a loss if I must (I have a full-time job after all), but always high-quality software.</p>
<h2>On de-emphasising the free-tier</h2>
<p>OnlineOrNot has a <a href="https://onlineornot.com/docs/free-tier">free tier</a>. It's for personal, non-commercial projects (like monitoring your personal blog), but I've always wondered if that drove the wrong type of folks (as in, not product teams building software) into trying OnlineOrNot.</p>
<p>So while I was away, I ran an (ultimately flawed) experiment to remove all mentions of OnlineOrNot's free tier from the marketing website (without actually removing it from the app), to see what would happen. My expectation was that sign-ups would take a small hit, while increasing the overall paid conversion rate.</p>
<p>I'm not going to sugar-coat it, sign-ups halved.</p>
<p>It being the month of August during my experiment (Europe/North America on holidays), me being away (spending zero time on marketing) likely influenced sign-ups more than removing the free tier.</p>
<p>I still need to wait a couple of weeks to see the impact on overall paid conversion rate, so I'll likely try running the experiment again once I've settled into a regular coding week/marketing week schedule again.</p>
<h2>On actually "finishing" MVPs</h2>
<p>I've mentioned it a few times in previous diaries: OnlineOrNot is built in two-hour increments, before I start work every morning. To avoid never releasing, my release cycle is quite different to regular software companies - instead of a "big bang" release once a feature is ready, or an agile/sprint fortnightly release, I try to release every single morning.</p>
<p>This works pretty well in practice for marketing, fixing bugs, and smaller features, but for large features (like introducing <a href="https://onlineornot.com/heartbeat-monitoring">heartbeat monitoring</a>), I found myself releasing far too early, before the feature was ready for customers: a bit too "minimal", and not enough "viable" product.</p>
<p>The fix is simple of course: the <a href="https://onlineornot.com/docs/early-access-program">Early Access Program</a>. My mistake was promoting the heartbeats feature from Early Access to public beta too quickly, and ending up with customers using low quality software.</p>
<p>As a result, I've spent my first week back rapidly iterating on Heartbeat Monitoring to make sure it's actually viable as a product. I've made the following changes:</p>
<ul>
<li>I've fixed a few bugs preventing heartbeat checks from alerting properly</li>
<li>I've integrated heartbeat checks into status pages, so customers can track their cron jobs on a status page</li>
<li>The heartbeat check graph now correctly shows incidents, not just successful runs</li>
<li>I've moved heartbeat checks into their own infrastructure
<ul>
<li>I didn't know if anyone would use it, so I built it inside OnlineOrNot's existing uptime check system. While convenient, it made the code far more complicated than necessary (split across several microservices).</li>
<li>With this change, the heartbeat check system can be monitored and deployed independently, it can fit inside a single serverless function AND it can accurately monitor OnlineOrNot's uptime check system</li>
</ul>
</li>
</ul>
<p>Next up, I'll be adding email, SMS, webhook, and on-call integrations to heartbeats, so that they match OnlineOrNot's regular uptime check alerts.</p>
<h2>Back into content marketing</h2>
<p>Even though I know in the short-term that content marketing doesn't really work for attracting customers, I like writing too much to give it up. For coming up with content ideas, I tend to watch forums (like <a href="https://news.ycombinator.com/">hacker news</a>) to see what folks are complaining about, and write articles that fix their issues.</p>
<p>So now that I'm back, I'll be writing more of my classic "fixing a problem"-style articles.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 14]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-14</link>
            <guid>https://maxrozen.com/onlineornot-diaries-14</guid>
            <pubDate>Fri, 28 Jul 2023 17:10:00 GMT</pubDate>
            <description><![CDATA[Back to regular programming.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-14">clicking here</a>.)</div><p>I'm writing this from a train between Florence and Rome, in Italy - I had a bit of time to kill, and wanted to reflect on the last two weeks.</p>
<p>In case you missed it, <a href="/onlineornot-diaries-13">last diary</a> I wrote about how I wasted a fortnight yet again trying to use a distributed monolith to monitor my users websites instead of serverless. It turned out ensuring that one customer's checks don't influence another's is a difficult problem to solve, and I'd rather pay the ~100% premium to not need to worry about that on AWS Lambda.</p>
<h2>Features people actually want</h2>
<p>So I started this fortnight eager to ship things my customers actually asked for. Someone on twitter suggested adding on-call integrations to OnlineOrNot. I had been meaning to do so since March 2021, a whole 26 days after I released the first public version of OnlineOrNot, and have had several requests to add on-call integrations (albeit as "nice to haves" than "blockers").</p>
<p>The reason I never got around to it is really dumb: the docs for the incumbents were god awful, and I couldn't bring myself to figure out the mapping between "services", "teams", and OnlineOrNot's checks.</p>
<p>It took seeing new players in the on-call market (shout-out to <a href="https://spike.sh/">spike.sh</a>) with really clear docs before I realized it wasn't actually that difficult - I could start with a webhook URL to POST messages from OnlineOrNot to the on-call service.</p>
<p>So over two mornings I integrated four on-call services into OnlineOrNot: <a href="https://onlineornot.com/docs/pagerduty-integration">PagerDuty</a>, <a href="https://onlineornot.com/docs/opsgenie-integration">Opsgenie</a>, <a href="https://onlineornot.com/docs/grafana-oncall-integration">Grafana OnCall</a>, and <a href="https://onlineornot.com/docs/spike-integration">Spike</a> (and wrote docs on my side for how to configure each integration).</p>
<h2>Landing pages, again</h2>
<p>I added even more information to my main landing page this week.</p>
<p>We'll see if this is a good idea, but for the longest time my main selling point was "modern uptime monitoring" (what does that even mean?!) - I eventually changed that to something people actually care about (ease of use), and gave my conversion rate a noticeable boost. Now that I have on-call integrations, I realized the main selling point is alerts, and the business processes that go into action when an alert is fired.</p>
<p>So now <a href="https://onlineornot.com/">OnlineOrNot's landing page</a> follows a sort of chronology by business processes: alerting your team -> status pages for keeping customers updated</p>
<p>What feels extremely natural to follow those features is a sort of incident management system, but I'm drawing the line at monitoring and status pages - it already feels like a sufficiently large feature set to maintain and iterate on as a solo-founder.</p>
<h2>Giving away free tools</h2>
<p>This week I also challenged myself to build another free tool in under 60 minutes.</p>
<p>I originally launched OnlineOrNot as a public webpage that could check your webpage from around the world: you enter a URL, I run the check, and tell you what OnlineOrNot saw in us-east-1, us-west-1, eu-central-1, ap-southeast-2, and ap-northeast-1.</p>
<p>Having built <a href="https://onlineornot.com/do-i-need-a-cdn">Do I need a CDN?</a> a few weeks back, and having a decent number of folks signed up for OnlineOrNot off that page, I wanted to bring the original tool back.</p>
<p>I present you: <a href="https://onlineornot.com/website-down-checker">Website Down Checker</a>.</p>
<p>The name is terrible, but it's visually a lot more appealing than the original OnlineOrNot tool!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 13]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-13</link>
            <guid>https://maxrozen.com/onlineornot-diaries-13</guid>
            <pubDate>Fri, 14 Jul 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[I tried building a monolith.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-13">clicking here</a>.)</div><p>It's Bastille Day here in France, and before I break out the BBQ and beers (I'm still Aussie, after all), I wanted to reflect on the last two weeks of OnlineOrNot.</p>
<p>If I summarized it in one word it'd be: distracting.</p>
<p>You see, this week was a marketing week, and I had 11 ideas for awesome things to do to keep bringing folks in, following up on the success <a href="https://onlineornot.com/do-i-need-a-cdn">"Do I need a CDN?"</a> had. Instead, I got baited by the prospect of cheaper compute and being able to build features like ping monitoring, so instead of marketing, I spent the week building OnlineOrNot into a monolith app and using <a href="https://github.com/mrsked/mrsk/">MRSK</a> to deploy to my servers.</p>
<p>I succeeded, by the way, at building the monolith app: it's actually pretty neat to be able to run OnlineOrNot end to end on a single machine (if you remember from a few months ago, OnlineOrNot is <a href="/onlineornot-diaries-10#refactoring-my-aws-architecture">a pipeline of AWS Lambda functions stuck together with SQS and SNS</a>). It works great for monitoring a single user's checks, but once you load up 1600 users checks, cracks start to appear.</p>
<p>As a result of running OnlineOrNot in a monolith, uptime checks were randomly timing out, overall latency increased significantly, and made the overall system less reliable. Adding additional monitoring especially for VPSes would cost roughly how much I'd save by moving off AWS Lambda. I spent a week tweaking code, increasing VPS size several times, trying different VPS providers, before eventually giving up, and deciding the AWS Lambda premium is worth paying for the type of business I want to run.</p>
<p>Thankfully, OnlineOrNot runs really well on AWS Lambda, so it's not all doom and gloom.</p>
<hr>
<p>On a more positive note, this week Abhi wrote in to ask: How do I come up with ideas for what to build?</p>
<p>I started OnlineOrNot as a pretty basic project to meet my needs, all it could do when I launched it was visit a webpage over HTTP, and send an email if OnlineOrNot couldn't reach your page.</p>
<p>My secret for coming up with new ideas to build into OnlineOrNot comes from asking my customers as they sign up: "What's the one thing you were hoping OnlineOrNot could do for you?"</p>
<p>From those basic beginnings, folks have emailed in to ask OnlineOrNot to support their use case, and if it matches my vision for what I want OnlineOrNot to be, I typically build it.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 12]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-12</link>
            <guid>https://maxrozen.com/onlineornot-diaries-12</guid>
            <pubDate>Fri, 30 Jun 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[On failing customers, failing to ship a new feature, learning sales, and giving up on ads.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-12">clicking here</a>.)</div><p>Another Friday morning here in Toulouse, let's go into how OnlineOrNot went this month (?!?).</p>
<p>I totally dropped the ball there, sorry friends. Trying to get back to a fortnightly cadence.</p>
<p>In this issue of OnlineOrNot Diaries, I talk about <a href="#on-shipping">what I shipped</a>, failing customers during an <a href="#aws-outage">AWS outage</a>, <a href="#trying-to-ship-a-new-type-of-check-and-failing">trying to ship a new type of uptime check and failing</a>, learning more about the world of <a href="#leaning-into-direct-sales">direct sales</a>, and <a href="#giving-up-on-ads">giving up on Google ads</a>.</p>
<h2>On Shipping</h2>
<p>I started June by continuing my "build basic SaaS features people ask for all the time" trend by making it possible to add multiple Slack integrations to OnlineOrNot, and making it possible to pick which Slack channels uptime checks and heartbeat checks send messages to.</p>
<p>I also ran into an embarrassing bug (to me, my users didn't seem to care) that has been hassling my paid users for the past 15 months: if you upgraded to a paid account while on a trial, my trial emails would keep sending, pestering you to upgrade!</p>
<p>I also realised I had been writing these diaries since February without writing a single product update to my users, so I'm doing that monthly now. While it feels good to ship all the time, shipping the <em>right thing</em> matters.</p>
<h2>The AWS Outage</h2>
<p>OnlineOrNot went down with the AWS us-east-1 outage on June 13, so I <a href="https://onlineornot.com/our-lessons-from-the-latest-us-east-1-outage">wrote about it, and how I'm going to stop it from happening again</a>. I have no idea why, but nearly twelve thousand people read that article. Google decided it was news, and shared it on the Google Discover tab.</p>
<p>The gist of it is, I thought I was operating entirely in a single region (us-east-2), I wasn't, and I let my customers down. In immediate aftermath I moved the component that failed into us-east-2, and added a feature to make it possible for me to failover OnlineOrNot between regions, so the entire OnlineOrNot stack can jump between us-east-1 and us-east-2 without customers being impacted.</p>
<p>This doesn't feel like it's enough though, so I'm building a second OnlineOrNot stack that runs entirely on VMs (think DigitalOcean, or Hetzner).</p>
<h2>Trying to ship a new type of check, and failing</h2>
<p>For a very long time now, customers have been asking to be able to ping their servers. Not every server exposes HTTP, so regular uptime checks weren't a good fit for them. While heartbeat checks make it better, it still requires logging into the server, and setting up a regular task to ping OnlineOrNot.</p>
<p>So I started by building the UI and backend for adding ping monitoring into OnlineOrNot, tested it in production, and realized AWS Lambda doesn't actually support running the ping command, on the operating system level. Even if you used Docker, it doesn't work. It's explicitly turned off by AWS, actually.</p>
<p>Lesson learned: build a dumb proof of concept showing off the raw feature (in this case, running a ping check on AWS Lambda) before you go off and build the frontend and backend to make the whole thing work.</p>
<p>All is not lost, however. Once I operate OnlineOrNot on a second stack with constantly running VMs, I'll be able to offer ping checks.</p>
<h2>Leaning into Direct Sales</h2>
<p>I recently read about how Nathan Barry leaned on direct sales to bootstrap ConvertKit in <a href="https://nathanbarry.com/sales/">this article</a>, and it made me realise that "direct sales" is effectively just customer research with a few extra steps.</p>
<p>I already do a bit of customer research (I ask everyone that signs up to chat about what they were hoping OnlineOrNot could do), so I realised calling people to discuss uptime monitoring is a good next step for better customer research data.</p>
<p>So I build a page to let folks <a href="https://onlineornot.com/schedule-a-demo">schedule a demo</a> with me. In two and a half years I've never actually demoed OnlineOrNot to a person live, so I'm a bit worried I'll screw up somehow. Though for the most part, I'm more interested in learning about my customer's businesses and how OnlineOrNot can help them, so I don't think I'll be doing much talking in these calls.</p>
<h2>Giving up on ads</h2>
<p>Yesterday, I read a comment on a forum arguing that you don't actually need a CDN, because some internet providers are faster than others (or something dumb like that). Rather than argue, I decided to build a free tool to show them why CDNs are useful. I called it <a href="https://onlineornot.com/do-i-need-a-cdn">"Do I need a CDN?"</a>, and in 24 hours folks have checked 1500 websites, and it has driven 10 new sign-ups to OnlineOrNot in that time.</p>
<p>For how little effort I put into this free tool, it has driven significantly more traffic per dollar spent than my ongoing ad campaign. So I'm deciding to give up on Google Ads, and lean more into building free tools like this (and maybe sponsor things my customers use, instead).</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 11]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-11</link>
            <guid>https://maxrozen.com/onlineornot-diaries-11</guid>
            <pubDate>Sat, 27 May 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[On building table-stakes features, and breaking through a plateau.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-11">clicking here</a>.)</div><p>Another Saturday morning here in Toulouse, let's go into how OnlineOrNot went this fortnight.</p>
<p>I'll be honest, a decent part of the last two weeks looked like this for me:</p>
<p><img src="/assets/onlineornot-diaries-11/may-break.png" alt="La côte basque"></p>
<p>Despite my time in the sun, quite a bit happened over the past two weeks, without needing to write too much code, or actively market the product.</p>
<ul>
<li>My <a href="#my-default-to-trial-experiment">default-to-trial period experiment</a> continued running</li>
<li>I listened to some customer feedback and <a href="#on-building-table-stakes-features">built table-stakes features</a></li>
</ul>
<h2>My default to trial experiment</h2>
<p>In case you missed it, in the <a href="/onlineornot-diaries-10#experimenting-with-my-onboarding">last diary</a> I mentioned I started an experiment in my onboarding flow, where I would require every new user to try the best of OnlineOrNot before letting them use the free tier.</p>
<p>I'm not going to bury the lede here, it's going <strong>very well</strong>:</p>
<p><img src="/assets/onlineornot-diaries-11/may-metrics.png" alt="OnlineOrNot&#x27;s May metrics"></p>
<p>The above is a graph of OnlineOrNot's MRR. I don't like to talk much about MRR as it's a lagging metric - things I did weeks ago affect it today. Though, without increasing the number of people creating OnlineOrNot accounts, just about every metric in the business shot up this month: average revenue per user (ARPU), customer lifetime value (CLTV), cash flow, etc.</p>
<p>One month of course does not "break a plateau", but things are looking a lot better than this time last month. I also replicated the experiment on an app I built in a weekend last year, and went further by removing the number of screens in that app's onboarding flow. The app's MRR is up 48.4% this month.</p>
<p>It turns out starting the business relationship with "this is a paid service, you'll need to add payment details to continue getting the good stuff" helps you get paid!</p>
<h2>On building table-stakes features</h2>
<p>There are two types of features I build for OnlineOrNot:</p>
<ul>
<li>Features that help customers monitor their application and manage their incidents</li>
<li>SaaS features that everyone expects a SaaS to have</li>
</ul>
<p>The secret is, as long as you build features that your customers actually want and give them value, you can get extremely far without building SaaS features like handling teams/invites/deleting accounts/etc.</p>
<p>For example, I released the ability to invite teams on March 19, 2021 - it wasn't until this week, May 25 2023, that I made it possible to remove people from your team. This whole time, folks have been emailing me whenever they needed to remove someone from their team. No one has ever told me "no, I won't pay for this, I can't remove people from my team". You get to spend that time building features that bring in customers instead.</p>
<p>Although this week someone asked for the ability to remove folks from their team, I decided it was time to just build the damn feature.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 10]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-10</link>
            <guid>https://maxrozen.com/onlineornot-diaries-10</guid>
            <pubDate>Sat, 13 May 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[On re-rearchitecting.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-10">clicking here</a>.)</div><p>Another Saturday morning here in Toulouse, let's go into how OnlineOrNot went this fortnight.</p>
<p>In the last two weeks I:</p>
<ul>
<li><a href="#optimizing-my-queries">optimized a query</a> that OnlineOrNot runs every 15 seconds, to run 90% faster</li>
<li>decided to release a landing page for heartbeat checks <a href="#my-incomplete-landing-page">before it was ready</a></li>
<li>started an <a href="#experimenting-with-my-onboarding">experiment with my onboarding</a> flow</li>
<li><a href="#refactoring-my-aws-architecture">refactored my AWS architecture</a> for an 87.5% cost saving</li>
</ul>
<h2>Optimizing an expensive query</h2>
<p>For OnlineOrNot I've opted to use RDS for simplicity, but as part of that simplicity, I don't get fancy lists of my most run queries with how long they take to run (a popular Heroku postgres feature) - unless you know something I don't? Let me know!</p>
<p>As a result, after two years of development, there are still unoptimized queries like this in my code:</p>
<pre><code class="language-js">// this isn't my actual code, but is a rough approximation of what it does
const checks = await sql`SELECT * FROM uptime_checks WHERE ready_to_run = TRUE`
// queue up the checks to run, then mark them as currently_checking
await Promise.all(checks.map((check)=>{
    await sql`UPDATE uptime_checks SET currently_checking = TRUE WHERE id = ${check.id};`
}));
</code></pre>
<p>There's nothing fundamentally <em>wrong</em> with this approach, but each time you send a query to the database, there's a bit of overhead. Running over thousands of records in my case, this added up to spending 5 seconds updating records every 15 seconds.</p>
<p>The fix was to only send a single query to update the database:</p>
<pre><code class="language-js">// this isn't my actual code, but is a rough approximation of what it does
const checks = await sql`SELECT * FROM uptime_checks WHERE ready_to_run = TRUE`;
// queue up the checks to run, then mark them as currently_checking
await sql`UPDATE uptime_checks AS t SET "val"=v."val","msg"=v."msg" 
            FROM (VALUES${checks
              .map((check) => `(${check.id}, TRUE)`)
              .join(',')})
            AS v("id","currently_checking") 
            WHERE v.id = t.id;`;
</code></pre>
<p>As a result, we now only spend 500ms updating the database, instead of 5000ms.</p>
<h2>My incomplete landing page</h2>
<p>I'm still working on heartbeat monitoring, and making it ready for general consumption. The thing is, starting marketing only when the feature is complete would be a mistake (this is what <a href="https://codingweekmarketingweek.com/">Coding Week, Marketing Week</a> solves).</p>
<p>So as part of my marketing week this fortnight, I shipped an <a href="https://onlineornot.com/heartbeat-monitoring">incomplete landing page</a> with the intention of iterating on it, as I add features:</p>
<p><img src="/assets/onlineornot-diaries-10/heartbeats-landing-page.png" alt="Heartbeat monitoring landing page"></p>
<h2>Experimenting with my onboarding</h2>
<p>As I wrote <a href="/onlineornot-diaries-7">about a month ago</a>, I started running ads for OnlineOrNot. It's still unclear if it's paying off, so I'm keeping the ads running for now.</p>
<p>It was hard to figure out if ads were working, because folks were signing up, and immediately using the free tier, instead of starting a free trial. People <em>do</em> end up upgrading their subscription on the free tier, but it takes significantly longer than folks that use the paid version (it's a lot harder to see the value with half the features disabled).</p>
<p>So for the month of May, I've decided to see what happens if every user starts with a free trial. They'll be able to downgrade to the free tier for personal projects at any time, but the idea is to let them play with the fully-featured version of OnlineOrNot first, so they know what they're missing out on.</p>
<p>I'll let you know how this went in future <a href="/articles?q=diaries">diaries</a>.</p>
<h2>Refactoring my AWS architecture</h2>
<p>As I (also) wrote <a href="/onlineornot-diaries-6#and-back-to-aws">just over a month ago</a>, I keep two versions of OnlineOrNot running for redundancy: one on fly.io, and another on AWS. Due to network reliability issues with fly.io, I moved the majority of the workload back onto AWS.</p>
<p>However, I never took the time to refactor how my AWS app was setup, so it more-or-less looked the same for 18 months or so:</p>
<p><img src="/assets/onlineornot-diaries-10/before.png" alt="Architecture before"></p>
<p>In short, a <code>scheduler</code> Lambda function runs every 15 seconds to find uptime checks it needs to queue up, sends the data over SQS to a set of <code>resultfwdr</code> Lambda functions which batch requests to the uptime checkers around the world. Only <code>resultfwdr</code> knows how to talk to my database, so I don't need to maintain thousands of connections to my database.</p>
<p>It's probably not obvious what's wrong with this architecture from the diagram, but the <code>resultfwdr</code> Lambdas have to start hundreds of uptime checker Lambdas simultaneously, and wait for them to finish before writing results to the database. Since this isn't Cloudflare Workers, I pay for the time AWS Lambda does nothing while waiting for the results to come back.</p>
<p>The main goal of this re-architecture was to minimize database load, as well as unnecessary AWS costs. So I decided to flip the architecture:</p>
<p><img src="/assets/onlineornot-diaries-10/after.png" alt="Architecture after"></p>
<p>With this new architecture, there's no more waiting around for Workers to finish (apart from inside the uptime checks themselves), it's 87.5% cheaper to run the <code>resultfwdr</code> Lambda, and thanks to SNS, the Workers run their checks within milliseconds of being queued.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 9]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-9</link>
            <guid>https://maxrozen.com/onlineornot-diaries-9</guid>
            <pubDate>Sat, 29 Apr 2023 07:10:00 GMT</pubDate>
            <description><![CDATA[On cleaning up the shop.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-9">clicking here</a>.)</div><p>Another Saturday morning here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>It was a coding week (that makes it three in a row, for those counting at home), and I took the time to clean-up a ton of tech debt. This article gets a little bit technical.</p>
<p>For context, I launched OnlineOrNot approximately two years and two months ago, and I never really treated the code like it might stick around for a while. A lot of my git commit messages are just me swearing. These are things you can get away with when you know you'll be the only person reading your code.</p>
<p>I built OnlineOrNot as a "mono-repo", but the folder structure was pretty chaotic, and if I wanted to use packages, I had to publish them to npm. I had the main Next.js app in one folder, a folder for my terraform configuration, and every AWS Lambda function in another:</p>
<pre><code>infrastructure/
lambdas/
|   |-- browser-checker/
|   |-- checker-scheduler/
|   |-- cron-tasks/
|   |-- uptime-checker/
|   |-- uptime-checker-runner/
|   |-- weekly-emails/
|   |-- ...more lambdas here
web/
</code></pre>
<p>For a long time that worked well. There were a couple of utility functions I copy pasted between lambda functions, and I had comments to remind me to update the code in multiple places (obviously not a great practice when you have a team), and never had any issues.</p>
<h2>Adding fly.io complicated things</h2>
<p>At some point last year, I duplicated OnlineOrNot's stack and made it run on fly.io. The resulting folder structure looked like this:</p>
<pre><code>infrastructure/
fly/
|   |-- checker-scheduler/
|   |-- uptime-checker-runner/
|   |-- ...more apps here
lambdas/
|   |-- browser-checker/
|   |-- checker-scheduler/
|   |-- cron-tasks/
|   |-- uptime-checker/
|   |-- uptime-checker-runner/
|   |-- weekly-emails/
|   |-- ...more lambdas here
web/
</code></pre>
<p>The trouble with this is, a good 95% of the code between the fly.io and AWS versions of OnlineOrNot were the same, adding a significant amount of code duplication. I figured it wasn't such a big deal at the time, since I'd just delete the AWS code one day, but then I realized OnlineOrNot could survive a hosting provider outage by keeping both stacks online.</p>
<p>So the duplicated code had to stay, until I had time to clean it up properly.</p>
<h2>Moving to a "real" mono-repo</h2>
<p>Since OnlineOrNot is mainly written in Node.js, I actually had a relatively easy way to split up code into packages that can be shared between apps: <a href="https://docs.npmjs.com/cli/v7/using-npm/workspaces/">npm workspaces</a>.</p>
<p>The gist of the change was:</p>
<ol>
<li>
<p>Run <code>npm init -y</code> in the root of my mono-repo, and add a workspace config to the package.json:</p>
<pre><code class="language-json">"workspaces": [
    "apps/*"
    "packages/*",
],
</code></pre>
</li>
<li>
<p>Move all the apps into the <code>apps/</code> folder, and cut common code out of apps, and move them into their own packages in the <code>packages/</code> folder</p>
</li>
</ol>
<p>In the end, it looks like this:</p>
<pre><code>infrastructure/
apps/
|   |-- checker-scheduler-aws/
|   |-- checker-scheduler-fly/
|   |-- uptime-checker-aws/
|   |-- uptime-checker-fly/
|   |-- web/
|   |-- ...more apps here
packages/
|   |-- common-queries/
|   |-- time-utils/
|   |-- ...more packages here
</code></pre>
<p>With the old folder structure, things were starting to feel a bit risky: if I updated duplicated code in one place but not others, parts of OnlineOrNot could behave differently as I changed hosting providers.</p>
<p>With the new structure, I don't even need to publish packages to npm to use them in my apps - it Just Works quite nicely.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 8]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-8</link>
            <guid>https://maxrozen.com/onlineornot-diaries-8</guid>
            <pubDate>Sat, 22 Apr 2023 09:45:00 GMT</pubDate>
            <description><![CDATA[Building a new feature in a couple of weeks.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-8">clicking here</a>.)</div><p>Another Saturday morning here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>(I'm still working out the right tempo for these diaries)</p>
<p>It's been one coding week, and one marketing week since I last wrote in this diary, and I've built a few things. To be honest, I cheated a little bit, and used the marketing week to get heartbeat monitoring to a point where folks can use it.</p>
<h2>Heartbeat monitoring</h2>
<p>Heartbeat monitoring felt like the never-ending story for a couple of weeks. Every time I thought something would be obvious and easy, there would be another bug or edge case to solve. Not to mention decision after decision to be made about the product itself.</p>
<p>Things like: what to do if a heartbeat happens before we expect, how to integrate heartbeats into OnlineOrNot's existing systems, how to build graphs for the UI, should we allow assertions against incoming data, etc.</p>
<p>I decided to keep it simple in the end (and leave the door open for further features if folks ask for them): by default, OnlineOrNot heartbeat monitoring just gives you a URL that listens for <code>GET</code>, <code>HEAD</code>, or <code>POST</code> requests. Users pick how often the URL should expect a request, (with an optional grace period in case their process takes too long to run), and after OnlineOrNot receives the first request, it'll send an alert if any subsequent requests take too long to arrive.</p>
<p><img src="/assets/onlineornot-diaries-8/onlineornot-heartbeat-monitoring.png" alt="OnlineOrNot&#x27;s new heartbeat monitoring feature"></p>
<p>I've released the feature to folks on the early access program, and it's far from finished (it still needs docs, email and SMS alerts, and the UI is a mess), but now I get to iterate and market it regularly!</p>
<h2>A quick update on ads</h2>
<p>So far it looks like it's working: the first week of ads lead to a decent sized cohort of users trying OnlineOrNot, and converted to a paying customer as expected, now they just need to stay a customer long enough to pay off the cost of acquiring them via ads.</p>
<p>I say "looks like" because I can't be 100% sure without integrating Google Ads conversion tracking into OnlineOrNot, which I don't want to do, since I would need to add cookie banners to the website. Thankfully folks tell me where they came from when they sign-up, so I don't <em>need</em> the tracking.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 7]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-7</link>
            <guid>https://maxrozen.com/onlineornot-diaries-7</guid>
            <pubDate>Fri, 07 Apr 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[In which I pay for people to check out OnlineOrNot to test my landing pages.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-7">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>It was a marketing week this week, and I felt like trying something different: Ads.</p>
<p>On top of that, I shared the story of my journey from aspiring indiehacker to running OnlineOrNot on Twitter, and about 30k people saw it.</p>
<p>I also cheated a little bit and got the head-start on next week's coding week by starting on OnlineOrNot's next feature: heartbeat monitoring.</p>
<h2>Ads: was it worth it?</h2>
<p>I wanted to test the changes I made to OnlineOrNot's landing and pricing pages without needing to wait around for traffic, so I paid for 105 people to check out OnlineOrNot via pay-per-click advertising.</p>
<p>Was it worth it? Let's run the numbers: 27 people signed up to OnlineOrNot this week. 17 people signed up the week prior, so let's say 105 clicks resulted in 10 additional sign-ups.</p>
<p>If 10% convert to paying customers, I can expect those sign-ups will result in one paying customer, on average. Assuming each click costs $1 (it doesn't, but the maths is easier), that's $105 to acquire one paying customer.</p>
<p>For my three plans ($19/mo, $49/mo and $149/mo as of today), the pay-off period is 5.5 months, 2.1 months, and 0.7 months respectively for that one customer.</p>
<p>In short, purely on a monetary basis, it might be worth it, but I'm not sure yet.</p>
<p>As OnlineOrNot gains more users, I've been learning more and more about what folks expect from their uptime monitoring. I've been using that to make an even better product, so even if it doesn't pay off financially in the short term, the product will get better as a result, and the pool of folks that OnlineOrNot meets the requirements for will increase.</p>
<h3>The last time I ran ads...</h3>
<p>The last time I tried this experiment (a few months into OnlineOrNot's existence, about two years ago), it went badly. I think I spent about $200 for 100 clicks, resulting in zero sign-ups.</p>
<p>So I gave up, and decided "ads don't work".</p>
<p>Since then, I've learned how to make landing pages that convert, and have built a feature-set that matches folk's expectations, and have gotten better at writing copy. This time around, I also iterated on my ads during the week, and managed to get better performance out of them.</p>
<p>In short, maybe ads <em>can</em> work for OnlineOrNot, it'll just take a bit of effort to learn how to write effective ads (like anything else).</p>
<h2>My journey so far</h2>
<p>If you're just joining now, here's a summary article for each of the last five years:</p>
<ul>
<li>year 1: https://maxrozen.com/2018-review-starting-an-internet-business</li>
<li>year 2: https://maxrozen.com/2019-further-reflections-trying-to-start-an-internet-business</li>
<li>year 3: https://maxrozen.com/indiehacking-3-year-review</li>
<li>year 4: https://maxrozen.com/2021-strangers-paid-my-macbook</li>
<li>year 5: https://maxrozen.com/2022-just-keep-shipping</li>
</ul>
<p>I shared these links on Hacker News and Twitter this week, and quite a few folks checked them out, so I figured I may as well share them here too.</p>
<h2>Heartbeat monitoring</h2>
<p>As I mentioned <a href="/onlineornot-diaries-6#our-upcoming-monitoring-service">last week</a>, a couple of customers got in touch asking if they could hit an OnlineOrNot endpoint when a cronjob runs, and get notified when the job doesn't run.</p>
<p>The answer was "not yet", but as of today I'm currently testing heartbeat checks against OnlineOrNot itself:</p>
<p><img src="/assets/onlineornot-diaries-7/heartbeat-monitoring.png" alt="OnlineOrNot&#x27;s upcoming heartbeat monitoring dashboard"></p>
<p>This UI is actually live on OnlineOrNot, just hidden behind the Early Access Program feature flag - I figured I may as well deploy it in an incomplete state and get early feedback as I build it.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 6]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-6</link>
            <guid>https://maxrozen.com/onlineornot-diaries-6</guid>
            <pubDate>Fri, 31 Mar 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[A new monitoring service for OnlineOrNot, and new learnings for my pricing page.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-6">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>I was very close to not writing this week's diary as I felt it's probably too boring for most folks, but lets see how it goes.</p>
<p>This week was a coding week, and I spent most of it planning a new monitoring service for OnlineOrNot, migrating uptime checks back to AWS, and learning new things from other founders on Twitter.</p>
<h2>Our upcoming monitoring service</h2>
<p>So, there are 2880 30-second increments in a day. Assuming you can check each website within 30 seconds, you should be able to easily reach 2880 uptime checks per day.</p>
<p>For some reason, OnlineOrNot falls short. Not by much, it's about 1% off for the URLs I care enough about to monitor, but I'm still curious as to why it happens.</p>
<p>To help investigate the issue, I started building a hacky thing just for myself, before realizing several users have asked for this feature.</p>
<p>So this week I started designing a new monitoring service for OnlineOrNot: Heartbeat checks (also known as reverse monitoring). Heartbeat checks are where your service sends a request to a URL OnlineOrNot exposes only for you, and sends you an alert if you haven't sent a request within a given time period (if you over-check, OnlineOrNot will ignore the extra requests). There's also an advanced use-case where you send OnlineOrNot a request containing data, and it sends you an alert if the data fails against certain criteria, but that'll come later.</p>
<h2>and back to AWS</h2>
<p><img src="/assets/onlineornot-diaries-6/fly-to-aws.png" alt="A graph showing a drop off in checks to fly.io and an increase in checks to AWS"></p>
<p>After a month of globally distributed uptime checks on fly.io, I'm cutting my losses and moving uptime checks back to AWS. The service itself is a joy to use (especially the <code>iad</code> region on the US east coast), but it's not stable enough to run uptime checks in each of their regions. It might make sense for running a web service, where a region's requests can failover to another region, but it just didn't work for me.</p>
<p>I found myself needing to run each check several times on fly.io, as well as backup checks on AWS AND Cloudflare to actually be sure an OnlineOrNot user's URL is actually down, rather than it being an issue with the VM I'm checking from.</p>
<p>Compare that to AWS, where 1 user out of 1305 has checks that consistently fail.</p>
<p>I'm not completely off fly.io though, my new architecture is a hybrid of fly.io and AWS, where a VM queues up checks to run in Redis every 15 seconds, another pulls checks from Redis, and runs the uptime check in a specific AWS region as needed.</p>
<h2>Pricing Page Driven Development</h2>
<p>I recently read <a href="https://buttondown.email/blog/pricing-page-driven-development">Pricing Page Driven Development</a> from Justin Duke, and it got me thinking about how I never really thought about expansion revenue. I was so excited about getting my first customers, that I was happy to keep the pricing page as-is.</p>
<p>Now that I have a decent number of paying customers, I'm beginning to notice patterns in the plans folks subscribe to, and realizing some changes need to be made to keep the business sustainable long-term. Of course, I don't plan on changing anything for my existing customers, but I'll be introducing a new "Freelancer" plan for solopreneurs to start with.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 5]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-5</link>
            <guid>https://maxrozen.com/onlineornot-diaries-5</guid>
            <pubDate>Fri, 24 Mar 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[In which I doubled my conversion rate without trying too hard.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-5">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>This week was a marketing week, and I spent it ~~writing landing pages~~ focusing on telling folks what OnlineOrNot actually does.</p>
<p>There are two types of marketing I care about:</p>
<ul>
<li>Marketing that brings in more traffic (typically via blog posts)</li>
<li>Marketing that improves the likelihood folks will try OnlineOrNot once they've landed on the website</li>
</ul>
<p>The first type is <em>extremely difficult</em> to influence (but what I often spend my marketing week doing, because the dopamine rush of succeeding is so good, I guess). Think months of consistent output before you start to get the numbers building up, apart from the rare popular article making the front page of Hacker News.</p>
<p>The second type is a lot easier to influence: you already have their attention (however little it might be these days), all you need to do is tell them what you do in 0.1 seconds, <em>easy</em>.</p>
<h2>Doubling my conversion rate without really trying</h2>
<p>OnlineOrNot's funnel is pretty simple while I'm still trying to optimize my landing pages: from every landing page, every call to action (CTA) on that page takes you to the pricing page. From there, folks either bounce, or sign up.</p>
<p>By doing this, you get to test two things: how effective your landing page copy is, and how effective your pricing page is.</p>
<p>This week I tested a few changes (because I did them all at once, I have no idea which one made all the difference, but there was a lot to fix) which doubled the number of people that ended up on my pricing page.</p>
<p>Just above the fold, here's what I changed:</p>
<ul>
<li>Updated tagline from "We detect incidents before your customers do" to "Uptime monitoring in 30 seconds"
<ul>
<li>Before this week, you couldn't tell what OnlineOrNot actually <em>did</em> from the main landing page, the tagline was trying to be too clever. This resulted in a large number of bounces.</li>
</ul>
</li>
<li>Updated every CTA to say "See Pricing ->" instead of "See Plans and Pricing ->"</li>
<li>Updated the explainer text under the first CTA from "No time limit, and no credit card required." to "Get started on the free tier, no credit card required."
<ul>
<li>Folks regularly ask if OnlineOrNot has a free tier despite having one from the start, so here's hoping this helps</li>
</ul>
</li>
<li>Added a screenshot of the actual product</li>
</ul>
<p>By the end of the week, twice as many people were visiting my pricing page having landed on my main landing page than a regular week.</p>
<h2>Telling potential customers what we actually do</h2>
<p>The trouble with having a single landing page, is that you dilute your message by trying to talk to too many people at the same time.</p>
<p>On the OnlineOrNot landing page, I was trying to tell folks that we support: <a href="https://onlineornot.com/api-monitoring">API Monitoring</a>, <a href="https://onlineornot.com/browser-checks">Browser Checks</a>, <a href="https://onlineornot.com/website-monitoring">Website Monitoring</a>, <a href="https://onlineornot.com/status-pages">Status Pages</a> all in a single page, while being brief.</p>
<p>I'd have people emailing me to ask if I supported Slack alerts, when it was the second feature I built for OnlineOrNot.</p>
<p>To fix this I added a new dropdown menu in the header for desktop users, linking them to each individual landing page I built this week:</p>
<p><img src="/assets/onlineornot-diaries-5/dropdown.png" alt="OnlineOrNot dropdown linking to API Monitoring, Browser Checks, Website Monitoring, and Status Pages"></p>
<p>Each individual landing page then explains what we do (with a focus on the type of user that would use that part of the product). Going forward, I'll be writing individual pages for every feature OnlineOrNot has too.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 4]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-4</link>
            <guid>https://maxrozen.com/onlineornot-diaries-4</guid>
            <pubDate>Fri, 17 Mar 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[On ripping out product analytics, updating the pricing page, and adding multi-region monitoring]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-4">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, let's go into how OnlineOrNot went this week.</p>
<p>This week was a coding week, and I managed to release a feature folks have been asking for, as well as some businessy things that aren't quite marketing, and aren't quite coding:</p>
<ul>
<li>I finally added <a href="#finally-proper-multi-region-monitoring">multi-region monitoring</a> back into OnlineOrNot after migrating off of AWS Lambda</li>
<li>I <a href="#refreshing-the-pricing-page">updated the pricing page</a>, to let folks know we do indeed have a free tier</li>
<li>I removed in-product analytics, and <a href="#a-new-post-sign-up-email">updated OnlineOrNot's welcome email</a> to invite customers to tell me more about why they monitor their sites.</li>
</ul>
<h2>Finally, proper multi-region monitoring</h2>
<p>First, some context.</p>
<p>I was never sure OnlineOrNot would attract any users. As a result, early on OnlineOrNot ran uptime checks entirely on AWS Lambda (with serverless products like AWS Lambda, you only pay for what you use, so if no one uses your product, you pay nothing).</p>
<p>Another benefit of using AWS Lambda is that running an uptime check in Tokyo, Paris, and LA simultaneously is effortless after you deploy - all you have to do is specify where your code should run.</p>
<p>After enough folks started using OnlineOrNot, it made sense to move off AWS Lambda in favour of a server that runs 24/7, and in doing so, OnlineOrNot lost the ability to easily pick where to run. This week, after a bit of work, I now have constantly running servers in:</p>
<ul>
<li>LA, USA</li>
<li>Virginia, USA</li>
<li>Tokyo, Japan</li>
<li>Sydney, Australia</li>
<li>Amsterdam, The Netherlands</li>
</ul>
<h2>Refreshing the pricing page</h2>
<p>After talking to a few customers, I realized my pricing page isn't clear.</p>
<p>In the original design, I wanted to keep it short so folks could skim the page, as well as get an understanding of what each plan lets you do. In reality, I made a page too long to glance at, and too short to properly sell all of OnlineOrNot's features.</p>
<p>Basically, in trying to make the page good on average for two use-cases, I made it suck for both.</p>
<p>To fix this, I split the page into two parts: an above-the-fold table highlighting the most popular features, and the original table I used to have, but with more detail.</p>
<h2>A new post sign-up email</h2>
<p>After reflecting a bit on what I use to decide whether I'll build a feature, I realized: I haven't even logged into PostHog (the tool I used for in-product analytics) in months, why do I bother keeping it in my web app?</p>
<p>The reality is, as a small independent business I spend a good deal of my time actually talking to customers to figure out how it's going, and what features are missing, rather than digging through analytics.</p>
<p>Vibes-driven product management.</p>
<p>So I also figured, perhaps it's time to update my post sign-up email to learn new things.</p>
<p>For the last two years, I've asked every person (via email) that signs up to OnlineOrNot:</p>
<blockquote>
<p>What's the one thing you were hoping OnlineOrNot could do for you?</p>
</blockquote>
<p>It's driven quite a bit of feature development and helped shaped my understanding of what people expect OnlineOrNot to do.</p>
<p>Now I'm trying a new question to understand the broader context of what OnlineOrNot gets used for:</p>
<blockquote>
<p>What led you to need uptime monitoring in the first place?</p>
</blockquote>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Getting your own good enough laptop for under $500]]></title>
            <link>https://maxrozen.com/getting-your-own-good-enough-laptop-for-under-500</link>
            <guid>https://maxrozen.com/getting-your-own-good-enough-laptop-for-under-500</guid>
            <pubDate>Mon, 13 Mar 2023 05:52:00 GMT</pubDate>
            <description><![CDATA[I recently bought a cheap, used Thinkpad T480, replaced a bunch of parts. Some folks were asking how they could do the same thing, so I figured I'd write about it]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/getting-your-own-good-enough-laptop-for-under-500">clicking here</a>.)</div><p>I recently bought a cheap, used Thinkpad T480, replaced a bunch of parts, then <a href="/replacing-my-macbook-m1-with-thinkpad-t480">wrote about the experience</a>. Some folks were asking exactly what parts they needed to do the same thing, so I figured I'd write about that too.</p>
<p>You don't actually need to replace anything if you want to use this laptop as-is (for around 250 USD), but you <em>can</em>, which is what attracted me.</p>
<h2>The laptop</h2>
<p>First of all, you're going to want a Lenovo Thinkpad T480, not a T480s, not a T470, and definitely not an <strong>L</strong>470/<strong>L</strong>480.</p>
<p>The specific laptop you're looking for has either an 8th Generation quad-core Intel i5-8250U or i5-8350U CPU. They did build versions of this laptop with an i7, but the i5 is significantly easier to find.</p>
<p>It's a 14 inch laptop, but in its base configuration came with a 1366x768 LCD screen with what feels like zero brightness and contrast, so if you want to save around 90 USD (the specific replacement I got was an Innolux N140HCE-EN1 Rev.C2), look for a model with a 1920x1080 IPS screen.</p>
<p>For a model in great condition with 16GB of RAM and a nice 1920x1080 screen, you're looking at around $250 USD on ebay, plus $125 USD for a new battery and trackpad. Over in the EU, it's around 350 euros, plus upgrades, plus potentially needing to swap out whichever keyboard you find for a US-international one (thankfully keyboards are $30 on AliExpress).</p>
<p>For a totally beaten up T480, I've seen them go as low as 150 USD and still look like they work, but you'll need to spend an hour or two swapping parts out.</p>
<h2>The OS</h2>
<p>This needs repeating: <a href="https://pop.system76.com/">Pop!_OS</a> 22.04 Just Works™ with this machine. I did not configure a single thing in arcane config files, not a single terminal command to run either.</p>
<p>Bluetooth, WiFi, the screen, the keyboard, the speakers, suspend/sleep, power management, Everything. Just. Works.</p>
<p>I installed the OS, installed my regular apps (Chrome, Firefox, Spotify, GitKraken, DataGrip, Slack, Discord, VS Code, Insomnia, 1Password), cloned my repos, and everything just worked.</p>
<h2>What do you mean good enough?</h2>
<p>I have <a href="https://onlineornot.com/">OnlineOrNot</a>'s dev server (several node servers and a postgres server) running with zero perceptible lag as I write this article with the following apps open: two VS Code windows, Chrome with about 10 tabs, Discord, Signal, Slack, DataGrip, GitKraken.</p>
<p>You might notice I don't build or compile binaries as part of my workflow. If you need to compile binaries as fast as possible on your dev machine, <strong>this is not the laptop for you.</strong></p>
<p>That being said, React hot reloading is pretty fast:</p>
<pre><code class="language-bash">wait  - compiling /404 (client and server)...
event - compiled client and server successfully in 264 ms (556 modules)
wait  - compiling /articles...
event - compiled client and server successfully in 267 ms (563 modules)
wait  - compiling /[slug]...
event - compiled client and server successfully in 495 ms (896 modules)
</code></pre>
<p>Jumping to my MacBook Air, sure it <em>feels</em> higher quality, but it doesn't <em>feel</em> faster when running the same workload.</p>
<h2>The upgrades</h2>
<p>In its default configuration, booting Pop!_OS 22.04, I was getting around 6.5 hours battery life, on a 5 year old battery that only charged to 80% of its capacity.</p>
<p>For my purposes, the first upgrade worth getting was a new battery. I opted for a genuine Lenovo 72wh battery (part number 01AV427), and that set me back 90 euros. With the new battery, I get 15 hours in "battery life" mode, and about 30% screen brightness.</p>
<p>The second upgrade I recommend is a glass trackpad from the Thinkpad X1 Extreme (possible part numbers: 01LX660, 01LX661, 01LX662). It's not exactly as good as a MacBook's trackpad, but it's one of the best trackpads I've tried on a non-Mac.</p>
<p>RAM: it's replaceable too. The T480 takes two DDR4 2666 MHz SO-DIMM sticks, up to 32GB total, but runs them at 2400MHz. Either 2400 or 2666 will work, get whatever is cheapest.</p>
<p>Optionally: you can upgrade the WiFi chip to an Intel AX210 for Bluetooth 5.2 and WiFi 6 support, but it isn't noticeably faster than the default chip.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 3]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-3</link>
            <guid>https://maxrozen.com/onlineornot-diaries-3</guid>
            <pubDate>Fri, 10 Mar 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[Marketing week? More like reliability week]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-3">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, I've poured myself a Delirium Tremens (I shit you not, that's what the beer is called), let's go into how OnlineOrNot went this week.</p>
<h2>What even is a marketing week?</h2>
<p>Jake recently replied to one of the OnlineOrNot diaries, asking:</p>
<blockquote>
<p>Have you ever detailed what a 'marketing week' is?</p>
</blockquote>
<p>To which I replied (paraphrasing here):</p>
<p>My marketing weeks are normally based on vibes, but I typically:</p>
<ul>
<li>update the changelog</li>
<li>write blog posts/screencasts/guides that are missing</li>
<li>tweet more</li>
<li>participate more in forums (mainly hacker news and reddit)</li>
<li>talk to existing and potential customers about how they monitor their URLs, alert their customers to incidents, and generally how they manage their incidents</li>
<li>work on landing pages</li>
</ul>
<h2>Actually, time for a reliability week</h2>
<p>This week was supposed to be a marketing week, and then over the weekend before this week started, <a href="https://community.fly.io/t/reliability-its-not-great/11253">fly.io had reliability issues</a>.</p>
<p>At the same time, I was in the process of rolling out a change that would make OnlineOrNot check uptime globally by default (rather than from a single data center + verifying downtime globally), and ended up mistakenly sending 2500% more emails than usual on Saturday and Sunday, thanks to a bad deployment in Singapore, followed by a bad deployment in Sydney.</p>
<p>The idea was to allow significantly faster checks, and use several failing checks in a row as a signal that your URL is indeed globally unavailable, but I had to roll it back and start from scratch.</p>
<p>After fixing all the issues I managed to step back, breathe, and write about it in <a href="https://onlineornot.com/monitoring-our-monitoring">monitoring our monitoring</a> (that counts as marketing, right?)</p>
<p>In short, I now have graphs that tell me if what I just merged caused false positives (or for the checks to stop entirely):</p>
<p><img src="/assets/onlineornot-diaries-3/passing-uptime-checks.png" alt="Passing uptime checks">
Side note: the spikyness in the graphs comes from previously only running checks at the start of each minute - as of this week, checks are run every 10 seconds, which will smooth out the graph eventually.
<img src="/assets/onlineornot-diaries-3/failing-uptime-checks.png" alt="Failing uptime checks"></p>
<p>This week I:</p>
<ul>
<li>started running uptime checks every 10 seconds</li>
<li>started logging each check in ClickHouse, and started monitoring our monitoring with Grafana</li>
<li>quadrupled how many uptime checks individual VMs perform (after verifying it had no impact on false positives)</li>
<li>tripled the default timeout of our uptime checks from 10 seconds to 30 seconds, and made it possible to choose how long is considered a timeout (some folks are okay with their users waiting 10-30 seconds for the page to respond, and don't consider that an outage - I'm not one to judge)</li>
<li>tripled the number of retries each VM performs before checking on another hosting provider</li>
<li>wrote about the reliability dramas</li>
<li>pitched OnlineOrNot to some ex-Atlassian alumni</li>
</ul>
<p>So in short, not much marketing, but in the end I think OnlineOrNot is better for it. Maybe I'll move towards a 3 week program of Coding Week -> Marketing Week -> Reliability Week in the future, who knows.</p>
<p>Got something you're curious about? Feel free to tweet at me, or subscribe to the mailing list below to get these every Friday.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 2]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-2</link>
            <guid>https://maxrozen.com/onlineornot-diaries-2</guid>
            <pubDate>Fri, 03 Mar 2023 17:45:00 GMT</pubDate>
            <description><![CDATA[In which I ship a CLI (for real this time), and improve a few things]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-2">clicking here</a>.)</div><p>Another Friday evening here in Toulouse, I've poured myself a Gallia West Coast IPA, let's go into how OnlineOrNot went this week.</p>
<p>It was a coding week according to <a href="https://codingweekmarketingweek.com/">CodingWeekMarketingWeek</a>, so I spent my mornings mainly working on the new <a href="https://github.com/onlineornot/onlineornot">onlineornot CLI</a>, and writing docs.</p>
<p>On Monday morning, I realized almost none of the API endpoints or CLI commands I built at the end of last week were documented, so I went through and documented everything:</p>
<ul>
<li>From <a href="https://onlineornot.com/docs/cli-installation">installation</a>,</li>
<li>to <a href="https://onlineornot.com/docs/cli-login">login</a> (it sucks that folks need an API token for this, I'll need to revisit login to piggy-back their OnlineOrNot app login in the future)</li>
<li>to <a href="https://onlineornot.com/docs/cli-commands">CLI commands</a></li>
<li>and the <a href="https://onlineornot.com/docs/api-uptime-checks">API endpoints</a> behind them.</li>
</ul>
<p>I also implemented and released <code>onlineornot checks create</code> with exactly as many options as when I initially released OnlineOrNot two years ago: <code>name</code> and <code>url</code>.</p>
<p>I considered that enough functionality for a v1, so I cut the <a href="https://github.com/OnlineOrNot/onlineornot/releases/tag/onlineornot%401.0.0">v1.0.0 release</a>, and sent out a quick email and <a href="https://onlineornot.com/announcing-onlineornot-uptime-check-cli">blog post</a> for the release.</p>
<p>Folks don't seem <em>that</em> interested in the CLI, but then again it does lack the features the web app has, and it's only a week old.</p>
<p>Since next week is a marketing week, I'll probably record a few screencasts explaining how to login and use the CLI. I recently remembered that I had even built <a href="https://onlineornot.com/screencasts">a page for screencasts</a> on the OnlineOrNot marketing site - judging by the screenshot, I haven't touched it in about 18 months.</p>
<p>Apart from the CLI, I also found time to help out a customer by making it possible to check client-side rendered web apps locked behind authentication (both via making it possible to add custom HTTP headers to Browser Checks, and getting past Basic HTTP Authentication).</p>
<p>That's all for this week, looking forward to writing and recording the screencasts next week (I also have a spicy blog post about uptime monitoring brewing in the back of my mind, if I have time for it).</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[OnlineOrNot Diaries 1]]></title>
            <link>https://maxrozen.com/onlineornot-diaries-1</link>
            <guid>https://maxrozen.com/onlineornot-diaries-1</guid>
            <pubDate>Fri, 24 Feb 2023 17:52:00 GMT</pubDate>
            <description><![CDATA[On marketing, and shipping a CLI]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/onlineornot-diaries-1">clicking here</a>.)</div><p>Alright, I've poured myself a beer, it's early Friday evening, let's quickly dig into how OnlineOrNot went this week.</p>
<p><a href="https://codingweekmarketingweek.com/">Coding Week, Marketing Week</a> tells me it's marketing week, I've spent at least half of this week building the <a href="https://github.com/onlineornot/onlineornot">onlineornot CLI</a>, but it wasn't a total loss.</p>
<p>(I'm starting a thing where I write about OnlineOrNot regularly in a barely edited, diary-like format, let me know what you think.)</p>
<h2>Actual marketing</h2>
<p>Today is OnlineOrNot's second birthday (roughly - I published a tweet about it on the 25th of February 2020, but published the day before), and to celebrate, I wrote up <a href="https://onlineornot.com/lessons-from-two-years-of-saas-operation">what I learned running a SaaS for a second year</a>.</p>
<p>Approximately 1.6k people read it this week, about 1/20th the number of readers for the <a href="https://onlineornot.com/what-learned-running-saas-for-year">article from last year</a>, but whatever.</p>
<p>I published a second article about <a href="https://maxrozen.com/replacing-my-macbook-m1-with-thinkpad-t480">the thinkpad I built last weekend</a>, which was read by 45k people this week, and drove 1.2k people to OnlineOrNot's home page, which is wild.</p>
<p>It led to a few folks to message me that the machine I built for myself doesn't work for them and their use case, which gave me a good chuckle.</p>
<h2>OnlineOrNot CLI</h2>
<p>The other day, I realized what annoys me most about OnlineOrNot is that I have to open a browser, type into the address bar, and navigate through the landing page and the app's home page before I can see how my checks are doing.</p>
<p>It got me thinking, it'd be cool if <code>onlineornot checks list</code> was just a command in your terminal.</p>
<p>In three days, I've made it possible. <code>npm install -g onlineornot</code> if you want a global command, or <code>npx onlineornot@latest</code> to just run the latest version every time (requires at least Node 16 LTS).</p>
<p>You can find the source code on <a href="https://github.com/onlineornot/onlineornot">GitHub</a>.</p>
<p>This week I shipped commands to:</p>
<ul>
<li>Get a list of all uptime checks in an account, and their live status (with JSON output support too)</li>
<li>Get a specific uptime check, and it's live status (with JSON output support too)</li>
<li>Delete a specific uptime check</li>
<li>Open a browser window to the <a href="https://onlineornot.com/docs/welcome">docs</a></li>
<li>Open a browser window to the <a href="https://onlineornot.com/app/settings/billing">billing settings</a></li>
<li>Open a browser window to the <a href="https://onlineornot.com/app/settings/developers">developer settings</a></li>
</ul>
<p>I'm pretty chuffed, the CLI menu looks like this at the moment:</p>
<pre><code>// npx onlineornot@latest
onlineornot

Commands:
  onlineornot docs     📚 Open OnlineOrNot's docs in your browser
  onlineornot checks   ✅ Manage your uptime checks
  onlineornot billing  🧾 Open OnlineOrNot's billing in your browser
  onlineornot login    🔓 Opens your browser to OnlineOrNot's Developer settings
  onlineornot whoami   🕵️  Retrieve your user info and test your auth config

Flags:
  -h, --help     Show help  [boolean]
  -v, --version  Show version number  [boolean]
</code></pre>
<p>Over this weekend I'll make it possible to add uptime checks too, probably via <code>onlineornot checks create</code>, and release it as the v1.0 release.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[On replacing my MacBook Air M1 with a Thinkpad T480]]></title>
            <link>https://maxrozen.com/replacing-my-macbook-m1-with-thinkpad-t480</link>
            <guid>https://maxrozen.com/replacing-my-macbook-m1-with-thinkpad-t480</guid>
            <pubDate>Tue, 21 Feb 2023 05:52:00 GMT</pubDate>
            <description><![CDATA[In which you probably wonder, but ...why?]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/replacing-my-macbook-m1-with-thinkpad-t480">clicking here</a>.)</div><p>I recently replaced my 2020 MacBook Air M1 with 8GB of RAM with a Lenovo Thinkpad T480.</p>
<p>It's not as insane of a downgrade as it sounds.</p>
<p><img src="/assets/replacing-my-macbook-m1-with-thinkpad-t480/t480-desktop.png" alt="My T480&#x27;s desktop"></p>
<p>Want one for yourself? I wrote about what you'll need to buy <a href="/getting-your-own-good-enough-laptop-for-under-500">here</a>.</p>
<h2>Specs</h2>
<p>My Thinkpad T480's specs:</p>
<ul>
<li>Intel® Core™ i5-8350U CPU @ 1.70GHz × 8</li>
<li>Internal 24wh battery + external 24wh battery (6 hours 15 minutes default battery life on linux, upgradeable to 92wh total)</li>
<li>16 GB RAM (upgradeable to 32GB officially, 64GB unofficially)</li>
<li>1366x768 LCD screen (upgradable to anything from a 1920x1080 screen to a 4k screen, more on that later)</li>
<li>Windows 10 Pro installed w/ OEM key</li>
<li>256GB Samsung NVMe drive</li>
<li>WiFi/Bluetooth module: Intel® Dual Band Wireless-AC 8265 (WiFi 5), again, upgradeable</li>
<li>Ports:
<ul>
<li>Two USB-C ports</li>
<li>Two USB 3.1 ports, with one Always On</li>
<li>One HDMI 1.4b port</li>
<li>Ethernet port</li>
<li>3.5mm headphone jack</li>
<li>SD card reader</li>
</ul>
</li>
</ul>
<p>I don't know if you noticed a theme in the specs, but <em>everything</em> in this laptop is upgradeable. Short of the CPU, which is a downer, but this isn't exactly an MNT Reform laptop.</p>
<h2>Upgrades</h2>
<p>The base model LCD screen that the T480 comes with is incredibly bad.</p>
<p>It hurt to look at, the resolution was too small for anything except using the terminal, and I couldn't use it sitting next to my window unless brightness was turned up to 100% (on a cloudy day!), and even then it wasn't great.</p>
<p><img src="/assets/replacing-my-macbook-m1-with-thinkpad-t480/t480-laptop.jpg" alt="My T480">
(one quick boot into Windows to check specs before blowing it all away)</p>
<p>So I ordered a replacement 1920x1080 IPS screen (Innolux N140HCE-EN1 Rev.C2), followed the <a href="https://www.ifixit.com/Guide/Lenovo+ThinkPad+T480+LCD+Screen+Replacement/140589">ifixit guide</a>, and had a decent screen after 30 minutes of tinkering.</p>
<p>Unfortunately upgrading the screen cut battery life to about 4 hours 15 minutes (with the screen set to high brightness), so I've ordered a 72wh battery, which should bring that back up to around 8 hours. Note that the battery that came with my T480 is not brand new: as it's a 5 year old enterprise laptop, it had about 80% max charge of its original design. I can also undervolt the CPU to cut power consumption further, but 8 hours is enough for me when traveling.</p>
<p>After a week of testing (using the machine as I normally would), the battery life on the new 1920x1080 screen ended up being around 6 hours, I must have just been slamming the CPU/RAM when I first tested it.</p>
<p><strong>Update:</strong> After the brand new 72wh battery came in (for a total system capacity of around 90wh), my battery life is now 14-15 hours.</p>
<p>While upgrading the LCD screen, I noticed I had a spare 16GB RAM stick lying around my home office, so I also upgraded to 32GB RAM. Not that I need that much as a web developer/writer (VS Code, GitKraken and Chrome take up 8GB), but it's nice to have. (It takes two DDR4 2666 MHz SO-DIMM sticks)</p>
<p>The trackpad is <em>good</em>, compared to other Windows laptops, but terrible compared to the MacBook Air. I've ordered a glass trackpad that comes with the Thinkpad X1 Extreme (and is compatible with the T480), so I'll update this article once I've installed that.</p>
<p><strong>Update:</strong> The new trackpad from the Thinkpad X1 Extreme is fantastic. Tap-to-click actually registers my clicks, unlike on the default T480 trackpad, and scrolling is extremely precise. Swiping to change workspaces feels just as nice as in macOS (though you'll need to learn new gestures).</p>
<p>I've also ordered an Intel® Wi-Fi 6 AX200 card for WiFi 6 and Bluetooth 5.2.</p>
<p><strong>Update:</strong> The Intel AX200 card causes boot issues for Pop!_OS (at least for me), so I just installed the old one back in. I'm not here to dig through forums and run scripts to tell you "it just works!", so I'm keeping the default wifi card that <em>does</em> Just Work.</p>
<p><strong>Update 2:</strong> I bought an Intel AX210 card which Just Works with Pop!_OS, supports WiFi 6, Bluetooth 5.2, so I'm using that now.</p>
<h2>But... why?</h2>
<p>You might be thinking, "wtf, isn't the M1 significantly faster?" - and it is!</p>
<p>I ran Geekbench:</p>
<ul>
<li>T480 scored 1136 on single core, and 2522 on multi core with default settings</li>
<li>T480 with <a href="https://github.com/erpalma/throttled">throttling removed</a> scored 1229 on single core, and 4240 on multi core</li>
<li>M1 scored 2355 on single core, and 8241 on multi core</li>
</ul>
<p>But is it <em>noticeably</em> 2x slower than the M1? Nope.</p>
<p>It's <em>enough</em>.</p>
<p>Hot reloads of <a href="https://onlineornot.com/">OnlineOrNot</a>'s development server take &#x3C; 1 second, alt-tabbing doesn't lag (it did on my Intel-based MacBook Pro 2019), and booting up is as fast as my MacBook Air. So far, none of my development workflow is slower on the T480.</p>
<p>I installed Pop!_OS 22.04 LTS as I've had quite enough of Microsoft's constant nagging to upgrade to Windows 11, and honestly, I like it?</p>
<p>Everything works out of the box. I haven't had to screw around with <em>anything</em>. The user experience isn't as polished as macOS, but this is the first time I've installed a distro where it Just Works. For what it's worth, I've been trying to get a stable Linux development machine since at least 2010, and I always ended up back on a MacBook.</p>
<p>You can get a Thinkpad T480 for around $200 USD, and upgrades will cost you around $150 total on AliExpress. If any individual part fails, there's a freely available <a href="https://download.lenovo.com/pccbbs/mobiles_pdf/t480_hmm_en.pdf">Hardware Maintenance Manual</a>, complete with part numbers that you can look up and order from 3rd party suppliers.</p>
<p>Having experienced Apple's wrath through the 2011 15" MacBook Pro dGPU problem (the motherboard is an overheating time bomb, that will inevitably fail and require replacement - I had two motherboards fail on me after four years, asked for a refund, had to quote Australian Consumer Law at them to get a fair deal), I'm happy to have a way out.</p>
<h2>No really, why?</h2>
<ul>
<li>I can't tell the difference in performance or build times as a web developer, my dev server is just as fast on Pop!_OS 22.04 as macOS 13.2</li>
<li>I once spilled an entire can of beer onto a Thinkpad X220. The fix involved a screwdriver, a sponge, a $50 replacement keyboard, and 15 minutes of my time (there are drainage holes on the bottom of Thinkpads).
<ul>
<li>for contrast, a little bit of dust can break an Apple butterfly keyboard</li>
</ul>
</li>
<li>I'm tired of Apple's ecosystem, Apple ID, not being able to change any individual part of my laptop, being charged $200 USD for 8GB of RAM, being charged $200 for 256 GB of SSD, and so on</li>
<li>I wanted a cheap travel laptop I could replace without blinking an eye at the price. I live in Europe, Thinkpads are the most common enterprise laptops I see while traveling. I might even slap an asset tag sticker on the cover to make it even more inconspicuous.</li>
<li>I felt like it? I'm not telling you what to do here, I'm just telling you my experience.</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[2022: I just kept shipping]]></title>
            <link>https://maxrozen.com/2022-just-keep-shipping</link>
            <guid>https://maxrozen.com/2022-just-keep-shipping</guid>
            <pubDate>Sat, 14 Jan 2023 05:52:00 GMT</pubDate>
            <description><![CDATA[Another year in review, in which I just keep shipping, with some stumbles along the way]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/2022-just-keep-shipping">clicking here</a>.)</div><p>At the end of each year I summarize how the year went, reflecting on how I progressed towards my goal of being self-sufficient as a web developer, here are the previous editions of this article:</p>
<ul>
<li>(2021) <a href="/2021-strangers-paid-my-macbook">Strangers from the internet paid for my MacBook Air: on my 4th year of indiehacking</a></li>
<li>(2020) <a href="/indiehacking-3-year-review">Indiehacking: a review of my 3rd year</a></li>
<li><a href="/2019/12/29/2019-further-reflections-trying-to-start-an-internet-business/">2019: Further reflections on trying to start an internet business</a></li>
<li><a href="/2018/12/31/2018-review-starting-an-internet-business/">2018: Reflections on trying to start an internet business</a></li>
</ul>
<p>As of last year (2021), the business makes money. Not nearly enough to be self-sufficient as a web developer, but this year it continued growing enough at a steady pace that it makes me think one day it'll get there.</p>
<p>For most of the year, I kept working on <a href="https://onlineornot.com/">OnlineOrNot</a>, for roughly 2 hours every day before my workday started. Not that working on OnlineOrNot felt like work - I was doing everything from answering customer support emails, writing blog posts to build trust with potential and current customers, building landing pages, writing docs, shitposting on twitter, etc. No status updates, no standups, just building value.</p>
<p><strong>Table of Contents</strong></p>
<ul>
<li><a href="#on-success">On Success</a></li>
<li><a href="#working-for-cloudflare">Working for Cloudflare</a></li>
<li><a href="#write-more">Write more</a></li>
<li><a href="#pay-attention-to-opportunity-cost">Pay attention to opportunity cost</a></li>
<li><a href="#register-your-damn-business">Register your damn business</a></li>
<li><a href="#black-friday-can-be-80-of-yearly-revenue">Black Friday can be 80% of yearly revenue</a></li>
</ul>
<h2>On Success</h2>
<p>Much of OnlineOrNot's success boils down "show up regularly, do the work, over a long enough time period" - at some point I wrote my thoughts about this into an article: <a href="https://onlineornot.com/unreasonable-effectiveness-shipping-daily">The unreasonable effectiveness of shipping every day</a>.</p>
<p>Just show up, do the work, and ship, daily. How hard can it be?</p>
<p>I actually failed a few times this year, and quickly learned to just keep working on OnlineOrNot.</p>
<p>| <img src="/assets/2022-just-keep-shipping/on-doing-two-projects.png" alt="Working on two projects at once, a tweet"> |
| :----------------------------------------------------------------------------------------------------: |
|                             <em>it was not different this time, dear reader.</em>                             |</p>
<p>In late January, I spent a week to split out the feature flag service I use for OnlineOrNot into its own product, and realized "it runs entirely on Cloudflare's network so it's real fast" isn't as big of a selling point as "we've been around forever, have an adequate feature set, and won't randomly disappear" (part of why OnlineOrNot wins in a sea of uptime monitoring services), so I shut down the feature flag service.</p>
<p>Around April, I came dangerously close to splitting out my docs into another paid service before I remembered the reason my customers love <a href="https://onlineornot.com/docs/welcome">my docs</a> - they're deeply integrated into my app and website, they load ridiculously fast, and they're in the same mono-repo so I can update them while updating my code. An external service just can't compete.</p>
<p>In September, I used some shiny new tech from Cloudflare to build a real-time chat Trello power-up called <a href="https://trello.com/power-ups/6326ce8793b58502b790340d/petit-chat">Petit Chat</a>. I built it over a couple of weekends, it gets users without marketing (the joy of being in an app marketplace). A fun distraction, but I'm keeping my focus on OnlineOrNot.</p>
<h2>Working for Cloudflare</h2>
<p>From reading what I write, hopefully you get the impression that I'm the type of engineer that enjoys shipping a tiny version of the end goal, and rapidly iterating while real customers use the product.</p>
<p>I don't enjoy work when my organization mandates a waterfall-like process, so at the start of the year after staring at the sea for a few days during my holidays, I realized I needed to change employers.</p>
<p>Building and releasing the feature flag service wasn't entirely a waste - I built it publicly on Twitter, got extremely lucky, and managed to score an interview at Cloudflare with the team building the tools I used. Long story short, now I get to work on Cloudflare Workers every day.</p>
<p>Since starting I've had the chance to improve <a href="https://github.com/cloudflare/workers-sdk">Wrangler</a> (our CLI for building on Workers) quite a bit, and now I'm working on <a href="https://blog.cloudflare.com/introducing-d1/">D1</a>'s developer experience.</p>
<h2>Write more</h2>
<p>In 2021 I noticed the value of content marketing - I would write an article, a huge rush of attention would come in, and traffic would fall back to a slightly increased baseline. If I wrote nothing in a month, around 1.5k visitors would still come check OnlineOrNot out.</p>
<p>In 2022, this trend continued as I started following <a href="https://codingweekmarketingweek.com/">Coding Week, Marketing Week</a> obsessively. I would work for one week on engineering for OnlineOrNot, then the following week I would write about what I did.</p>
<p>These days, if I write nothing, around 3.3k visitors still visit OnlineOrNot per month.</p>
<p>I noticed a similar effect with this blog - roughly 13k visitors come to <a href="https://maxrozen.com">MaxRozen.com</a> on the months I write nothing, and that number steadily grows the more I write.</p>
<p>My favorite trick for writing more is turning comments/tweets into articles. I'll notice I've repeated the same information a couple of times in several forum comments, paste the comment directly into my IDE, add some context, and immediately release it as an article. You're going to write comments anyway, may as well turn your useful comments into articles.</p>
<h2>Pay attention to opportunity cost</h2>
<p>As a good example of this, I started OnlineOrNot as a bunch of AWS Lambda functions, knowing that if no one used the service, it would cost me nothing, but if I succeeded, it would cost me a premium over a continuously running server.</p>
<p>Around June this year, I noticed that my AWS bill had grown to around $100 USD per month just for AWS Lambda, for something that could fit entirely within a single $5/mo VM.</p>
<p>While as an engineer you'd be tempted to immediately move it over, I realized I could deliver much more value to my customers in the week of engineering time a rewrite would take, than saving $95/mo.</p>
<p>This all changed once I launched a trial plan and suddenly a rush of users could cost me serious money, so in the end I rewrote and moved off AWS Lambda to save me from anxiety: <a href="https://onlineornot.com/on-moving-million-uptime-checks-onto-fly-io">On moving over a million uptime checks per week onto fly.io</a>.</p>
<h2>Register your damn business</h2>
<p>For a long time, I ran OnlineOrNot as a business using my Australian sole-trader tax registration, and mixed personal and business bank accounts.</p>
<p>This year I finally got my shit together and registered <a href="https://rozenmd.com/">ROZENMD</a> as a software development business here in France, and now there are business bank accounts and everything.</p>
<p>Being able to see all of your business expenses in one place, and not have to sift through credit card bills to figure out if something was a business expense is absolutely worth the bureaucracy.</p>
<h2>Black Friday can be 80% of yearly revenue</h2>
<p>Back in 2021, I wrote a <a href="https://rozenmd.gumroad.com/l/useEffect-by-example">book about a single function in React</a>. I actually didn't try particularly hard to sell it - this blog has a few articles about useEffect, and they link to the book for folks that want a single resource to finally understand useEffect.</p>
<p>This year for Black Friday/Cyber Monday I sent out a couple of emails putting the book on sale for 50% off and was completely blown away at how many people bought the book.</p>
<p>Something like 80% of the year's revenue came from two emails I sent.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Managing your Node.js installation]]></title>
            <link>https://maxrozen.com/managing-your-node-installation</link>
            <guid>https://maxrozen.com/managing-your-node-installation</guid>
            <pubDate>Mon, 05 Dec 2022 05:52:00 GMT</pubDate>
            <description><![CDATA[If you're finding yourself manually installing different versions of Node, you need to know there's a *much* better way.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/managing-your-node-installation">clicking here</a>.)</div><p>Ever get weird errors about unsupported features when running your React app's dev server? Chances are your codebase is written for a specific version of Node, and you don't have it installed.</p>
<p>Or perhaps you know which version of Node each project uses, and you manually install a specific version of Node before getting started.</p>
<p>There's a few tools we can use to manage this for us!</p>
<h2>nvm - node version manager</h2>
<p><a href="https://github.com/nvm-sh/nvm">Node version manager</a> is one of the more popular ways to manage your local Node version when developing.</p>
<p>You install a new version of Node by running <code>nvm install 18</code> in your Terminal, and swap Node versions by running <code>nvm use 18</code> (or whichever version of Node you want to use).</p>
<p>You can also lock the version of Node your project uses by including an <code>.nvmrc</code> file in your project's root, containing the version of Node you want everyone to use.</p>
<p>The downside to using nvm is that you tend to forget which version of Node you're using, as it doesn't automatically change versions between projects. The fix is to setup up a script in your Terminal to automatically search for an <code>.nvmrc</code> file and change Node versions when you change directory, but other tools do this automatically, like <code>volta</code>.</p>
<h2>Volta</h2>
<p><a href="https://docs.volta.sh/guide/getting-started">Volta</a> does things a little differently. Firstly, it's written in Rust, so changing versions should be <em>very</em> fast, and secondly, it's designed to get out of your way - so you don't even need to manually change Node versions when switching projects.</p>
<p>To do this, you need to run <code>volta pin node@18</code> in each of your projects, and it'll update your package.json file to add config that volta uses to automatically switch versions.</p>
<p>I personally use this at work and it feels like magic. The only annoying thing I've noticed is how Volta handles global installs. You can run <code>npm install -g wrangler</code> for example, but if you want to update it afterwards, confusingly, you have to run <code>volta install wrangler</code>.</p>
<h2>pnpm</h2>
<p>You might know <a href="https://pnpm.io/">pnpm</a> as a ridiculously fast package manager that supports monorepos, but it also <a href="https://pnpm.io/cli/env">manages your node version</a>.</p>
<h2>asdf</h2>
<p>If you use more than one runtime, you might be interested in <a href="https://asdf-vm.com/">asdf</a> as an all-in-one tool for managing your node, java, ruby (and more) runtime versions.</p>
<p>asdf also supports automatically updating Node versions via <code>.nvmrc</code> and <code>.node-version</code> files - described in <a href="https://github.com/asdf-vm/asdf-nodejs#nvmrc-and-node-version-support">asdf-nodejs</a>.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How I became a React developer]]></title>
            <link>https://maxrozen.com/how-i-became-react-developer</link>
            <guid>https://maxrozen.com/how-i-became-react-developer</guid>
            <pubDate>Tue, 29 Nov 2022 22:50:56 GMT</pubDate>
            <description><![CDATA[On how I left a 'prestigious' consulting job to start from scratch as a React developer]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-i-became-react-developer">clicking here</a>.)</div><p>I started my career having finished a double Bachelor's degree in Software Engineering and Commerce, thinking I could <em>just</em> get a finance job, and automate the boring parts with my Python skills.</p>
<p>Then reality hit. The European debt crisis was in full swing, and Sydney, Australia was a backwater for finance jobs (especially as an average student).</p>
<p>Roughly a year of job hunting later (including one call back, in which the recruiter asked me why I didn't go back to university and get a degree in accounting if I wanted to work in finance), I realised I should probably rewrite my resume to be more Software Engineering focused.</p>
<p>Eventually a friend from university suggested I pitch his hedge fund a dashboard built in <a href="https://d3js.org/">D3.js</a> - this cool new technology (at the time) that made their Excel dashboards look ancient.</p>
<p><img src="d3-dashboard.png" alt="D3 Dashboard"></p>
<h2>So I started learning JavaScript</h2>
<p>I spent a week learning just enough JavaScript to make a convincing dashboard of all stocks trading on the ASX 200, with data fetching from Yahoo Finance. I didn't get the gig, but I ended up getting the attention of the agency that worked for the hedge fund.</p>
<p>I ended up working for the agency, building data pipelines, learning more Python, building sites in Django and picking up more JavaScript via angular.js. After about a year, I started longing for more, so I started interviewing with the Big Four Consulting Firms (EY, PwC, Deloitte, KPMG), and found myself with an offer for a role at Ernst and Young (EY).</p>
<p>I was so keen for a "big name" on my resume, I was willing to take a 10% pay cut to work there.</p>
<h2>So I became a consultant</h2>
<p>The consulting job didn't quite turn out as planned.</p>
<p>You had to wear a suit, most clients already hated you before meeting you (I assume due to the cost of hiring the firm). To top it off, it felt weird being told to perform manual tasks in ancient drag-and-drop software with your team after having spent a year writing Python to automate that sort of work.</p>
<p>I'm told since then, the latest advancement is that they're using a sort of scripting language (they're selling it as Robotic Process Automation or RPA to clients) to automate away the manual work. I'm still glad I didn't stick around.</p>
<p>I started hatching a plan to escape after a few months. I wanted to go back to being a software engineer, but jobs in Python were rare in Sydney.</p>
<h2>So I started learning React</h2>
<p>I had written enough JavaScript to know React was probably going to be a big deal, so I hit the tutorials. I found the official documentation at the time to be quite dense, it almost felt like you needed to already know React to be able to finish the tutorial.</p>
<p>A couple of tutorials really stood out, (and are amazingly still online):</p>
<ul>
<li><a href="https://github.com/kay-is/react-from-zero">React From Zero</a></li>
<li><a href="https://github.com/the-road-to-learn-react/the-road-to-learn-react">The Road to React</a> - I used the pre-hooks version (since they didn't exist at the time)</li>
</ul>
<p>After the tutorials, I started building side projects in React and GraphQL while hunting for a React job.</p>
<p>It took about four months to find a job, but what I found almost doubled my salary.</p>
<h2>So I became an entrepreneur</h2>
<p>I never really stopped building side projects while employed, mainly out of imposter syndrome, as I felt I needed to catch up to my colleagues to be a useful member of the team. I wrote about how that's going for me <a href="/2021-strangers-paid-my-macbook">here</a>.</p>
<p>I personally wouldn't recommend it, unless you're <em>really good</em> at managing your own psychology, particularly around burn-out, self-talk, and managing your own expectations. I eventually learned those skills, but it was a bit of a rollercoaster along the way.</p>
<p>Over the years, I've built:</p>
<ul>
<li>a job board</li>
<li>an appointment scheduler</li>
<li>a room booking system</li>
<li>a GraphQL snapshot monitoring service</li>
<li>a REST API monitoring service</li>
<li>a frontend performance monitoring service</li>
<li>an uptime monitoring service (<a href="https://onlineornot.com">OnlineOrNot</a> - I'm currently working on this one)</li>
</ul>
<p>After a few years of being a React developer I finally got a frontend job at Atlassian, followed by a software engineering job at Cloudflare.</p>
<h2>What's next?</h2>
<p>I still think React (or something like it) is going to remain, and simultaneously be, the next big thing. The numbers tend to agree with me, <a href="https://www.hntrends.com/2021/dec-remote-work-trend-still-climbing.html?compare=AngularJS&#x26;compare=Ember&#x26;compare=React&#x26;compare=Vue">React was #1 for hiring on Hacker News in 2016, 2017, 2018, 2019, 2020, and 2021</a>.</p>
<p>Learning React has greatly improved my quality of life and my job prospects, so I want to help others do what I did. I tend to write about common pain points that folks experience while using React (most recently that has been useEffect), and how to overcome them.</p>
<p>I use my newsletter to share what I'm working on, so you can <a href="/newsletter">follow me there</a> if you'd like to see it first.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Learn Integration Testing with React Hook Form]]></title>
            <link>https://maxrozen.com/learn-integration-testing-react-hook-form</link>
            <guid>https://maxrozen.com/learn-integration-testing-react-hook-form</guid>
            <pubDate>Tue, 15 Nov 2022 09:48:48 GMT</pubDate>
            <description><![CDATA[Testing is super important, and yet it's hard to find practical examples to show you what to do. This article intends to fix that, by teaching integration testing with React Hook Form.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/learn-integration-testing-react-hook-form">clicking here</a>.)</div><p>Do you sometimes worry that your tests don't make sense? Struggling to <em>get</em> what people mean by "test from the user's perspective" and the classic piece of advice "test functionality, not implementation details"?</p>
<p>You're not alone!</p>
<p>I felt the same thing when I started using React Testing Library. I used to think of testing as just checks you had to do on individual components, directly asserting against props and state, and would struggle to think about how to test the integration of several components together.</p>
<h2>What does "integration test" even mean?</h2>
<p>It helps to think of integration testing like a bigger unit test, except the unit you're testing is the combination of several smaller components.</p>
<p>More concretely, instead of just testing a <code>Button</code> component, or a <code>TextField</code> component in isolation, we're going to test that they work when placed together into a form.</p>
<h2>Let's get started!</h2>
<p>We're going to be testing a form almost every public web app you're going to build has: a Login form. It's probably one of the most important parts of your app (in terms of business value), so let's be confident it actually works!</p>
<p><img src="/assets/understanding-integration-testing-react/react-login-form.png" alt="React Login Form"></p>
<h2>Setup</h2>
<p>We're going to be using <a href="https://reactjs.org/docs/create-a-new-react-app.html">create-react-app</a>, because it comes bundled with <a href="https://testing-library.com/docs/react-testing-library/intro">@testing-library/react</a>. I'm also using <a href="https://react-hook-form.com/">react-hook-form</a> to build our form, because it's the fastest way I know to build a form in React apps.</p>
<h3>Steps</h3>
<ol>
<li>
<p>Clone the <a href="https://github.com/rozenmd/react-integration-testing">repo</a></p>
</li>
<li>
<p>Run:</p>
<pre><code class="language-bash">yarn start
</code></pre>
<p>You should see something like this: <img src="/assets/understanding-integration-testing-react/react-login-form-before.png" alt="React Login Form after starting"></p>
</li>
<li>
<p>At this point, if you ran <code>yarn test</code>, you would see the following:</p>
</li>
</ol>
<pre><code> PASS  src/pages/Login.test.js
  ✓ integration test (177ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.134s
Ran all test suites.

Watch Usage: Press w to show more.
</code></pre>
<h2>So how do we get here?</h2>
<p>First off, here's the code from our integration test:</p>
<pre><code class="language-jsx">import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import Login from './Login';

test('integration test', async () => {
  const USER = 'some-username';
  const PASS = 'some-pass';

  render(&#x3C;Login />);

  const userInput = screen.getByLabelText(/username/i);
  user.type(userInput, USER);
  const passwordInput = screen.getByLabelText(/password/i);
  user.type(passwordInput, PASS);
  const submitButton = screen.getByText(/submit/i);

  fireEvent.click(submitButton);
  expect(await screen.findByText(/your username/i)).toBeInTheDocument();
  expect(await screen.findByText(/your password/i)).toBeInTheDocument();
});
</code></pre>
<p>It might look like we're doing a few complicated things above, but essentially, we've taken the steps a user takes when logging in, and turned it into a test:</p>
<ol>
<li>Load the Login screen</li>
<li>Click on the username input, and type the username</li>
<li>Click on the password input, and type the password</li>
<li>Click the Submit button</li>
<li>Wait around for some sign that the Login worked</li>
</ol>
<p>Some testing-library specific things worth calling out:</p>
<ul>
<li>We import <code>'@testing-library/user-event'</code> to let us to type into our inputs</li>
<li>We import <code>fireEvent</code> from the <code>'@testing-library/react'</code> library to let us to click on our <code>Button</code> component</li>
<li>We've marked the test <code>async</code> to enable us to use <code>findByText()</code>
<ul>
<li>We use <code>findByTest()</code> after performing an action that may be asynchronous - like filling out a form. (findByText returns a Promise, letting us await until it finds the text it's looking for before continuing)</li>
</ul>
</li>
</ul>
<p>We've built a test that can type into our <code>TextField</code> components, click on our <code>Button</code> component, and trigger the <code>Form</code> component's <code>onSubmit</code> function, without ever referring to implementation details!</p>
<p>If you're confused about <code>findByText</code> vs <code>getByText</code>, don't worry - that's normal. In general though, findBy functions are for use after async actions (like clicking the Submit button), and getBy functions are for general use.</p>
<p>React Testing Library also has a <a href="https://testing-library.com/docs/react-testing-library/cheatsheet">cheatsheet</a> with tips to help you decide which one to use.</p>
<h2>Conclusion</h2>
<p>You've just started to understand integration testing, but you best believe there's a lot more to it than this article!</p>
<p>If you want a more advanced perspective of integration testing your forms, I highly recommend reading the testing section of React Hook Form's <a href="https://react-hook-form.com/advanced-usage/#TestingForm">Advanced Usage</a> guide.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Strangers from the internet paid for my MacBook Air: on my 4th year of indiehacking]]></title>
            <link>https://maxrozen.com/2021-strangers-paid-my-macbook</link>
            <guid>https://maxrozen.com/2021-strangers-paid-my-macbook</guid>
            <pubDate>Thu, 16 Dec 2021 05:52:00 GMT</pubDate>
            <description><![CDATA[In which I finally make money from strangers off the internet.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/2021-strangers-paid-my-macbook">clicking here</a>.)</div><p>At the end of each year I take the time to reflect on how I progressed towards my goal of being self-sufficient as a web developer, here are the previous editions of this article:</p>
<ul>
<li><a href="/indiehacking-3-year-review">(2020) Indiehacking: a review of my 3rd year</a></li>
<li><a href="/2019/12/29/2019-further-reflections-trying-to-start-an-internet-business/">2019: Further reflections on trying to start an internet business</a></li>
<li><a href="/2018/12/31/2018-review-starting-an-internet-business/">2018: Reflections on trying to start an internet business</a></li>
</ul>
<p>I started 2021 <em>knowing</em> I was going to make money on the internet if I stuck to consistent technical writing. Thanks to 30x500 (a course on selling products), I learned the value of consistency, and the power of giving away free content as a means of building trust at scale.</p>
<p>At some point in February, a recruiter reached out to ask if I wanted to write articles full-time for their SaaS to help drive their content marketing growth engine.</p>
<p>I thought about their offer for a solid day or so before realising that I had built a SaaS before, why not just use my content marketing skills to drive traffic to one of my own products?</p>
<p>And so my end of year wrap-up post is <strong>yet again</strong> about how I built a SaaS (my 7th attempt at building a bootstrapped SaaS for those of you counting at home).</p>
<p>Keen to see what happened? Read on, dear reader.</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#will-write-technical-content-for-cash">Will write technical content for cash</a>
<ul>
<li><a href="#book-1">Book 1</a></li>
<li><a href="#book-2">Book 2</a></li>
</ul>
</li>
<li><a href="#so-i-wrote-another-saas">So I wrote another SaaS</a>
<ul>
<li><a href="#picking-a-name">Picking a name</a></li>
<li><a href="#the-approach">The approach</a></li>
<li><a href="#how-it-went">How it went</a></li>
</ul>
</li>
<li><a href="#total-internet-revenue-for-the-year">Total Internet Revenue for the year</a></li>
<li><a href="#advice-for-fellow-makers">Advice for fellow makers</a></li>
</ul>
<h2>Will write technical content for cash</h2>
<h3>Book 1</h3>
<p>At the start of the year, I had just finished writing an <a href="https://rozenmd.gumroad.com/l/useEffect-by-example">ebook on React's useEffect hook</a>, and I was ready to launch.</p>
<p>On January 6, I launched to about 500 email subscribers, and uhh other things happened that day to distract people from some guy on the internet selling a single resource to learn useEffect.</p>
<p>I ended up making 27 total sales, and $301.84 USD.</p>
<p>I was actually pretty satisfied - in 2020 I made around $100 USD over the entire year from publishing my React articles on Medium, and this only took a few weeks to do.</p>
<p>Throughout the rest of the year, without trying particularly too hard to market/sell the book, I ended up making $721.94 USD, after fees.</p>
<h3>Book 2</h3>
<p>In mid-February, I decided I would give away an <a href="https://rozenmd.gumroad.com/l/beginners-guide-to-react-testing">ebook on React testing for beginners</a>.</p>
<p>I was still writing and releasing articles about React almost every two weeks. I noticed I was getting decent traffic on some of my articles about React testing, and realised React could use a resource for beginners to get started.</p>
<p>Over one particularly caffeinated weekend I combined all of my React testing articles into a single book, polished it up, made a bunch of codesandboxes for people to play with, and put it up on Gumroad with "pay what you want pricing", and ended up having 1353 people download it, and I made $143.99 USD in donations after fees.</p>
<p>As I wrap up the year, I made $865.93 USD from technical writing alone.</p>
<h2>So I wrote another SaaS</h2>
<p>As I mentioned earlier, I decided to write another SaaS after realising consistent content marketing could be valuable for driving traffic.</p>
<p>As well as that, I found my technical writing business to be incredibly manual and repetitive - I would write articles to get email subscribers, write more articles to send them to email subscribers, and eventually write an info product to sell to my most interested subscribers while continuing to write new articles for my email subscribers.</p>
<p>My thinking at the time was that at least with a SaaS the product could continue to exist and maybe even pay for the content marketing with recurring revenue.</p>
<h3>Picking a name</h3>
<p>Over the years I've built several SaaS products, and kept their marketing sites online even after I shut down the SaaS behind them.</p>
<p>Only <a href="https://onlineornot.com/">OnlineOrNot</a> (my GraphQL API checker I built and killed in 2019) continued to slowly get enquiries after I shut it down (and not just from folks looking for GraphQL API monitoring, but web monitoring in general).</p>
<p>It didn't take me long to realise that:</p>
<ul>
<li>There was a problem to be solved here</li>
<li>I had personally experienced the problem</li>
<li>I had leads for potential customers from my professional network</li>
<li>I could probably find people willing to pay for uptime monitoring, even if it was the 200th alternative to Pingdom</li>
</ul>
<p><em><strong>Side note</strong> - on doing something other people are doing</em>: I was not the first person to write about React, and yet people were still interested in what I had to say. Similarly, I am not the first developer to try to solve uptime monitoring, but my bet is that my take on the problem is unique enough that others will find it valuable.</p>
<h3>The approach</h3>
<p>I gave myself one week to ship.</p>
<p>It took me a few hours to have a simple webpage where people could enter their URL, and check if it was online from 12 cities around the world.</p>
<p>I shipped that simple webpage, and showed the internet. A few people followed me on Twitter out of curiosity, and I had a <a href="https://twitter.com/RozenMD/status/1364881512500404224?s=20">twitter thread</a> going to keep track of my progress, and I still use that same twitter thread today for progress updates.</p>
<p>By the end of the week, I had:</p>
<ul>
<li>
<p>email-only authentication and Google login via OAuth2.0:
<img src="/assets/2021-strangers-paid-my-macbook/login.jpeg" alt="OnlineOrNot v0 login"></p>
</li>
<li>
<p>a form to add URLs:
<img src="/assets/2021-strangers-paid-my-macbook/onlineornot-add-form-v0.jpeg" alt="OnlineOrNot V0 Form"></p>
</li>
<li>
<p>a dashboard to see your URLs being monitored:
<img src="/assets/2021-strangers-paid-my-macbook/onlineornot-dashboard-v0.jpeg" alt="OnlineOrNot V0 Dashboard"></p>
</li>
</ul>
<p>By the end of the second week, I added:</p>
<ul>
<li>a Stripe integration to accept payments
<img src="/assets/2021-strangers-paid-my-macbook/stripe-payments.jpeg" alt="OnlineOrNot Stripe Integration"></li>
</ul>
<p>After about five weeks of development, I had my first paying customer:</p>
<p><img src="/assets/2021-strangers-paid-my-macbook/first-paying-customer.png" alt="OnlineOrNot First Paying Customer"></p>
<p>I kept shipping, and I kept updating that Twitter thread, and I kept writing about my approach to product development, while also writing useful articles for my audience.</p>
<h3>How it went</h3>
<p>I'll save you the excessive details, as I've already written about it here:</p>
<ul>
<li><a href="https://onlineornot.com/building-saas-in-one-week-how-built-onlineornot">Building a SaaS in one week: How I built OnlineOrNot</a></li>
<li><a href="https://onlineornot.com/what-learned-running-saas-for-year">What I learned running a SaaS for a year</a></li>
</ul>
<p>The gist of my approach is shipping the absolute minimal viable feature as soon as possible, using content marketing to drive traffic to my site, and working approximately two hours a day before work.</p>
<p>I'm ending the year with OnlineOrNot earning an ARR of $3000 USD (having gained back some MRR I lost recently), and I've used some of that revenue to pay for a MacBook Air M1 (effectively bought thanks to strangers on the internet), which I'm using to ship things even faster.</p>
<p>Prior to buying the MacBook Air I was writing all my code and articles on a 2011 13" MacBook Pro, and while I appreciated the heat in winter, it was SLOW.</p>
<h2>Total Internet Revenue for the year</h2>
<p>Technical writing brought in $865.93 USD, and Stripe tells me that OnlineOrNot brought in $1989 USD between April and the day I wrote this article, so about $2854 USD for the year in total, entirely from strangers on the internet.</p>
<p>Now you might be thinking, "if you put in three months of writing effort for $865.93, if you kept going for the whole year you'd have almost $3500", which is a fair point, but writing revenue isn't necessarily recurring.</p>
<p>I managed to have a burnout, not work on OnlineOrNot for nearly two months while I got married and moved my entire life from Sydney, Australia to France during a pandemic, and OnlineOrNot just kept monitoring my customers' sites, and bringing in revenue. Of course the business didn't grow while I was away, but it didn't fall to pieces either.</p>
<h2>Advice for fellow makers</h2>
<p>Don't try to do a SaaS, especially not for your first paid product.</p>
<p>Actually writing code is perhaps 10% of the total work of running your own SaaS. If you just want to write code, consider selling a code solution to a common problem instead (see Tailwind UI or Divjoy as an example).</p>
<p>Selling people an info product once is <strong>significantly</strong> easier than trying to sell them a subscription.</p>
<p>Once you understand how to sell someone a thing once, it becomes significantly easier to understand how to sell someone a subscription.</p>
<p>Once you <strong>do</strong> start building a SaaS, make sure it's a problem you <em>actually</em> care about, because you'll be thinking about the problem space for years to come.</p>
<p>Similarly, make sure you're solving the problem for a group of people you <em>actually</em> care about, because you'll be working with the same group of people for years to come.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[@forge/api: Cannot read properties of undefined (reading ‘fetch’)]]></title>
            <link>https://maxrozen.com/forge-api-cannot-read-properties-fetch</link>
            <guid>https://maxrozen.com/forge-api-cannot-read-properties-fetch</guid>
            <pubDate>Sun, 02 May 2021 11:52:00 GMT</pubDate>
            <description><![CDATA[How to fix a cannot read properties of undefined error in Forge]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/forge-api-cannot-read-properties-fetch">clicking here</a>.)</div><h2>The Problem</h2>
<p>So you're working on a Custom UI Forge App, thinking it'd be cool to use the <a href="https://developer.atlassian.com/platform/forge/runtime-reference/storage-api/">storage</a> API.</p>
<p>You import storage like so: <code>import { storage } from "@forge/api";</code>, try to use it, similar to this:</p>
<pre><code class="language-jsx">// some-forge-app/static/hello-world/src/App.js
import { storage } from "@forge/api";
// more imports here...

const App () => {
    useEffect(()=>{
        const fetchData = async () => {
            await storage.set('test','someValue');
            const data = await storage.get('test);

            console.log('data: ', data);
        }

        fetchData();
    }, []);

    return &#x3C;div>test&#x3C;/div>
}
</code></pre>
<p>You build your app, run <code>forge deploy</code>, and get this scary runtime error:</p>
<p><img src="/assets/forge-api-cannot-read-properties-fetch/fetch-undefined.png" alt="Forge fetch undefined error message"></p>
<h2>The Fix</h2>
<p>You can't actually import from <code>@forge/api</code> directly in Custom UI apps.</p>
<p>Instead you need to define a resolver function (in Forge template apps, a basic one is provided in <code>src/index.js</code>), like so:</p>
<pre><code class="language-js">// src/index.js
import Resolver from '@forge/resolver';
import { storage } from '@forge/api';

const resolver = new Resolver();

resolver.define('get', async ({ payload, context }) => {
  const data = await storage.get(payload.key);
  console.log('data fetched: ', data);
  return data;
});
</code></pre>
<p>Then in your Custom UI app, you add an import for <code>@forge/bridge</code>'s invoke function and call it in your component like so:</p>
<pre><code class="language-js">// some-forge-app/static/hello-world/src/App.js
import { invoke } from '@forge/bridge';
// ... the rest of your React component here
const data = await invoke('get', { key: saveKey });
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Keeping up with React Libraries]]></title>
            <link>https://maxrozen.com/keeping-up-with-react-libraries</link>
            <guid>https://maxrozen.com/keeping-up-with-react-libraries</guid>
            <pubDate>Mon, 08 Feb 2021 09:48:48 GMT</pubDate>
            <description><![CDATA[React libraries are hard to keep track of. This article attempts to put the ones worth talking about on one page.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/keeping-up-with-react-libraries">clicking here</a>.)</div><p>It's no secret React has a library discoverability problem.</p>
<p>While the number of stars in GitHub and weekly downloads in npm might be a good starting point for finding quality libraries, normally you have to wade through a lot of reddit, hacker news, dev.to and individual blog posts to find the best ones.</p>
<p>In this (continually updated) article, I'll be adding libraries (excluding component libraries, I track those <a href="https://maxrozen.com/guide-to-component-ui-libraries-react/">here</a>) worth talking about on a single page.</p>
<h2>Table of Contents</h2>
<ul>
<li><a href="#table-of-contents">Table of Contents</a>
<ul>
<li><a href="#3d">3D</a></li>
<li><a href="#accessibility">Accessibility</a></li>
<li><a href="#animation">Animation</a></li>
<li><a href="#browser-features">Browser Features</a></li>
<li><a href="#data-fetching-libraries">Data Fetching Libraries</a></li>
<li><a href="#data-visualisation">Data Visualisation</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#mocking-apis">Mocking APIs</a></li>
<li><a href="#state-management">State Management</a></li>
<li><a href="#testing">Testing</a></li>
<li><a href="#videos">Videos</a></li>
</ul>
</li>
</ul>
<h3>3D</h3>
<p><a href="https://github.com/pmndrs/react-three-fiber">react-three-fiber</a> is a React renderer for threejs on the web and react-native.</p>
<p>Chances are, if you've seen really cool 3D animations on a website, it was probably built in three.js - react-three-fiber gives you a way to tap into React while building your 3d scenes. There's also a <a href="https://github.com/pmndrs/react-three-next">Next.js + Tailwind starter</a> worth looking into.</p>
<h3>Accessibility</h3>
<p><a href="https://react-spectrum.adobe.com/react-aria/getting-started.html#installation">React Aria</a></p>
<p>React Aria provides you with Hooks that provide accessibility for your components, so all you need to worry about is design and styling. Particularly useful for those building design systems.</p>
<p>Example usage - <a href="https://react-spectrum.adobe.com/react-aria/useButton.html">useButton</a>:</p>
<pre><code class="language-jsx">import { useButton } from '@react-aria/button';

function Button(props) {
  let ref = React.useRef();
  let { buttonProps } = useButton(props, ref);

  return (
    &#x3C;button {...buttonProps} ref={ref}>
      {props.children}
    &#x3C;/button>
  );
}

&#x3C;Button onPress={() => alert('Button pressed!')}>Press me&#x3C;/Button>;
</code></pre>
<h3>Animation</h3>
<p>Animation adds soul to otherwise boring things. These libraries let you build the web app equivalent of <a href="https://www.youtube.com/watch?v=PGKmexNTHNE">Pixar's Intro Animation</a>, but in React.</p>
<p>Both libraries have similar APIs and support spring physics over time-based animation, though Framer Motion seems to be used more often on GitHub.</p>
<p><a href="https://www.framer.com/motion/">Framer Motion</a></p>
<p>Framer Motion is an animation and gesture library built by <a href="https://www.framer.com/">Framer</a>. The added benefit of Framer Motion is that your designers can build animations in Framer, then hand-off designs to be accurately implemented using Framer's own library.</p>
<p><a href="https://www.react-spring.io/">React Spring</a></p>
<p>React Spring uses spring physics rather than time-based animation to animate your components. Relative to Framer Motion, React Spring has been under development for longer, with a greater number of open-source contributors.</p>
<h3>Browser Features</h3>
<p>Ever been asked to implement random features that someone on the product team saw on another website and thought was cool? These libraries save you time on building those features.</p>
<p><a href="https://github.com/CharlesStover/use-clippy">useClippy</a></p>
<p>useClippy is a React hook that lets you read and write to your user's clipboard. Particularly useful for improving UX, letting you save your users from double clicking on your data fields, by providing them a button to copy values.</p>
<p><a href="https://www.npmjs.com/package/react-player">ReactPlayer</a></p>
<p>ReactPlayer is an awesome library that lets you embed video from major sources (YouTube, Facebook, Twitch, SoundCloud, and more), and define your own controls to the video.</p>
<p><a href="https://github.com/fkhadra/react-toastify">React Toastify</a></p>
<p>React Toastify allows you to add fancy in-app notifications (like the "Message Sent" notification in Gmail) to your React app with only four additional lines of code.</p>
<h3>Data Fetching Libraries</h3>
<p>You might be wondering why you'd even need a data fetching library, when you could use <code>useEffect</code> and <code>fetch()</code>. The short answer is that these libraries also handle caching, loading and error states, avoiding redundant network requests, and much more.</p>
<p>You could spend hundreds of hours implementing these features in a Redux-like state manager, or just install one of these libraries.</p>
<p><a href="https://github.com/tannerlinsley/react-query">React Query</a></p>
<p>React Query lets you request data from the same API endpoint (for example <code>api/users/1</code>) across multiple components, without resulting in multiple network requests.</p>
<p><a href="https://github.com/vercel/swr">SWR</a></p>
<p>Similar to React Query (in fact, based on the same premise, see this <a href="https://github.com/vercel/swr/issues/127">issue</a> for more info), SWR is another data fetching library worth checking out. SWR has the added security of being used by Vercel in production as part of their platform.</p>
<h3>Data Visualisation</h3>
<blockquote>
<p>Everyone wants to have beautiful charts, but nobody wants to learn no complicated-ass D3</p>
</blockquote>
<ul>
<li>Ronnie Coleman, probably</li>
</ul>
<p><a href="https://github.com/airbnb/visx">visx</a></p>
<p>If you've ever used a popular charting library such as Recharts or Charts.js, you'll know it's surprisingly easy to reach the limits of what a charting library can do for you.</p>
<p>visx is different, in that it provides you with lower-level React components that are much closer to D3 than other charting libraries. This makes it easier to build your own re-usable charting library, or hyper-customised charts.</p>
<h3>Forms</h3>
<p>Forms suck. Take it from someone who once had to build a "smart" form with 26 possible fields to fill out - you want to pass off <strong>as much as possible</strong> to your form library, leaving you with only quick field names to enter.</p>
<p><a href="https://react-hook-form.com/">React Hook Form</a></p>
<p>React Hook Form is different to other form libraries, in that by default, you're not building controlled components and watching their state. This means your app's performance won't get slower as you add more fields to your form.</p>
<p>On top of that, it's probably one of the best documented libraries out there - every example has a CodeSandbox, making it easy to fork and try out your particular use case.</p>
<h3>Mocking APIs</h3>
<p>You could probably argue mocking libraries belong more under testing utilities, but they have added benefits during development too. While not specifically tied to React, they can be extremely useful when you want dynamic responses from your mocked endpoints.</p>
<p><a href="https://mswjs.io/">Mock Service Worker</a></p>
<p>MSW is a library that lets you mock both REST and GraphQL APIs, that uses service workers to generate actual network requests.</p>
<p>Why is that cool?</p>
<p>You can actually use MSW as part of your development workflow regardless of technology choice, <strong>not only</strong> while testing! It's so cool that Kent C. Dodds <a href="https://kentcdodds.com/blog/stop-mocking-fetch">wrote a whole article about it</a>.</p>
<p>In short, you can use MSW to experiment with building frontend features before the backend is ready AND mock your backend for tests.</p>
<h3>State Management</h3>
<p>There's been a fair bit of innovation in state management since the early days of Redux, it's worth taking a look again if you're interested in using global state.</p>
<p><a href="https://github.com/pmndrs/jotai">Jotai</a></p>
<p>Jotai describes itself as a primitive state management solution for React, and a competitor to Recoil. It's quite minimalist, meaning less API to learn, and if you understand React's <code>useState</code> hook, you'll probably understand Jotai's <code>useAtom</code> hook.</p>
<p><a href="https://mobx.js.org/README.html">Mobx</a></p>
<p>Mobx is a global state management library that requires a bit of a mental model shift to get used to. Once you <em>get</em> it though, Mobx provides a solution to state management and change propagation without the boilerplate.</p>
<p><a href="https://recoiljs.org/docs/introduction/motivation">Recoil</a></p>
<p>Recoil is a state management library - think Redux meets React Hooks, minus the boilerplate.</p>
<p><a href="https://redux-toolkit.js.org/">Redux Toolkit</a></p>
<p>Redux Toolkit (or RTK), is the official, opinionated way to manage your state using Redux.</p>
<p>RTK greatly reduces the amount of boilerplate necessary for using Redux, provides sensible defaults, and keeps the same immutable update logic that we know and love.</p>
<p><a href="https://github.com/davidkpiano/xstate">xstate</a></p>
<p>XState is a library that lets you formalise your React app as a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">finite state machine</a>.</p>
<p>State machines aren't a particularly new concept, but developers have only recently started to realise that maybe our apps could be less buggy if we explicitly define the states they can be in, and the inputs required to transition between states.</p>
<p>XState also generates <a href="https://xstate.js.org/viz/?gist=bbcb4379b36edea0458f597e5eec2f91">charts</a> for you based on your app's xstate configuration, meaning your documentation will stay up to date as you code.</p>
<p><a href="https://github.com/pmndrs/zustand">Zustand</a></p>
<p>Zustand is another state management library, that aims to be simple and un-opinionated. The main difference is that you use hooks as the main way to fetch state, and doesn't require peppering your app with context providers.</p>
<h3>Testing</h3>
<p><a href="https://testing-library.com/docs/react-testing-library/intro">React Testing Library</a></p>
<p>React Testing Library (RTL) has become kind of a big deal for testing in React (and other libraries/frameworks). It's now the default testing library that comes with create-react-app.</p>
<p>RTL replaces Enzyme in your testing stack. While both libraries provide methods for rendering React components in tests, RTL exposes functions that nudge developers away from <a href="https://maxrozen.com/dont-test-implementation-details-react/">testing implementation details, and towards testing functionality</a>.</p>
<h3>Videos</h3>
<p>Videos?! You ask? Why yes, dear reader. There are now libraries for making videos in React.</p>
<p><a href="https://www.remotion.dev/">Remotion</a></p>
<p>Remotion lets you make videos of your React components rendering - whether that involves fetching data from an API and displaying it, or showing cool animations.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Demystifying useEffect's clean-up function]]></title>
            <link>https://maxrozen.com/demystifying-useeffect-cleanup-function</link>
            <guid>https://maxrozen.com/demystifying-useeffect-cleanup-function</guid>
            <pubDate>Tue, 02 Feb 2021 00:00:48 GMT</pubDate>
            <description><![CDATA[useEffect's clean-up function can be pretty confusing, especially if you're still trying to think in lifecycle methods. Let's clarify the clean-up function in this article.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/demystifying-useeffect-cleanup-function">clicking here</a>.)</div><p>When you're starting to write React hooks, it can be hard to understand what's going on with useEffect, particularly if you're trying to translate Hooks to lifecycle methods in class components.</p>
<p>You read everywhere that you <em>shouldn't</em> compare useEffect to lifecycle methods, but then where do you start?</p>
<p>Thankfully, not knowing how useEffect's clean-up function works isn't as bad as getting the <a href="/learn-useeffect-dependency-array-react-hooks">dependency array wrong</a>, or passing <a href="/stop-useeffect-running-every-render-with-usecallback">constantly redeclared functions into useEffect</a>.</p>
<p>That being said though, there are some nifty uses of the clean-up function that you should know about.</p>
<h2>useEffect's clean-up function doesn't just run once</h2>
<p>If you take nothing else away from this article, remember this: useEffect's clean-up function doesn't <em>just</em> run on unmount (assuming your dependency array isn't empty).</p>
<p>See this often overlooked sentence in the <a href="https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect">React Hooks API reference</a>:</p>
<blockquote>
<p>Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect</p>
</blockquote>
<h2>So when does clean-up run?</h2>
<p>useEffect's clean-up runs after the next render, before the next useEffect.</p>
<p>This might mess with your brain a little bit, but check out this example:</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function App() {
  const [state, setState] = useState(null);

  useEffect(() => {
    console.log('I am the effect');
    return () => {
      console.log('I run after re-render, but before the next useEffect');
    };
  });

  console.log('I am just part of render');
  return (
    &#x3C;>
      &#x3C;button
        onClick={() => {
          setState('Some v. important state.');
        }}
      >
        Click me
      &#x3C;/button>
      &#x3C;p>state: {state}&#x3C;/p>
    &#x3C;/>
  );
}
</code></pre>
<p>|                                                                                                                                 |
| :-----------------------------------------------------------------------------------------------------------------------------: |
| <em>This example is also available as a <a href="https://codesandbox.io/s/useeffect-cleanup-example-1qgw2?file=/src/App.js">CodeSandbox</a>.</em> |</p>
<p>When the above component first renders, in the console you see:</p>
<pre><code>> I am just part of render
> I am the effect
</code></pre>
<p>If you then click the button (triggering a re-render), the following lines are printed underneath:</p>
<pre><code>> I am just part of render
> I run after re-render, but before the next useEffect
> I am the effect
</code></pre>
<p>Even though the clean-up function is <em>running</em> in the new render, it still has the old prop values since it was <em>declared</em> in the previous render.</p>
<p>More concretely:</p>
<pre><code class="language-jsx">useEffect(() => {
  console.log('id: ', id);
  return () => {
    console.log('id: ', id);
  };
}, [props.id]);
</code></pre>
<ul>
<li><code>id</code> starts as <code>1</code>.</li>
<li>Component renders, displaying <code>id</code> as <code>1</code> in the UI</li>
<li>useEffect runs, calling <code>console.log</code> and prints <code>id: 1</code></li>
<li>Props change, setting <code>id</code> to <code>2</code></li>
<li>Component re-renders, displaying <code>id</code> as <code>2</code> in the UI</li>
<li>useEffect clean-up function fires, calling <code>console.log</code> and prints <code>id: 1</code></li>
<li>useEffect runs, calling <code>console.log</code> and prints <code>id: 2</code></li>
</ul>
<h2>What to actually use useEffect's clean-up functions for</h2>
<p>Honestly, it's pretty rare that I find a use for useEffect's clean-up function in my day-to-day work as I don't use subscriptions at work (so I never need to unsubscribe from connections in the clean-up function).</p>
<p>You <em>can</em> use it to avoid race conditions in async requests, which is pretty nifty.</p>
<p>From <a href="/race-conditions-fetching-data-react-with-useeffect">Fixing Race Conditions in React with useEffect</a>:</p>
<pre><code class="language-jsx">useEffect(() => {
  // highlight-next-line
  let active = true;

  const fetchData = async () => {
    const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
    const newData = await response.json();
    // highlight-next-line
    if (active) {
      setFetchedId(props.id);
      setData(newData);
    }
  };

  fetchData();
  // highlight-next-line
  return () => {
    // highlight-next-line
    active = false;
    // highlight-next-line
  };
}, [props.id]);
</code></pre>
<p>You can also use an AbortController to cancel your requests instead of a boolean flag, see <a href="https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect#useeffect-clean-up-function-with-abortcontroller">the example here</a>.</p>
<h2>Dedicated unmount hook</h2>
<p>While React doesn't have a dedicated unmount hook, you can always use useEffect's clean-up function with an empty dependency array:</p>
<pre><code class="language-jsx">import React, { useEffect } from 'react';

const SomeComponent => () => {
     useEffect(() => {
        return () => {
             // This code runs when the component unmounts
         }
     }, [])
     // The rest of your component goes here.
 }
</code></pre>
<p>I'm not sure why you'd want this (nor have I needed this), but it's worth knowing that it's possible.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[A Walkthrough of migrating MaxRozen.com from Gatsby to Next.js]]></title>
            <link>https://maxrozen.com/walkthrough-migrating-maxrozen-com-gatsby-to-nextjs</link>
            <guid>https://maxrozen.com/walkthrough-migrating-maxrozen-com-gatsby-to-nextjs</guid>
            <pubDate>Wed, 27 Jan 2021 00:00:48 GMT</pubDate>
            <description><![CDATA[I recently migrated MaxRozen.com from Gatsby to Next.js, in this article I'll detail the steps it took.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/walkthrough-migrating-maxrozen-com-gatsby-to-nextjs">clicking here</a>.)</div><p>I've been super keen on Gatsby.js since about 2017. Back then, I re-wrote this
blog from a clunky old Django site that required an AWS EC2 instance running
24/7 to Gatsby, and it was amazing.</p>
<p>Over time, I've come to notice that Gatsby's core doesn't actually do much, and
each project needs a different set of plugins to do what you want, and every new
project with Gatsby has completely different headaches, so you never actually
get better at using it (as a casual user).</p>
<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#background-info">Background info</a>
<ul>
<li><a href="#content-folder-structure">Content Folder Structure</a></li>
<li><a href="#plugins">Plugins</a></li>
</ul>
</li>
<li><a href="#migrating-to-next">Migrating to Next</a>
<ul>
<li><a href="#first-steps-with-nextjs">First steps with Next.js</a></li>
<li><a href="#folder-structure-and-file-naming">Folder structure and file naming</a></li>
<li><a href="#deleting-almost-everything-and-copying-my-pages-across">Deleting (almost) everything, and copying my pages across</a></li>
<li><a href="#gatsby-link-vs-nextjs-link">Gatsby Link vs Next.js Link</a></li>
<li><a href="#highlight-next-line-i-just-want-good-code-snippets-in-nextjs">//highlight-next-line: I just want good code snippets in Next.js</a></li>
<li><a href="#autolinking-headers-in-mdx">Autolinking headers in MDX</a></li>
<li><a href="#scrolling-to-an-id">Scrolling to an id</a></li>
<li><a href="#sitemap">Sitemap</a></li>
<li><a href="#rss-feed-in-nextjs">RSS Feed in Next.js</a></li>
</ul>
</li>
</ul>
<h2>Background info</h2>
<p>So for some background information, <a href="https://maxrozen.com">MaxRozen.com</a> is
where I write weekly articles about React, and occasionally launch products (my
most recent being <a href="https://useeffectbyexample.com">useEffectByExample.com</a>).</p>
<h3>Content Folder Structure</h3>
<p>When I used Gatsby, all of my content lived in a <code>content/</code> folder, with a
folder named after the date for each post, and the folder containing the
markdown and images for the post.</p>
<p>Basically, something like this:</p>
<pre><code class="language-js">content/
|-- 2021-01-01/
|   |-- index.md
|   |-- cool-image.jpg
|-- 2021-01-08/
|   |-- index.md
|   |-- some-other-image.jpg
</code></pre>
<p>Besides that, my site was thankfully pretty straightforward, I had:</p>
<ul>
<li>A single template for my articles</li>
<li>A few pages (home, articles/, newsletter/, uses/)</li>
</ul>
<p>For styling, I just used Emotion (CSS in JS) with Tailwind via twin.macro (you
can read more about that <a href="/using-tailwind-css-in-react-css-in-js">here</a>)</p>
<h3>Plugins</h3>
<p>I eventually settled on the following core Gatsby plugins for any of my content
sites:</p>
<ul>
<li>gatsby-plugin-canonical-urls</li>
<li>gatsby-plugin-emotion</li>
<li>gatsby-transformer-remark
<ul>
<li>gatsby-remark-autolink-headers
<ul>
<li>gatsby-remark-prismjs</li>
</ul>
</li>
</ul>
</li>
<li>gatsby-plugin-feed</li>
<li>gatsby-plugin-react-helmet</li>
<li>gatsby-plugin-no-javascript</li>
<li>gatsby-plugin-no-javascript-utils</li>
</ul>
<p>Since I've come to rely heavily on these plugins in Gatsby, I wanted to figure
out how to get the same functionality in Next.js.</p>
<p>If you're keen to find out how I did it, read on.</p>
<h2>Migrating to Next</h2>
<p>Since I was already using React, very little in my site actually needed to
change.</p>
<p>Most of my development effort was in replicating the plugins I loved from
Gatsby, and in having to figure out what assumptions code examples in Next.js
had (a few, it turns out).</p>
<h3>First steps with Next.js</h3>
<p>To get started with Next.js, I headed over to their
<a href="https://github.com/vercel/next.js/">repo</a>, and dove into the
<a href="https://github.com/vercel/next.js/tree/canary/examples">examples folder</a>. I
searched for "blog", and to my surprise found exactly the example I wanted:
<a href="https://github.com/vercel/next.js/tree/canary/examples/blog-starter-typescript">"blog-starter-typescript"</a>.</p>
<p>From there, I ran <code>create-next-app</code>:</p>
<pre><code class="language-bash">npx create-next-app --example blog-starter-typescript blog-starter-typescript-app
# or
yarn create next-app --example blog-starter-typescript blog-starter-typescript-app
</code></pre>
<p>The example came with Tailwind already, so all I had to do was install Emotion,
and follow the
<a href="https://github.com/ben-rogerson/twin.examples/tree/master/next-emotion">Next.js + Emotion guide</a>
provided by twin.macro to get the same CSS-in-JS + Tailwind setup I was using
with Gatsby.</p>
<h3>Folder structure and file naming</h3>
<p>The examples in Next.js' repo assume you keep your content like this:</p>
<pre><code class="language-js">content/
|-- your-articles-title.md
|-- some-other-title.md
</code></pre>
<p>Whereas I used to keep my content like this:</p>
<pre><code class="language-js">content/
|-- 2021-01-01/
|   |-- index.md
|   |-- cool-image.jpg
|-- 2021-01-08/
|   |-- index.md
|   |-- some-other-image.jpg
</code></pre>
<p>Since Gatsby had a plugin that could recursively dive through your content
folder to find all markdown files, my preferred setup worked.</p>
<p>Unfortunately not the case (out of the box) with Next.js, so I had to go through
and rename some files.</p>
<h3>Deleting (almost) everything, and copying my pages across</h3>
<p>With Tailwind and CSS in JS set up, all I had to do was copy over my pages and
templates, and delete all of Gatsby's GraphQL.</p>
<p>Whereas in Gatsby I had a <code>templates/article.js</code> file to control how my articles
looked, in Next.js if I didn't want my articles in a folder, I had to create a
Dynamic route: <code>pages/[slug].tsx</code>.</p>
<p>Dynamic routes get their content via <code>getStaticProps</code> and <code>getStaticPaths</code>
(described further
<a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation">here</a>
and
<a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation">here</a>).</p>
<p>After digging through how some of the examples used these two functions, it
<em>clicked</em>, and I figured out how to make it work for my blog:</p>
<ul>
<li>getStaticPaths can be as simple as looping through a directory, and making an
array of slugs</li>
<li>getStaticProps then takes the slug passed in via the URL (for this article,
the slug is <code>walkthrough-migrating-maxrozen-com-gatsby-to-nextjs</code>), and
fetches the content (and transforming MDX to HTML)</li>
</ul>
<h3>Gatsby Link vs Next.js Link</h3>
<p>One weird thing I noticed is a slight implementation difference between Gatsby's
<code>Link</code> and Next's <code>Link</code>: while Gatsby would effectively spread props, and pass
every prop you provide <code>Link</code> down, Next had a stricter API.</p>
<p>So while in Gatsby this worked:</p>
<pre><code class="language-jsx">const StyledLink = tw(Link)`
flex justify-center items-center text-gray-500 hover:text-gray-600
`

&#x3C;StyledLink to={'/'}>
  Home
&#x3C;/StyledLink>
</code></pre>
<p>In Next.js, I had to do this:</p>
<pre><code class="language-jsx">const StyledLink = tw.a`
flex justify-center items-center text-gray-500 hover:text-gray-600
`;

&#x3C;Link passHref href={'/'}>
  &#x3C;StyledLink>Home&#x3C;/StyledLink>
&#x3C;/Link>;
</code></pre>
<h3>//highlight-next-line: I just want good code snippets in Next.js</h3>
<p>It turns out that the code formatter in Gatsby was extremely smart, and required
very little actual setup. Funny how it takes giving something up to really
appreciate it.</p>
<p>For example, if you added <code>//highlight-next-line</code> to your code snippet, it would
make the line stand out. Like this:</p>
<pre><code class="language-jsx">useEffect(() => {
  const fetchData = async () => {
    // highlight-next-line
    const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
    const newData = await response.json();
    // highlight-next-line
    setData(newData);
  };

  fetchData();
  // highlight-next-line
}, [props.id]);
</code></pre>
<p>Because the markdown plugin could handle that for free, I never felt the urge to
try out MDX (in case you're wondering what MDX is:
<a href="https://mdxjs.com/">check out the docs</a>).</p>
<p>It turned out the "simplest" way to customise code formatting was to rename all
of my content from <code>.md</code> to <code>.mdx</code>, and use mdx-remote (see
<a href="https://github.com/vercel/next.js/tree/canary/examples/with-mdx-remote">example</a>).</p>
<p>Here's how I managed to customise my code snippets in Next.js:</p>
<ul>
<li>
<p>First I installed
<a href="https://github.com/FormidableLabs/prism-react-renderer">prism-react-renderer</a>.
It's a React component that lets you customise what the Prism.js code snippet
output looks like.</p>
</li>
<li>
<p>Then I added a Code component using prism-react-renderer:</p>
<pre><code class="language-jsx">//components/Code.js
import React from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';

const isHighlightedLine = (line, mark = '// highlight-next-line') =>
  line?.some((prevLine) => {
    return prevLine?.content === mark;
  });

const Code = ({ children, className }) => {
  const language = className?.replace(/language-/, '');

  return (
    &#x3C;Highlight
      {...defaultProps}
      code={children}
      language={language}
      theme={undefined}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        &#x3C;pre
          className={className}
          style={{ ...style }}
          data-language={language}
        >
          {tokens.map((line, i) => {
            const lineProps = getLineProps({ line, key: i });
            const classNameArr = [lineProps.className];

            if (isHighlightedLine(line)) {
              return null;
            }

            if (isHighlightedLine(tokens?.[i - 1])) {
              classNameArr.push('gatsby-highlight-code-line');
            }

            const finalLineProps = {
              ...lineProps,
              className: classNameArr.join(' '),
            };

            return (
              &#x3C;div key={i} {...finalLineProps}>
                {line.map((token, key) => (
                  &#x3C;span key={key} {...getTokenProps({ token, key })} />
                ))}
              &#x3C;/div>
            );
          })}
        &#x3C;/pre>
      )}
    &#x3C;/Highlight>
  );
};

export default Code;
</code></pre>
<p>(My site contains custom CSS that targets <code>gatsby-highlight-code-line</code>, and I
didn't want to change it for Next.js)</p>
</li>
<li>
<p>Then in my article template (<code>[slug].tsx</code>), I could override the default
<code>code</code> markup with my own:</p>
<pre><code class="language-jsx">const components = {
  Head,
  pre: (props: any) => &#x3C;div className="gatsby-highlight" {...props} />,
  code: Code,
  // ...
};
</code></pre>
</li>
</ul>
<h3>Autolinking headers in MDX</h3>
<p>What I really liked about my Gatsby site was how effortlessly plugins like
gatsby-remark-autolink-headers worked. You installed it, and bam: all headings
in your markdown content now let you grab the link to that heading.</p>
<p>To get the same behaviour in Next.js, I had to add the following:</p>
<ul>
<li>
<p>First, install <code>remark-slug</code> and <code>remark-autolink-headings</code>:</p>
<pre><code class="language-bash">yarn add remark-slug remark-autolink-headings
</code></pre>
</li>
<li>
<p>Then add them to your article template:</p>
<pre><code class="language-jsx">// `[slug].tsx`
// ...imports etc up here

//svgIcon shamelessly stolen from gatsby-remark-autolink-headers
const svgIcon = `&#x3C;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">&#x3C;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">&#x3C;/path>&#x3C;/svg>`;

//then when calling next-mdx-remote's renderToString:
const mdxSource = await renderToString(content, {
  components,
  // Optionally pass remark/rehype plugins
  mdxOptions: {
    remarkPlugins: [
      // highlight-next-line
      require('remark-slug'),
      [
        // highlight-next-line
        require('remark-autolink-headings'),
        {
          behavior: 'prepend',
          linkProperties: {
            'aria-label': `permalink`, // this could be more useful
            class: 'anchor before',
          },
          content: {
            type: `raw`,
            value: svgIcon,
          },
        },
      ],
    ],
    rehypePlugins: [],
  },
  scope: data,
});
</code></pre>
</li>
<li>
<p>To get the anchor to actually appear, I added the following to my global CSS:</p>
<pre><code class="language-css">.anchor.before {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-100%);
  padding-right: 4px;
}
.anchor.after {
  display: inline-block;
  padding-left: 4px;
}
h1 .anchor svg,
h2 .anchor svg,
h3 .anchor svg,
h4 .anchor svg,
h5 .anchor svg,
h6 .anchor svg {
  visibility: hidden;
}
h1:hover .anchor svg,
h2:hover .anchor svg,
h3:hover .anchor svg,
h4:hover .anchor svg,
h5:hover .anchor svg,
h6:hover .anchor svg,
h1 .anchor:focus svg,
h2 .anchor:focus svg,
h3 .anchor:focus svg,
h4 .anchor:focus svg,
h5 .anchor:focus svg,
h6 .anchor:focus svg {
  visibility: visible;
}
</code></pre>
</li>
</ul>
<h3>Scrolling to an id</h3>
<p>By default, Next.js doesn't provide anything to make scrolling to an id work (to
see what I mean, click
<a href="/walkthrough-migrating-maxrozen-com-gatsby-to-nextjs#scrolling-to-an-id">here</a>) -
<em>that</em> doesn't work by default.</p>
<p>To work around this, I could think of two options:</p>
<ol>
<li>Use useEffect to scroll to the linked id</li>
<li>Do it the old school way and use an event listener on <code>DOMContentLoaded</code></li>
</ol>
<p>I chose the old fashioned way, so I added this to my <code>_document.tsx</code>:</p>
<pre><code class="language-jsx">&#x3C;script
  dangerouslySetInnerHTML={{
    __html: `
    document.addEventListener("DOMContentLoaded", function(event) {
      var hash = window.decodeURI(location.hash.replace('#', ''))
      if (hash !== '') {
        var element = document.getElementById(hash)
        if (element) {
          var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
          var clientTop = document.documentElement.clientTop || document.body.clientTop || 0
          var offset = element.getBoundingClientRect().top + scrollTop - clientTop
          // Wait for the browser to finish rendering before scrolling.
          setTimeout((function() {
            window.scrollTo(0, offset - 0)
          }), 0)
        }
      }
    })`,
  }}
>&#x3C;/script>
</code></pre>
<h3>Sitemap</h3>
<p>Creating a sitemap in Next.js can be done by iterating through your markdown
files, the files in the <code>pages/</code> directory, and wrapping some XML around them.</p>
<p>Here's how I did it:</p>
<ul>
<li>
<p>In your Next.js app's root folder, create a new folder called <code>scripts</code>, and
create a file called <code>generateSitemap.js</code></p>
</li>
<li>
<p>Paste the following, and change the domain name as needed:</p>
<pre><code class="language-js">// scripts/generateSitemap.js
const fs = require('fs');

const globby = require('globby');
const prettier = require('prettier');

(async () => {
  const prettierConfig = await prettier.resolveConfig('./.prettierrc');
  //exclude pages below by adding ! in front of the path
  const pages = await globby([
    'pages/*.tsx',
    '_content/articles/*.mdx',
    '!pages/404.tsx',
    '!pages/_*.tsx',
    '!pages/[*.tsx',
    '!pages/api',
  ]);

  const sitemap = `
        &#x3C;?xml version="1.0" encoding="UTF-8"?>
        &#x3C;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
            ${pages
              .map((page) => {
                const path = page
                  .replace('pages', '')
                  .replace('_posts', '')
                  .replace('.tsx', '')
                  .replace('.mdx', '');
                const route = path === '/index' ? '' : path;
                return `
</code></pre>
</li>
</ul>
<pre><code>const formatted = prettier.format(sitemap, {
  ...prettierConfig,
  parser: 'html',
});

// eslint-disable-next-line no-sync
fs.writeFileSync('public/sitemap.xml', formatted);
</code></pre>
<p>})();</p>
<pre><code>
- Then add the following to your next.config.js:

```js
// next.config.js
module.exports = {
  // modify your webpack export if you've already got one
  webpack: (config, { isServer }) => {
    if (isServer) {
      require('./scripts/generateSitemap');
    }

    return config;
  },
};
</code></pre>
<ul>
<li>Finally, add <code>public/sitemap.xml</code> to your <code>.gitignore</code></li>
</ul>
<p>This way, Next will only generate the sitemap as part of the build, and not
while you're developing/writing articles.</p>
<h3>RSS Feed in Next.js</h3>
<p>I originally planned on copying the gatsby plugin to recreate my site's RSS
feed, before I ran into this
<a href="https://phiilu.com/generate-rss-feeds-for-your-static-next-js-blog">blog post</a>
by Florian.</p>
<p>The gist of the blog post is to use <a href="https://www.npmjs.com/package/feed">Feed</a>,
an npm package that abstracts away the pain of generating RSS, Atom, and JSON
feeds.</p>
<p>Here's how I did it:</p>
<ul>
<li>
<p>In the <code>scripts</code> folder we created for the sitemap, create a file called
<code>generateRss.js</code></p>
</li>
<li>
<p>Install the following packages (if you're not using markdown files, all you
need is feed):</p>
<pre><code class="language-bash">yarn install feed gray-matter remark remark-html
</code></pre>
</li>
<li>
<p>Paste the following, and update the metadata:</p>
<pre><code class="language-js">const Feed = require('feed').Feed;
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const postsDirectory = path.join(process.cwd(), '_content/articles');
const remark = require('remark');
const remarkHtml = require('remark-html');

async function markdownToHtml(markdown) {
  const result = await remark().use(remarkHtml).process(markdown);
  return result.toString();
}

function getPostSlugs() {
  return fs.readdirSync(postsDirectory);
}

function getPostBySlug(slug, fields = []) {
  const realSlug = slug.replace(/\.mdx$/, '');
  const fullPath = path.join(postsDirectory, `${realSlug}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  // parse the markdown file
  const { data, content } = matter(fileContents);

  const items = {};

  // Ensure only the minimal needed data is exposed
  fields.forEach((field) => {
    if (field === 'slug') {
      items[field] = realSlug;
    }
    if (field === 'content') {
      items[field] = content;
    }

    if (data[field]) {
      items[field] = data[field];
    }
  });

  return items;
}

function getAllPosts(fields = []) {
  const slugs = getPostSlugs();
  const posts = slugs
    .map((slug) => getPostBySlug(slug, fields))
    // sort posts by date in descending order
    .sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
  return posts;
}

(async () => {
  if (process.env.NODE_ENV === 'development') {
    return;
  }
  const baseUrl = 'https://YOURDOMAINHERE.com';
  const date = new Date();
  const author = {
    name: 'Some Person',
    email: 'hey@YOURDOMAINHERE.com',
    link: 'https://twitter.com/sometwitterhandlethattotallyexists',
  };
  const feed = new Feed({
    title: `My Awesome site`,
    description: 'This is my Cool RSS Feed',
    id: baseUrl,
    link: baseUrl,
    language: 'en',
    image: `${baseUrl}/images/logo.svg`,
    favicon: `${baseUrl}/favicon.ico`,
    copyright: `All rights reserved ${date.getFullYear()}, Your Name`,
    updated: date,
    generator: 'Next.js',
    feedLinks: {
      rss2: `${baseUrl}/rss.xml`,
      json: `${baseUrl}/feed.json`,
      atom: `${baseUrl}/atom.xml`,
    },
    author,
  });
  const posts = getAllPosts(['title', 'date', 'path', 'excerpt', 'content']);

  const postPromises = posts.map(async (post) => {
    const url = `${baseUrl}${post.path}`;
    //convert the markdown to HTML that we'll pass to the RSS feed
    const content = await markdownToHtml(post.content);

    feed.addItem({
      title: post.title,
      id: url,
      link: url,
      description: post.excerpt,
      content: content,
      author: [author],
      contributor: [author],
      date: new Date(post.date),
    });
  });
  await Promise.all(postPromises);

  fs.writeFileSync('./public/rss.xml', feed.rss2());
  fs.writeFileSync('./public/atom.xml', feed.atom1());
  fs.writeFileSync('./public/feed.json', feed.json1());
})();
</code></pre>
</li>
<li>
<p>Add the script to the next.config.js file, similar to what we did for the
sitemap:</p>
<pre><code class="language-js">// next.config.js
module.exports = {
  // modify your webpack export if you've already got one
  webpack: (config, { isServer }) => {
    if (isServer) {
      //if you followed the sitemap instructions, you only need the next line
      require('./scripts/generateRss');
    }

    return config;
  },
};
</code></pre>
</li>
<li>
<p>Finally, add <code>public/rss.xml</code>, <code>public/atom.xml</code>, and <code>public/feed.json</code> to
your <code>.gitignore</code></p>
</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Protecting your React routes]]></title>
            <link>https://maxrozen.com/protecting-react-routes</link>
            <guid>https://maxrozen.com/protecting-react-routes</guid>
            <pubDate>Tue, 19 Jan 2021 09:48:48 GMT</pubDate>
            <description><![CDATA[React apps send their code directly to users, so you might be a little confused about how React 'guards' routes from unauthorised viewing. This article explains how.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/protecting-react-routes">clicking here</a>.)</div><p>If you've ever looked into your network tab when visiting a React app, chances are you've noticed that some apps send their entire bundle to their visitors. You might then wonder, how is that secure? Surely a motivated user could dive through React DevTools and find your protected routes?</p>
<p>First of all, you'd be correct - you won't stop a motivated user from seeing your admin panel UI, but that shouldn't worry us.</p>
<p>Secondly, it's your backend's job to worry about authentication and authorization before sending data to the frontend.</p>
<h2>How it works</h2>
<ul>
<li>When users first load a React app, the user state is empty, meaning the user is unauthenticated</li>
<li>We protect routes in React by redirecting unauthenticated users to our login page (see <a href="https://reactrouter.com/web/example/auth-workflow">React Router example</a>)</li>
<li>When the user logs in, we send a request to our backend, and check if the credentials are valid (via database query, or passing off the entire exchange to a third-party service like <a href="https://auth0.com/">Auth0</a> or <a href="https://aws.amazon.com/cognito/">AWS Cognito</a>)</li>
<li>If the credentials are valid, the backend generates a token that's been <strong>signed</strong> with a secret we keep in the backend (so we know it's from <em>our</em> backend), and sends the token to the frontend</li>
<li>Back on the frontend, we keep the token in state so that we stop redirecting users to the login page</li>
<li>Now that the user can visit protected routes, we also use the token (normally using the <code>Authorization</code> header) in all API requests to our backend</li>
<li>Our backend checks if the token is signed correctly, as well as which user the token belongs to, and whether the user has permission before sending back any data
<ul>
<li>If the token is invalid, or not provided in the request, the backend sends back a response with a <code>401 Unauthorized</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401">HTTP status</a></li>
<li>If the token <strong>is</strong> valid, but the user doesn't have permission to view the data, the backend sends back a response with a <code>403 Forbidden</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403">HTTP status</a></li>
<li>We can then display a big ERROR page on our frontend if the API returns a <code>401 Unauthorized</code> response, and none of our precious data gets send to unauthorized users</li>
</ul>
</li>
<li>If everything is valid, we display our protected UI along with our data, and the user gets to use your app</li>
</ul>
<h2>Summary</h2>
<p>In short, we shouldn't be worried about protecting our frontend from being downloaded. At worst, they'll find out what your admin/protected routes looks like, but won't get anything valuable.</p>
<p>It's the backend's role to ensure the user is both who they say they are, and that they are allowed to see the data.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Deploying Node and React Shopify apps on Vercel]]></title>
            <link>https://maxrozen.com/deploying-node-react-shopify-apps-on-vercel</link>
            <guid>https://maxrozen.com/deploying-node-react-shopify-apps-on-vercel</guid>
            <pubDate>Sat, 16 Jan 2021 09:48:48 GMT</pubDate>
            <description><![CDATA[Shopify's tutorial is awesome for getting started, but doesn't let you deploy onto Vercel. In this article, I'll show you how.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/deploying-node-react-shopify-apps-on-vercel">clicking here</a>.)</div><p>Building Shopify apps in React and Node the way Shopify teaches you is cool and all, but what if you wanted to take advantage of Next.js API routes, and deploy on Vercel?</p>
<p>Luckily for you - I've scoured the web to figure this out.</p>
<h2>Prerequisites</h2>
<p>To start off, you're going to want to go through the <a href="https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react">official Shopify app tutorial</a>.</p>
<p>It's probably one of the <em>best</em> tutorials I've seen for doing <em>anything</em> in React, so just take an hour, and learn how Shopify apps are built.</p>
<h2>Actually deploying to Vercel</h2>
<p>The issue with the official Shopify tutorial is that it makes you build a Next.js app with a custom server.</p>
<p>In case you don't know (I didn't), you can't deploy custom servers on Vercel:</p>
<p>So we could either re-write the custom server example to use API routes, or find an example on GitHub that does that already.</p>
<p><a href="https://github.com/bluebeel/nextjs-shopify">This repo</a> by <a href="https://github.com/bluebeel">bluebeel</a> is the example you're looking for. It even comes with a "Deploy to Vercel" button.</p>
<p>Smash that "Deploy to Vercel" button.</p>
<p>On Vercel, set your <code>SHOPIFY_API_KEY</code> and <code>SHOPIFY_API_SECRET</code>, as well as <code>HOST</code> and <code>NEXT_PUBLIC_HOST</code> (I guessed the Vercel deployment URL and used the same value for these two).</p>
<p>You'll also want to update your Shopify app's "App URL" and "Allowed redirection URL(s)"
so that authentication works. At time of writing this was <code>{VERCEL_URL}//api/shopify/auth/callback</code>. Note the double slash there.</p>
<p><strong>Update:</strong> as of March 2021, I've received reports that the single slash version now works as expected, so try using <code>{VERCEL_URL}/api/shopify/auth/callback</code>.</p>
<p>Once deployed, and authenticated, eventually you'll see this screen:
<img src="vercel-shopify-app.png" alt="Vercel Shopify app"></p>
<p>Now the real work begins.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Using Tailwind CSS with React and CSS-in-JS]]></title>
            <link>https://maxrozen.com/using-tailwind-css-in-react-css-in-js</link>
            <guid>https://maxrozen.com/using-tailwind-css-in-react-css-in-js</guid>
            <pubDate>Mon, 11 Jan 2021 09:48:48 GMT</pubDate>
            <description><![CDATA[If you're already using a CSS-in-JS library, trying out Tailwind CSS with your existing app might be much easier than you think.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/using-tailwind-css-in-react-css-in-js">clicking here</a>.)</div><p>If you've looked into adding Tailwind CSS to your React project, chances are you've seen the <a href="https://tailwindcss.com/docs/installation">official installation guides</a>, but what if you're already using <a href="https://emotion.sh/docs/introduction">Emotion</a>, or <a href="https://styled-components.com/">styled-components</a>?</p>
<p>Sure, you could probably try some funky combination of className and styled components such as:</p>
<pre><code class="language-jsx">const MyStyledDiv = styled.div`
  color: red;
`;

const MyComponent = () => &#x3C;MyStyledDiv className="mt-4 flex" />;
</code></pre>
<p>Assuming that would even work...</p>
<p>But what if you could use Tailwind classnames <strong>with</strong> your current CSS-in-JS configuration?</p>
<h2>Introducing twin.macro</h2>
<p><a href="https://github.com/ben-rogerson/twin.macro">twin.macro</a> by <a href="https://github.com/ben-rogerson">Ben Rogerson</a> lets you write Tailwind CSS inside your CSS-in-JS, lints your Tailwind classnames, <strong>and</strong> doesn't make your bundle bigger.</p>
<p>twin.macro gives you access to a few things:</p>
<ul>
<li>
<p>a <code>tw</code> prop on JSX elements:</p>
<pre><code class="language-jsx">import 'twin.macro';

const MyDiv = () => &#x3C;div tw="border hover:border-black" />;
</code></pre>
</li>
<li>
<p>Tailwind class linting (this is the main reason I use this library):</p>
<pre><code class="language-bash">✕ ml-7 was not found

Try one of these classes:
ml-0 [0] / ml-1 [0.25rem] / ml-2 [0.5rem] / ml-3 [0.75rem] / ml-4 [1rem] / ml-5 [1.25rem] / ml-6 [1.5rem]
ml-8 [2rem] / ml-10 [2.5rem] / ml-12 [3rem] / ml-16 [4rem] / ml-20 [5rem] / ml-24 [6rem] / ml-32 [8rem]
ml-40 [10rem] / ml-48 [12rem] / ml-56 [14rem] / ml-64 [16rem] / ml-auto [auto] / ml-px [1px]
</code></pre>
</li>
<li>
<p>The standard Styled Component API:</p>
<pre><code class="language-jsx">import tw from 'twin.macro';

const MyDiv = tw.div`border hover:border-black`;
// and cloning/styling other components
const PurpleDiv = tw(MyDiv)`border-purple-500`;
</code></pre>
</li>
<li>
<p>The <code>css</code> prop:</p>
<pre><code class="language-jsx">import tw from 'twin.macro';

const Input = ({ hasHover }) => (
  &#x3C;input css={[tw`border`, hasHover &#x26;&#x26; tw`hover:border-black`]} />
);
</code></pre>
</li>
</ul>
<p>The library currently supports both Emotion and styled-components when using React, Preact, Create React App, Gatsby, Next.js, Snowpack, and more.</p>
<p>You'll want to see the <a href="https://github.com/ben-rogerson/twin.macro#get-started">Get started</a> section of the README for instructions on how to install it for your particular combination of CSS-in-JS library and React framework.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Indiehacking: a review of my 3rd year]]></title>
            <link>https://maxrozen.com/indiehacking-3-year-review</link>
            <guid>https://maxrozen.com/indiehacking-3-year-review</guid>
            <pubDate>Tue, 22 Dec 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[A review of my third year of trying to start an internet business.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/indiehacking-3-year-review">clicking here</a>.)</div><p>Each year I take this time of year to reflect on how I progressed towards my goal of being self-sufficient as a web developer:</p>
<ul>
<li><a href="/2018/12/31/2018-review-starting-an-internet-business/">2018: Reflections on trying to start an internet business</a></li>
<li><a href="/2019/12/29/2019-further-reflections-trying-to-start-an-internet-business/">2019: Further reflections on trying to start an internet business</a></li>
</ul>
<p>This year is no different, even if it <em>was</em> a total mess.</p>
<p>Table of contents</p>
<ul>
<li><a href="#how-i-indiehack">How I Indiehack</a></li>
<li><a href="#investing-in-tooling">Investing in tooling</a></li>
<li><a href="#books-i-read">Books I Read</a></li>
<li><a href="#year-in-review">Year in review</a>
<ul>
<li><a href="#mercenary-for-hire">Mercenary for hire</a></li>
<li><a href="#day-rate-contracting">Day-rate contracting</a></li>
<li><a href="#startup-tech-stack">Startup Tech Stack</a></li>
<li><a href="#perfbeacon-on-the-side">PerfBeacon on the side</a></li>
<li><a href="#back-into-stable-full-time-work">Back into stable full-time work</a>
<ul>
<li><a href="#trade-offs-trade-offs-everywhere">Trade-offs, trade-offs everywhere</a></li>
</ul>
</li>
<li><a href="#writing-weekly">Writing weekly</a>
<ul>
<li><a href="#taking-notes">Taking notes</a></li>
</ul>
</li>
<li><a href="#writing-a-book">Writing a book</a></li>
</ul>
</li>
<li><a href="#financials">Financials</a></li>
<li><a href="#learnings">Learnings</a></li>
<li><a href="#how-my-fourth-year-is-going">How my fourth year is going</a></li>
</ul>
<h2>How I Indiehack</h2>
<p>To save you reading through the previous year's summaries, here's how I indiehack.</p>
<p>I try to keep either a full-time job that doesn't expect me to work 18 hour days to meet tough deadlines, or I take contract/day-rate jobs where I can negotiate to work 4 days a week.</p>
<p>Before and after business hours, and occassionally on weekends, I work on side-projects.</p>
<p>I tend to work in 1-2 hour increments, partly because that's how long I can focus at a time, and partly because I prefer to chip away at a problem, rather than making one big push.</p>
<p>I started the year with the firm belief that running your own one-person SaaS was the only means of starting your own business as a developer. I now see that SaaS is just one type of fix, in a spectrum of fixes - from books/videos/workshops to consulting, to SaaS and other products.</p>
<h2>Investing in tooling</h2>
<p>I'm not a big fan of re-inventing the wheel when I build my side-projects, so in previous years I invested time in building my own personal framework to spin up SaaS-like solutions.</p>
<p>This time around, instead of investing time, I spent a few hundred dollars on licenses for <a href="https://tailwindui.com/">Tailwind UI</a> and <a href="https://divjoy.com/?via=oon">Divjoy</a> (referral link).</p>
<p>I still maintain a terraform config for spinning up the required infrastructure for my projects, but now I don't need to think about styling my side-projects, or fiddly integrations like Auth0 and Stripe.</p>
<p>I was able to productionise a web app for one of my projects this year within a couple of hours thanks to these investments.</p>
<h2>Books I Read</h2>
<p>Until this year, I rarely sat down to read books. Instead, I'd use my commute time to work to listen to audiobooks. As a result, when 2020 happened and I stopped commuting, I started having to make time to read, and as a result of <em>that</em>, I was much more impatient with boring or long-winded books.</p>
<p>Here are the ones that made the cut:</p>
<ul>
<li>This is Marketing - Seth Godin</li>
<li>How I Built This - Guy Raz</li>
<li>How to Take Smart Notes - Sönke Ahrens</li>
<li>Originals - Adam Grant</li>
<li>Tribes - Seth Godin</li>
<li>Obviously Awesome - April Dunford</li>
<li>The Tipping Point - Malcolm Gladwell</li>
</ul>
<h2>Year in review</h2>
<h3>Mercenary for hire</h3>
<p>I started the year completely burnt out from working for companies that claimed to have certain values, while exhibiting behaviour that ran counter to those values.</p>
<p>So I started looking for React gigs where I could jump in, either fix or build a React app, and jump out as the client needed.</p>
<h3>Day-rate contracting</h3>
<p>At the end of 2019 I complained across my network long enough that I managed to find myself working as a contractor for a scrappy startup building an entire IoT platform (from devices up to a frontend to display data). Thankfully they had hired a consulting company to handle the IoT device and data ingestion, so I only needed to worry about the web platform.</p>
<p>What started off as a couple of weeks to help the company get to demo day ended up being a few months helping build the company's entire tech stack.</p>
<p>As a result, overnight I unofficially became the:</p>
<ul>
<li>Product Manager</li>
<li>Frontend Dev</li>
<li>Backend Dev</li>
<li>Data Engineer</li>
<li>SRE</li>
<li>DevOps</li>
</ul>
<p>and it was surprisingly doable, with the right tech stack (Spoiler: I'm a huge fan of <a href="http://boringtechnology.club/">Choose Boring Technology</a>).</p>
<h3>Startup Tech Stack</h3>
<p>I opted for the same tech stack I know and love for my side projects, and it worked quite well.</p>
<ul>
<li>For managing tasks/documentation I used <a href="https://www.atlassian.com/software/jira">Jira</a> and <a href="https://www.atlassian.com/software/confluence">Confluence</a>,</li>
<li>for the frontend I used React via <a href="https://create-react-app.dev/">create-react-app</a>.</li>
<li>for the backend I used postgres with a node GraphQL server,</li>
<li>for Infrastructure as Code (IaC) I used <a href="https://www.terraform.io/">terraform</a>,</li>
<li>and for CI/CD I used <a href="https://circleci.com/">CircleCI</a> (though these days I'd probably look at using GitHub Actions).</li>
</ul>
<p>On the data engineering side of things, we would regularly need to transform ad-hoc user-provided data into a format that our system could understand. Essentially, clients would already have IoT devices from competitors, and we would make it possible for them to join our platform via our data engineering efforts.</p>
<p>I was able to automate most of this using Python and AWS S3's event system, so we wouldn't need to wrangle the client's custom datasets more than once.</p>
<p>Side note: it blew my mind how incumbent companies can still charge $10k+ for systems built more than 20 years ago, that these days could be replaced by an ESP32, wifi/3g modem and a python script. Though being able to compete with these companies requires a large professional network and willingness to run high-touch sales that I don't have.</p>
<h3>PerfBeacon on the side</h3>
<p>While contracting four days a week, I would also work on <a href="https://perfbeacon.com/">PerfBeacon</a> before and after work, as well as on Wednesdays. For those that don't know, PerfBeacon helps you keep your app/site fast by automating <a href="https://developers.google.com/web/tools/lighthouse">Google Lighthouse</a> checks.</p>
<p>I managed to finish building the minimum viable product early this year, and started marketing and sales.</p>
<p>After launching PerfBeacon to the world, I built a quick CRM in Airtable, and started selling it to people in my network. I estimate I spent around 40 hours trying to sell PerfBeacon to my network directly, netting approximately zero in sales.</p>
<p>It didn't take long for me to realise that most of running a side business is marketing, rather than product building. Realising I was terrible at marketing, and didn't really know what I was doing lead me to pay for <a href="https://30x500.com/academy/">30x500</a>.</p>
<p>I still run PerfBeacon, it only takes a couple of hours a month on average to keep it running.</p>
<p>Funnily enough, while job hunting this year I ran into some resistance from startups "wanting me to be 100% focused, even outside of work hours on their project". In other words, I would have to drop my side-projects to be able to work for them. Often within minutes of telling me my "vast experience with React" made my profile interesting (I wouldn't have a vast experience in React without side-projects).</p>
<p>Total non-starter, so I kept looking.</p>
<h3>Back into stable full-time work</h3>
<p>Effectively doing any one of six jobs each day grew old surprisingly quickly, and by February I started to look for long-term contracts at more established companies.</p>
<p>By chance I decided to reach out to a recruiter from Atlassian that ran me through the interview process a year prior, and I managed to get another run through the interview process to be a frontend developer on the Growth team at Atlassian.</p>
<p>I ended up getting the job, and it's been fantastic.</p>
<h4>Trade-offs, trade-offs everywhere</h4>
<p>Working at a huge corporation like Atlassian has made me appreciate trade-offs much more than any small company ever did.</p>
<p>We regularly run through <a href="https://www.atlassian.com/team-playbook/plays/daci">DACIs</a> (basically a fancy RFC) when making technical decisions. Where before I'd run a technical decision past an architect/CTO, I'm now able to tap into our entire department's expertise to get more context, and see any gaps in what I'm trying to build.</p>
<h3>Writing weekly</h3>
<p>I started the year only writing whenever I got inspired (basically, after something made me angry enough to write about it). While cathartic, this didn't do much for people who read my blog.</p>
<p>Instead of focusing on things that annoyed me, in July I decided to focus on regularly helping people. I knew quite a bit about React from using it in production for several years (both on side-projects and at work), so it'd be a shame not to share the knowledge.</p>
<p>Since starting to blog more regularly, I went from an average of 20 readers on my blog per day, to about 140 readers per day on days when I don't post my articles anywhere, up to 2500 a day when I do share my articles.</p>
<h4>Taking notes</h4>
<p>Writing weekly has become easier as I started to treat my articles like "semi-permanent notes" to myself about certain topics in React.</p>
<p>I tend to take notes (whether in a notebook, on my phone, or on scrap pieces of paper) from wherever I see people discussing React (Twitter threads, reddit posts, etc) and eventually type them up into an article. Eventually, I revise the article as I find new information. A good example of this is <a href="https://maxrozen.com/keeping-up-with-react-libraries/">Keeping up with React Libraries</a>.</p>
<p>I learned this process after reading <a href="https://www.amazon.com/gp/product/1542866502/ref=as_li_tl?ie=UTF8&#x26;camp=1789&#x26;creative=9325&#x26;creativeASIN=1542866502&#x26;linkCode=as2&#x26;tag=rozenmd-20&#x26;linkId=a766abac80f4e8d5f39e5d87804ec00c">How to Take Smart Notes</a> by Sönke Ahrens, which I'd highly recommend to aspiring writers out there.</p>
<h3>Writing a book</h3>
<p>After writing consistently for a few months, I started to notice the articles that would get significantly more traffic and links than others.</p>
<p>In the end, I decided to write a book to try help people more than a few blog posts ever could. It's called <a href="http://useeffectbyexample.com/">useEffect By Example</a>, and I launched it in January 2021.</p>
<h2>Financials</h2>
<p><a href="https://perfbeacon.com/">PerfBeacon</a> is now break-even (in terms of running costs), averaging between $1-200 MRR.</p>
<p>Judging by pre-orders alone, <a href="http://useeffectbyexample.com/">useEffect By Example</a> looks like it'll beat PerfBeacon in terms of revenue, even in the long term, and, even though PerfBeacon runs on a recurring subscription model.</p>
<p>I'm still <strong>very</strong> far from being able to employ myself, but at least now I have a repeatable process that works - writing consistently.</p>
<h2>Learnings</h2>
<p>SaaS is only one way of solving problems for your customers. Explore all possible means (books, videos, workshops, consulting, code snippets, plugins) of solving a problem before diving into building a SaaS. SaaS is often the most difficult, and time consuming way to run a business, and there are no prizes for playing on hard mode.</p>
<p>Pre-sales are an incredibly powerful tool to gauge buyer interest. Indiehackers can easily start a gumroad pre-sale to take valid credit card details, and only process the payment when they ship.</p>
<p>Write regularly. Similar to running/working out, the more you work out the muscle, the easier it becomes. There are unintentional benefits to this:</p>
<ul>
<li>Writing documentation, emails, tickets, etc at work becomes easier</li>
<li>SEO: the more you write, the more long-tail keywords your articles will pick up - note that helping people is the goal here, not "winning" the top spot on Google</li>
<li>You grow an audience that wants to see what you're regularly creating</li>
<li>You get to practice shipping in a way you wouldn't get if you only focused on building product.
<ul>
<li>Building <a href="http://onlineornot.com/">OnlineOrNot</a> and <a href="https://perfbeacon.com/">PerfBeacon</a>, I got to experience launch anxiety perhaps once every six months, while with my writing on <a href="https://MaxRozen.com">MaxRozen.com</a> I experience it weekly.</li>
<li>This in turn has taught me lessons on editing, marketing, running a newsletter, engaging with the community, and more that I probably don't even realise yet.</li>
</ul>
</li>
</ul>
<h2>How my fourth year is going</h2>
<p>In case you're wondering how my fourth year of indiehacking is going, check out my post on building <a href="https://onlineornot.com/building-saas-in-one-week-how-built-onlineornot">OnlineOrNot</a>.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Guidelines for developing Custom Hooks in React]]></title>
            <link>https://maxrozen.com/guidelines-for-developing-custom-hooks-react</link>
            <guid>https://maxrozen.com/guidelines-for-developing-custom-hooks-react</guid>
            <pubDate>Tue, 15 Dec 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[You've just gotten over the fact we call them 'Hooks', but how do you stop repeating the same code everywhere? Let's learn about Custom Hooks.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guidelines-for-developing-custom-hooks-react">clicking here</a>.)</div><p>It's pretty common to ask whether there's a rule of thumb for deciding whether or not something should be a custom hook. While it's down to developer preference how you write your custom hooks, there are still some things that aren't immediately obvious.</p>
<p>Luckily for us, hooks are (almost, but with some caveats) functions, meaning we can extract them and test them as we like.</p>
<p>At the risk of repeating the <a href="https://reactjs.org/docs/hooks-custom.html">docs</a>, here's the gist of custom hooks:</p>
<ul>
<li>Name your custom hooks starting with <code>use</code> (<code>useDataFetcher</code>, for example)</li>
<li><a href="https://reactjs.org/docs/hooks-rules.html">Rules of hooks</a> still apply</li>
<li>You can call other hooks in your custom hooks</li>
<li>Hooks don't share state, meaning you can use the same hook that uses <code>useState</code> in two different places, and they'll be isolated from each other</li>
<li>As they're still functions, you can pass the results from one hook as an argument to the next hook</li>
</ul>
<p>Table of Contents</p>
<ul>
<li><a href="#why-custom-hooks">Why custom hooks?</a></li>
<li><a href="#what-do-custom-hooks-look-like">What do custom hooks look like?</a></li>
<li><a href="#document-your-custom-hooks">Document your custom hooks!</a></li>
<li><a href="#testing-custom-hooks">Testing custom hooks</a></li>
<li><a href="#on-folder-structure">On Folder Structure</a></li>
</ul>
<h2>Why custom hooks?</h2>
<p>Since hooks can be treated as functions in a lot of ways, what we know about functions still applies.</p>
<ul>
<li>They're reusable</li>
<li>We can break our larger hooks into smaller hooks</li>
<li>We can compose our smaller hooks together, like lego bricks</li>
<li>They're testable</li>
</ul>
<p>Sure, you could always re-use the same sequence of hooks in each component (useState, useEffect, update state...), but why repeat yourself several times when you could extract the logic out into a well-tested custom hook?</p>
<h2>What do custom hooks look like?</h2>
<p>This is a relatively contrived example, but focus on the concepts for a moment.</p>
<p>Say you have a React component that fetches data and displays it:</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function MyComponent() {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://swapi.dev/api/people/1/');
      const newData = await response.json();
      setData(newData);
    };

    fetchData();
  });

  return (
    &#x3C;div>
      &#x3C;h1>Hello!&#x3C;/h1>
      {data ? &#x3C;div>{data.name}&#x3C;/div> : null}
    &#x3C;/div>
  );
}
</code></pre>
<p>You decide you want to use the same useEffect logic in another component.</p>
<p>So you extract it into a custom hook:</p>
<pre><code class="language-jsx">//useFetchData.js
import { useEffect, useState } from 'react';

export const useFetchData = () => {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://swapi.dev/api/people/1/');
      const newData = await response.json();
      setData(newData);
    };

    fetchData();
  });
  return data;
};
</code></pre>
<p>Our original component becomes much simpler:</p>
<pre><code class="language-jsx">import React from 'react';
import { useFetchData } from './useFetchData';

export default function MyComponent() {
  const data = useFetchData();

  return (
    &#x3C;div>
      &#x3C;h1>Hello!&#x3C;/h1>
      {data ? &#x3C;div>{data.name}&#x3C;/div> : null}
    &#x3C;/div>
  );
}
</code></pre>
<p>In the real world, we probably wouldn't extract a data fetcher like this, as it's <a href="/race-conditions-fetching-data-react-with-useeffect">vulnerable to race conditions</a> and we'd probably use an existing library such as <a href="https://github.com/tannerlinsley/react-query">react-query</a> instead of <code>useFetchData</code>.</p>
<h2>Document your custom hooks!</h2>
<p>As with most things in a large organisation, the more custom hooks your team writes, the easier it is to lose track of them.</p>
<p>I recommend documenting each hook you write. If not for your team, for future-you. Whether it's a README in each hook's folder or a doc in a wiki - ensure people know your hook exists, what it does, and what your intentions were.</p>
<p>Leaving a note like "This was a massive hack where we only covered case X due to this buggy API we use" is significantly more valuable than writing nothing.</p>
<p>Adding <a href="https://jsdoc.app/about-getting-started.html">JSDOC</a> comments above your hooks is also useful (<a href="https://devhints.io/jsdoc">here's a cheat sheet</a>):</p>
<pre><code class="language-jsx">/**
 * This is a JSDOC comment.
 * VS Code's tooltip will show extra information because of me.
 */
const useMyCustomHook = (props) => {
  // ...
};
</code></pre>
<p>Don't assume your code is self-documenting/easy enough to understand. Chances are you just think your code is simple because it's still fresh in your mind.</p>
<h2>Testing custom hooks</h2>
<p>Thanks to React Testing Library's <a href="https://github.com/testing-library/react-hooks-testing-library">react-hooks-testing-library</a>, it's never been easier to test hooks in isolation.</p>
<p>Here's an example from their docs. Given a hook extracted as <code>useCounter</code>:</p>
<pre><code class="language-jsx">//useCounter.js
import { useState, useCallback } from 'react';

function useCounter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => setCount((x) => x + 1), []);

  return { count, increment };
}

export default useCounter;
</code></pre>
<p>Your tests would look like this:</p>
<pre><code class="language-jsx">//useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});
</code></pre>
<h2>On Folder Structure</h2>
<p>As many things in React go, it's up to developer preference how your code is structured. I <a href="/guidelines-improve-react-app-folder-structure/">wrote some words</a> on the topic already, but here's how I would structure my folders when writing custom hooks:</p>
<pre><code>src/
|-- components/
|   |-- Avatar/
|   |   |-- Avatar.js
|   |   |-- Avatar.test.js
|   |-- Button/
|   |   |-- Button.js
|   |   |-- Button.test.js
|-- hooks/
|   |-- useCounter/
|   |   |-- useCounter.js
|   |   |-- useCounter.test.js
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Guidelines for Deploying React]]></title>
            <link>https://maxrozen.com/guidelines-for-deploying-react</link>
            <guid>https://maxrozen.com/guidelines-for-deploying-react</guid>
            <pubDate>Tue, 01 Dec 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[These days there are a heck of a lot of options for deploying React. It can be a bit overwhelming. This article simplifies your choices.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guidelines-for-deploying-react">clicking here</a>.)</div><p>Figuring out how and where to deploy React can be pretty confusing. A common question for newcomers from other frameworks is "where does React run?", while others wonder what the best practices for deploying React to production are.</p>
<p>Let's clear up some of that confusion.</p>
<p>Table of Contents</p>
<ul>
<li><a href="#on-static-files">On static files</a></li>
<li><a href="#so-where-does-react-run">So where does React "run"?</a></li>
<li><a href="#deploying-react-diy-vs-platforms">Deploying React: DIY vs Platforms</a>
<ul>
<li><a href="#diy">DIY</a></li>
<li><a href="#platforms">Platforms</a></li>
</ul>
</li>
</ul>
<h2>On static files</h2>
<p>When using create-react-app, you trigger a React build by running <code>npm run build</code> or <code>yarn build</code>. The output you'd see looks like this:
<img src="./build-output.png" alt="React Build Output"></p>
<p>We call these static files. When building client-side rendered apps (such as when using create-react-app), we only need to worry about where to host these files. Server-side rendering or SSR (running a server that generates the HTML the users see as they request it) also uses these files, but that's for a future article to cover.</p>
<p>Static files are awesome because you <em>technically</em> only need a file host to upload them to, in order to deploy your app. I say technically, because when we deploy to production, we typically also want a CDN and HTTPS to make our app fast and secure around the world.</p>
<h2>So where does React "run"?</h2>
<p>In short, if you're using create-react-app, React runs in your user's browser. Your static files get sent to them when they visit your app's URL.</p>
<h2>Deploying React: DIY vs Platforms</h2>
<p>There are two routes to deploying a React app on the internet:</p>
<ul>
<li><a href="#diy">DIY</a>, or set-up your own infrastructure</li>
<li><a href="#platforms">Platforms</a>, such as Netlify and Vercel</li>
</ul>
<h3>DIY</h3>
<p>Having worked for some old-school engineers who wanted to be able to see all of the magic before they trusted it, I basically learned how to build React apps while DIYing all of the infrastructure, and CI/CD.</p>
<p>Scaling the client-side part of React isn't particularly challenging: the key to not going offline during high load is a decent CDN, and good caching settings.</p>
<p>To DIY your React production setup, you'll want to:</p>
<ul>
<li>Choose a web host</li>
<li>Choose a CDN provider (particularly one that provides free HTTPS)</li>
<li>Choose a DNS provider</li>
<li>Set up CI/CD
<ul>
<li>(not mandatory, I've worked with very small teams that would just use package.json scripts to test and deploy their apps)</li>
</ul>
</li>
</ul>
<p>These days, I wouldn't particularly recommend small teams use the DIY approach. Your engineers' time is better spent building features or writing tests. God help you if you decide to deploy to Kubernetes before achieving product-market fit.</p>
<p>In my case with the old-school engineers, we had to support government clients, so we wanted to configure every part of the system by hand (via terraform), and to know exactly where our code was being deployed to. We ended up going with AWS to handle hosting (S3), CDN (CloudFront), DNS (Route 53), and CI/CD (Self-hosted Jenkins on AWS).</p>
<p>Years later, I would still pick AWS when going down the DIY route, the thing I'd change is opting for <a href="https://github.com/features/actions">GitHub Actions</a> or <a href="https://circleci.com/">CircleCI</a> for CI/CD.</p>
<h3>Platforms</h3>
<p>Platforms such as <a href="https://www.netlify.com/">Netlify</a> and <a href="https://vercel.com/">Vercel</a> are <em>absolute magic</em> compared to the DIY route.</p>
<p>Setting up a project with them is as simple as logging into the platform, authenticating to GitHub, picking a repo, double checking the default settings are correct, and hitting "Done".</p>
<p>For that, you automatically get:</p>
<ul>
<li>Hosting</li>
<li>CDN with free HTTPS</li>
<li>CI/CD</li>
</ul>
<p>All that's left to do is point your DNS records to the platform.</p>
<p>You also get access to serverless functions, though limited compared to running directly on AWS.</p>
<p>The other day I setup <a href="http://useeffectbyexample.com/">useEffectByExample.com</a> on Vercel. It took me about two hours total from completely new Next.js app to deployed production-grade React app. Roughly two minutes of which were spent setting up Vercel.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Fixing Race Conditions in React with useEffect]]></title>
            <link>https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect</link>
            <guid>https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect</guid>
            <pubDate>Tue, 24 Nov 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[If you're using useEffect to fetch data, chances are you've either run into a race condition, or have one without realising it. Let's learn how to fix them in this article.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect">clicking here</a>.)</div><p>So you've got a component that fetches data in React. The component accepts an <code>id</code> as a prop, uses the <code>id</code> to fetch data with useEffect, and display it.</p>
<p>You notice something strange: sometimes the component displays correct data, and sometimes it's invalid, or out of date.</p>
<p>Chances are, you've run into a <a href="https://en.wikipedia.org/wiki/Race_condition">race condition</a>.</p>
<p>You would typically notice a race condition (in React) when two slightly different requests for data have been made, and the application displays a different result depending on which request completes first.</p>
<p>In <a href="/fetching-data-react-with-useeffect/">fetching data with useEffect</a>, we wrote a component that <em>could</em> have a race condition, if <code>id</code> changed fast enough:</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
<p>It might not seem obvious that the snippet above is vulnerable to race conditions, so I cooked up a <a href="https://codesandbox.io/s/beating-async-race-conditions-in-react-7759f?file=/src/DataDisplayer.js">CodeSandbox</a> to make it more noticeable (I added a random wait period of up to 12 seconds per request).</p>
<p>You can see the intended behaviour by clicking the "Fetch data!" button once: a simple component that displays data in response to a single click.</p>
<p>Things get a bit more complicated if you rapidly click the "Fetch data!" button several times. The app will make several requests which finish randomly out of order. The last request to complete will be the result displayed.</p>
<p>The updated DataDisplayer component now looks like this:</p>
<pre><code class="language-jsx">export default function DataDisplayer(props) {
  const [data, setData] = useState(null);
  const [fetchedId, setFetchedId] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      // highlight-next-line
      setTimeout(async () => {
        const response = await fetch(
          `https://swapi.dev/api/people/${props.id}/`
        );
        const newData = await response.json();

        setFetchedId(props.id);
        setData(newData);
        // highlight-next-line
      }, Math.round(Math.random() * 12000));
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return (
      &#x3C;div>
        &#x3C;p style={{ color: fetchedId === props.id ? 'green' : 'red' }}>
          Displaying Data for: {fetchedId}
        &#x3C;/p>
        &#x3C;p>{data.name}&#x3C;/p>
      &#x3C;/div>
    );
  } else {
    return null;
  }
}
</code></pre>
<h2>Fixing the useEffect race condition</h2>
<p>There are a couple of approaches we can take here, both taking advantage of useEffect’s clean-up function:</p>
<ul>
<li>
<p>If we're okay with making several requests, but only rendering the last result, we can use a boolean flag.</p>
</li>
<li>
<p>Alternatively, if we don't have to support users on Internet Explorer, we can use <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController">AbortController</a>.</p>
</li>
</ul>
<h3>useEffect Clean-up Function with Boolean Flag</h3>
<p>First, the gist of our fix in code:</p>
<pre><code class="language-jsx">useEffect(() => {
  // highlight-next-line
  let active = true;

  const fetchData = async () => {
    setTimeout(async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      // highlight-next-line
      if (active) {
        setFetchedId(props.id);
        setData(newData);
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  // highlight-next-line
  return () => {
    // highlight-next-line
    active = false;
    // highlight-next-line
  };
}, [props.id]);
</code></pre>
<p>This fix relies on an often overlooked sentence in the <a href="https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect">React Hooks API reference</a>:</p>
<blockquote>
<p>Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect</p>
</blockquote>
<p>In the example above:</p>
<ul>
<li>changing <code>props.id</code> will cause a re-render,</li>
<li>every re-render will trigger the clean-up function to run, setting <code>active</code> to <code>false</code>,</li>
<li>with <code>active</code> set to <code>false</code>, the now-stale requests won't be able to update our state</li>
</ul>
<p>You'll still have a race-condition in the sense that multiple requests will be in-flight, but only the results from the last one will be used.</p>
<p>It's likely not immediately obvious why the clean-up function in useEffect would fix this issue. I'd recommend you see this fix in action, by checking out the <a href="https://codesandbox.io/s/beating-async-race-conditions-in-react-cleanupfn-45hlu?file=/src/DataDisplayer.js">CodeSandbox</a> (I also added a counter to track the number of active requests, and couple of helper functions).</p>
<h3>useEffect Clean-up Function with AbortController</h3>
<p>Again, let's start with the code:</p>
<pre><code class="language-jsx">useEffect(() => {
  // highlight-next-line
  const abortController = new AbortController();

  const fetchData = async () => {
    setTimeout(async () => {
      try {
        const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
          // highlight-next-line
          signal: abortController.signal,
        });
        const newData = await response.json();

        setFetchedId(id);
        setData(newData);
        // highlight-next-line
      } catch (error) {
        // highlight-next-line
        if (error.name === 'AbortError') {
          // highlight-next-line
          // Aborting a fetch throws an error
          // highlight-next-line
          // So we can't update state afterwards
          // highlight-next-line
        }
        // Handle other request errors here
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  // highlight-next-line
  return () => {
    // highlight-next-line
    abortController.abort();
    // highlight-next-line
  };
}, [id]);
</code></pre>
<p>As with the previous example, we've used the fact that React runs the clean-up function before executing the next effect. You can check out the <a href="https://codesandbox.io/s/beating-async-race-conditions-in-react-abortcontroller-9umlc?file=/src/DataDisplayer.js">CodeSandbox</a> too (this time we're not counting the number of requests as there can only be one at any time).</p>
<p>However, this time we're:</p>
<ul>
<li>initialising an AbortController at the start of the effect,</li>
<li>passing the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal">AbortController.signal</a> to <code>fetch</code> via the options argument,</li>
<li>catching any AbortErrors that get thrown (when abort() is called, the fetch() promise rejects with an AbortError, see <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort">MDN reference</a>), and</li>
<li>calling the abort function inside the clean-up function</li>
</ul>
<p>With this example, we're faced with the following trade-off: drop support for Internet Explorer/use a polyfill, in exchange for the ability to cancel in-flight HTTP requests.</p>
<p>Personally, I'm lucky enough to work for a company where Internet Explorer is no longer supported, so I'd prefer to avoid wasting user bandwidth and use AbortController.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Fetching Data in React with useEffect]]></title>
            <link>https://maxrozen.com/fetching-data-react-with-useeffect</link>
            <guid>https://maxrozen.com/fetching-data-react-with-useeffect</guid>
            <pubDate>Tue, 17 Nov 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[If you're confused about side-effects and pure functions, it can be hard to understand useEffect. Let's learn them both, to fetch data with useEffect.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/fetching-data-react-with-useeffect">clicking here</a>.)</div><p>So you're building a component, and need to fetch some data from an API before rendering your component. You've been told that useEffect is <em>the way</em> for fetching data, but all you can find is discussion about side-effects (particularly in the official <a href="https://reactjs.org/docs/hooks-reference.html#useeffect">useEffect docs</a>).</p>
<p>Let's unpack what side-effects are, and fetch ourselves some data.</p>
<h2>What are side-effects?</h2>
<p>To talk about side-effects, we should probably first talk about pure functions.</p>
<p>A pure function is a function, that when given an input, will always returns the same output. Pure functions also don't modify the arguments passed to them.</p>
<p>While you can have pure functions as simple as this:</p>
<pre><code class="language-js">function myPureFunction(count) {
  return count + 1;
}
</code></pre>
<p>We can also have pure functions that return JSX in React:</p>
<pre><code class="language-jsx">function MyComponent(props) {
  return &#x3C;div className={props.className}>Some content goes here&#x3C;/div>;
}
</code></pre>
<p>Side-effects then, are operations that change things outside of your function, making the function impure.</p>
<p>Fetching data from an API, communicating with a database, and sending logs to a logging service are all considered side-effects, as it's possible to have a different output for the same input. For example, your request might fail, your database might be unreachable, or your logging service might have reached its quota.</p>
<p>This is why useEffect is the hook for us - by fetching data, we're making our React component impure, and useEffect provides us a safe place to write impure code.</p>
<h2>Fetching data</h2>
<p>Here's how to use useEffect to <em>only</em> fetch data (there are a few more steps to make it useful):</p>
<pre><code class="language-jsx">useEffect(() => {
  const fetchData = async () => {
    const response = await fetch(`https://swapi.dev/api/people/1/`);
    const newData = await response.json();
  };

  fetchData();
});
</code></pre>
<p>You might notice a few things are missing from this example:</p>
<ul>
<li>we're not doing anything with the data once we fetch it</li>
<li>we've hardcoded the URL to fetch data from</li>
</ul>
<p>To make this useEffect useful, we'll need to:</p>
<ul>
<li>update our useEffect to pass a prop called <code>id</code> to the URL,</li>
<li>use a <a href="/learn-useeffect-dependency-array-react-hooks/">dependency array</a>, so that we only run this useEffect when <code>id</code> changes, and then</li>
<li>use the useState hook to store our data so we can display it later</li>
</ul>
<pre><code class="language-jsx">useEffect(() => {
  const fetchData = async () => {
    // highlight-next-line
    const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
    const newData = await response.json();
    // highlight-next-line
    setData(newData);
  };

  fetchData();
  // highlight-next-line
}, [props.id]);
</code></pre>
<p>Now let's build the rest of the component. It needs to:</p>
<ul>
<li>store our fetched data using useState</li>
<li>render part of the data when we do have data</li>
<li>render nothing if we don't have data</li>
</ul>
<pre><code class="language-jsx">import React, { useState } from 'react';

export default function DataDisplayer(props) {
  // highlight-next-line
  const [data, setData] = useState(null);
  // highlight-next-line
  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
    // highlight-next-line
  } else {
    return null;
  }
}
</code></pre>
<p>Finally, putting our pure React component together with our side-effect:</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
<p>You might find fetching data in this way results in quite a bit of repeated code, especially if you're using <code>fetch</code>, and handle error and loading states. To avoid this, a common solution is to <a href="https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook">write a custom hook</a>.</p>
<p>One caveat however: this code works fine if you want to fetch data once -
but if you attach a button to let users change <code>id</code> too quickly, you'll run into
a race condition!</p>
<p>You might also want to read this <a href="/race-conditions-fetching-data-react-with-useeffect/">article about fixing race conditions with useEffect</a>.</p>
<p>If you also want caching (to avoid requesting the same data twice), I highly recommend looking into one of these <a href="/keeping-up-with-react-libraries/#data-fetching-libraries">data fetching libraries</a> instead.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Guidelines for choosing React Libraries]]></title>
            <link>https://maxrozen.com/guidelines-for-choosing-react-libraries</link>
            <guid>https://maxrozen.com/guidelines-for-choosing-react-libraries</guid>
            <pubDate>Tue, 03 Nov 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[There are a *lot* of React libraries out there. So how do you pick which one to use in your app? This article provides guidelines for choosing.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guidelines-for-choosing-react-libraries">clicking here</a>.)</div><p>After publishing my previous article tracking <a href="/keeping-up-with-react-libraries/">React Libraries worth talking about</a>, a few people asked me how I decide which library to use in my own projects.</p>
<p>Truth is, I don't have a single hard and fast rule, but rather a feeling that comes from looking at how a library stacks up against a set of quick checks that I've developed over the years of being burned by dodgy libraries.</p>
<p>I've also been asked this several times over the years as an interview question for frontend developer roles. I've heard different answers every time I asked the question back to the interviewer - keep in mind that some teams care about different things, and <strong>you might have to decide for yourself</strong>.</p>
<p>Also, if you strongly disagree with these points, feel free to write up an article, and share it <a href="https://bsky.app/profile/rozenmd.com">with me</a>! I'm genuinely interested in what other developers look at when picking their libraries.</p>
<h2>Docs</h2>
<p>One of the first things I check before installing a library is the documentation.</p>
<ul>
<li>Are the docs up to date?</li>
<li>Are they versioned (if the maintainers update the library over time, can I go back and still read the docs from the version I use)?</li>
<li>Are they detailed enough that I can figure out how to use the library for my use case?</li>
</ul>
<h2>Issues</h2>
<p>I also check GitHub Issues to see if the library still works, and to see how active the maintainers are. Not so much to outright reject libraries with inactive maintainers, but more to guide my expectations.</p>
<ul>
<li>Will I have to support myself/contribute before I can even use the library?</li>
<li>Are there glaring open issues that I might care about, such as the library not working any more, or not supporting certain use cases.</li>
</ul>
<h2>Browser/Mobile Compatibility</h2>
<p>Especially in the case of Component/UI libraries, I want to know the extent of browser and mobile support. I wouldn't <em>always</em> check this, but checking GitHub Issues for problems with browsers can save you pain later.</p>
<p>Unfortunately there are still end-users out there that still want IE11 to be supported (particularly government agencies), so some teams may wish to check for Internet Explorer compatibility when picking libraries.</p>
<h2>Library size</h2>
<p>If a library is small enough, or I only need a small portion of the library, I might not even install it with <code>yarn</code> or <code>npm</code>. I'll just copy the function into a utils folder and write my own tests for it.</p>
<p>In the end, we're hunting for functionality, and I'd prefer to avoid writing fresh code where possible. If a library exists, you may as well use it rather than writing your own from scratch.</p>
<p><em>Side note</em>: I witnessed the cost of ignoring this, particularly with <a href="https://swr.vercel.app/">SWR</a> - a team I worked with implemented their own version, experienced and fixed the same bugs/pitfalls, and learned the same lessons in state and cache management. Easily hundreds of hours that could have been saved with a <code>yarn add swr</code>/<code>npm install swr</code>.</p>
<p>On the other hand, is it too big? Unless I'm building a super quick prototype, I will always compare libraries in <a href="https://bundlephobia.com/">Bundlephobia</a>, and prefer the smaller library (and consider the other points in this article).</p>
<h2>Examples</h2>
<p>If I have to choose between two libraries, (and all else being equal), I'd pick the one with examples. Bonus points for using <a href="https://codesandbox.io/">CodeSandbox</a> or providing running examples that I can copy paste out of the repo.</p>
<p>If you want to see how well this can be done in the React ecosystem, see <a href="https://github.com/jaredpalmer/razzle/tree/master/examples">Razzle</a> and <a href="https://react-hook-form.com/">React Hook Form</a>.</p>
<h2>Popularity</h2>
<p>I'd be lying if I said popularity didn't bias my decision, so I've included it in this article. That being said though, I wouldn't shy away from being an early adopter of a library if it solved a problem I was looking to fix.</p>
<p>There are a few means of understanding how popular a library is:</p>
<ul>
<li>
<p>Number of stars on GitHub, combined with "Used by" on GitHub</p>
<p>(In my opinion) the number of stars alone will just tell you how many blog posts people write about a library, and how famous the maintainers are. Combining the number of stars with the number of projects that use the library as a dependency gives you a better understanding of whether it's actually used.</p>
</li>
<li>
<p>Weekly downloads on npm</p>
<p>It's worth keeping in mind that this number is not always bulletproof.</p>
<p>For example, <code>@testing-library/react</code> recently beat <code>enzyme</code> on weekly downloads, however <code>@testing-library/react</code> is now downloaded as part of create-react-app, regardless of whether it's used or not.</p>
</li>
</ul>
<h2>Summary</h2>
<p>In conclusion, this article was an attempt at explaining what I look at (often without even realising) when deciding to use a library.</p>
<p>Different developers will have different opinions and criteria - we haven't all had the same career, we've all seen and experienced different things to shape what we value.</p>
<p>If you're just starting out in the frontend, hopefully this article gives you guidance in terms of what to look for.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Understanding when to use useMemo]]></title>
            <link>https://maxrozen.com/understanding-when-use-usememo</link>
            <guid>https://maxrozen.com/understanding-when-use-usememo</guid>
            <pubDate>Tue, 20 Oct 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[If you've just learned what useCallback is, you might be wondering why on earth useMemo exists, and why we don't use it everywhere.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/understanding-when-use-usememo">clicking here</a>.)</div><p>It's pretty common for people to say</p>
<blockquote>
<p>Don't use useCallback/useMemo everywhere!</p>
</blockquote>
<p>Without <em>actually</em> explaining cases where you <strong>would</strong> want to use useCallback/useMemo.</p>
<p>In my <a href="/stop-useeffect-running-every-render-with-usecallback/">last article</a> we learned that useCallback is useful for passing stable references to functions down to the children of a React component, particularly when using those functions in a child's useEffect.</p>
<p>If you're scratching your head wondering "...but then, what the hell is useMemo for?", you're not alone!</p>
<p>One hint that the React docs give is:</p>
<blockquote>
<p>useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).</p>
</blockquote>
<p>Which is great if you're well versed on the significance of passing a function reference versus passing an arrow function that calls the function (while quickly scanning docs for an answer), but for the rest of us, hopefully this article will help.</p>
<h2>What useMemo does</h2>
<p>In short, <strong>useMemo</strong> calls a function when dependencies change, and memoizes (remembers) the <strong>result of the function</strong> between renders.</p>
<p>This is in contrast with <strong>useCallback</strong> which remembers an <strong>existing value</strong> (typically a function's definition), between renders.</p>
<p>You want to use useMemo to save yourself from rerunning an expensive calculation to generate a new value, and you want to use useCallback to store an existing value.</p>
<h2>When to use useMemo</h2>
<p>This part is where it's easy to get frustrated. There are a lot of blog posts out there describing useMemo, and then presenting examples of when <strong>not</strong> to use it.</p>
<p>Unfortunately, it needs repeating: don't use useMemo until you notice parts of your app are frustratingly slow. "Premature optimisation is the root of all evil", and throwing useMemo everywhere is premature optimisation.</p>
<p>Here are a couple of cases when you <strong>should</strong> consider using useMemo:</p>
<ul>
<li>You're noticing a component's render is frustratingly slow, and you're passing a calculation to an unknowable number of children, such as when rendering children using <code>Array.map()</code></li>
<li>Your app often becomes unresponsive because you're fetching a large amount of data, and having to transform it into a usable format</li>
</ul>
<p>The key is to focus on the problem.</p>
<p>"My app is slow, and calculation-heavy" is a problem that useMemo helps to solve. Run your app through React DevTools Profiler, as well as <a href="https://developers.google.com/web/tools/lighthouse">Google Lighthouse</a> or <a href="https://webpagetest.org/">WebPageTest</a> to understand its performance metrics, wrap your calculation in useMemo, then measure again.</p>
<p>"I just learned useMemo, and want to use it everywhere" is focusing on the solution, and will lead you to premature optimisation, and a potentially slower app.</p>
<h2>Why not use useMemo everywhere then?</h2>
<p>In short, it's not a free performance optimisation.</p>
<p>There's an additional cost (memory usage, for one) incurred when setting up <code>useMemo</code>, that can very quickly outweigh the performance benefit of remembering every single function's possible value.</p>
<p>When we use useMemo, we're taking up more memory in order to free up CPU time. If your app is hammering the CPU with a lot of calculations, that's when you might consider taking up some memory and use useMemo instead.</p>
<h2>What about stable references?</h2>
<p>If you want to keep a stable reference to an object/array <strong>that doesn't require recalculation</strong>, consider using useRef.</p>
<p>On the other hand if you need to recalculate the value when dependencies change, useMemo is the hook for you.</p>
<h2>Potential mistakes when using useMemo</h2>
<p>Using useMemo isn't free of pitfalls as well - one of the big ones is that the cache isn't guaranteed to keep all of its values between renders. Taken from the docs:</p>
<blockquote>
<p>You may rely on useMemo as a performance optimization, not as a semantic guarantee</p>
</blockquote>
<p>Translation: the cache isn't stable!</p>
<p>Meaning if you <strong>absolutely</strong> want to avoid recalculations with your useMemo call, it's not guaranteed. For a version of useMemo with a stable cache, see <a href="https://github.com/alexreardon/use-memo-one">useMemoOne</a>.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Stop useEffect from running on every render with useCallback]]></title>
            <link>https://maxrozen.com/stop-useeffect-running-every-render-with-usecallback</link>
            <guid>https://maxrozen.com/stop-useeffect-running-every-render-with-usecallback</guid>
            <pubDate>Tue, 13 Oct 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[Did ESLint tell you to add a function to your dependency array, and now you're getting infinite re-renders? Let's learn how useEffect can help.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/stop-useeffect-running-every-render-with-usecallback">clicking here</a>.)</div><p>What happens if you add a function to your useEffect's dependency array, and suddenly start to get infinite re-renders?</p>
<p>Chances are, you've run in to the perfect opportunity to use useCallback!</p>
<p>The syntax is:</p>
<pre><code class="language-jsx">const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
</code></pre>
<p>useCallback returns you a new version of your function only when its dependencies change. In the example above, that's only when <code>a</code> or <code>b</code> changes.</p>
<p>This means even when your component re-renders, you can be sure your function wrapped in useCallback won't be re-declared, preventing the dreaded infinite re-render/useEffect loop.</p>
<p>Before we go too far with useCallback, let's have a quick refresher on React components.</p>
<h2>Quick refresher</h2>
<p>Here's a React component that renders a child component. Occassionally, something will happen that will make the component re-render (such as props changing, or useState being called).</p>
<pre><code class="language-jsx">function SomeComponent(props) {
  //do some other stuff here that might cause re-renders, like setting state
  return &#x3C;DataFetcher />;
}
</code></pre>
<p>Now, if we wanted to pass <code>&#x3C;DataFetcher/></code> a prop, such as a function that generates a URL to fetch data from, you might define a function as follows:</p>
<pre><code class="language-jsx">function SomeComponent(props){
  // highlight-next-line
  function getUrl(id){
    // highlight-next-line
    return "https://some-api-url.com/api/" + id + "/"
    // highlight-next-line
  }
  //do some other stuff here that might cause re-renders, like setting state
  return &#x3C;DataFetcher getUrl={getUrl}>
}
</code></pre>
<p>Before we had to worry about hooks, we could define a function in a class, and pass it down to children that could fire off <code>this.getUrl(id)</code> without a worry. However, now that we're in a function component, the way we've defined <code>getUrl</code> means something different.</p>
<p>Since by default all of <code>SomeComponent</code>'s code re-runs on render, <code>getUrl</code> gets re-defined every render.</p>
<p>If <code>DataFetcher</code> then uses <code>getUrl</code> as part of a useEffect hook, even if you add <code>getUrl</code> to the dependency array, your useEffect would fire every single render.</p>
<pre><code class="language-jsx">useEffect(() => {
  fetchDataToDoSomething(getUrl);
}, [getUrl]); // 🔴 re-runs this effect every render
</code></pre>
<h2>How do we stop useEffect from running every render?</h2>
<p>Back in <code>SomeComponent</code>, we have two options to deal with this (assuming we can't just move <code>getUrl</code> into the troublesome useEffect hook).</p>
<ol>
<li>
<p>The old fashioned way: move <code>getUrl</code> outside of the component, so it doesn't get re-declared every render:</p>
<pre><code class="language-jsx">// highlight-next-line
function getUrl(id){
   // highlight-next-line
   return "https://some-api-url.com/api/" + id + "/"
   // highlight-next-line
}

function SomeComponent(props){
   //do some other stuff here that might cause re-renders, like setting state
   return &#x3C;DataFetcher getUrl={getUrl}>
}
</code></pre>
</li>
</ol>
<ol start="2">
<li>
<p>The Hooks way, which is to wrap <code>getUrl</code> in a useCallback:</p>
<pre><code class="language-jsx">function SomeComponent(props){
 // highlight-next-line
   const getUrl = useCallback(function (id) {
   // highlight-next-line
     return "https://some-api-url.com/api/" + id + "/";
   // highlight-next-line
   }, []); // &#x3C;-- Note that we can't add id to the deps array in this case

   //do some other stuff here that might cause re-renders, like setting state
   return &#x3C;DataFetcher getUrl={getUrl}>
}
</code></pre>
<p>Of course, this is a pretty simplified example to illustrate what you could do to fix much more complicated code.</p>
<p>I'm not suggesting you should pass a function that returns a string in your real code, when you could pass the string, and avoid using useCallback/useMemo everywhere.</p>
</li>
</ol>
<hr>
<p>The key takeaway here is that useCallback returns you a new version of your function only when its dependencies change, saving your child components from automatically re-rendering every time the parent renders. This is particularly useful when used with useEffect, as we can safely add functions wrapped in useCallback to the dependency array without fear of infinite re-renders.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How the React Hooks ESLint plugin saved me hours debugging useEffect]]></title>
            <link>https://maxrozen.com/react-hooks-eslint-plugin-saved-hours-debugging-useeffect</link>
            <guid>https://maxrozen.com/react-hooks-eslint-plugin-saved-hours-debugging-useeffect</guid>
            <pubDate>Tue, 06 Oct 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[useEffect is great for fetching and rendering data, but are you using it correctly? There's an eslint plugin to check!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/react-hooks-eslint-plugin-saved-hours-debugging-useeffect">clicking here</a>.)</div><p>Ever find yourself chasing <em>weird</em> bugs in React, like sometimes it seems as though your components fetch all of the data they need, and sometimes they don't?</p>
<p>Chances are you've probably got yourself a race condition.</p>
<p>That's the special insight I'm giving you after spending several days at work chasing weird behaviour in one of our React apps, that could only be reproduced once in 75 page refreshes.</p>
<p>In case you're wondering, here's some backstory.</p>
<p>Imagine your web app needs to make calls to several APIs to get the information it needs to render your component:</p>
<pre><code class="language-jsx">useEffect(() => {
  // assume these API calls just write to a global state store
  // and the input state is also coming from that global state store
  getDataFromFirstAPI(someState);
  getDataFromSecondAPI(someOtherState);
  getDataFromThirdAPI(someThirdState);
}, [someState, someOtherState]);
</code></pre>
<p>What would happen is that either of <code>getDataFromFirstAPI</code> or <code>getDataFromSecondAPI</code> could mutate the value of <code>someThirdState</code> (as a feature of our particular implementation), making calls to <code>getDataFromThirdAPI</code> use stale data.</p>
<p>Sharp readers will notice the fix pretty quickly: <code>someThirdState</code> is missing from the dependency array, but if your component was complex and had several useEffect calls already, chances are you probably wouldn't notice it. Especially if several of <em>those</em> useEffect calls also have missing dependencies.</p>
<p>In fact, create-react-app would only tell you about it via a warning, not an error. In our case, the warning would be buried deep in a stack of noisy logs (as part of the review into this type of bug, as a team we decided to make it an error, but it's up to you and your team).</p>
<p>Turns out there's an eslint rule specifically for this class of bug: <code>react-hooks/exhaustive-deps</code>, AND it's part of a package <a href="https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks">maintained by the React team</a>.</p>
<p>That's where <code>eslint-plugin-react-hooks</code> comes in. You already have it if you use create-react-app, but chances are you don't have it if you've decided to go with a custom webpack config.</p>
<p>You add it via npm/yarn:</p>
<pre><code class="language-bash">yarn add eslint-plugin-react-hooks -D
# OR
npm i eslint-plugin-react-hooks -D
</code></pre>
<p>Afterwards, you can add it to your ESLint config like so:</p>
<pre><code class="language-js">// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    // highlight-next-line
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}
</code></pre>
<p>Now every time you hit save in your codebase with missing dependencies in your useEffects, you'll have a similar warning/error to this show up:</p>
<pre><code>Compiled with warnings.

./src/App.js
  Line 16:6:  React Hook useEffect has a missing dependency: 'someThirdState'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
</code></pre>
<p>Since enabling the <code>react-hooks/exhaustive-deps</code> rule, I've already saved myself hours of head scratching and general outrage by saving myself from committing buggy code several times.</p>
<p>Do yourself a favour and check if you've got the rule enabled.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[The Definitive Guide to Commonly Used Words in React]]></title>
            <link>https://maxrozen.com/definitive-glossary-guide-commonly-used-words-react</link>
            <guid>https://maxrozen.com/definitive-glossary-guide-commonly-used-words-react</guid>
            <pubDate>Tue, 29 Sep 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[Do some words in React have you scratching your head? Ref? Stateless component? Let's learn some words!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/definitive-glossary-guide-commonly-used-words-react">clicking here</a>.)</div><p>React has a lot of jargon and three-letter acronyms. Even as an intermediate React developer you might find yourself forgetting what certain things mean.</p>
<p>Got any React terms or expressions you'd like defined here? Feel free to message me via <a href="mailto:max@maxrozen.com">Email</a> or <a href="https://bsky.app/profile/rozenmd.com">BlueSky</a>.</p>
<h2>Table of Contents</h2>
<ul>
<li><a href="#table-of-contents">Table of Contents</a>
<ul>
<li><a href="#class-component">Class Component</a></li>
<li><a href="#component">Component</a></li>
<li><a href="#cra">CRA</a></li>
<li><a href="#enzyme">Enzyme</a></li>
<li><a href="#eslint">ESLint</a></li>
<li><a href="#functional-component">Functional Component</a></li>
<li><a href="#flow">Flow</a></li>
<li><a href="#higher-order-component">Higher-order component</a></li>
<li><a href="#hoc">HOC</a></li>
<li><a href="#hooks">Hooks</a></li>
<li><a href="#jest">Jest</a></li>
<li><a href="#linting">Linting</a></li>
<li><a href="#props">Props</a></li>
<li><a href="#proptypes">PropTypes</a></li>
<li><a href="#react-testing-library">React Testing Library</a></li>
<li><a href="#ref">Ref</a></li>
<li><a href="#rtl">RTL</a></li>
<li><a href="#stateless-functional-component">Stateless Functional Component</a></li>
<li><a href="#styled-components">Styled Components</a></li>
<li><a href="#typescript">TypeScript</a></li>
<li><a href="#webpack">webpack</a></li>
</ul>
</li>
</ul>
<h3>Class Component</h3>
<p>At its simplest, a Class Component is a component written using a JavaScript ES6 Class.</p>
<pre><code class="language-jsx">class Welcome extends React.Component {
  render() {
    return &#x3C;h1>Hello, {this.props.name}&#x3C;/h1>;
  }
}
</code></pre>
<h3>Component</h3>
<p>A Component is (most-commonly) a function that accepts <a href="#props">props</a>, and returns things to display in the browser.</p>
<p>See also: <a href="#class-component">Class Component</a>, <a href="#functional-component">Functional Component</a></p>
<h3>CRA</h3>
<p>CRA, or create-react-app, refers to a set of scripts open sourced by Facebook to make it easier to get started with your React app. It comes with its own opinions on <a href="#webpack">webpack</a>, testing, and <a href="#linting">linting</a>.</p>
<p>CRA has become more popular recently as developers begin to realise maintaining their own webpack config and keeping related dependencies up to date isn't worth the time or energy.</p>
<p>Links:
<a href="https://create-react-app.dev/docs/getting-started">docs</a> | <a href="https://create-react-app.dev/">website</a></p>
<h3>Enzyme</h3>
<p>Enzyme is a utility that renders React components in tests, and provides methods for querying their state and props.</p>
<p>Used in conjunction with <a href="#jest">Jest</a>.</p>
<p>Links:
<a href="https://enzymejs.github.io/enzyme/">docs</a> | <a href="https://www.npmjs.com/package/enzyme">npm</a></p>
<h3>ESLint</h3>
<p>ESLint is a program that is run to analyse your codebase, typically looking for ways to improve your code.</p>
<p>These "ways to improve your code" are typically distributed as a set of rules, such as <a href="https://www.npmjs.com/package/eslint-config-react-app#usage-outside-of-create-react-app">eslint-config-react-app</a>.</p>
<p>Some sets of rules are considered highly opinionated (such as <a href="https://www.npmjs.com/package/eslint-config-airbnb">eslint-config-airbnb</a>), and aren't recommended for use as often.</p>
<p>Links:
<a href="https://eslint.org/docs/user-guide/getting-started">docs</a> | <a href="https://eslint.org/">website</a></p>
<h3>Functional Component</h3>
<p>A <a href="#component">component</a> written using a JavaScript function.</p>
<p>Thanks to the advent of <a href="#hooks">Hooks</a>, Functional components are considered "the way" components are written now.</p>
<p>Functional components can be written either by using an arrow function, or using the <code>function</code> keyword.</p>
<p>Using an arrow function:</p>
<pre><code class="language-jsx">const Welcome = (props) => {
  return &#x3C;h1>Hello, {props.name}&#x3C;/h1>;
};
</code></pre>
<p>Using the <code>function</code> keyword:</p>
<pre><code class="language-jsx">function Welcome(props) {
  return &#x3C;h1>Hello, {props.name}&#x3C;/h1>;
}
</code></pre>
<h3>Flow</h3>
<p>A type checker that can analyse types on <strong>any</strong> function, not just React components. It also checks your code at compile-time, rather than run-time.</p>
<p>Has fallen by the wayside due to <a href="#typescript">TypeScript</a>'s meteoric rise to fame, thousands of unresolved GitHub issues, and sluggish performance.</p>
<p>Links:
<a href="https://flow.org/en/docs/">docs</a> | <a href="https://flow.org/en/">website</a></p>
<h3>Higher-order component</h3>
<p>A fancy way of saying a component whose sole purpose is to wrap other components and pass props.</p>
<p>Links:
<a href="https://reactjs.org/docs/higher-order-components.html">docs</a></p>
<h3>HOC</h3>
<p>See <a href="#higher-order-component">Higher-order component</a></p>
<h3>Hooks</h3>
<p>Hooks let developers use state and other React features previously only available in class components, in functional components.</p>
<p>Links: <a href="https://reactjs.org/docs/hooks-intro.html">docs</a> | <a href="https://reactjs.org/docs/hooks-reference.html">hooks reference</a> | <a href="https://reactjs.org/docs/hooks-rules.html">rules of hooks</a></p>
<h3>Jest</h3>
<p>Jest is a testing framework responsible for running your tests. Supports both Node and React (and other JS frameworks/languages).</p>
<p>Links: <a href="https://jestjs.io/docs/en/getting-started">docs</a> | <a href="https://jestjs.io/en/">website</a></p>
<h3>Linting</h3>
<p>Linting means to run a program that analyses your code for issues. Issues could mean syntax or style issues, as well as potential bugs.</p>
<p>The most commonly used linter in React is <a href="#eslint">ESLint</a>.</p>
<h3>Props</h3>
<p>Props are arguments you pass as input to a component in React.</p>
<h3>PropTypes</h3>
<p>PropTypes is a package (once part of React) that provides runtime type checking for your React components' props. It was one of the first ways to type-check React code.</p>
<p>Effectively superseded by <a href="#flow">Flow</a> and <a href="#typescript">TypeScript</a>.</p>
<p>Usage (from the <a href="https://github.com/facebook/prop-types#usage">docs</a>):</p>
<pre><code class="language-jsx">import React from 'react';
import PropTypes from 'prop-types';

class MyComponent extends React.Component {
  render() {
    // ... do things with the props
  }
}

MyComponent.propTypes = {
  // These are optional by default
  optionalArray: PropTypes.array,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalString: PropTypes.string,
  requiredFunc: PropTypes.func.isRequired,

  // A value of any data type, required
  requiredAny: PropTypes.any.isRequired,
};
</code></pre>
<p>Links:
<a href="https://www.npmjs.com/package/prop-types">npm</a> | <a href="https://github.com/facebook/prop-types">repo</a></p>
<h3>React Testing Library</h3>
<p>This is a library for testing React, literally named "React Testing Library". It's also commonly referred to as RTL.</p>
<p>RTL grew in popularity due to its focus on <a href="https://maxrozen.com/dont-test-implementation-details-react/">testing functionality, not implementation details</a>, as well as its inclusion in <a href="#CRA">create-react-app</a>.</p>
<p>Recently became popular over existing library <a href="#enzyme">Enzyme</a>. Both libraries provide methods for rendering React components in tests, however RTL exposes functions that nudge developers away from testing implementation details.</p>
<p>Links: <a href="https://www.npmjs.com/package/@testing-library/react">npm</a> | <a href="https://testing-library.com/docs/react-testing-library/intro">website</a></p>
<h3>Ref</h3>
<p>Ref, short for reference, is used for accessing and modifying the child of a React component.</p>
<p>Usage:</p>
<pre><code class="language-jsx">function MyFunctionalComponent() {
  const myDiv = useRef(null);

  return &#x3C;div ref={myDiv} />;
}
</code></pre>
<p>Particularly useful when integrating with legacy libraries that modify the DOM, and you just want to fetch a value from the DOM and continue using React.</p>
<p>Links:
<a href="https://reactjs.org/docs/refs-and-the-dom.html">docs</a></p>
<h3>RTL</h3>
<p>See <a href="#react-testing-library">React Testing Library</a>.</p>
<h3>Stateless Functional Component</h3>
<p>How we used to call <a href="#functional-component">Functional Components</a> before Hooks were added to React.</p>
<p>Basically, a stateless functional component is a functional component:</p>
<pre><code class="language-jsx">function Welcome(props) {
  return &#x3C;h1>Hello, {props.name}&#x3C;/h1>;
}
</code></pre>
<h3>Styled Components</h3>
<p>Styled Components is a library that allows developers to style their components by writing CSS in their JavaScript files (a solution known as CSS-in-JS).</p>
<p>Links:
<a href="https://styled-components.com/docs">docs</a> | <a href="https://styled-components.com/">website</a></p>
<h3>TypeScript</h3>
<p>A typed language that compiles to JavaScript, TypeScript rapidly checks types across your entire codebase at compile-time.</p>
<p>Now (optionally) included as part of <a href="https://create-react-app.dev/docs/adding-typescript/">CRA</a>.</p>
<p>Links: <a href="https://react-typescript-cheatsheet.netlify.app/">cheatsheet</a> | <a href="https://basarat.gitbook.io/typescript/">free book</a> | <a href="https://www.typescriptlang.org/docs/handbook/intro.html">handbook</a> | <a href="https://www.typescriptlang.org/">website</a></p>
<h3>webpack</h3>
<p>webpack (with a lower-case w), is a program that reads through your codebase, follows your imports and exports, removes unused code, and outputs a bundle ready to be served by a server.</p>
<p>When reading webpack's docs, pay close attention to the version (in the top left hand corner). As the API greatly changes between major versions, you may think you've found a solution to your problems, but it may be unavailable in your version of webpack.</p>
<p>Links: <a href="https://webpack.js.org/concepts/">docs</a> | <a href="https://webpack.js.org/">website</a></p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Understanding Integration Testing in React]]></title>
            <link>https://maxrozen.com/understanding-integration-testing-react</link>
            <guid>https://maxrozen.com/understanding-integration-testing-react</guid>
            <pubDate>Wed, 23 Sep 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[Moving from Enzyme to React Testing Library is rough. You can't do a lot of things you used to, and there are new best practices. Let's learn them!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/understanding-integration-testing-react">clicking here</a>.)</div><p>Do you sometimes worry that your tests don't make sense? Struggling to <em>get</em> what people mean by "test from the user's perspective" and the classic piece of advice "test functionality, not implementation details"?</p>
<p>You're not alone!</p>
<p>I felt the same thing when I moved from Enzyme to React Testing Library. I used to think of testing as just checks you had to do on individual components, directly asserting against props and state, and would struggle to think about how to test the integration of several components together.</p>
<h2>What does "integration test" even mean?</h2>
<p>It helps to think of integration testing like a bigger unit test, except the unit you're testing is the combination of several smaller components.</p>
<p>More concretely, instead of just testing a <code>Button</code> component, or a <code>TextField</code> component in isolation, we're going to test that they work when placed together into a form.</p>
<h2>Let's get started!</h2>
<p>We're going to be testing a form almost every public web app you're going to build has: a Login form. It's probably one of the most important parts of your app (in terms of business value), so let's be confident it actually works!</p>
<p><img src="react-login-form.png" alt="React Login Form"></p>
<h2>Setup</h2>
<p>We're going to be using <a href="https://reactjs.org/docs/create-a-new-react-app.html">create-react-app</a>, because it comes bundled with <a href="https://testing-library.com/docs/react-testing-library/intro">@testing-library/react</a>. I'm also using <a href="https://react-hook-form.com/">react-hook-form</a> to build our form, because it's the quickest way I could think of to display the form data in our web app.</p>
<p>You can either:</p>
<ol>
<li>Clone the <a href="https://github.com/rozenmd/react-integration-testing">repo</a></li>
<li>Follow the steps <a href="#manual-steps">below</a> to set this up manually (you'll still need to download the <a href="https://github.com/rozenmd/react-integration-testing">repo</a>)</li>
</ol>
<h3>Manual steps</h3>
<ol>
<li>
<p>Run:</p>
<pre><code class="language-bash">npx create-react-app &#x3C;YOUR_APP_NAME>
</code></pre>
</li>
<li>
<p>Run:</p>
<pre><code class="language-bash">cd &#x3C;YOUR_APP_NAME>
</code></pre>
</li>
<li>
<p>[Only for react-hook-form] Run:</p>
<pre><code class="language-bash">yarn add react-hook-form mutationobserver-shim
# OR
npm install react-hook-form mutationobserver-shim
</code></pre>
</li>
<li>
<p>[Only for react-hook-form] In <code>src/setupTests.js</code>, in a new line, add <code>import 'mutationobserver-shim';</code></p>
</li>
<li>
<p>Copy the <code>src/</code> directory from the <a href="https://github.com/rozenmd/react-integration-testing">repo</a> into your <code>src/</code> directory, overriding existing files</p>
</li>
<li>
<p>Run:</p>
<pre><code class="language-bash">yarn start
</code></pre>
<p>You should see something like this:
<img src="react-login-form-before.png" alt="React Login Form after starting"></p>
</li>
<li>
<p>At this point, if you ran <code>yarn test</code>, you would see the following:</p>
</li>
</ol>
<pre><code> PASS  src/pages/Login.test.js
  ✓ renders all inputs (75ms)
  ✓ integration test (110ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.883s
Ran all test suites.

Watch Usage: Press w to show more.
</code></pre>
<h2>So how do we get here?</h2>
<p>Let's start off with a render test:</p>
<pre><code class="language-jsx">import React from 'react';
import { render } from '@testing-library/react';
import Login from './Login';

test('renders all inputs', () => {
  const { getByLabelText } = render(&#x3C;Login />);

  // here we're using regex so that updating the
  // component to UPPER CASE won't fail the test
  const userInput = getByLabelText(/username/i);
  expect(userInput).toBeInTheDocument();

  const passwordInput = getByLabelText(/password/i);
  expect(passwordInput).toBeInTheDocument();
});
</code></pre>
<p>It's not the most complicated test out there, and it's already giving us benefits beyond what a unit test would give. It's also technically an integration test.</p>
<p>In particular, we're saying "on my login form, I want a username input to be visible at the same time as the password input".</p>
<p>Now let's test the happy path of our login form:</p>
<pre><code class="language-jsx">import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import user from '@testing-library/user-event';
import Login from './Login';

test('integration test', async () => {
  const USER = 'some-username';
  const PASS = 'some-pass';

  const { getByLabelText, findByText, getByText } = render(&#x3C;Login />);

  const userInput = getByLabelText(/username/i);
  user.type(userInput, USER);

  const passwordInput = getByLabelText(/password/i);
  user.type(passwordInput, PASS);

  const submitButton = getByText(/submit/i);
  fireEvent.click(submitButton);
  expect(await findByText(/your username/i)).toBeInTheDocument();
  expect(await findByText(/your password/i)).toBeInTheDocument();
});
</code></pre>
<p>This integration test builds on top of our initial test in a few ways:</p>
<ul>
<li>We've imported the <code>'@testing-library/user-event'</code> library to allow us to type into our inputs</li>
<li>We've imported <code>fireEvent</code> from the <code>'@testing-library/react'</code> library to allow us to click on our <code>Button</code> component</li>
<li>We've marked the test <code>async</code> to enable us to use <code>findByText()</code>
<ul>
<li>findByText is neat because it returns a Promise, letting us wait until it finds the text it's looking for before continuing</li>
</ul>
</li>
<li>Most importantly, we've built a test that can type into our <code>TextField</code> components, click on our <code>Button</code> component, and trigger the <code>Form</code> component's <code>onSubmit</code> function!</li>
</ul>
<p>If you're confused about <code>findByText</code> vs <code>getByText</code>, don't worry - that's normal. React Testing Library's <a href="https://testing-library.com/docs/react-testing-library/cheatsheet">cheatsheet</a> has tips to help you decide which one to use.</p>
<h2>Conclusion</h2>
<p>You've just started to understand integration testing, but you best believe there's a lot more to it than this article!</p>
<p>If you want a more advanced perspective of integration testing your forms, I highly recommend reading the testing section of React Hook Form's <a href="https://react-hook-form.com/advanced-usage/#TestingForm">Advanced Usage</a> guide.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Guidelines to improve your React folder structure]]></title>
            <link>https://maxrozen.com/guidelines-improve-react-app-folder-structure</link>
            <guid>https://maxrozen.com/guidelines-improve-react-app-folder-structure</guid>
            <pubDate>Tue, 15 Sep 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[People often say to 'move files around until it feels right', but what does 'right' even mean? Let's learn in this article.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guidelines-improve-react-app-folder-structure">clicking here</a>.)</div><p>If you've done any amount of research into the best way to structure your React app's folders, you've probably run into the following advice copy-pasted into forum threads:</p>
<blockquote>
<p>Move files around until it feels right.</p>
</blockquote>
<p>To me that reads a lot like unhelpfully telling people to <a href="https://en.wikipedia.org/wiki/RTFM">RTFM</a>, so let's develop a better answer than that (the quote originated from Dan Abramov in a long-ago deleted Tweet, and has since taken on a life of it's own).</p>
<h2>Folder structures for React apps</h2>
<p>I'll start by showing you the folder structure I tend to use, then teach you the guiding principles I use for my structure to "feel right" for my purposes.</p>
<h3>My Project Structure</h3>
<pre><code>src/
|-- components/
|   |-- Avatar/
|   |   |-- Avatar.ts
|   |   |-- Avatar.test.ts
|   |-- Button/
|   |   |-- Button.ts
|   |   |-- Button.test.ts
|   |-- TextField/
|   |   |-- TextField.ts
|   |   |-- TextField.test.ts
|-- contexts/
|   |-- UserContext/
|   |   |-- UserContext.ts
|-- hooks/
|   |-- useMediaQuery/
|   |   |-- useMediaQuery.ts
|-- pages/
|   |-- UserProfile/
|   |   |-- Components/
|   |   |   |-- SomeUserProfileComponent/
|   |   |   |   |-- SomeUserProfileComponent.ts
|   |   |   |   |-- SomeUserProfileComponent.test.ts
|   |   |-- UserProfile.ts
|   |   |-- UserProfile.test.ts
|   |-- index.ts
|-- utils/
|   |-- some-common-util/
|   |   |-- index.ts/
|   |   |-- index.test.ts
|-- App.ts
|-- AuthenticatedApp.ts
|-- index.ts
|-- UnauthenticatedApp.ts
</code></pre>
<h3>Guiding Principles</h3>
<ul>
<li><a href="#folder-structures-for-react-apps">Folder structures for React apps</a>
<ul>
<li><a href="#my-project-structure">My Project Structure</a></li>
<li><a href="#guiding-principles">Guiding Principles</a>
<ul>
<li><a href="#1-on-testing">1. On testing</a></li>
<li><a href="#2-naming-your-component-files">2. Naming your component files</a></li>
<li><a href="#3-separate-your-pages-from-your-components">3. Separate your pages from your components</a></li>
<li><a href="#4-keep-styled-components-in-your-component-files">4. Keep styled components in your component files</a></li>
<li><a href="#5-split-things-out-only-when-you-need-to">5. Split things out only when you need to</a></li>
<li><a href="#6-advanced-split-up-your-codebase-by-team">6. [Advanced] Split up your codebase by team</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h4>1. On testing</h4>
<p>Many guides out there will suggest using a <code>__tests__</code> folder whose structure mimics your app's.</p>
<p>I strongly suggest you avoid this pattern for two reasons:</p>
<ol>
<li>You'll discourage newcomers from testing by hiding your tests</li>
<li>You'll need to refactor to keep in sync every time you want to move your components/pages around</li>
</ol>
<p>Instead, use either an <code>index.test.ts</code> file, or a <code>MyComponent.test.ts</code> file. Keep your tests with your components so you remember to test them.</p>
<h4>2. Naming your component files</h4>
<p>There are two ways you can name your component files:</p>
<ol>
<li>
<p><code>index.ts</code></p>
<p>The advantage of naming your file <code>index.ts</code> is that that you don't repeat yourself - you've already named the folder <code>MyComponent/</code>, why name the file the same?</p>
<p>The disadvantage is that when you search for <code>index.ts</code> you'll have dozens (if not hundreds, or thousands) of <code>index.ts</code> files, making it hard to find the file you're looking for.</p>
</li>
<li>
<p><code>MyComponent.ts</code></p>
<p>The advantage of using <code>MyComponent.ts</code> is that VS Code and similar code search tools will find exactly the file you're looking for faster, rather than listing all files in the directory.</p>
</li>
</ol>
<p>It doesn't particularly matter which way you choose to name your components, as long as you pick one, and use it <strong>consistently</strong>.</p>
<h4>3. Separate your pages from your components</h4>
<p>React frameworks like <a href="https://nextjs.org/">Next.js</a> make this easier by using page-based routing, but you can use it in any React project.</p>
<p>For example:</p>
<pre><code>src/
|-- components/
|   |-- Avatar/
|   |   |-- Avatar.ts
|   |   |-- Avatar.test.ts
|   |-- Button/
|   |   |-- Button.ts
|   |   |-- Button.test.ts
|   |-- TextField/
|   |   |-- TextField.ts
|   |   |-- TextField.test.ts
|-- pages/
|   |-- UserProfile/
|   |   |-- Components/
|   |   |   |-- SomeUserProfileComponent/
|   |   |   |   |-- SomeUserProfileComponent.ts
|   |   |   |   |-- SomeUserProfileComponent.test.ts
|   |   |-- UserProfile.ts
|   |   |-- UserProfile.test.ts
|   |-- index.ts
</code></pre>
<p>In your <code>pages/index.ts</code> file you can then keep your <a href="https://reactrouter.com/web/guides/quick-start">React-Router</a> routes.</p>
<h4>4. Keep styled components in your component files</h4>
<p>I tend to keep my styled-components in the same file as my components. This is mainly to avoid context switching when trying to figure out what a component does.</p>
<p>You can also choose not to do this, up to you.</p>
<p>In practice, this looks something like this:</p>
<pre><code class="language-jsx">//DataChip.js
import React from 'react';
import styled from '@emotion/styled';

const Title = styled.p`
  font-size: 16px;
  line-height: 16px;
  font-weight: 700;
  margin-bottom: 4px;
`;

const Value = styled.p`
  font-weight: 700;
  font-size: 32px;
  line-height: 32px;
  margin-bottom: 0;
  color: ${(props) => {
    if (props.sentiment === 'GOOD') return '#30B17E';
    if (props.sentiment === 'BAD') return '#E94740';
  }};
`;

const ChipHolder = styled.div`
  @media (max-width: 600px) {
    margin-bottom: 8px;
  }
`;

export const DataChip = ({ label, value, sentiment }) => {
  return (
    &#x3C;ChipHolder>
      &#x3C;Title>{label}&#x3C;/Title>
      &#x3C;Value sentiment={sentiment}>{value}&#x3C;/Value>
    &#x3C;/ChipHolder>
  );
};
</code></pre>
<p>In the past I would use <a href="https://github.com/css-modules/css-modules">CSS Modules</a>, and keep the <code>MyComponent.module.css</code> file in the same folder as <code>MyComponent.ts</code>.</p>
<h4>5. Split things out only when you need to</h4>
<p>If you have a component that only gets imported by one other component, it either belongs in the same file that imports it, or at least in the same folder.</p>
<p>Don't move things up into <code>components/</code> that aren't shared across multiple components. Also don't move things to a higher level than they get used in general (such as moving a component-specific utility function into the <code>src/</code> level utils).</p>
<p>We're trying to organise our folders so that when you modify a folder, it has a minimal impact on other folders, and the impact of that change is predictable.</p>
<h4>6. [Advanced] Split up your codebase by team</h4>
<p>Eventually with a large enough project and engineering team, the <a href="https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners">CODEOWNERS</a> file just doesn't cut it, and it becomes necessary to split up your codebase by team.</p>
<p>Like all good things in tech, there are trade-offs:</p>
<ul>
<li>On the one hand, your teams stop treading on each other's toes as often (with merge conflicts, and implementing things differently to the team's standard approach)</li>
<li>On the other hand, you risk creating silos and having teams re-implement work that another team has done</li>
</ul>
<p>For example:</p>
<pre><code>src/
|-- growth/
|   |-- components/
|   |   |-- GrowthSpecificAvatar/
|   |   |   |-- GrowthSpecificAvatar.ts
|   |   |   |-- GrowthSpecificAvatar.test.ts
|   |-- pages/
|   |   |-- UserProfile/
|   |   |   |-- Components/
|   |   |   |   |-- SomeUserProfileComponent/
|   |   |   |   |   |-- SomeUserProfileComponent.ts
|   |   |   |   |   |-- SomeUserProfileComponent.test.ts
|   |   |   |-- UserProfile.ts
|   |   |   |-- UserProfile.test.ts
|-- edgyTeamName/
|   |-- components/
|   |   |-- EdgyTeamNameSpecificAvatar/
|   |   |   |-- EdgyTeamNameSpecificAvatar.ts
|   |   |   |-- EdgyTeamNameSpecificAvatar.test.ts
|   |-- pages/
|   |   |-- UserList/
|   |   |   |-- Components/
|   |   |   |   |-- SomeUserListComponent/
|   |   |   |   |   |-- SomeUserListComponent.ts
|   |   |   |   |   |-- SomeUserListComponent.test.ts
|   |   |   |-- UserList.ts
|   |   |   |-- UserList.test.ts
</code></pre>
<p>At this point you might wonder whether a monorepo would make sense, but that opens up a whole new set of problems to solve, outside of the scope of this article.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[What it means to not test implementation details in React]]></title>
            <link>https://maxrozen.com/dont-test-implementation-details-react</link>
            <guid>https://maxrozen.com/dont-test-implementation-details-react</guid>
            <pubDate>Tue, 08 Sep 2020 09:48:48 GMT</pubDate>
            <description><![CDATA[Most guides to testing React these days preach: 'Test functionality, not implementation details'. This article explains what that means, and how to do it.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/dont-test-implementation-details-react">clicking here</a>.)</div><p>Are you unsure of what exactly you're trying to achieve when writing tests? Do you write tests based on other existing tests in your codebase? Perhaps you've quickly glanced at the React Testing Library <a href="https://testing-library.com/docs/react-testing-library/example-intro">documentation</a> to try figure out "the right way" of testing.</p>
<p>At some point, you need to step back and think:</p>
<blockquote>
<p>What does 'the right way' even mean when testing React?</p>
</blockquote>
<blockquote>
<p>What are we trying to achieve here?</p>
</blockquote>
<p>If you've heard of React Testing Library, chances are you've heard the almost-slogan "test functionality, not implementation details".</p>
<p>Not testing implementation details requires a pretty large mindset shift if you're used to testing with Enzyme, as it provides several utilities explicitly for that purpose, such as <code>find()</code>, <code>instance()</code> and <code>state()</code>.</p>
<p>For me, it helps to think from the perspective of a malicious user. Think about a sign-up form. I might try submit the form with no data to see what happens, maybe try submit the form without a password, or supply an invalid email address.</p>
<p>Just like that, you've got tests to write with no knowledge of implementation details:</p>
<h2>Test 1 - no data</h2>
<ol>
<li>Render the form</li>
<li>Find the submit button, and click it</li>
<li>Assert that the error message appears</li>
</ol>
<h2>Test 2 - no password</h2>
<ol>
<li>Render the form</li>
<li>Find the email field, and enter a random valid email address</li>
<li>Find the submit button, and click it</li>
<li>Assert that the error message appears</li>
</ol>
<h2>Test 3 - invalid email address</h2>
<ol>
<li>Render the form</li>
<li>Find the username field, and enter an invalid email address</li>
<li>Find the submit button, and click it</li>
<li>Assert that the error message appears</li>
</ol>
<p><em>Side note: Once you've got a few tests like this, you can look at refactoring them to avoid duplicating code in your tests.</em></p>
<p>React Testing Library helps quite a bit with utility functions that let you easily write tests like this, but Enzyme will let you do the same thing too. The difference is, Enzyme won't stop you from going further, and writing tests against your <code>props</code> and <code>state</code> too, while React Testing Library goes out of its way to stop you from doing this.</p>
<p>The important thing is to test functionality, because it'll let you avoid writing brittle tests that rely too much on knowing which functions to call, and hopefully means you'll be rewriting your tests less often as your code gets refactored.</p>
<h2>Conclusion</h2>
<p>I find the easiest way to avoid testing implementation details is to pretend I'm not a developer, and try to perform a specific task in my application just like a user would.</p>
<p>For example, typing into a sign-up form, and trying to submit the form.</p>
<p>The tests I write would then seek to automate the clicking and typing the user would do (via React Testing Library's <code>getByRole</code> queries), rather than calling <code>find(MyComponent)</code> and calling functions directly.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Examples of large production-grade, open-source React apps]]></title>
            <link>https://maxrozen.com/examples-of-large-production-grade-open-source-react-apps</link>
            <guid>https://maxrozen.com/examples-of-large-production-grade-open-source-react-apps</guid>
            <pubDate>Tue, 01 Sep 2020 08:42:05 GMT</pubDate>
            <description><![CDATA[All of the to-do apps are great for starting to learn React, but what about real-life? Let's see what real production React apps are like.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/examples-of-large-production-grade-open-source-react-apps">clicking here</a>.)</div><p>It's bloody hard to find a good example of a large production-grade, open-source React app.</p>
<p>There are a few reasons for this:</p>
<ol>
<li>
<p>No one writes amazing code the first time they write it</p>
<ul>
<li>Experienced product engineers know there's a trade-off between quality and getting features in the hands of customers. You're more likely to find ugly tech debt with a <code>//TODO: Improve this later!</code> comment than best practices</li>
</ul>
</li>
<li>
<p>Most companies keep their apps in private repos</p>
<ul>
<li>Trade secrets and all that jazz.</li>
</ul>
</li>
<li>
<p>There's a <em>lot</em> of beginner-grade repos out there</p>
<ul>
<li>Without a ton of experience, it's hard to tell the good from the bad</li>
</ul>
</li>
</ol>
<p>On that note, I've reviewed the following well-known open-source React apps, and reckon they're worth checking out.</p>
<p>I've split this article into two parts:</p>
<ol>
<li><a href="#example-apps">Example Apps</a></li>
<li><a href="#apps-with-users">Apps with Users</a></li>
</ol>
<h2>Example Apps</h2>
<h3>Jira Clone by Ivor Reic</h3>
<p><img src="jira.jpeg" alt="Jira Clone"></p>
<p>Project link: <a href="https://github.com/oldboyxx/jira_clone">Jira Clone</a></p>
<p>Jira Clone is... a Jira clone built in modern React, entirely out of functional components with Hooks. It's noteworthy because its stack is more or less what I'd use on my own side-projects:</p>
<ul>
<li>TypeScript with TypeORM on the backend, communicating with a Postgres database</li>
<li>React on the frontend with a custom webpack config</li>
<li>Relies on Cypress for end-to-end testing</li>
</ul>
<p>On top of that, the author has done an amazing job making it look very similar to Jira (when the clone was built), using a mix of <a href="https://styled-components.com/">styled-components</a> and global styles.</p>
<p>This repo also highlights the benefits of just working on a project by yourself with a clear understanding of what you're trying to build. You'll see the contrast when you check out the other projects in this article.</p>
<h3>RealWorld aka Conduit by Thinkster</h3>
<p><img src="realworld-thinkster.png" alt="realworld-thinkster"></p>
<p>Project links:</p>
<ul>
<li><a href="https://github.com/gothinkster/react-redux-realworld-example-app">Repo</a></li>
<li><a href="https://react-redux.realworld.io/">Demo</a></li>
</ul>
<p>RealWorld by Thinkster re-implements the same app (a Medium.com clone called Conduit) in over 24 different languages and frameworks. In this case, we're reviewing the React/Redux version.</p>
<p>It uses create-react-app, with react-router for routing, Redux for state management, classNames for styling, superagent for data fetching, with no testing, or type-checking.</p>
<p>It seems like a decent way to understand the syntactical differences between dozens of language and framework combinations, but isn't exactly a beacon of best practices.</p>
<h3>Real World App by Cypress</h3>
<p><img src="cypress-realworld-app.png" alt="Cypress Real World App"></p>
<p>Project link: <a href="https://github.com/cypress-io/cypress-realworld-app">Real World App</a></p>
<p>Real World App is a great example of best-practices to use for end-to-end testing an application with Cypress, with example data in the repo, so both tests and the app run out-of-the-box without the need for a database.</p>
<p>It's built with create-react-app, written in TypeScript with an Express backend, using Material UI as a UI/component library, Formik for forms, and react-router for routing.</p>
<p>It looks like a pretty solid example of an application you would see in the real world, (especially since it uses forms), though data-fetching and form submission are optimised for ease of testing/portability rather than best practices.</p>
<h2>Apps with Users</h2>
<h3>HospitalRun by HospitalRun</h3>
<p><img src="hospitalrun.png" alt="HospitalRun"></p>
<p>Project links:</p>
<ul>
<li><a href="https://github.com/HospitalRun/hospitalrun-frontend">Repo</a></li>
<li><a href="https://github.com/HospitalRun"><code>components</code> src</a></li>
</ul>
<p>HospitalRun is an offline-first electronic health record (EHR) and hospital information system (HIS) web application. It's an incredibly complete solution for OSS without a massive company building it (though it <em>is</em> sponsored by some companies).</p>
<p>It's written in modern React (as far as I can tell, not a single class component), with TypeScript, and uses SCSS for styles. Most components live in the <code>components</code> package (also worth <a href="https://github.com/HospitalRun/components">checking out</a>).</p>
<h3>Simorgh by the BBC</h3>
<p><img src="bbc.png" alt="BBC News Afrique"></p>
<p>Project links:</p>
<ul>
<li><a href="https://github.com/bbc/simorgh">Repo</a></li>
<li><a href="https://github.com/bbc/psammead"><code>@bbc/psammead</code> src</a></li>
</ul>
<p>Simorgh is the BBC's React SPA, currently serving millions of users in production around the world. It is being progressively rolled out to each BBC World Service News website, and you can track the progress <a href="https://github.com/bbc/simorgh/blob/latest/docs/Simorgh-Release-Info.md">here</a>.</p>
<p>It uses PropTypes for type-checking, Jest and Enzyme for unit testing, Cypress for end-to-end testing, styled-components for styling (with most components living in <code>@bbc/psammead</code>, which is an awesome component library to check out too), react-router for routing, with Express handling server-side rendering.</p>
<p>It's extremely well-tested (98% coverage) and well-documented, with READMEs throughout the project to explain large directories.</p>
<h3>AST Explorer by Felix Kling and open-source community</h3>
<p><img src="ast-explorer.png" alt="AST Explorer"></p>
<p>Project link: <a href="https://github.com/fkling/astexplorer">AST explorer</a></p>
<p>AST Explorer is a tool for exploring <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">Abstract Syntax Trees</a>.</p>
<p>It's worth checking out as a React project because it's a good example of a project under continuous development for several years. It's a classic React project that uses PropTypes for type-checking, and Redux for state management while still using local state where possible.</p>
<p>While you won't find anything fancy like TypeScript/Flow here, the code is well commented, with hacks and workarounds clearly pointed out.</p>
<p>Some might say there's some "tech debt", as the authors have chosen to stick with class components, and not immediately rewrite to functional components + Hooks. Instead, they've maintained class components in older files, with functional components + Hooks in newer files.</p>
<p>Also worth noting, that like many real life, open-source projects, AST Explorer was started with the intention of adding tests later, but only one contributor in four years mentioned wanting to add tests in the GitHub Issues.</p>
<h3>Excalidraw by Vjeux and open-source community</h3>
<p><img src="excalidraw.png" alt="Excalidraw"></p>
<p>Project link: <a href="https://github.com/excalidraw/excalidraw/">Excalidraw</a></p>
<p>Excalidraw is a tool for drawing flow-charts, and various other diagrams with a hand-drawn style. I actually use it quite a bit for my own projects.</p>
<p>It's worth checking out because it's one of the most popular (in terms of daily users) examples of an un-ejected <a href="https://create-react-app.dev/">create-react-app</a> application I've seen in production.</p>
<p>It's written in relatively modern React (mostly functional components with Hooks) with TypeScript, uses SCSS for styles, there are tests, and rather than using Redux, they've <a href="https://reactjs.org/docs/lifting-state-up.html">lifted state up</a> and persisted it in localStorage.</p>
<h3>Spectrum by Spectrum</h3>
<p><img src="spectrum.png" alt="Spectrum"></p>
<p>Project link: <a href="https://github.com/withspectrum/spectrum">Spectrum</a></p>
<p>Spectrum is a community site, seeking to combine the features of a real-time chat app with the features of forums. It's been under active development since early 2017, and was acquired by GitHub at the end of 2018.</p>
<p>Spectrum was very interesting to follow during its early years, as it used RethinkDB for (almost) live updates on queries, server-side rendering and GraphQL (very cutting edge at the time).</p>
<p>The codebase uses Flow for type-checking, Apollo (GraphQL) for data-fetching, Redux for state management, an <a href="https://github.com/withspectrum/spectrum/blob/alpha/hyperion/index.js">Express server</a> for server-side rendering, and uses functional components + Hooks, with custom hooks <a href="https://github.com/withspectrum/spectrum/tree/alpha/src/hooks">here</a>. Naturally, as one of the founders is <a href="https://mxstbr.com/">Max Stoiber</a> (creator of styled-components), the app is styled with styled-components.</p>
<p>Since the acquistion, Max has written a post called <a href="https://mxstbr.com/thoughts/tech-choice-regrets-at-spectrum">Tech Choices I Regret at Spectrum</a> that's also worth checking out.</p>
<h3>Sentry by Sentry</h3>
<p><img src="sentry.png" alt="Sentry"></p>
<p>Project links:</p>
<ul>
<li><a href="https://github.com/getsentry/sentry">Repo</a></li>
<li><a href="https://github.com/getsentry/sentry/tree/master/src/sentry/static/sentry/app">App src</a></li>
</ul>
<p>Sentry is a service that allows you to report on errors in your web app.</p>
<p>It runs on a Django backend, with a React frontend written in TypeScript (with PropTypes too), uses <a href="http://emotion.sh/">Emotion</a> for styling, react-router for routing, Redux for state management, and is a great example of trade-offs and <code>//TODO</code> comments in a SaaS app with users that pay for it.</p>
<p>One peculiar thing to check out is that the app is internationalized, using a translation service to convert strings <a href="https://github.com/getsentry/sentry/blob/master/src/sentry/static/sentry/app/locale.tsx">here</a>.</p>
<h3>Grafana by Grafana</h3>
<p><img src="grafana.png" alt="Grafana"></p>
<p>Project links:</p>
<ul>
<li><a href="https://github.com/grafana/grafana">Repo</a></li>
<li><a href="https://github.com/grafana/grafana/tree/master/public/app">App src</a></li>
<li><a href="https://github.com/grafana/grafana/tree/master/packages/grafana-ui"><code>@grafana/ui</code> src</a></li>
</ul>
<p>I almost didn't add Grafana as an example here, since it's an AngularJS project in the process of migrating to React.</p>
<p>It's an AngularJS app being progressively rewritten into a modern React app, written in TypeScript. It uses Redux for state management, an internal design system that uses emotion for styling (<code>@grafana/ui</code>).</p>
<p>It's kind of a nightmare to navigate as a codebase, mainly due to mixing folders only used by AngularJS with folders only used by React, and that's exactly why it's in this article, as an example of what you'll see in the real world.</p>
<h3>GoAlert by Target</h3>
<p><img src="goalert.png" alt="GoAlert"></p>
<p>Project links:</p>
<ul>
<li>
<p><a href="https://github.com/target/goalert">Repo</a></p>
</li>
<li>
<p><a href="https://github.com/target/goalert/tree/master/web/src">App src</a></p>
</li>
</ul>
<p>GoAlert is an open source on-call scheduler and notifier (simlar to PagerDuty or Opsgenie).</p>
<p>It runs on a Go backend, using Apollo (GraphQL) for data-fetching, react-router for routing, Redux for state management, Cypress for end-to-end testing, Material UI for some components, and plain old CSS for styling.</p>
<p>Surprisingly, it's a pretty good example of Cypress testing in a real application, using <code>data-cy</code> for selecting elements (for example).</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Understanding useState's initial value]]></title>
            <link>https://maxrozen.com/how-to-use-react-usestate-hook-initial-value</link>
            <guid>https://maxrozen.com/how-to-use-react-usestate-hook-initial-value</guid>
            <pubDate>Tue, 25 Aug 2020 08:42:05 GMT</pubDate>
            <description><![CDATA[If you're used to class components and setState, the useState Hook might be confusing at first. Let's learn more about it in this article.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-to-use-react-usestate-hook-initial-value">clicking here</a>.)</div><p>If you're coming over from React's class components, you might be confused about how <code>useState</code> works, particularly since you can now define what the initial value is.</p>
<p>Let's take a closer look.</p>
<p>You'd typically use it like this:</p>
<pre><code class="language-jsx">import React, { useState } from 'react';

const SomeComponent = () => {
  const [someState, setSomeState] = useState('starting value');

  return &#x3C;div onClick={() => setSomeState('new value')}>{someState}&#x3C;/div>;
};
</code></pre>
<p>If you want to play with this example, check out the <a href="https://codesandbox.io/s/basic-usestate-example-28fsh">CodeSandbox</a>.</p>
<p>In class-based components, you might be used to defining a whole bunch of initial state values inside the component's <code>state</code> object. That's no longer the case with <code>useState</code>. Instead, we tend to declare a single state value at a time, like so:</p>
<pre><code class="language-jsx">import React, { useState } from 'react';

const SomeComponent = () => {
  const [someState, setSomeState] = useState('starting value');
  const [someOtherState, setSomeOtherState] = useState(null);
  const [importantState, setImportantState] = useState(42);

  return &#x3C;div onClick={() => setSomeState('new value')}>{someState}&#x3C;/div>;
};
</code></pre>
<p>The key difference is that the initial value of the state defined by <code>useState</code> can be <em>anything</em> you want it to be. It no longer has to be an object. A string, a number, an object, <code>undefined</code>, <code>null</code> - anything goes!</p>
<p>The other difference, is that calling <code>useState</code> returns you an array with two things in it: the value, and a function to set a new value. The typical approach to the array <code>useState</code> returns is to <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">destructure</a> it in the same line it's declared, as we saw above:</p>
<pre><code class="language-jsx">const [someState, setSomeState] = useState('starting value');
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Methods for styling your React app]]></title>
            <link>https://maxrozen.com/guide-to-styling-react-app</link>
            <guid>https://maxrozen.com/guide-to-styling-react-app</guid>
            <pubDate>Tue, 18 Aug 2020 08:42:05 GMT</pubDate>
            <description><![CDATA[It can be pretty confusing to pick how to style your React app. This guide attempts to simplify your choice.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guide-to-styling-react-app">clicking here</a>.)</div><p>If you're new to React, you might be wondering why there are so many different tutorials that teach different ways to style your React app. The truth is, we're all still figuring out the best way to do things.</p>
<p>Styles in React were more-or-less worked out in this order:</p>
<ol>
<li>Global CSS</li>
<li>CSS Modules</li>
<li>CSS in JS (<a href="https://styled-components.com/">styled-components</a>, <a href="http://emotion.sh/">Emotion</a>, etc)
<ul>
<li>Utility-first CSS</li>
<li>Styled System</li>
</ul>
</li>
<li>Statically extracted CSS in JS</li>
</ol>
<p>These days, I recommend starting with CSS in JS. If you'd like to know why, read on.</p>
<h2>Quick note</h2>
<p>When I say styling, I mean writing your CSS styles more-or-less from scratch. If you're looking for pre-built components, I wrote a <a href="https://maxrozen.com/guide-to-component-ui-libraries-react/">guide to commonly used React component libraries</a>.</p>
<h2>Global CSS</h2>
<p>Global CSS is likely the way you're used to styling webpages. You have a giant <code>styles.css</code> file, and try to write <a href="http://getbem.com/naming/">BEM</a> or <a href="http://smacss.com/">SMACSS</a> names for all of your classes. Alternatively, you have a ton of tiny files, and don't always know where each class lives.</p>
<p>We as frontend developers quickly realised that global CSS doesn't really scale. The more teams you have editing a single file, the more likely you are to have CSS that doesn't do anything (people become too afraid to delete anything in case it breaks).</p>
<p>If you still want to use Global CSS in your React app, all you need to do is import the CSS file at the top level of your React app (assuming you've configured webpack to do so, or are using create-react-app).</p>
<pre><code class="language-jsx">//App.js
import './styles.css';
import React from 'react';

const App = () => {
  return &#x3C;div className="some-class">some other stuff in here&#x3C;/div>;
};
</code></pre>
<h2>CSS Modules</h2>
<p><a href="https://github.com/css-modules/css-modules">CSS Modules</a> <em>look</em> a lot like Global CSS, in the sense that you're importing a CSS file into your React component, but under the hood it's quite different.</p>
<p>A lot of the problems we used to have with Global CSS are gone with CSS Modules.</p>
<p>Your CSS looks like this:</p>
<pre><code class="language-css">/* style.css */
.makeItGreen {
  color: green;
}
</code></pre>
<p>and your React Component looks like this:</p>
<pre><code class="language-jsx">import React from 'react';
import styles from './style.css';

const MyComponent = () => {
  return &#x3C;div className={styles.makeItGreen}>Green!&#x3C;/div>;
};
</code></pre>
<p>The key difference here is that only files that import <code>style.css</code> will be able to access the class names that it defines, and the class names that get generated during the build process will be unique.</p>
<p>No more conflicts, no more "too afraid to delete things in case it breaks", just locally scoped CSS. You can also set-up SCSS/LESS support, if you need it.</p>
<p>The really cool thing about this is that you can start to play around with JavaScript to change a component's styles.</p>
<pre><code class="language-jsx">import React from 'react';
import styles from './style.css';

const MyComponent = (props) => {
  const myStyle = props.color === 'RED' ? styles.makeItRed : styles.makeItGreen;
  return &#x3C;div className={myStyle}>{props.color}!&#x3C;/div>;
};
</code></pre>
<p>Although, that starts to get a bit messy if you're using several props to change the style and behaviour of your components. What if your styles could just be components?</p>
<h2>CSS in JS</h2>
<p>That's where CSS in JS comes in.</p>
<p>Libraries like <a href="https://styled-components.com/">styled-components</a> and <a href="https://emotion.sh/docs/introduction">Emotion</a> make it possible to wrap components (including divs, spans, <code>&#x3C;p></code> tags, <code>&#x3C;a></code> tags) with styles, and use them as React components.</p>
<p>The best part is, you can use all of the standard CSS features you're used to, such as media queries, and <code>:hover</code> and <code>:focus</code> selectors.</p>
<p>Our example from above now becomes:</p>
<pre><code class="language-jsx">import React from 'react';
import styled from '@emotion/styled';
// OR import styled from 'styled-components'

const StyledGreenThing = styled.div`
  color: ${(props) => (props.color === 'RED' ? 'red' : 'green')};
`;

const MyComponent = (props) => {
  return (
    &#x3C;StyledGreenThing color={props.color}>{props.color}!&#x3C;/StyledGreenThing>
  );
};
</code></pre>
<p>As of 2020, Emotion and styled-components are evenly matched performance-wise. Contributors to styled-components worked hard to bring their performance up to Emotion's level, so deciding which one to use isn't as much of a big deal any more (even if people argue endlessly about them as though they're sports teams).</p>
<p>Emotion <em>does</em> provide some extra options for styling, such as the <a href="https://emotion.sh/docs/css-prop">css prop</a>, while styled-components tries to keep a single, standard way of doing things via the <code>styled</code> API.</p>
<h2>Utility-first CSS</h2>
<p>A guide to styling React apps wouldn't be complete without mentioning utility-first CSS frameworks such as <a href="https://tailwindcss.com/">Tailwind</a>. You don't actually need React to use utility-first CSS, but in my opinion, it makes for a better developer experience when you add React and CSS in JS.</p>
<p>In short, Tailwind lets you style your components one class at a time. Here's what it looks like:</p>
<pre><code class="language-html">&#x3C;div className="md:flex bg-white rounded-lg p-6">
  &#x3C;img
    className="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
    src="avatar.jpg"
  />
  &#x3C;div className="text-center md:text-left">
    &#x3C;h2 className="text-lg">Erin Lindford&#x3C;/h2>
    &#x3C;div className="text-purple-500">Product Engineer&#x3C;/div>
    &#x3C;div className="text-gray-600">erinlindford@example.com&#x3C;/div>
    &#x3C;div className="text-gray-600">(555) 765-4321&#x3C;/div>
  &#x3C;/div>
&#x3C;/div>
</code></pre>
<p>Which creates a component that looks like this:
<img src="tailwind-css-example.png" alt="Tailwind CSS Example Component"></p>
<p>You might be thinking that it's not a particularly re-usable solution, however it's possible to use Tailwind class names with your favourite CSS in JS library using <a href="https://github.com/ben-rogerson/twin.macro">twin</a>.</p>
<p>You can then have styled Tailwind components:</p>
<pre><code class="language-jsx">import tw, { styled } from 'twin.macro';

const Input = styled.input`
  color: purple;
  ${tw`border rounded`}
`;

export const MyStyledInput = () => {
  return &#x3C;Input />;
};
</code></pre>
<h2>Styled System</h2>
<p><a href="https://styled-system.com/">Styled System</a> takes the <code>styled</code> API supplied by styled-components or Emotion, and adds utilities as props, rather than class names.</p>
<p>The Styled System approach is particularly powerful when it comes to theming/white labelling, as changing the entire appearance of your app can be done by replacing the <code>theme.js</code> file you provide.</p>
<p>Your components end up looking like this:</p>
<pre><code class="language-jsx">import styled from '@emotion/styled';
import { typography, space, color } from 'styled-system';

const Box = styled('div')(typography, space, color);

const UsedBox = () => {
  return (
    &#x3C;Box
      fontSize={4}
      fontWeight="bold"
      p={3}
      mb={[4, 5]}
      color="white"
      bg="primary"
    >
      Hello
    &#x3C;/Box>
  );
};
</code></pre>
<h2>Statically extracted CSS in JS</h2>
<p>The trouble with CSS in JS is that it takes JavaScript to load your CSS. This slows things down big time, so people started looking for ways to extract the CSS from CSS-in-JS during build time.</p>
<p>There are a few libraries that can do this:</p>
<ul>
<li><a href="https://compiledcssinjs.com/">Compiled</a></li>
<li><a href="https://github.com/callstack/linaria">linaria</a></li>
<li><a href="https://github.com/seek-oss/treat">treat</a></li>
</ul>
<p>Compiled and linaria let you use the <code>styled</code> API that you know and love, while giving you the performance benefit of not having CSS in your bundle.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[A Guide to Commonly Used React Component Libraries]]></title>
            <link>https://maxrozen.com/guide-to-component-ui-libraries-react</link>
            <guid>https://maxrozen.com/guide-to-component-ui-libraries-react</guid>
            <pubDate>Thu, 13 Aug 2020 21:37:42 GMT</pubDate>
            <description><![CDATA[There are a *lot* of React Component libraries to choose from. This guide attempts to put them all on one page.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/guide-to-component-ui-libraries-react">clicking here</a>.)</div><h2>Ant Design</h2>
<p><img src="antdesignpro.png" alt="Ant Design Pro"></p>
<p>Project link: <a href="https://ant.design/">Ant Design</a></p>
<p>Bundle Size (from <a href="https://bundlephobia.com/result?p=antd@4.5.4">BundlePhobia</a>): 1.2 mB minified, 349.2kB minified + gzipped, less with treeshaking</p>
<p>Pros:</p>
<ul>
<li>Ant Design comes with a <em>huge</em> amount of supporting documentation, community, including a separate project (Ant Design Pro) with pre-made templates</li>
<li>The kind of UI library you'd use to quickly throw up as a back-office/internal app design</li>
</ul>
<p>Cons:</p>
<ul>
<li>Accessibility is lacking</li>
<li>It's huge. Expect a sizable performance impact when using</li>
<li>Pollutes your CSS (expect to add <code>!important</code> to prevent it styling your non-Ant components)</li>
</ul>
<h2>Bootstrap</h2>
<p><img src="bootstrap.png" alt="Bootstrap"></p>
<p>I actually rate Bootstrap relatively highly as a UI library. It's not going to win you any design awards, but it gets the job done for side projects and minimum viable products.</p>
<p>It depends what you want to use it for, though. If you're new to React, it's a great library to use to get started. As a more experienced developer, chances are you'll want to look into styled-components/Emotion.</p>
<p>There are two popular libraries with React bindings for Bootstrap, I've personally only used Reactstrap.</p>
<p>Project links:</p>
<ul>
<li>
<p><a href="https://react-bootstrap.github.io/">React Bootstrap</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=react-bootstrap@1.3.0">BundlePhobia</a>): 111kB minified, 34.4kB minified + gzipped, less with treeshaking</li>
</ul>
</li>
<li>
<p><a href="https://reactstrap.github.io/">Reactstrap</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=reactstrap@8.5.1">BundlePhobia</a>): 152.1kB minified, 39.4kB minified + gzipped, less with treeshaking</li>
</ul>
</li>
</ul>
<p>Pros:</p>
<ul>
<li>The Bootstrap library that you know and love, with React bindings</li>
<li>Easily customised via CSS-in-JS</li>
<li>It's been around long enough with widespread usage that bugs/issues aren't a worry</li>
<li>Quick to get started</li>
<li>No jQuery dependency as it's been reimplemented entirely in React</li>
</ul>
<p>Cons:</p>
<ul>
<li>It's Bootstrap: your site will look like everyone elses if you don't customise it</li>
</ul>
<h2>Bulma</h2>
<p><img src="bulma.png" alt="Bulma"></p>
<p>Bulma is different to most libraries presented here because it's purely a CSS framework, no JS required. You can choose to either use the classes from Bulma directly, or use a wrapper library such as <code>react-bulma-components</code>.</p>
<p>Project links:</p>
<ul>
<li><a href="https://bulma.io/">Bulma</a></li>
<li><a href="https://github.com/couds/react-bulma-components">react-bulma-components</a>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=react-bulma-components@3.4.0">BundlePhobia</a>): 179kB minified, 20.1kB minified + gzipped</li>
</ul>
</li>
</ul>
<p>Pros:</p>
<ul>
<li>Doesn't have the Bootstrap look and feel</li>
<li>Good for getting something up and running quickly</li>
<li>Modern features (Flexbox/Grid under the hood)</li>
</ul>
<p>Cons:</p>
<ul>
<li>Accessibility: there's some, but doesn't follow WCAG guidelines as strongly as other libraries</li>
</ul>
<h2>Chakra UI</h2>
<p><img src="chakra-ui.png" alt="Chakra UI"></p>
<p>Project link: <a href="https://chakra-ui.com/">Chakra UI</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=@chakra-ui/core@0.8.0">BundlePhobia</a>): 326.2kB minified, 101.2kB minified + gzipped, less with treeshaking</li>
</ul>
<p>Pros:</p>
<ul>
<li>Accessibility: follows WAI-ARIA guidelines and components use aria tags</li>
<li>Discord server for support</li>
<li>Easily customisable (with theming support)</li>
<li>Highly modular, so treeshaking actually removes code you don't use</li>
</ul>
<p>Cons:</p>
<ul>
<li>Quite new.</li>
</ul>
<h2>Material UI</h2>
<p><img src="material-ui.png" alt="Material UI"></p>
<p>Material UI is one of those libraries that I love to hate. It has helped me get through some extremely tight deadlines for clients in the past, but I always end up removing it in favour of almost anything else as soon as possible.</p>
<p>In the past you could only customise Material UI's styles by writing <a href="https://cssinjs.org/?v=v10.3.0#jss-example">JSS</a>, but thankfully it's now <a href="https://material-ui.com/guides/interoperability/#styled-components">possible to override styles</a> with styled-components and Emotion.</p>
<p>Project link: <a href="https://material-ui.com/">Material UI</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=@material-ui/core@4.11.0">BundlePhobia</a>): 325.7kB minified, 92kB minified + gzipped, less with treeshaking</li>
</ul>
<p>Pros:</p>
<ul>
<li>Comprehensive documentation</li>
<li>Icon library is <a href="https://material-ui.com/components/material-icons/">massive</a></li>
<li>Simple to use (at first)</li>
</ul>
<p>Cons:</p>
<ul>
<li>Customisation is difficult, and painful, yet necessary (to improve visuals)</li>
<li>Performance: known to render excessive DOM nodes</li>
<li>Your app will look like Google made it (which could be a pro, for some people)</li>
</ul>
<h2>Semantic UI</h2>
<p><img src="semantic-ui.png" alt="Semantic UI"></p>
<p>Project links:</p>
<ul>
<li>
<p><a href="https://semantic-ui.com/">Semantic UI</a></p>
</li>
<li>
<p><a href="https://github.com/Semantic-Org/Semantic-UI-React">Semantic-UI-React</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=semantic-ui-react@1.2.0">BundlePhobia</a>): 300.8kB minified, 80.9kB minified + gzipped, less with treeshaking</li>
</ul>
</li>
</ul>
<p>Pros</p>
<ul>
<li>Composable (using the <code>as</code> prop to pass components)</li>
<li>Easily customisable</li>
<li>Helpful docs</li>
<li>High profile users (Netflix internally, Amazon publishing)</li>
<li>TypeScript support</li>
</ul>
<p>Cons</p>
<ul>
<li>Potential uncertainty about the open source project.
<ul>
<li>See issues: https://github.com/Semantic-Org/Semantic-UI/issues/6109 https://github.com/Semantic-Org/Semantic-UI/issues/6413</li>
<li>Community-run fork exists: https://github.com/fomantic/Fomantic-UI</li>
</ul>
</li>
</ul>
<h2>Honourable mentions</h2>
<h3>Reach UI</h3>
<p><a href="https://reach.tech/">Reach UI</a> is a low-level component library, focusing on allowing developers to build accessible React components in their design system.</p>
<p>There's no bundle size available as each component is exported individually as its own npm package.</p>
<h3>Reakit</h3>
<p><a href="https://reakit.io/">Reakit</a> is another low-level component library. It's technically a UI library, but doesn't come with CSS. So you still need to come up with a styling solution.</p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=reakit@1.2.1">BundlePhobia</a>): 119.9kB minified, 32.1kB minified + gzipped, less with treeshaking</li>
</ul>
<h3>Rebass</h3>
<p><img src="rebass.png" alt="Rebass"></p>
<p>Rebass has been on my radar for some time. It's an extremely powerful component library that doesn't come with a theme, but can be themed easily. For an example of how this works in practice, see their <a href="https://rebassjs.org/demo">demo</a>.</p>
<p>Project link: <a href="https://rebassjs.org/">Rebass</a></p>
<ul>
<li>Bundle Size (from <a href="https://bundlephobia.com/result?p=rebass@4.0.7">BundlePhobia</a>): 43kB minified, 14.4kB minified + gzipped, less with treeshaking</li>
</ul>
<h2>Notes</h2>
<p>In making this list, I've attempted to avoid corporate design systems, however some (Material UI) have achieved such widespread adoption that this list would be incomplete without them.</p>
<p>I've also intentionally left out CSS-in-JS such as <a href="https://styled-components.com/">styled-components</a> and <a href="https://emotion.sh/docs/introduction">Emotion</a>, and utility CSS systems such as <a href="https://tailwindcss.com/">Tailwind</a>, as they are not explicitly "React Component libraries", but rather tools with which to make your components.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to use SVGs in your React App]]></title>
            <link>https://maxrozen.com/how-to-use-svg-react-app</link>
            <guid>https://maxrozen.com/how-to-use-svg-react-app</guid>
            <pubDate>Mon, 10 Aug 2020 21:37:42 GMT</pubDate>
            <description><![CDATA[Using SVG icons instead of PNG or JPG has a few performance benefits, but they're not always straightforward to use. Here's how you do it.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-to-use-svg-react-app">clicking here</a>.)</div><p>Trying to render an SVG in your React app, and getting errors? You're not alone - it's a relatively common issue.</p>
<p>There are two ways to do it, and both have tradeoffs.</p>
<h2>Using <code>&#x3C;img></code> tags, and passing your SVG's URL</h2>
<p>Here's a basic example:</p>
<pre><code class="language-js">import React from 'react';
import logoSrc from './logo.svg';

const MyLogo = () => {
  return &#x3C;img src={logoSrc} />;
};
</code></pre>
<p>The benefit of this approach is that your logo will not end up in your bundle, but rather exported as a static file when you run <code>yarn build</code> (assuming you're using a standard webpack config, such as the one found in create-react-app).</p>
<p>This then gives you the option of aggressively caching icons that you know won't change.</p>
<p>You would typically use this approach for larger company logos on your marketing site, or for illustrations in your app.</p>
<h2>Creating a React component, and passing props</h2>
<p>The other option is to create a React component containing your SVG. Also known as "inlining" your SVG.</p>
<p>This done by pasting your raw <code>svg</code> markup into a new React component.</p>
<p>There are a few ways to achieve this:</p>
<ul>
<li>Manually, byremoving/replacing all HTML props with the React equivalent, and adding <code>{...props}</code> to the main <code>svg</code> element),</li>
<li>CLI via <a href="https://github.com/gregberge/svgr">SVGR</a> - a utility to automate this process</li>
<li>Webpack config via <a href="https://react-svgr.com/docs/webpack/">SVGR</a></li>
</ul>
<p>If you're using create-react-app, it already has SVGR's webpack config built-in, so you can already use your SVGs like React components:</p>
<pre><code class="language-js">import Star from './star.svg';
const App = () => (
  &#x3C;div>
    &#x3C;Star />
  &#x3C;/div>
);
</code></pre>
<p>Here's what a manually created SVG-based React component looks like:</p>
<pre><code class="language-js">import React from 'react';

export const DeleteIcon = (props) => (
  &#x3C;svg
    xmlns="http://www.w3.org/2000/svg"
    height="24px"
    viewBox="0 0 24 24"
    {...props}
  >
    &#x3C;path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
    &#x3C;path d="M0 0h24v24H0z" fill="none" />
  &#x3C;/svg>
);
</code></pre>
<p>This approach lets you easily access props on your SVG icon. For example, changing the fill color:</p>
<pre><code class="language-js">&#x3C;DeleteIcon fill="#fff" />
</code></pre>
<p>The downside being that your icons won't be as easily cached, so I would use this approach for smaller icons, such as the <a href="https://material.io/resources/icons/?style=baseline">Material Design Icons</a>.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Why you should use functional components + Hooks over class components in React]]></title>
            <link>https://maxrozen.com/react-components-hooks-functions-vs-classes</link>
            <guid>https://maxrozen.com/react-components-hooks-functions-vs-classes</guid>
            <pubDate>Thu, 06 Aug 2020 09:22:09 GMT</pubDate>
            <description><![CDATA[If you find yourself asking whether to use functional components with Hooks over class components, you'll find a lot of outdated information out there. Let's settle this once and for all in this article.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/react-components-hooks-functions-vs-classes">clicking here</a>.)</div><p>If you're new to React, and you've been working through tutorials, chances are you've run into examples of both functional components with Hooks, and class components, with no strong indication of which one you should be using. Even as a seasoned developer, you might still be using class components, wondering if its worth the rewrite.</p>
<p>You're probably thinking:</p>
<blockquote>
<p>I've been digging for answers, but I just can't find a clear answer for this!</p>
</blockquote>
<p>That's fair enough, even the official documentation didn't have a strong recommendation until the middle of 2020.</p>
<h2>Which one should you use?</h2>
<p>The official React team stance (according to the <a href="https://reactjs.org/docs/hooks-faq.html#should-i-use-hooks-classes-or-a-mix-of-both">docs</a>), is:</p>
<blockquote>
<p>When you’re ready, we’d encourage you to start trying Hooks in new components you write. [...] We don’t recommend rewriting your existing classes to Hooks unless you planned to rewrite them anyway (e.g. to fix bugs).</p>
</blockquote>
<p>To summarise:</p>
<ul>
<li>New code should use functional components with Hooks, when you're ready</li>
<li>Old code can keep using class components, unless you want to rewrite</li>
</ul>
<h2>Should I just focus on hooks then?</h2>
<p>It's not that simple.</p>
<p>You still need class components to build <a href="https://reactjs.org/docs/error-boundaries.html">Error Boundaries</a>. Sure, you could head to npm and find a Hook-based Error Boundary library to use, but ignorance doesn't justify adding yet another dependency to your project.</p>
<p>On top of that, most code written before 2019 will likely still use class components, as there is no immediate need to rewrite them to functional components with Hooks. If you want to understand existing code in a codebase, you'll need to also learn class components. <em>Side note</em>: The company I currently work at has class components with >1k lines of code used by tens of thousands of users per day. You better believe we're not rewriting them unless we <strong>have to</strong>.</p>
<p>You'll also find that companies that ask React questions during their interviews will still ask you about classes.</p>
<h2>Should we rewrite our old class-based code to use Hooks?</h2>
<p>As with all good things, there are tradeoffs to consider here.</p>
<p>Hooks result in much cleaner, easier to understand components compared to class components of a similar complexity.</p>
<p>To illustrate my point, compare this component that fetches some data from <a href="https://swapi.dev/">The Star Wars API</a>, written first as a class, then as a functional component with hooks:</p>
<pre><code class="language-js">import React from 'react';

export default class DataDisplayer extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      data: null,
    };
  }

  async componentDidMount() {
    const response = await fetch(
      `https://swapi.dev/api/people/${this.props.id}/`
    );
    const newData = await response.json();
    this.setState({ data: newData });
  }

  render() {
    const { data } = this.state;
    if (data) {
      return &#x3C;div>{data.name}&#x3C;/div>;
    } else {
      return null;
    }
  }
}
</code></pre>
<p>As your app grows, the lifecycle methods grow larger, and the context switching involved just from scrolling through the file increases.</p>
<p>I don't know about you, but my thought process when scrolling through classes is like:</p>
<blockquote>
<p>Okay so I'm in <code>componentDidMount</code>, so we'll be fetching data here</p>
</blockquote>
<blockquote>
<p>Okay so here we're rendering, so this code runs every single time</p>
</blockquote>
<blockquote>
<p>I need to add some extra functionality... hmm which lifecycle method does <em>that</em> go in again?</p>
</blockquote>
<p>On the other hand, you have Hooks:</p>
<pre><code class="language-js">import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState('');

  useEffect(() => {
    const getData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };
    getData();
  }, [props.id]);

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
<p>With Hooks, writing code that follows sequentially is much easier, and I find reading functional components with Hooks requires less context switching, as you're not jumping around the file to find which lifecycle method you think something happened in.</p>
<p>That's the main benefit of rewriting to Hooks - your codebase's developer experience improves as it takes less time to understand what each component does.</p>
<p>The main drawback is time - time spent rewriting is time you could have spent building new features or writing integration tests.</p>
<h2>Where to from here?</h2>
<p>When introducing Hooks to my team in the past I recommended the following approach (we used it to rewrite our entire codebase from Flow to TypeScript), and it worked quite well:</p>
<ul>
<li>All new code should be written as functional components with Hooks</li>
<li>Existing code should only be rewritten if it gets modified frequently (for example, if you're fixing a bug or adding functionality, take the time to swap the component over to Hooks)</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Keeping your sites fast with regular performance tests]]></title>
            <link>https://maxrozen.com/keep-site-fast-regular-performance-tests</link>
            <guid>https://maxrozen.com/keep-site-fast-regular-performance-tests</guid>
            <pubDate>Tue, 04 Aug 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[You pride yourself on delivering fast websites to your clients, but do the websites stay fast as the months go by?]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/keep-site-fast-regular-performance-tests">clicking here</a>.)</div><p>Do you have clients, or stakeholders asking about improving their site performance, or wondering why their site seems to have gotten slower over time?</p>
<p>It happens to everyone: you deliver a fast website from the start, but then over time you get feature request after feature request, deadlines get rushed in, and the next thing you know you've got a client asking why some pages are taking up to ten seconds to load.</p>
<p>It doesn't have to be this way.</p>
<p>It's actually a simple fix: regularly check your site speed.</p>
<p>There are several manual tools you can use for free:</p>
<ul>
<li><a href="https://webpagetest.org/">WebPageTest</a></li>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">Google PageSpeed Insights</a> or <a href="https://developers.google.com/web/tools/lighthouse">Google Lighthouse</a></li>
</ul>
<p>Pro tip: Export the numbers, and use Excel or Google Sheets to graph your performance over time.</p>
<p>Once you start checking your site speed daily, it becomes easier to notice your performance slipping over time.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Preventing infinite re-renders when using useEffect and useState]]></title>
            <link>https://maxrozen.com/learn-useeffect-dependency-array-react-hooks</link>
            <guid>https://maxrozen.com/learn-useeffect-dependency-array-react-hooks</guid>
            <pubDate>Tue, 04 Aug 2020 05:52:00 GMT</pubDate>
            <description><![CDATA[React's useEffect hook is an incredibly useful tool for fetching data, but if you're not careful, can cause infinite re-renders.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/learn-useeffect-dependency-array-react-hooks">clicking here</a>.)</div><p>Let's take a step back, pause for a moment, and think about what useEffect and useState <em>actually</em> do.</p>
<p>Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a dependency array when using useEffect to fetch data, and use useState to display it, you will always trigger another render after useEffect runs.</p>
<p>Unless you provide useEffect a dependency array.</p>
<h2>The dependency array</h2>
<p>The dependency array in useEffect lets you specify the conditions to trigger it. If you provide useEffect an empty dependency array, it'll run exactly once, as in this example (<a href="https://codesandbox.io/s/react-useeffect-simple-iqpxp?file=/src/DataDisplayer.js">CodeSandbox link</a>):</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function DataDisplayer() {
  const [data, setData] = useState('');

  useEffect(() => {
    const getData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/1/`);
      const newData = await response.json();
      setData(newData);
    };

    getData();
    // highlight-next-line
  }, []); //&#x3C;-- This is the dependency array

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
<p>What if you wanted to let users decide which <code>id</code> they wanted to query, and forgot to add the dependency array? You'd cause an infinite loop.</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState('');

  useEffect(() => {
    const getData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };

    getData();
    // highlight-next-line
  }); //&#x3C;-- Notice the missing dependency array

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
<h2>Why does the above example cause an infinite loop?</h2>
<ol>
<li>Your first render runs, and because <code>data</code> is falsey, render returns null and kicks off useEffect</li>
<li>useEffect runs, fetching your data, and updating it via <code>setData</code></li>
<li>Since <code>data</code> has been updated, the component re-renders to display the new <code>data</code> value</li>
<li>However, useEffect runs after each render, so it runs again, updating data via <code>setData</code></li>
<li>Repeat steps 3 and 4 until your app crashes, or the API rate-limits your requests</li>
</ol>
<h2>Preventing infinite loops</h2>
<p>This is where the dependency array comes in handy.</p>
<p>Adding variables to the dependency array tells useEffect: "Hey, I need you to run <strong>only</strong> if this value changes".</p>
<p>In this case, adding <code>props.id</code> will ensure useEffect only runs if <code>props.id</code> changes:</p>
<pre><code class="language-jsx">import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState('');

  useEffect(() => {
    const getData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };
    getData();
    // highlight-next-line
  }, [props.id]); //&#x3C;-- This is the dependency array, with a variable

  if (data) {
    return &#x3C;div>{data.name}&#x3C;/div>;
  } else {
    return null;
  }
}
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to choose a median result when running Google Lighthouse multiple times]]></title>
            <link>https://maxrozen.com/how-to-choose-median-lighthouse-multiple-runs</link>
            <guid>https://maxrozen.com/how-to-choose-median-lighthouse-multiple-runs</guid>
            <pubDate>Mon, 27 Jul 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[Running Google Lighthouse five times will half the variability of your test results. So how do you figure out which run is the median?]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-to-choose-median-lighthouse-multiple-runs">clicking here</a>.)</div><p>According to <a href="https://github.com/GoogleChrome/lighthouse/blob/master/docs/variability.md">the docs</a>, running Google Lighthouse five times against the same URL will reduce the variability of your results by around half if you pick the median result.</p>
<p>The problem is, how do you pick the right result to use as your median, or representative result?</p>
<p>A naïve approach would be to take the median of the performance score, however since the performance score itself is a weighted average of other scores, and is more prone to outliers, it isn't as useful as you'd think.</p>
<p>Lighthouse actually exports a function to help you calculate which result to use: <code>computeMedianRun</code>.</p>
<h2>How to use computeMedianRun in Node</h2>
<pre><code class="language-js">const spawnSync = require('child_process').spawnSync;
const lighthouseCli = require.resolve('lighthouse/lighthouse-cli');
const {
  computeMedianRun,
} = require('lighthouse/lighthouse-core/lib/median-run.js');

const results = [];
for (let i = 0; i &#x3C; 5; i++) {
  console.log(`Running Lighthouse attempt #${i + 1}...`);
  const { status = -1, stdout } = spawnSync('node', [
    lighthouseCli,
    'https://example.com',
    '--output=json',
  ]);
  if (status !== 0) {
    console.log('Lighthouse failed, skipping run...');
    continue;
  }
  results.push(JSON.parse(stdout));
}

const median = computeMedianRun(results);
console.log(
  'Median performance score was',
  median.categories.performance.score * 100
);
</code></pre>
<p>Under the hood, <code>computeMedianRun</code> finds the run that's closest to the median First Contentful Paint (FCP), AND the closest to the median Time to Interactive (TTI).</p>
<p>FCP and TTI are used because they're the earliest and latest moments in a page's lifecycle.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Frustrated by Formik? Here's a simpler way to do forms in React]]></title>
            <link>https://maxrozen.com/frustrated-formik-simpler-react-forms</link>
            <guid>https://maxrozen.com/frustrated-formik-simpler-react-forms</guid>
            <pubDate>Thu, 23 Jul 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[Do you find yourself struggling for hours to solve problems with Formik? I've started using a different library that's much simpler.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/frustrated-formik-simpler-react-forms">clicking here</a>.)</div><p>Is this you?</p>
<blockquote>
<p>I've tried so many different things I've found online</p>
</blockquote>
<blockquote>
<p>Holy crap it works now, such a small problem I've lost hours to :(</p>
</blockquote>
<blockquote>
<p>All I want to do is a simple thing, this is all so unnecessarily complex</p>
</blockquote>
<p>Try using <a href="https://react-hook-form.com/">react-hook-form</a>.</p>
<p>React-hook-form is different, because it (mostly) keeps its state in the DOM (like classic HTML form elements).</p>
<p>Here's an example of a single field form in react-hook form (taken from their docs):</p>
<pre><code class="language-js">import React from 'react';
import { useForm } from 'react-hook-form';

const Example = () => {
  const { handleSubmit, register, errors } = useForm();
  const onSubmit = (values) => console.log(values);

  return (
    &#x3C;form onSubmit={handleSubmit(onSubmit)}>
      &#x3C;input
        name="email"
        ref={register({
          required: 'Required',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'invalid email address',
          },
        })}
      />
      {errors.email &#x26;&#x26; errors.email.message}

      &#x3C;input
        name="username"
        ref={register({
          validate: (value) => value !== 'admin' || 'Nice try!',
        })}
      />
      {errors.username &#x26;&#x26; errors.username.message}

      &#x3C;button type="submit">Submit&#x3C;/button>
    &#x3C;/form>
  );
};
</code></pre>
<p>Compare that to your existing form code. I don't know about you, but my React forms with Formik would have twice as many lines of code to achieve the same thing.</p>
<p>You'll notice that it's using <code>ref</code>, rather than tracking state within the library. Since moving to react-hook-form, I no longer have to worry about state not syncing correctly in huge forms.</p>
<p>Everything just works as expected, and my code is much simpler too.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Apollo vs Relay Modern: An unbiased look at which GraphQL client to use]]></title>
            <link>https://maxrozen.com/apollo-vs-relay-which-graphql-client-to-use-2019</link>
            <guid>https://maxrozen.com/apollo-vs-relay-which-graphql-client-to-use-2019</guid>
            <pubDate>Wed, 22 Jul 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[If you're building an app using GraphQL, picking a client to use can be hard. Apollo? Relay? Urql? Let's look into some options.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/apollo-vs-relay-which-graphql-client-to-use-2019">clicking here</a>.)</div><h2>Do we even need a client?</h2>
<p>First things first — depending on how large your application is, and what you’re trying to achieve with GraphQL, you may not even need a GraphQL client.</p>
<p>If you're just testing the waters with GraphQL and don't want to change your existing app too much, you can just use <code>fetch</code> in your component like so:</p>
<pre><code class="language-js">fetch('/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({ query: '{ hello }' }),
})
  .then((r) => r.json())
  .then((data) => console.log('data returned:', data));
</code></pre>
<p>The main benefit to adopting a GraphQL client is the cache implementation. Using fetch is fine to begin with, but you don't want to be using it in an app where users quickly jump between views.</p>
<p>In <a href="https://PerfBeacon.com">PerfBeacon</a> we use Apollo to cache query results - which gives us quite a noticable boost in performance. How it works in practice:</p>
<p>You view a list of websites: this list of websites is now cached in memory, for example:</p>
<pre><code class="language-json">[
  {
    "id": "123",
    "url": "https://perfbeacon.com"
  },
  {
    "id": "124",
    "url": "https://maxrozen.com"
  }
]
</code></pre>
<p>You then choose to review reports for your first website. As the GraphQL client already knows the id and url, it doesn't need to refetch this data. It only needs to fetch the array of reports, as shown below:</p>
<pre><code class="language-json">[
  {
    id: '123',
    url: 'https://perfbeacon.com'
    reports: [
      {
        id: '1',
        date: '2020-01-01',
      }
    ]
  }
]
</code></pre>
<p>As you now have the list of websites, and a list of reports for the first website, clicking back in your browser to view the list of websites is instant, as no GraphQL request is made.</p>
<p>Essentially, the more the user clicks around your application, the faster your user experience becomes.</p>
<h2>Apollo Client</h2>
<p>The setup is considerably easier than Relay - it involves installing one package, and adding the <code>ApolloProvider</code> to the root of your React tree.</p>
<p>The API is nice - they have an equivalent to Relay's QueryRenderer called <code>Query</code> that does what it says:</p>
<pre><code class="language-jsx">&#x3C;Query
  query={gql`
    {
      rates(currency: "USD") {
        currency
        rate
      }
    }
  `}
>
  {({ loading, error, data }) => {
    if (loading) return &#x3C;p>Loading...&#x3C;/p>;
    if (error) return &#x3C;p>Error :(&#x3C;/p>;

    return data.rates.map(({ currency, rate }) => (
      &#x3C;div key={currency}>
        &#x3C;p>{`${currency}: ${rate}`}&#x3C;/p>
      &#x3C;/div>
    ));
  }}
&#x3C;/Query>
</code></pre>
<p>It can be used to manage state in your React app - that is, you can directly write to Apollo's Redux-like store and consume that data in another part of the React tree. Though with React's new Context API, and React's best practices of <a href="https://reactjs.org/docs/lifting-state-up.html">Lifting State Up</a> you probably won't need it.</p>
<pre><code class="language-jsx">import React from 'react';
import { ApolloConsumer } from 'react-apollo';

import Link from './Link';

const FilterLink = ({ filter, children }) => (
  &#x3C;ApolloConsumer>
    {(client) => (
      &#x3C;Link
        onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
      >
        {children}
      &#x3C;/Link>
    )}
  &#x3C;/ApolloConsumer>
);
</code></pre>
<h4>Downsides to Apollo</h4>
<ul>
<li>It's huge. It weighs in at 10x more than the smallest GraphQL client I'd consider using, and 3x more than urql</li>
</ul>
<h4>Quirks</h4>
<p>Apollo is not without quirks however:</p>
<ul>
<li>Since Apollo uses <code>id</code> to build its cache, forgetting to include <code>id</code> in your query can cause some interesting bugs and error messages</li>
</ul>
<h3>On Bundle size</h3>
<p>One of the biggest complaints I hear about adopting Apollo is the bundle size (Apollo Boost, the "easiest way to get started with Apollo Client" weighs in at 30.7 kB min+gzipped), so luckily there are also alternative lightweight clients to consider:</p>
<ul>
<li>Formidable Labs' <a href="https://github.com/FormidableLabs/urql">urql</a> - 12kB min+gzipped (with perhaps the best README I've ever seen)</li>
<li>Prisma's <a href="https://github.com/prisma/graphql-request">graphql-request</a> - 4 kB min+gzipped</li>
<li>Adam Rackis' <a href="https://github.com/arackaf/micro-graphql-react">micro-graphql-react</a> - 3.1kB min+gzipped</li>
</ul>
<h2>Relay</h2>
<h4>Setup</h4>
<p>The main benefit to using Relay is that <code>relay-compiler</code> doesn't get included in your frontend bundle, saving your user from downloading the whole GraphQL parser - it "pre-compiles" the GraphQL queries at build time.</p>
<p>What annoys me about Relay is that it requires a fair bit of work to even add to a project. Just to get it running on the client side, you need to:</p>
<ul>
<li>add a relay plugin to your <code>.babelrc</code> config</li>
<li>set up relay-compiler as a yarn script</li>
<li>setup a "relay environment" (essentially your own <code>fetch</code> utility to pass data to the relay-runtime), and</li>
<li>add <code>QueryRenderer</code> components to the React Components you wish to pass your data to</li>
</ul>
<p>On the server side, you need to:</p>
<ul>
<li>Ensure the IDs your app returns are unique across all of your types (meaning you can't return nice ID values like <code>1, 2, 3</code>, they need to be like <code>typename_1, typename_2</code>)</li>
</ul>
<h4>Developer Experience</h4>
<p>The developer experience itself is pretty unpleasant too - <code>relay-compiler</code> needs to run each time you modify any GraphQL query, or modify the schema. In large frontend teams this means teaching everyone to run <code>relay-compiler</code> every time you change branches in Git, since almost all of our work involves fetching data from GraphQL in some way.</p>
<p>Update: Thanks to <a href="https://github.com/danielholmes/relay-compiler-webpack-plugin">relay-compiler-webpack-plugin</a> you no longer need to remember to re-run the <code>relay-compiler</code> every time you make a change, webpack will do it for you.</p>
<h4>Quirks</h4>
<p>Being one of Facebook's Open Source projects doesn't necessarily mean issues get fixed quickly. Occasionally, things break in unexpected ways:</p>
<ul>
<li>Using an old version of <code>graphql</code> breaks <code>relay</code>: https://github.com/facebook/relay/issues/2428</li>
<li>Errors don't get sent via the error object in GraphQL when using QueryRenderer, instead one needs to create an <code>error</code> type, and send the errors through the data object: https://github.com/facebook/relay/issues/1913</li>
</ul>
<h2>In Summary</h2>
<p>If I were starting a new project today in the middle of 2020, I’d use Apollo Client by default.</p>
<p>If my users were using cellular data, bandwidth constrained, or using lower-spec devices, I would opt to use <a href="https://github.com/FormidableLabs/urql">urql</a> instead.</p>
<p>I wouldn’t bother rewriting an application from Relay to Apollo. There are benefits, mainly from developer experience/ergonomics of the API, but not enough to outweigh the cost of rewriting. You may also consider writing new features using Apollo, and heavily using <a href="https://maxrozen.com/2020/02/26/react-loadable-half-react-app-load-time">code-splitting</a> to avoid sending two GraphQL implementations to your users — I’ve used a similar approach to slowly roll-out TypeScript for companies I’ve worked at.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Books every developer should read]]></title>
            <link>https://maxrozen.com/books-every-developer-should-read</link>
            <guid>https://maxrozen.com/books-every-developer-should-read</guid>
            <pubDate>Tue, 21 Jul 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[Whether you want to start a career as a developer, or have been a developer for 20 years, you should check out these books.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/books-every-developer-should-read">clicking here</a>.)</div><p>Here is a non-exhaustive list of books you should read as a developer. Not specifically because they'll make you a better developer, but a better person, too.</p>
<p>While this blog post has a date attached, please consider this advice <em>timeless</em>.</p>
<h2>Badass: Making Users Awesome by Kathy Sierra</h2>
<p>Badass teaches you to stop thinking purely in terms of solutions, and start thinking of what your users want to achieve by using your software.</p>
<p>Hence the name Badass. It's about helping your users become badass at your product, but also at what your users want your product to help them do.</p>
<p>It's a short read (with lots of visuals!), highly recommended.</p>
<h2>How to Win Friends and Influence People by Dale Carnegie</h2>
<p>A <em>lot</em> of people refuse to read this book due to the title. Whether you think it's manipulative, or a book to give as an offensive joke, just read it.</p>
<p>Dale Carnegie emphasises an honest interest in other people's lives that is rare in the modern era.</p>
<p>If you have any other recommendations that I've missed, I'd love to know!</p>
<h2>Nonviolent Communication by Marshall Rosenberg PhD</h2>
<p>Nonviolent Communication is another book with an unfortunate name that often stops people from reading it (I personally procrastinated from reading it for several months because of the name).</p>
<p>Also highly recommended, as it teaches you to both actively listen when others are describing their problems, but also more effective ways to communicate your pain.</p>
<p>As an example, the book insists the reader use phrases such as "I feel anxious when I deploy, because..." rather than "Deploying sucks, it's terrible!".</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Automatically testing for accessibility (a11y) issues with jest-axe]]></title>
            <link>https://maxrozen.com/automatic-a11y-testing-with-jest-axe</link>
            <guid>https://maxrozen.com/automatic-a11y-testing-with-jest-axe</guid>
            <pubDate>Fri, 17 Jul 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[Automatically test your code for accessibility issues in the same way you catch bugs with jest-axe.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/automatic-a11y-testing-with-jest-axe">clicking here</a>.)</div><p>Hey, you! Does your project have <code>jest</code> already installed?</p>
<p>Do you wish management would care more about accessibility but just can't get them to invest or pay attention?</p>
<p>A few design systems I've worked with had the following code to prevent app-wide accessibility regressions, and it's so simple yet powerful that I had to share.</p>
<pre><code class="language-jsx">import React from 'react';
import { render } from '@testing-library/react`;
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

function AccessibleForm() {
  return (
    &#x3C;form>
      &#x3C;label htmlFor="email">Email&#x3C;/label>
      &#x3C;input id="email" placeholder="email" />
    &#x3C;/form>
  );
}

test('accessible forms pass axe', async () => {
  const { container } = render(&#x3C;AccessibleForm />);
  expect(await axe(container)).toHaveNoViolations();
});
</code></pre>
<p>Credit to Kent C. Dodds' <a href="https://testingjavascript.com">TestingJavaScript.com</a> course for the above snippet</p>
<p>With a few lines of code, auditing your entire design system for accessibility issues suddenly becomes surprisingly doable.</p>
<p>Without <code>@testing-library/react</code>, the code is more or less the same. Using Enzyme, you <code>mount()</code> your component, and pass it to <code>jest-axe</code> to validate.</p>
<p>It's worth noting that while this can find issues with inaccessible HTML, it doesn't guarantee your components are actually accessible. To do that, you'll want to involve actual people with disabilities involved in your research. For more information about jest-axe, see the <a href="https://github.com/nickcolley/jest-axe">GitHub repo</a>.</p>
<h2>Further reading</h2>
<p>Google's actually has a pretty comprehensive article on auditing accessibility at https://web.dev/accessibility-auditing-react/ - however it mainly touches on catching issues via eslint or while running the application in development, rather than using your test suite.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Introducing PerfBeacon - Continuously Measure Site Speed via API or Schedule]]></title>
            <link>https://maxrozen.com/introducing-perfbeacon</link>
            <guid>https://maxrozen.com/introducing-perfbeacon</guid>
            <pubDate>Wed, 08 Jul 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[PerfBeacon was a wrapper around Google Lighthouse that lets you continuously measure your site's performance via API, or a schedule]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/introducing-perfbeacon">clicking here</a>.)</div><p>Hey there, I'm Max. I'm the creator, engineer, designer and customer support at PerfBeacon. During the day, I work as a Frontend Software Engineer at Atlassian, and have worked at Ernst &#x26; Young (EY), and various startups around Sydney.</p>
<p>I built PerfBeacon as an automated wrapper around Google Lighthouse to automate a repetitive part of my job as a frontend developer. With the API set up to run at the end of CI/CD, it makes it fast to iterate on performance tweaks.</p>
<p>PerfBeacon lets you:</p>
<ul>
<li>Continuously monitor the performance of unlimited sites
<img src="perfbeacon-unlimited-webperf-monitors.png" alt="Continuously monitor the performance of unlimited sites"></li>
<li>Schedule your performance checks to run throughout the day</li>
<li>Use an API to run a performance check at any time</li>
<li>Export the underlying Google Lighthouse report to share with your team
<img src="perfbeacon-export-lighthouse-report.png" alt="Export Google Lighthouse Report"></li>
<li>Invite your entire team at no extra cost
<img src="perfbeacon-collaborate-with-team.png" alt="Invite your entire team at no extra cost"></li>
<li>... and more coming soon!</li>
</ul>
<p>You can get started for free at <a href="https://perfbeacon.com/">PerfBeacon.com</a></p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Blogging lessons from working for a large blogging company]]></title>
            <link>https://maxrozen.com/blogging-lessons</link>
            <guid>https://maxrozen.com/blogging-lessons</guid>
            <pubDate>Sat, 27 Jun 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[For a short period, I was employed by a company that drove most of its revenue from blogging. Here's what I learned while I was there.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/blogging-lessons">clicking here</a>.)</div><p>For a short period I worked for a company whose business revolved around writing articles on WordPress. It would rank extremely well on Google for these articles, and then sell ad space on those articles to the companies it displaced.</p>
<p>This worked mainly because the company hired writers of authority - people who knew the topic well, and had written about the topic extensively before working there.</p>
<p>Anyway, here's what I learned.</p>
<h2>Fundamental tips</h2>
<ul>
<li>Shorter sentences
<ul>
<li>In general, you should aim to write sentences with 8-13 words each.</li>
</ul>
</li>
<li>Shorter words
<ul>
<li>You may think of this as "dumbing-down" your writing, but think of your readers.</li>
</ul>
</li>
<li>One idea per sentence
<ul>
<li>In other words, don't write <a href="https://en.wikipedia.org/wiki/Non_sequitur_(literary_device)">non sequiturs</a>.</li>
</ul>
</li>
<li>One concept per paragraph</li>
</ul>
<h2>Structure</h2>
<ul>
<li>Spend lots of time on headline
<ul>
<li>Your headline is likely what will end up in Google. Ensure it's catchy enough to get your readers to check it out, but not so clickbaity that they avoid you.</li>
</ul>
</li>
<li>Spend lots of time on your first paragraph
<ul>
<li>Most people will judge whether your content is worth reading in the first paragraph. If it fails to land, they'll bounce.</li>
</ul>
</li>
<li>Start your articles with the positives, and what's important, rather than exceptions.
<ul>
<li>Think of it like a newspaper article: most important facts first, then gradually taper off.</li>
</ul>
</li>
</ul>
<h2>Editing</h2>
<ul>
<li>Rewriting is important
<ul>
<li>I personally don't agree with this for personal blogging, though some authors rewrite their content until it sounds "right".</li>
</ul>
</li>
<li>Change the screen you read the content on.
<ul>
<li>For example, if you write your article on your laptop, edit on your phone. Potentially even print the article, and edit on paper. Changing the format will force your brain to actually read what you wrote, rather than skimming.</li>
</ul>
</li>
<li>Change font size
<ul>
<li>In a similar vein to changing the screen, changing font size can help catch typos.</li>
</ul>
</li>
</ul>
<h2>Human tips</h2>
<ul>
<li>Have empathy for your audience
<ul>
<li>Writing things like, "I'm going to avoid explaining this because it's obvious" can and will piss off your audience. Either explain things or don't.</li>
</ul>
</li>
<li>Be specific
<ul>
<li>Don't list generic tips without examples. For example, see this entire article.</li>
</ul>
</li>
<li>Admit what you don't know, and link to a reputable authority
<ul>
<li>Hyper-specific example: if you're writing an article about <code>useEffect()</code> in React and you think your readers would benefit from a 49 minute read, you could link out to <a href="https://overreacted.io/a-complete-guide-to-useeffect/">Dan Abramov's guide</a>.</li>
</ul>
</li>
<li>Think about what your audience knows
<ul>
<li>Don't assume your audience knows what something is. There are two schools of thought here (especially in web development), some authors just assume audience knowledge, others preface their articles with links for required reading.</li>
<li>See my previous tip on having empathy for your audience.</li>
</ul>
</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to implement a Higher-order component in React with TypeScript]]></title>
            <link>https://maxrozen.com/implement-higher-order-component-react-typescript</link>
            <guid>https://maxrozen.com/implement-higher-order-component-react-typescript</guid>
            <pubDate>Fri, 10 Apr 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[You probably know how to implement a HoC, but do you know how to write types for one?]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/implement-higher-order-component-react-typescript">clicking here</a>.)</div><p>In TypeScript, typing a HoC can be a little bit confusing, especially if you read some of the blog posts out there.</p>
<p>I came across a situation at work in which I needed to use a React Hook in a class-based component. My initial instinct was to rewrite from a class component to a function component, however upon further inspection I realised it had 1100 lines of code, with relatively complicated lifecycle methods.</p>
<p>I decided to wrap the component with a functional HoC that called the Hook, and passed the result down to the component as a prop.</p>
<p>In normal JavaScript, it isn't <em>too</em> complicated, you'd do something like this:</p>
<pre><code class="language-tsx">import React, { useState } from 'react';

export function withExtraInfo(WrappedComponent) {
  const [extraInfo, setExtraInfo] = useState('');

  const ComponentWithExtraInfo = (props) => {
    return &#x3C;WrappedComponent {...props} extraInfo={extraInfo} />;
  };

  return ComponentWithExtraInfo;
}
</code></pre>
<p>If you tried to run TypeScript against the code above, you'd need to fix a few things:</p>
<ol>
<li>Both <code>WrappedComponent</code> and <code>props</code> have an implicit <code>any</code> type</li>
<li>Make the function generic</li>
</ol>
<p>Here's how we'd do that:</p>
<pre><code class="language-ts">import React, { useState } from 'react';

// First we need to add a type to let us extend the incoming component.
type ExtraInfoType = {
  extraInfo: string;
};
// Mark the function as a generic using P (or whatever variable you want)
export function withExtraInfo&#x3C;P>(
  // Then we need to type the incoming component.
  // This creates a union type of whatever the component
  // already accepts AND our extraInfo prop
  WrappedComponent: React.ComponentType&#x3C;P &#x26; ExtraInfoType>
) {
  const [extraInfo, setExtraInfo] = useState('');
  setExtraInfo('important data.');

  const ComponentWithExtraInfo = (props: P) => {
    // At this point, the props being passed in are the original props the component expects.
    return &#x3C;WrappedComponent {...props} extraInfo={extraInfo} />;
  };
  return ComponentWithExtraInfo;
}
</code></pre>
<p>You'll probably notice we marked <code>withExtraInfo</code> as a generic using <code>&#x3C;P></code>. For more information, see the TypeScript Handbook.</p>
<p>To wrap things up, you'll want to <a href="https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging">add a displayName</a> to your HoC, which I've left as an exercise for the reader.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How code splitting can make your React app load significantly faster]]></title>
            <link>https://maxrozen.com/react-loadable-half-react-app-load-time</link>
            <guid>https://maxrozen.com/react-loadable-half-react-app-load-time</guid>
            <pubDate>Wed, 26 Feb 2020 15:52:00 GMT</pubDate>
            <description><![CDATA[Chances are, your React app's bundle is much, much larger than it needs to be.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/react-loadable-half-react-app-load-time">clicking here</a>.)</div><p><a href="https://github.com/jamiebuilds/react-loadable">React-Loadable</a> provides you with a component you can use to load a React component later, rather than immediately as you load the React app.</p>
<h2>Why would I want to load a component later?</h2>
<p>For example, lets say you've inherited a project from another engineer, and they've decided to use <a href="https://momentjs.com/">moment.js</a> in one of the components.</p>
<p>The output of your webpack build gives you these chunk files, with the main one being 500kB. Of this massive bundle, 65.9kB belongs to the minified + gzipped moment.js library.</p>
<p>Since you're only using the component in a couple of places, it doesn't really make sense to load moment.js immediately as your users load your app. After all, they may not even use the component that uses moment.js!</p>
<p>If instead, you wrapped your component in <code>Loadable</code>, your main bundle would be (roughly) 65.9kB smaller, and only the people that need your component that uses moment.js would download that bundle.</p>
<h2>How do I use it?</h2>
<p>First, install it:</p>
<pre><code>yarn add react-loadable
</code></pre>
<p>or</p>
<pre><code>npm install react-loadable
</code></pre>
<p>React-Loadable lets you wrap your massive component like this:</p>
<pre><code class="language-js">import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-massive-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return &#x3C;LoadableComponent />;
  }
}
</code></pre>
<p>Resulting in a much smaller initial load time for your React app.</p>
<p><code>&#x3C;LoadableComponent></code> doesn't have to be in your App file, it can be anywhere in your component hierarchy.</p>
<h2>Halving my React app's load time</h2>
<p>Using the above approach was all I needed to shave 200KB from the main bundle of the performance monitoring tool I built (<a href="https://perfbeacon.com">PerfBeacon</a>).</p>
<p>Results:</p>
<p>| <img src="perfbeacon-web-performance-monitoring-result.png" alt="PerfBeacon results when using react-loadable"> |
| :-----------------------------------------------------------------------------------------------: |
|         <em>Shaving 200KB off PerfBeacon's initial bundle reduced the TTI by more than half</em>         |</p>
<p>More specifically, I combined <code>react-loadable</code> with <code>react-router-dom</code> to split my bundle by the routes of my web app, resulting in a dozen or so Loadable components like this one:</p>
<pre><code class="language-js">// pages.js
export default pages = {
  NotFound: Loadable({
    loader: () => import('./NotFound'),
    loading: Loading,
  });
}
</code></pre>
<p>While this is great for a start, there's still quite a bit more optimisation work to be done here.</p>
<p>Jamie has a much more <a href="https://github.com/jamiebuilds/react-loadable#route-based-splitting-vs-component-based-splitting">in-depth explanation</a> but essentially, any place with tabs, modals, or even low priority content at the bottom of a page can benefit from using react-loadable.</p>
<h2>Conclusion</h2>
<p>So that's how you can use react-loadable to massively speed up your react app.</p>
<p>Do you manually test your web performance? Do you wish you didn't have to? I'd love to help!</p>
<p>I built <a href="https://perfbeacon.com">PerfBeacon.com</a> to automatically test web performance after each deployment.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[2019: Further reflections on trying to start an internet business]]></title>
            <link>https://maxrozen.com/2019-further-reflections-trying-to-start-an-internet-business</link>
            <guid>https://maxrozen.com/2019-further-reflections-trying-to-start-an-internet-business</guid>
            <pubDate>Sun, 29 Dec 2019 05:52:00 GMT</pubDate>
            <description><![CDATA[A review of what I said I would do in 2019, and what I actually did.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/2019-further-reflections-trying-to-start-an-internet-business">clicking here</a>.)</div><p>Hey there! I've been working on building a sustainable internet business for just over two years now, and I figured I should share some lessons I've learned along the way.</p>
<p>In summary, over the years I've built: a job board, an appointment scheduler, a room booking tool based on the appointment scheduler, a GraphQL monitoring service, an uptime monitoring service, and I'm currently building a web performance monitoring service.</p>
<p>So, at the end of 2018 I wrote this article: <a href="https://maxrozen.com/2018/12/31/2018-review-starting-an-internet-business">2018: Reflections on trying to start an internet business</a>, in which I promised I would:</p>
<ul>
<li>Launch OnlineOrNot, the GraphQL monitoring solution</li>
<li>Go camping more</li>
</ul>
<p>How did I do? Well:</p>
<h2>Real-life</h2>
<p>We travelled and camped quite a bit!</p>
<p>We went camping with friends from work in Mudgee, NSW in the middle of the year, where it was as low as 0 celsius overnight. The upside of the cold being a lack of insects. Mudgee's great if you like wine and fresh air.</p>
<p><img src="winter_camping.jpg" alt="Winter Camping"></p>
<p>Later in the year we camped in the Myall Lakes National Park, south of Forster. Much warmer this time, and many, many more insects. Also it's next to a ridiculously long beach, great for chilling out.</p>
<p><img src="myall_lakes.jpg" alt="Camping in Myall Lakes"></p>
<p>Over Christmas, we hiked Mount Kosciuszko (highest mountain in Australia) - a 13 kilometer walk from the Thredbo chairlift. It was a really nice break from sitting in front of a laptop all day long.</p>
<p><img src="mount-kosciuszko.jpg" alt="Hiking Mount Kosciuszko"></p>
<h2>Work</h2>
<p>I worked at Expert360 as a full-time employee, working on the React frontend. After speeding up their frontend's performance (Time to Document Complete) by 75% and scrapping the microfrontend architecture in favour of a monolithic React app to save 4 seconds on each page load, I decided to go off in search of bigger and better challenges. In doing so, I also noticed I really enjoyed the challenges of tracking down performance issues on the web.</p>
<p>I ended up working for a large Australian marketing/advertising business, and yet again found myself working at a company that advertised a job description that didn't meet the reality of what the business needed. I lasted about two months before realising they wouldn't be needing my skills for months (if not years), and I didn't want to sit around building features for their WordPress site.</p>
<p>So I became a contractor! I'm currently building <a href="http://sitehive.co/">SiteHive's</a> entire React frontend, data pipelines, and Node/GraphQL backend, while also building the foundation for a design system in the future.</p>
<p>SiteHive is an extremely early stage startup, having just made it out of an accelerator program. It's been incredibly eye-opening to witness first-hand how to find customers (mainly through personal networks), and how we use customer conversations to feed the product roadmap.</p>
<h2>After Work</h2>
<h3>OnlineOrNot</h3>
<p>OnlineOrNot was a GraphQL monitoring service that took snapshots of your queries, and checked them at intervals to ensure your resolvers would return what you expected them to.</p>
<p>My main means of acquiring users was content marketing, targeting two main keywords: 'graphql testing' and 'graphql monitoring'. Neither was particularly successful.</p>
<p>'graphql testing' received 929 impressions, and 3 clicks.</p>
<p>'graphql monitoring' received 83 impressions, and 1 click.</p>
<p>So if directly targeting keywords didn't work, what did?</p>
<p>Complaining!</p>
<p>I wrote an article <a href="https://medium.com/@RozenMD/apollo-vs-relay-modern-an-unbiased-look-at-which-graphql-client-to-use-b0143663e0ec">comparing two GraphQL clients: Apollo and Relay</a>, in which I mainly complained about their shortcomings, and in total received 20.2k impressions, and 2.28k clicks for "Relay", and 21.1k impressions, 2.41k clicks for "Apollo".</p>
<p>In the past 12 months OnlineOrNot received 65k total impressions, leading to 4.82k clicks, leading to 54 real signups, which lead to 1 paid user (who I sold to personally, rather than relying on content marketing).</p>
<p>Lesson learnt: recruit your first customers.</p>
<p>In short, OnlineOrNot launched on HackerNews, ProductHunt, and IndieHackers. It received very little attention, I gave it a few months to see how well content marketing would go, and I sort of gave up.</p>
<p>Kind of.</p>
<p>In September I started rewriting OnlineOrNot to be a simple uptime monitoring solution (a space with many, many competitors), with the aim of answering a single question: is my site still online, if not, what's happening?</p>
<p>I have no plans on a public launch, and I'm happy with just using it while contracting as a web developer.</p>
<h3>Using a template: revisited</h3>
<p>Though that's not the end of my indie-hacker story. Last year I <a href="https://maxrozen.com/2018/12/31/2018-review-starting-an-internet-business#using-a-template-to-build-side-projects">built a template to make launching SaaS ideas faster</a>. This year I spent some time developing the overall "framework" a SaaS idea needs, adding:</p>
<ul>
<li>Auth0 integration</li>
<li>More robust authentication code (seriously, check this out)
<img src="auth_code.jpeg" alt="React Authentication"></li>
<li>Rewrote most of the frontend to use React Hooks (for readability, mostly)</li>
<li>Stripe integration</li>
<li>Mailchimp integration</li>
<li>Subscription handling (particularly feature availability in trials, but also pricing/subscription pages within the apps)</li>
</ul>
<p>While building these features I noticed the AWS Lambda architecture I used in OnlineOrNot made swapping out the "core" of the business extremely simple, and launching another business in parallel wouldn't take much effort.</p>
<h3>Introducing PerfBeacon</h3>
<p>At Expert360, I found the manual work involved in optimising a web application's performance frustrating, and went searching for better tooling. <a href="https://perfbeacon.com">PerfBeacon</a> was started out of a desire to help improve performance across the web, and the realisation that one cannot improve without first measuring.</p>
<p>PerfBeacon is a synthetic frontend performance monitor, similar to how OnlineOrNot worked when it tested GraphQL queries. It runs Google Lighthouse from anywhere in the world you'd like (assuming there's an AWS datacentre there...) against a webpage, extracts key metrics you'd like to visualise, and graphs them for you.</p>
<p>The coolest thing I've found about it so far is that it lets you find where in the world your CDN isn't performing as well as it could be:</p>
<p><img src="perfbeacon.png" alt="PerfBeacon Performance Monitor"></p>
<p>In terms of idea validation, I mainly just put up a landing page, and started a <a href="https://twitter.com/PerfBeacon">Twitter account</a> to follow interesting people in web performance. Since starting in mid October, I've had 307 visitors on the website, resulting in 19 subscriptions to the mailing list (I still have no idea what I'm doing with mailing lists, I plan on sending a "We've launched!" email, and that's about it).</p>
<p>While this is roughly half the attention OnlineOrNot received, people are actually signing up to the mailing list, and I'm thinking this idea might have legs after all. If not, I'll still use it daily to monitor frontend performance every time I deploy, so not all will be lost.</p>
<h2>Main things I learned this year</h2>
<h3>Recruit your first customers</h3>
<p>This year I read <a href="http://momtestbook.com/">The Mom Test</a>, and <a href="https://stripe.com/atlas/guides/starting-sales">Your first ten customers</a>, and I think I finally <em>get</em> the whole customer acquisition/sales thing.</p>
<p>In my experience, getting your first 10 customers from content marketing alone is:</p>
<ol>
<li>Extremely difficult to get right</li>
<li>Kind of a waste of time (unless you've got thousands of people in your mailing list)</li>
</ol>
<p>Talking to customers is the fastest way to figure out if the problem you're solving is even a problem at all.</p>
<h3>Your MVP can really be extremely minimal</h3>
<p>As part of the accelerator program, SiteHive had to demonstrate its solution to potential investors and customers by a certain date. I shipped a demo with barely functioning login (hardcoded values in the backend), no database, and just a bunch of charts stuck together in React.</p>
<p>While the solution was quite sparse, it was already solving the problem at this point.</p>
<p>We were able to have highly valuable conversations with potential customers and investors with only the core features they cared about (the data visualisation). It made me realise that I really over-engineer my side-projects, thinking that everything had to be <em>done</em> to be able to launch.</p>
<h3>Your tech stack does not matter</h3>
<p>This year I worked for a company that pulls in millions in revenue per month from an insanely beefy WordPress installation. The engineering team's velocity of being able to ship features was probably the lowest I've ever seen, and yet the business was still making bank.</p>
<p>The takeaway here is to just use whatever technology you're comfortable with.</p>
<p>There is no shame in using Ruby on Rails, Django, or even a code-free solution like Airtable/Zapier in 2020.</p>
<p>Shipping something that people find useful/valuable beats using Svelte/Vue/Angular/React or whatever people are arguing about in JavaScript frameworks.</p>
<h2>2020 Plans</h2>
<h3>Finish up PerfBeacon, and launch!</h3>
<p>I originally was going to wait until PerfBeacon was "feature-complete" before launching, but after working for SiteHive for a few months, I've realised getting <em>anything</em> to market, and being able to talk to people about it is much more valuable.</p>
<p>After launching, I'll be making it possible to test on-demand (like during a CI process), and also show screenshots of different points in the tested page's load process.</p>
<p>Writing this section feels like déjà vu - I essentially had an identical goal for OnlineOrNot at the end of 2018.</p>
<p>This time it'll be different though. I won't be relying on/praying for content marketing to do the job for me, and instead will be reaching out to companies in real life to discuss how they track their web performance.</p>
<h3>Screencasts</h3>
<p>Around October this year I bought a microphone with the intention of making screencasts explaining things in JavaScript and React that took me ages to understand. I didn't get around to actually doing it in between hunting for jobs and building PerfBeacon and OnlineOrNot, so hoping to get some out there next year. I intend to follow these <a href="https://howtoegghead.com/instructor/screencasting/">awesome guides</a> from egghead.io.</p>
<h2>Ciao!</h2>
<p>Anyway, here’s me signing off for 2019, Happy New Year folks!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Why early stage startups are the worst for junior developers]]></title>
            <link>https://maxrozen.com/why-early-stage-startups-are-the-worst-for-junior-developers</link>
            <guid>https://maxrozen.com/why-early-stage-startups-are-the-worst-for-junior-developers</guid>
            <pubDate>Wed, 11 Dec 2019 05:52:00 GMT</pubDate>
            <description><![CDATA[Startups can be the best or worst thing for your career as a junior developer. Let's discuss why.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/why-early-stage-startups-are-the-worst-for-junior-developers">clicking here</a>.)</div><p>Alternative title: Why early startups are either the worst or best idea for junior developers, depending on what they're after.</p>
<p>Before I dive into what I have to say, I feel like I should introduce myself.</p>
<p>Hey 👋! I'm Max, I work as a software engineer at an early stage startup. I'm building a <a href="https://perfbeacon.com/">web performance monitoring service</a> on the side, I've worked in the corporate world as well as various stages of startup, from seed round through to Series C. I feel like I finally understand what I want out of my career, and I wanted to share my thoughts.</p>
<p>At the risk of grossly generalising, I'd say there are three types of developer that you'll meet.</p>
<ol>
<li>You've got the people that want to be the best version of themselves they can be, by mastering their craft.</li>
<li>Then there are the people that want to build the best product, and understand how to make their users happy.</li>
<li>Finally, you have people that just want to do the job from 9 to 5, and not think about programming once they're done.</li>
</ol>
<p>Personally, I jump between the three types several times per day depending on my mood, though up until recently I'd say I focused most on trying to master my craft.</p>
<p>From what I've seen (from myself), being the first type of developer easily leads you to dissatisfaction and burn-out if you're not in the right organisation. Your typical early stage startup is more worried about finding a product that matches a user's need much more than it worries about writing unit tests, or typing its JavaScript correctly. Although having a product focus isn't unique to early stage startups. I've seen it first-hand in several organisations, as well as the impact on the organisation when its best engineers get frustrated by the "ship it now!" culture and leave.</p>
<p>If as a junior developer your goal is to learn as much as possible about the best ways of doing things, I'd argue a startup that is still finding its product-market fit is a bad idea.</p>
<p>The trick to building a career that lets you work towards being the best developer you can be is to find an organisation that has already found its product-market fit, and is comfortably guided by a tight engineering culture.</p>
<p>It's not all doom and gloom for early-stage startups however.</p>
<p>If your goal is to build the best product, develop the skills you need to build things that people actually want, maybe an early-stage startup is the right fit.</p>
<p>As a developer, one of the hardest things you need to learn is that the user does not care what technology your product is running. You could still be using PHP 5.6.40 and Zend framework - as long as you're building product that does what the user needs to be awesome at their job, and your product doesn't piss them off, they'll be happy.</p>
<p>In the end, you need to:</p>
<ul>
<li>figure out what you want from an organisation, whether it's a product focus, engineering focus, or just want to do the job</li>
<li>interview the organisation as much as they're interviewing you, and figure out what kind of focus they have, and whether that's compatible with your goals</li>
<li>be comfortable with ending an interview process for a role that doesn't meet your goals</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[What is the difference between style-loader and mini-css-extract-plugin?]]></title>
            <link>https://maxrozen.com/difference-between-style-loader-mini-css-extract-plugin</link>
            <guid>https://maxrozen.com/difference-between-style-loader-mini-css-extract-plugin</guid>
            <pubDate>Fri, 01 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Looking to optimise your webpack config? Understanding the difference between style-loader and mini-css-extract-plugin can help you speed up your page loads.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/difference-between-style-loader-mini-css-extract-plugin">clicking here</a>.)</div><h2>style-loader</h2>
<p><code>style-loader</code> takes CSS you've imported in your JavaScript files, and injects them as <code>&#x3C;style>&#x3C;/style></code> tags into the DOM. It's particularly useful for inlining <a href="https://www.smashingmagazine.com/2015/08/understanding-critical-css/">Critical CSS</a> into the <code>&#x3C;head></code> of your page.</p>
<p>If you decide to self-host your fonts instead of using Google Fonts, <code>style-loader</code> can also help you inline your font declarations so the browser knows immediately what to do with the font files it downloads.</p>
<h3>How do you use style-loader?</h3>
<pre><code class="language-css">/* style.css */
body {
  background: green;
}
</code></pre>
<pre><code class="language-js">/* component.js */
import './style.css';
</code></pre>
<pre><code class="language-js">/* webpack.config.js */
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};
</code></pre>
<h2>mini-css-extract-plugin</h2>
<p><code>mini-css-extract-plugin</code> on the other hand, extracts your CSS into separate files.</p>
<p>It generates a CSS file for each JS file that imports CSS. It's more useful for CSS that you want to load asynchronously.</p>
<h3>How do you use mini-css-extract-plugin?</h3>
<pre><code class="language-js">/* webpack.config.js */
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // all options are optional
      filename: '[name].css',
      chunkFilename: '[id].css',
      ignoreOrder: false, // Enable to remove warnings about conflicting order
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              // you can specify a publicPath here
              // by default it uses publicPath in webpackOptions.output
              publicPath: '../',
              hmr: process.env.NODE_ENV === 'development',
            },
          },
          'css-loader',
        ],
      },
    ],
  },
};
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How do you make relay-compiler run automatically?]]></title>
            <link>https://maxrozen.com/how-do-you-automatically-run-relay-compiler</link>
            <guid>https://maxrozen.com/how-do-you-automatically-run-relay-compiler</guid>
            <pubDate>Fri, 01 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Getting tired of running the relay-compiler every time you change your GraphQL schema/resolvers? There's a plugin in webpack for that!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-do-you-automatically-run-relay-compiler">clicking here</a>.)</div><h2>Making relay-compiler run automatically</h2>
<p>Relay can be annoying to work with as <code>relay-compiler</code> needs to run every time you change your GraphQL schema, or your resolvers.</p>
<p>With <a href="https://github.com/danielholmes/relay-compiler-webpack-plugin">relay-compiler-webpack-plugin</a> that's no longer the case - webpack can run it for you!</p>
<p>Here's how you use it:</p>
<pre><code class="language-js">const RelayCompilerWebpackPlugin = require('relay-compiler-webpack-plugin');
// rest of your config
new RelayCompilerWebpackPlugin({
    schema: 'schema.graphql',
    src: 'src/',
    extensions: ['js', 'jsx', 'ts', 'tsx'],
    artifactDirectory: 'src/__generated__',
}),
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How do you use variables in the HTML webpack outputs?]]></title>
            <link>https://maxrozen.com/how-do-you-use-variables-in-webpack-html</link>
            <guid>https://maxrozen.com/how-do-you-use-variables-in-webpack-html</guid>
            <pubDate>Fri, 01 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Looking to inject variables from CI into the HTML your webpack build outputs? It's relatively simple to do.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-do-you-use-variables-in-webpack-html">clicking here</a>.)</div><p>Through the use of <a href="https://github.com/jantimon/html-webpack-plugin">html-webpack-plugin</a> you can inject variables into the HTML your webpack build generates - no more hardcoding!</p>
<p>Here's how you use it:</p>
<p>First, run <code>yarn add -D html-webpack-plugin</code> or <code>npm install --save-dev html-webpack-plugin</code></p>
<p>Then, add the following to your webpack config:</p>
<pre><code class="language-js">/* webpack.config.js */
const HtmlWebpackPlugin = require('html-webpack-plugin');
// rest of your config
plugins: [
  new HtmlWebpackPlugin({
    template: 'index.tmp.html',
    filename: 'index.html',
    templateParameters: {
      some_variable: process.env.SOME_VAR,
    },
  }),
];
</code></pre>
<p>index.tmp.html</p>
<pre><code class="language-html">&#x3C;!DOCTYPE html>
&#x3C;html lang="en">
  &#x3C;head>
    &#x3C;meta http-equiv="X-UA-Compatible" content="IE=edge" />
    &#x3C;meta charset="utf-8" />
    &#x3C;meta name="viewport" content="width=device-width, initial-scale=1" />
    &#x3C;meta name="description" content="" />
    &#x3C;meta name="author" content="" />
    &#x3C;title>&#x3C;%= some_variable %>&#x3C;/title>
  &#x3C;/head>
  &#x3C;body>
    &#x3C;div id="root">&#x3C;/div>
  &#x3C;/body>
&#x3C;/html>
</code></pre>
<p>In order for <code>process.env.SOME_VAR</code> to have a variable, you can either use a <code>.env</code> file, or enter the values as part of your CI's configuration (services such as CircleCI and Netlify support this).</p>
<p>Remember that variables injected this way are visible to your users!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How do you use resolve.alias in webpack?]]></title>
            <link>https://maxrozen.com/how-to-use-resolve-alias-in-webpack</link>
            <guid>https://maxrozen.com/how-to-use-resolve-alias-in-webpack</guid>
            <pubDate>Fri, 01 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[Want to stop webpack from including multiple versions of the same package in your bundle? It only takes a couple of lines of code.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-to-use-resolve-alias-in-webpack">clicking here</a>.)</div><p>webpack has an option called <a href="https://webpack.js.org/configuration/resolve/">resolve</a> which can help prevent some gnarly errors in packages like React and GraphQL.</p>
<p>What happens is, some packages choose to include common packages as dependencies rather than as peer dependencies. Net result is that your bundle ends up larger, or in the case of React or GraphQL packages, you get random errors.</p>
<p>In React, having more than one version of React in your dependencies can cause this common error: <code>hooks can only be called inside the body of a function component</code>.</p>
<p>Luckily for us, the fix is pretty simple - we add the following to our webpack config:</p>
<pre><code class="language-js"> resolve: {
    alias: {
      react: path.dirname(require.resolve('react')),
    },
  }
</code></pre>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Understanding your webpack config]]></title>
            <link>https://maxrozen.com/understanding-your-webpack-config</link>
            <guid>https://maxrozen.com/understanding-your-webpack-config</guid>
            <pubDate>Fri, 01 Nov 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[webpack is a complicated beast. The more you get to know it though, the better it can serve you.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/understanding-your-webpack-config">clicking here</a>.)</div><p>webpack has a ridiculous number of settings and plugins that you can fine-tune to make your frontend more performant, have smaller bundles, and generally behave the way you expect it to.</p>
<p>The trick is taking the time to actually understand what the settings in your webpack config do.</p>
<p>The articles below cover a few settings and plugins (that were correct as of 2019-10-26) I use regularly, and can help optimise your webpack configuration.</p>
<ul>
<li>Settings
<ul>
<li><a href="/how-to-use-resolve-alias-in-webpack">Using resolve.alias to set which package your code should use</a></li>
<li><a href="/difference-between-style-loader-mini-css-extract-plugin">The difference between style-loader and mini-css-extract-plugin</a></li>
</ul>
</li>
<li>Plugins
<ul>
<li><a href="/how-do-you-use-variables-in-webpack-html">Injecting variables into HTML that webpack builds</a></li>
<li><a href="/how-do-you-automatically-run-relay-compiler">Making webpack compile Relay automatically</a></li>
</ul>
</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Prefetch your Google Fonts for Performance Gains in Gatsby]]></title>
            <link>https://maxrozen.com/prefetch-google-fonts-with-gatsby</link>
            <guid>https://maxrozen.com/prefetch-google-fonts-with-gatsby</guid>
            <pubDate>Thu, 10 Oct 2019 05:52:00 GMT</pubDate>
            <description><![CDATA[Self-hosting your Google Fonts can save your customers around 3-400ms per page load. There's a Gatsby plugin that makes it super easy.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/prefetch-google-fonts-with-gatsby">clicking here</a>.)</div><h3>Why is Google Fonts an issue?</h3>
<p>The cost of using a call such as</p>
<pre><code class="language-css">@import url('https://fonts.googleapis.com/css?family=Lato:300,400,900');
</code></pre>
<p>is that the browser first has to download the CSS file, and parse it. Afterwards, the browser downloads your fonts. This process is particularly syncronous, so we can't download your fonts until we ask Google for the location of the font files we need.</p>
<h3>How do we speed things up?</h3>
<p>We can get a large performance boost by self-hosting our fonts. To do this, you would need to:</p>
<ol>
<li>Use <a href="https://google-webfonts-helper.herokuapp.com/fonts">google-webfonts-helper</a> to figure out which fonts you needed to download</li>
<li>Check them into your repo</li>
<li>Reference them in your CSS</li>
<li>Ensure that CSS is in-lined in your HTML, and among the first CSS to be read by browsers</li>
</ol>
<p>This is a bit involved, but totally worth it. At a previous employer I managed to shave 300ms off of page load time by self-hosting fonts.</p>
<h3>Enter gatsby-plugin-prefetch-google-fonts</h3>
<p>Now with <a href="https://www.gatsbyjs.org/packages/gatsby-plugin-prefetch-google-fonts/">gatsby-plugin-prefetch-google-fonts</a>, you can save 3-400ms with a little bit of configuration.</p>
<ol>
<li>Install:
<code>yarn add gatsby-plugin-prefetch-google-fonts</code></li>
<li>Configure <code>gatsby-config.js</code>:</li>
</ol>
<pre><code class="language-js">  {
      resolve: `gatsby-plugin-prefetch-google-fonts`,
      options: {
        fonts: [
          {
            family: `Lato`,
            variants: [`300`, `400`, `900`],
          },
        ],
      },
    },
</code></pre>
<h3>Results</h3>
<h4>Before:</h4>
<p><img src="google-font-css.png" alt="Before prefetching Google Fonts">
<a href="https://webpagetest.org/result/191009_JR_9137ae367dcfa3e9ada6ac38bb7be68e/">WebPageTest result</a></p>
<p>If you look at the time spent by Resource 8, 10, and 11, you can see we're waiting quite a bit just to get the page's fonts loaded.</p>
<p>It's also not the first thing the browser does, so we're likely going to see a flash of missing font. This also depends on what your <code>font-display</code> setting is in CSS.</p>
<h4>After:</h4>
<p><img src="gatsby-prefetch-font.png" alt="After prefetching Google Fonts">
<a href="https://webpagetest.org/result/191009_2F_fc704f0002d28867cc09b7c43bdc3831/">WebPageTest result</a></p>
<p>After adding the prefetch plugin, we can see Resource 2, 3, and 4 being immediately loaded once we fetch the page.</p>
<p>I was so impressed with these results that I wrote this blog post!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to write semantic HTML]]></title>
            <link>https://maxrozen.com/how-to-write-semantic-html</link>
            <guid>https://maxrozen.com/how-to-write-semantic-html</guid>
            <pubDate>Sat, 13 Jul 2019 05:52:00 GMT</pubDate>
            <description><![CDATA[Writing semantic HTML markup is one of the first steps to writing accessible websites. Let's learn how to get started!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/how-to-write-semantic-html">clicking here</a>.)</div><h2>What does "semantic" mean?</h2>
<p>To write semantic HTML is to give it meaning, rather than just describing how it should look in the browser.</p>
<p>Take this code snippet for example:</p>
<pre><code class="language-html">&#x3C;div class="heading">My Heading&#x3C;/div>
&#x3C;div class="paragraph">Here's a sentence I wrote just for you.&#x3C;/div>
</code></pre>
<p>While it is possible to write CSS to make your <code>heading</code> and <code>paragraph</code> classes appear as you like, software tooling that reads your markup has no idea what it means. It'll just see text within a div, and not know how the text in the first div relates to the second div.</p>
<p>Here's the semantic way of writing the previous example:</p>
<pre><code class="language-html">&#x3C;h1>My Heading&#x3C;/h1>
&#x3C;p>Here's a sentence I wrote just for you.&#x3C;/p>
</code></pre>
<p>The advantages of writing your HTML semantically are:</p>
<ul>
<li>You get a set of default styles for free (which you can override with CSS if you wish)</li>
<li>Communication: Developers who read your code in the future instantly know what was intended - there is no ambiguity to semantic markup</li>
<li>SEO: Search engines like Google look for <code>&#x3C;h1></code> tags to determine which keywords to associate your content with</li>
<li>Screen readers can now read your content as you intended</li>
</ul>
<h2>More examples of Semantic vs Non-semantic markup</h2>
<h3>Buttons</h3>
<p>Good:</p>
<pre><code class="language-html">&#x3C;button>Click me&#x3C;/button>
</code></pre>
<p>Bad:</p>
<pre><code class="language-html">&#x3C;div>Click me&#x3C;/div>
</code></pre>
<p><strong>Why is this bad?</strong></p>
<p>Divs are not focusable by keyboards. We could make them focusable by adding <code>tabindex="0"</code>, but this can still be hacky in the case of inputs.</p>
<h3>Inputs</h3>
<p>Good:</p>
<pre><code class="language-html">&#x3C;input type="checkbox" />
</code></pre>
<p>Bad:</p>
<pre><code class="language-html">&#x3C;div role="checkbox">&#x3C;/div>
</code></pre>
<p><strong>Why is this bad?</strong></p>
<p>As mentioned before, divs are not focusable by default. Adding <code>tabindex="0"</code> here will make your div focusable, but then you would also need to write additional JavaScript to submit the value of your "checkbox" when the rest of the form submits.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[The books I read in 2018]]></title>
            <link>https://maxrozen.com/books-i-read-in-2018</link>
            <guid>https://maxrozen.com/books-i-read-in-2018</guid>
            <pubDate>Wed, 02 Jan 2019 05:52:00 GMT</pubDate>
            <description><![CDATA[A list of books I read in 2018]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/books-i-read-in-2018">clicking here</a>.)</div><h3>How I read:</h3>
<p>I used to absolutely suck at reading books (and I still do, come to think of it). The whole having a physical book in your hands thing, trying to find a comfortable position to read just before going to sleep while your elbows are going numb - so annoying. Don't get me wrong though, I absolutely love the physical act of reading, I just sucked at making it a habit, and being comfortable while reading.</p>
<p>Then I discovered podcasts, which eventually lead me to Audible. Instead of reading 3-4 books a year (mostly while on flights/trains to holiday destinations), I now read around 14-16. I typically listen while commuting to work, though occasionally while doing household chores I'll also put on a book. You'd be surprised how quickly those 30 minute commutes add up!</p>
<h3>The books that didn't make it</h3>
<p>A few books I bought either didn't resonate with me, or the narration was atrocious - so I just returned them. The books below are only the ones I actually wanted to finish reading.</p>
<h3>The list</h3>
<ul>
<li>Why We Sleep - Matthew Walker</li>
<li>Bullshit Jobs - David Graeber</li>
<li>It Doesn't Have To Be Crazy at Work - DHH, Jason Fried</li>
<li>Rework - Jason Fried, DHH</li>
<li>Angel - Jason Calacanis</li>
<li>Blitzscaling - Reid Hoffman, Chris Yeh</li>
<li>Debt - David Graeber (half-finished)</li>
<li>Hooked: How to Build Habit-Forming Products - Nir Eyal</li>
<li>Thinking in Bets - Annie Duke</li>
<li>High Growth Handbook - Elad Gil (half-finished)</li>
<li>The Upstarts - Brad Stone</li>
<li>The Everything Store - Brad Stone</li>
<li>All Marketers Are Liars - Seth Godin</li>
<li>Creativity Inc - Ed Catmull</li>
<li>The Icarus Deception - Seth Godin</li>
<li>Purple Cow - Seth Godin</li>
</ul>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[2018: Reflections on trying to start an internet business]]></title>
            <link>https://maxrozen.com/2018-review-starting-an-internet-business</link>
            <guid>https://maxrozen.com/2018-review-starting-an-internet-business</guid>
            <pubDate>Mon, 31 Dec 2018 05:52:00 GMT</pubDate>
            <description><![CDATA[A review post on what I've done this year in terms of trying to start an Internet business]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/2018-review-starting-an-internet-business">clicking here</a>.)</div><h2>2018, what a year</h2>
<h3>On the personal side</h3>
<p>I Interviewed at Facebook, first for front-end, then for full-stack. Considering the year Facebook has had, I'm okay with not getting the job.</p>
<p>Roughly around the time I was interviewing at Facebook, I took up the #vanlife with my partner and toured through California in a <a href="https://www.instagram.com/p/Bi2qt2lhDal/">purple van</a>. When I came back home, I finally took the driving test and got a <a href="https://www.instagram.com/p/BnFONoYF5HQ/">wagon</a> to continue the #vanlife here in Australia.</p>
<p>I then quit my job at Arup (building engineering consultancy) to go work at a Fintech startup aiming to revolutionise the retail investing space for Mum &#x26; Dad investors. After 3 weeks I quit the startup job, once we realised the job they advertised for and the job they wanted me to do weren't exactly aligned with my skillset (I see myself as probably 75% front-end, while 100% of the work they had for me was back-end for the foreseeable future).</p>
<p>In between jobs I picked up freelancing as a way to make some extra money, migrating <a href="https://ethicalequities.com.au">EthicalEquities</a> from WordPress to Django, and added a forum to build more of a community around ethical investing.</p>
<p>After about 6 days of freelancing full-time, I joined <a href="https://expert360.com">Expert360</a> as an employee, where I (mostly) work on the front-end in React. I've been helping to build a Design System as well as some product work.</p>
<h3>On the business side, chronologically</h3>
<h4>JobsOk.io</h4>
<p>I finished building an MVP for <a href="https://jobsok.io">JobsOk.io</a> (A tech job board where posting an ad would only cost $199 US) in a thinly veiled attempt at emulating Pieter Levels' success with <a href="https://remoteok.io">RemoteOk</a>.</p>
<p>What I learned here is that your implementation is meaningless to the user if you're not delivering the value they came for. In this case, I wanted to take a technologically inferior product, make it extremely efficient, scalable and "blazing fast", and pocket pure profit from running a serverless job board (effectively free to run for the first million visits a month)</p>
<p>I was failing to deliver value to users solely because I didn't have the traffic to compete with other job boards. The reason StackOverflow can charge as much as it does is the millions of page views per month it receives, creating value to its job posters.</p>
<p>In total, JobsOk logged 761 sessions for 661 users in 2018. While a total commercial failure, I did learn <em>a lot</em> about running GraphQL in production, and using off the shelf Design Systems (Material-UI).</p>
<p>I eventually revisited JobsOk, and migrated it from being a client-side app to server-side rendered app in AWS Lambda (Check out <a href="https://github.com/jaredpalmer/razzle/">Razzle</a>).</p>
<h4>AppointmentScheduler.org</h4>
<p>I finished building an MVP for <a href="https://appointmentscheduler.org">AppointmentScheduler.org</a> - another hero-inspired product (in this case, <a href="https://www.kalzumeus.com/">patio11</a>), AppointmentScheduler was going to have a freemium product model to take appointments for any industry that needed appointment scheduling software. I intended to use content marketing to acquire users.</p>
<p>I honestly thought I had learned "if you build it, they will not come" before building AppointmentScheduler. Turns out I did not. It took about a month of weekends, and time after work to build. I also spent part of that month writing pretty vague, average blog posts to try attract users, to no avail. I had planned to build beyond the free feature set if I had seen enough users using the site, however most people came for the blog posts, and didn't stick around to see the app.</p>
<p>The site was unique in that it was a 100% pure <a href="https://www.gatsbyjs.org/">Gatsby.js</a> site, where I added a client-only section under the <code>/app</code> path.</p>
<p>In total, AppointmentScheduler logged 561 sessions for 496 users in 2018.</p>
<h4>RoomBooking.co</h4>
<p>I started work on an MVP of a spin-off product from AppointmentScheduler - Roombooking.co, only to find the Google API restrictions meant I would have to implement most of it myself. RoomBooking would let you select slots in a Room's Google Calendar and see when it would be available for use.</p>
<p>I was starting to finally understand the lean start-up methodology at this point, so I had spoken to a few people working in businesses too small to afford using Microsoft Outlook for meeting room bookings, but large enough to need a system to book meeting rooms. There seemed to be a clear need for a means of booking rooms in a business. I only spent a week of time after work on this, as I wasn't particularly interested in the problem space.</p>
<p>I wanted to be able to create a public calendar in Google Calendar, while also allowing anyone to create events on that public calendar, while restricting the viewing of events to the Owner of the calendar. Turns out Google won't let you do it. Unwilling to implement it myself, I canned the project, and wrote a <a href="https://maxrozen.com/2018/02/26/so-i-had-an-idea">blog post</a> about the experience.</p>
<h4>Using a template to build side-projects</h4>
<p>Half-way through the year, I realised I had been building all of these side projects from scratch each time, and had been pretty burned out by the experience. I basically didn't write any fresh side-project code for four months, though I did try to keep coming up with ideas (I actually bought the domain for two of the ideas, RecordMyWeight.com and IndiePhotogs.com - though I never used them).</p>
<p>In August, I collected all of the code I had written all year and built myself a React + GraphQL server template in Terraform that deploys a complete site with HTTPS, CDN caching via CloudFront, GraphQL server &#x26; SSR server running in AWS Lambda.</p>
<p>To avoid having to go back and update version numbers in the template, I actually wrote it in the form of a three-part blog:</p>
<ul>
<li><a href="https://maxrozen.com/2018/08/08/start-your-own-app-with-react-part-1/">Server-side Rendering, Serverlessly!</a></li>
<li><a href="https://maxrozen.com/2018/08/15/start-your-own-app-with-react-part-2/">Deploying to a real domain name</a></li>
<li><a href="https://maxrozen.com/2018/08/16/start-your-own-app-with-react-part-3/">Let's build a GraphQL backend!</a></li>
</ul>
<h4>OnlineOrNot - finally a problem worth solving</h4>
<p>Since starting my career as a software engineer I've found APIs to be a pain to work with.</p>
<p>My first experience working with a "back-end" developer as a front-end developer to build a dashboard for financial data essentially involved having a financial quant dump a spreadsheet into the Python pandas library, and outputting the result to JSON. At one point in the project, the API's result would break the front-end every 3 hours, despite there being contracts in place to prevent this from happening. If we had a tool capable of snapshotting the API result and alerting our boss whenever it changed, the project would have gone much more smoothly.</p>
<p>Most developers I spoke to while validating this project's commercial viability had a similar story, or could relate to the problem. Being able to ensure your API is operating correctly is particularly tricky with GraphQL, as GraphQL servers do not respond with 5xx error codes when they fail.</p>
<p>My approach to deciding whether or not to build OnlineOrNot was slightly more rigorous than my previous side-projects, as I really wanted to make sure people would actually pay for this. I spoke to former coworkers, and current coworkers to try understand how developers ensure GraphQL is running - and in short, it's really hard!</p>
<p>So after a month of testing whether people liked the idea, I set off to build <a href="https://onlineornot.com">OnlineOrNot</a> using my side-project template I had developed back in August. I had quite ambitiously wanted to launch my beta by December 1, however when you're working on a product after work and on weekends, occasionally life gets in the way. I have still not yet officially "launched" the product, I'm still actively recruiting my first ten customers to help beta test the tool.</p>
<p>It being December 31 today, I'd say OnlineOrNot is very close to a public launch - it's embarrassingly under-featured, but still executes on its core-value proposition of automating GraphQL testing &#x26; uptime monitoring.</p>
<p>Since October 1 the marketing site has received 557 sessions from 491 users (roughly 4x each of my previous side-projects in a comparable period of time).</p>
<h2>2019 Plans</h2>
<h3>Launch OnlineOrNot, make an actual "Company"</h3>
<p>I plan on launching OnlineOrNot on ProductHunt, IndieHackers and Hacker News to get more users for the public beta. I'm not really expecting much, if I can get around 10 users trialling the product and leaving feedback I would be over the moon.</p>
<p>If I manage to get a few customers I'll also start a company to own the IP. As much as I love being a sole-trader, something about being exposed to unlimited liability when dealing with B2B transactions sounds a touch unpleasant.</p>
<p>My longer-term play for user acquisition will still be classic content marketing, but also launching on LinkedIn, and using StackOverflow questions to help potential users learn about the service.</p>
<h3>Go camping more</h3>
<p>My partner and I bought a Subaru Outback 2005 to go car camping around Australia, and when (if?) the weather cools down a bit we'll try car camping around the East Coast of NSW.</p>
<h3>Sell some camera gear</h3>
<p>Since I started photography as a hobby about 5 years ago, I've accumulated around 26 cameras and various lenses. While I've been slowly selling off bits and pieces to friends and family, I'd love to be able to spend that money on investing, instead of cameras.</p>
<p>Anyway, here's me signing off for 2018, Happy New Year folks!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 3 - Let's build a GraphQL backend!]]></title>
            <link>https://maxrozen.com/start-your-own-app-with-react-part-3</link>
            <guid>https://maxrozen.com/start-your-own-app-with-react-part-3</guid>
            <pubDate>Thu, 16 Aug 2018 05:52:00 GMT</pubDate>
            <description><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 3 - Let's build a GraphQL backend!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/start-your-own-app-with-react-part-3">clicking here</a>.)</div><p>Note that this series was written in 2018, using an older version of Terraform (version 12), available <a href="https://releases.hashicorp.com/terraform/">here</a>. As it also uses an old version of Node, this series is left online as more of a guideline.</p>
<p>To start, open up your terminal, navigate to your app, and type:</p>
<pre><code>mkdir api
</code></pre>
<p>We'll now start a new npm project for our GraphQL server (you could integrate more with the Razzle project, but I'm not entirely sure how).</p>
<pre><code>npm init -y
</code></pre>
<p>And now we'll add all the dependencies:</p>
<pre><code>yarn add cors graphql graphql-tag graphql-tools request request-promise-native
</code></pre>
<p>and dev-dependencies:</p>
<pre><code>yarn add -D webpack babel-core babel-loader babel-plugin-inline-import babel-plugin-transform-class-properties babel-plugin-transform-object-rest-spread cross-env zip-webpack-plugin webpack-cli
</code></pre>
<h3>Time to add add some JavaScript files</h3>
<p>Create a <code>.babelrc</code> file in <code>api/</code>, containing:</p>
<pre><code>{
  "presets": ["env"],
  "plugins": [
    "inline-import",
    "transform-class-properties",
    "transform-object-rest-spread"
  ]
}
</code></pre>
<p>In your <code>package.json</code> add the following scripts:</p>
<pre><code>"dev": "cross-env BABEL_DISABLE_CACHE=1 node -r babel-register local.js",
"build": "cross-env BABEL_DISABLE_CACHE=1 webpack --config=webpack.config.babel.js --env=production"
</code></pre>
<p>Create a <code>local.js</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-js">import cors from 'cors';
import express from 'express';
import schema from './schema';
import { json } from 'body-parser';
import { graphql } from 'graphql';

const PORT = process.env.PORT || 3002;

const app = express();

app.use(cors());

app.post('/graphql', json(), (req, res) => {
  const { query, variables } = req.body;
  const rootValue = {};
  const context = {};
  let operationName;
  console.log('query: ', query);
  console.log('variables: ', variables);
  graphql(schema, query, rootValue, context, variables, operationName)
    .then((d) => {
      console.log('d: ', d);
      res
        .status(200)
        .set('Content-Type', 'application/json')
        .send(JSON.stringify(d));
    })
    .catch((e) => {
      console.log('e: ', e);
      res
        .status(500)
        .set('Content-Type', 'application/json')
        .send(JSON.stringify(e));
    });
});

app.listen(PORT, (err) => {
  if (err) {
    throw err;
  }
  console.log(`Listening on http://localhost:${PORT}/`);
});
</code></pre>
<p>Create an <code>index.js</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-js">import { graphql } from 'graphql';
import schema from './schema';

export const handler = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;
  console.log('event:', event);

  if (event.httpMethod == 'OPTIONS') {
    callback(null, {
      statusCode: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Authorization, Content-Type',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
        'Access-Control-Allow-Credentials': 'true',
        'Content-Type': 'application/json',
      },
      body: null,
    });
  } else {
    const { query, variables, operationName } = JSON.parse(event.body);
    const rootValue = {};
    const ctx = {};
    graphql(schema, query, rootValue, ctx, variables, operationName)
      .then((d) => {
        callback(null, {
          statusCode: 200,
          headers: {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Authorization, Content-Type',
            'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
            'Access-Control-Allow-Credentials': 'true',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(d),
        });
      })
      .catch((e) => {
        callback(null, {
          statusCode: 503,
          headers: {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Authorization, Content-Type',
            'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH',
            'Access-Control-Allow-Credentials': 'true',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(e),
        });
      });
  }
};
</code></pre>
<p>Create a <code>resolvers.js</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-js">export default {
  Query: {
    hello: () => 'world',
  },
  // Mutation: {
  //
  // },
};
</code></pre>
<p>Create a <code>schema.graphql</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-graphql">type Query {
  hello: String!
}

#type Mutation {
#
#}

schema {
  query: Query
  #mutation: Mutation
}
</code></pre>
<p>Create a <code>schema.js</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-js">import { makeExecutableSchema } from 'graphql-tools';
import typeDefs from './schema.graphql';
import resolvers from './resolvers';

export default makeExecutableSchema({
  typeDefs,
  resolvers,
});
</code></pre>
<p>Finally, create a <code>webpack.config.babel.js</code> file in <code>api/</code>, containing:</p>
<pre><code class="language-js">import path from 'path';
import webpack from 'webpack';
import zip from 'zip-webpack-plugin';

export default {
  entry: path.resolve(__dirname),
  output: {
    path: path.join(__dirname, '..', 'build'),
    filename: 'index.js',
    library: 'index',
    libraryTarget: 'umd',
  },
  target: 'node',
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, '..'),
        exclude: path.resolve(__dirname, '..', 'node_modules'),
        loader: 'babel-loader',
      },
      {
        test: /\.graphql$/,
        include: path.resolve(__dirname, '..'),
        exclude: path.resolve(__dirname, '..', 'node_modules'),
        loader: 'graphql-tag/loader',
      },
    ],
  },

  plugins: [
    new webpack.ProvidePlugin({
      XMLHttpRequest: ['xmlhttprequest', 'XMLHttpRequest'],
    }),
    new zip({
      filename: 'graphql.zip',
    }),
  ],
};
</code></pre>
<p>That's all you should need to get the GraphQL service running locally.</p>
<h3>Terraform changes</h3>
<p>We also need to make some changes to our Terraform configuration to:</p>
<ul>
<li>Spin up a new Lambda for GraphQL</li>
<li>Spin up a new API Gateway for the new Lambda</li>
<li>Tell Cloudfront to forward all <code>/graphql</code> requests to our API Gateway</li>
</ul>
<p>Back in <code>infrastructure/</code>, create a <code>api_gateway_lambda.tf</code> file containing:</p>
<pre><code>resource "aws_api_gateway_rest_api" "graphql_api" {
  name        = "${var.name}_api"
  description = "${var.comment}"
}

resource "aws_api_gateway_deployment" "graphql_api_deployment" {
  rest_api_id = "${aws_api_gateway_rest_api.graphql_api.id}"
  stage_name  = "prod"
}


resource "aws_api_gateway_resource" "grapqhl_endpoint" {
  rest_api_id = "${aws_api_gateway_rest_api.graphql_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.graphql_api.root_resource_id}"
  path_part   = "graphql"
}

resource "aws_api_gateway_method" "graphql_path_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.graphql_api.id}"
  resource_id   = "${aws_api_gateway_resource.grapqhl_endpoint.id}"
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "graphql_path_integration" {
  rest_api_id             = "${aws_api_gateway_rest_api.graphql_api.id}"
  resource_id             = "${aws_api_gateway_resource.grapqhl_endpoint.id}"
  http_method             = "${aws_api_gateway_method.graphql_path_post.http_method}"
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${aws_lambda_function.graphql.arn}/invocations"
  content_handling        = "CONVERT_TO_TEXT"
}

resource "aws_api_gateway_method" "graphql_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.graphql_api.id}"
  resource_id   = "${aws_api_gateway_rest_api.graphql_api.root_resource_id}"
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "graphql_integration" {
  rest_api_id             = "${aws_api_gateway_rest_api.graphql_api.id}"
  resource_id             = "${aws_api_gateway_rest_api.graphql_api.root_resource_id}"
  http_method             = "${aws_api_gateway_method.graphql_post.http_method}"
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${aws_lambda_function.graphql.arn}/invocations"
  content_handling        = "CONVERT_TO_TEXT"
}
</code></pre>
<p>In <code>infrastructure/</code> create a <code>lambda_graphql.tf</code> file containing:</p>
<pre><code>resource "aws_lambda_function" "graphql" {
  function_name    = "graphql_${var.name}"
  role             = "${aws_iam_role.lambda_iam.arn}"
  handler          = "index.handler"
  runtime          = "nodejs8.10"
  timeout          = "45"
  memory_size      = "512"
  filename         = "../build/graphql.zip"
  source_code_hash = "${base64sha256(file("../build/graphql.zip"))}"

  tags {
    Site = "${var.name}"
  }
}


resource "aws_lambda_permission" "allow_api_to_exec_lambda_graphql" {
  statement_id  = "allow_api_to_exec_lambda_graphql"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.graphql.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_api_gateway_rest_api.graphql_api.execution_arn}/*/*/*"
}
</code></pre>
<p>In our existing <code>cloudfront.tf</code> file, we'll want to add this immediately after the <code>ssr</code> origin_id block:</p>
<pre><code> origin {
   origin_id = "api"

   domain_name = "${aws_api_gateway_rest_api.graphql_api.id}.execute-api.${var.aws_region}.amazonaws.com"

   custom_origin_config {
     http_port              = "80"
     https_port             = "443"
     origin_protocol_policy = "match-viewer"
     origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
   }

   origin_path = "/${aws_api_gateway_deployment.graphql_api_deployment.stage_name}"
 }
</code></pre>
<p>Similarly, still in the <code>cloudfront.tf</code> file, add the following immediately after the <code>ordered_cache_behavior</code> for <code>/static/*</code>:</p>
<pre><code> ordered_cache_behavior {
   path_pattern           = "graphql"
   allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
   cached_methods         = ["GET", "HEAD"]
   target_origin_id       = "api"
   default_ttl            = 0
   max_ttl                = 0
   min_ttl                = 0
   viewer_protocol_policy = "https-only"
   compress               = true

   forwarded_values {
     query_string = true

     cookies {
       forward = "all"
     }

     headers = [
       "Accept",
       "Authorization",
       "Origin",
     ]
   }
 }
</code></pre>
<p>At this point you can run <code>terraform apply</code> - You will most likely get an error regarding an API Gateway integration not existing. This is a race condition, and we can get around it just by re-running <code>terraform apply</code>.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 2 - Deploying to a real domain name]]></title>
            <link>https://maxrozen.com/start-your-own-app-with-react-part-2</link>
            <guid>https://maxrozen.com/start-your-own-app-with-react-part-2</guid>
            <pubDate>Wed, 15 Aug 2018 05:52:00 GMT</pubDate>
            <description><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 2 - Let's get this running in production]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/start-your-own-app-with-react-part-2">clicking here</a>.)</div><p>Note that this series was written in 2018, using an older version of Terraform (version 12), available <a href="https://releases.hashicorp.com/terraform/">here</a>. As it also uses an old version of Node, this series is left online as more of a guideline.</p>
<p>Welcome to Part 2 of Starting your own app. In Part 1, we got an AWS Lambda function to render React, and in Part 2, we'll be setting that function up to run on our own domain name, with a free SSL certificate!</p>
<p>To begin, you'll need to already own a domain in AWS Route 53. Check out https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/registrar.html for more details on getting that set up.</p>
<ol>
<li>
<p>Create a new file in your <code>infrastructure/</code> folder, called <code>route53.tf</code>, containing:</p>
<pre><code>data "aws_route53_zone" "site_zone" {
  name = "${var.domain_name}"
}

resource "aws_route53_record" "cf_alias_A" {
  zone_id = "${data.aws_route53_zone.site_zone.zone_id}"
  name    = "${var.domain_name}"
  type    = "A"

  alias {
    name                   = "${aws_cloudfront_distribution.site.domain_name}"
    zone_id                = "${aws_cloudfront_distribution.site.hosted_zone_id}"
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "cf_alias_AAAA" {
  zone_id = "${data.aws_route53_zone.site_zone.zone_id}"
  name    = "${var.domain_name}"
  type    = "AAAA"

  alias {
    name                   = "${aws_cloudfront_distribution.site.domain_name}"
    zone_id                = "${aws_cloudfront_distribution.site.hosted_zone_id}"
    evaluate_target_health = true
  }
}
</code></pre>
<p>This creates records pointing to the CloudFront distribution we'll create later on</p>
</li>
<li>
<p>Edit your <code>vars.tf</code> file, adding the following variable (swap out <code>recordmyweight.com</code> with your own domain name):</p>
<p><code>variable "domain_name" {default = "recordmyweight.com"}</code></p>
</li>
<li>
<p>You'll also want to change the <code>bucket_site</code> variable in <code>vars.tf</code> to match your domain (Remove <code>.</code>'s in your <code>bucket_site</code> to avoid issues with SSL):</p>
<p><code>variable "bucket_site" {default = "recordmyweightcom"}</code></p>
<p>This renames the <code>my-app-12345</code> bucket to your new variable value.</p>
</li>
<li>
<p>In your <code>main.tf</code> file, add the following:</p>
<pre><code>provider "aws" {
  alias  = "east"
  region = "us-east-1"
}
</code></pre>
<p>This will let us create an SSL certificate in the us-east-1 zone later, for CloudFront's use.</p>
</li>
<li>
<p>Create a <code>acm_certificate.tf</code> file, containing the following (making sure to replace <code>www.recordmyweight.com</code> with your own subdomain):</p>
<pre><code>variable "san_domains" {
  default = [
    "www.recordmyweight.com"
  ]
}

data "aws_route53_zone" "zone" {
  name = "${var.domain_name}."
  private_zone = false
}

resource "aws_acm_certificate" "cert" {
  domain_name               = "${var.domain_name}"
  validation_method         = "DNS"
  subject_alternative_names = "${var.san_domains}"
  provider = "aws.east"
}

resource "aws_route53_record" "cert" {
  count   = "${length(var.san_domains) + 1}"
  zone_id = "${data.aws_route53_zone.zone.id}"
  name    = "${lookup(aws_acm_certificate.cert.domain_validation_options[count.index], "resource_record_name")}"
  type    = "${lookup(aws_acm_certificate.cert.domain_validation_options[count.index], "resource_record_type")}"
  records = ["${lookup(aws_acm_certificate.cert.domain_validation_options[count.index], "resource_record_value")}"]
  ttl     = 60
}

resource "aws_acm_certificate_validation" "cert" {
    provider = "aws.east"
    certificate_arn = "${aws_acm_certificate.cert.arn}"
    validation_record_fqdns = ["${aws_route53_record.cert.*.fqdn}" ]
}
</code></pre>
<p>This creates a globally accessible SSL certificate, and verifies that you own the domain you're registering the certificate for.</p>
</li>
<li>
<p>The final step is to add a <code>cloudfront.tf</code> file, containing the following:</p>
<pre><code>resource "aws_cloudfront_distribution" "site" {
  origin {
    domain_name = "${var.bucket_site}.s3-website-${var.aws_region}.amazonaws.com"
    origin_id   = "s3"

    custom_origin_config {
      http_port              = "80"
      https_port             = "443"
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  origin {
    domain_name = "${aws_api_gateway_rest_api.ssr.id}.execute-api.${var.aws_region}.amazonaws.com"
    origin_id = "ssr"

    custom_origin_config {
      http_port              = "80"
      https_port             = "443"
      origin_protocol_policy = "match-viewer"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }

    origin_path = "/${aws_api_gateway_deployment.ssr_deployment.stage_name}"
  }

  enabled             = true
  is_ipv6_enabled     = true
  comment             = ""
  default_root_object = "index.html"
  retain_on_delete    = true
  aliases             = ["${var.domain_name}"]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "ssr"
    compress         = true
    forwarded_values {
      query_string = true

      cookies = {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    max_ttl                = 0
    default_ttl            = 0
  }

  ordered_cache_behavior {
    path_pattern           = "/static/*"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3"
    compress               = true
    default_ttl            = 0
    max_ttl                = 0
    min_ttl                = 0
    viewer_protocol_policy = "https-only"

    forwarded_values {
      query_string = true

      cookies {
        forward = "all"
      }

      headers = [
        "Accept",
        "Authorization",
        "Origin",
      ]
    }
  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  tags {
    Project = "${var.name}"
  }

  viewer_certificate {
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1"
    acm_certificate_arn   = "${aws_acm_certificate_validation.cert.certificate_arn}"

  }
}
</code></pre>
<p>This one might seem quite complicated initially - but it only does a few things:</p>
<ul>
<li>Sets up two origins (ssr and s3)</li>
<li>Creates a default cache (pointing at the ssr origin), and a secondary cache (pointing to the s3)</li>
<li>Uses the SSL certificate created earlier to secure the CDN</li>
</ul>
</li>
<li>
<p>Finally, run <code>terraform apply</code> again, type <code>yes</code> to confirm and wait for terraform to finish building your infrastructure.</p>
</li>
<li>
<p>At this point, go into <code>razzle.config.js</code> and remove this line:</p>
<pre><code>appConfig.output.publicPath = `${process.env.PUBLIC_PATH}`
</code></pre>
</li>
<li>
<p>Also go into <code>package.json</code> and remove this part of the <code>build</code> script:</p>
<pre><code>PUBLIC_PATH=https://recordmyweightcom.s3-ap-southeast-2.amazonaws.com/public/
</code></pre>
<p>You also want to change this section:</p>
<pre><code>aws s3 sync \"./build/public/\" s3://recordmyweightcom/public
</code></pre>
<p>to be:</p>
<pre><code>aws s3 sync \"./build/public/\" s3://recordmyweightcom
</code></pre>
<p>So in the end it should look like:</p>
<pre><code>"razzle build &#x26;&#x26; aws s3 sync \"./build/public/\" s3://recordmyweightcom &#x26;&#x26; cd ./build &#x26;&#x26; zip -r ./server.zip ./server.*",
</code></pre>
</li>
<li>
<p>Also, in <code>infrastructure/s3.tf</code>, change this line:</p>
<pre><code>allowed_origins = ["https://${aws_api_gateway_deployment.ssr_deployment.rest_api_id}.execute-api.${var.aws_region}.amazonaws.com"]
</code></pre>
<p>to read:</p>
<pre><code>allowed_origins = ["https://${var.domain_name}"]
</code></pre>
</li>
<li>
<p>Now run <code>npm run build</code>, wait for it to complete, then go into <code>infrastructure/</code> and run <code>terraform apply</code>. This'll redeploy our app using a CDN, rather than your local S3 to serve static resources.
This concludes Part 2: Let's get this running in production</p>
</li>
</ol>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 1 - Server-side Rendering, Serverlessly!]]></title>
            <link>https://maxrozen.com/start-your-own-app-with-react-part-1</link>
            <guid>https://maxrozen.com/start-your-own-app-with-react-part-1</guid>
            <pubDate>Wed, 08 Aug 2018 05:52:00 GMT</pubDate>
            <description><![CDATA[Start your own app with React, GraphQL and Serverless architecture: Part 1 - Server-side Rendering, Serverlessly!]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/start-your-own-app-with-react-part-1">clicking here</a>.)</div><p>Note that this series was written in 2018, using an older version of Terraform (version 12), available <a href="https://releases.hashicorp.com/terraform/">here</a>. As it also uses an old version of Node, this series is left online as more of a guideline.</p>
<p>In this blog post series I'll walk you through what you need to do (tech-wise) to have a React app where your customers can login, pay for things and use your internet business.</p>
<p>Pre-work - Know:</p>
<ol>
<li>Basics of JavaScript ES6</li>
<li>AWS (we'll be spinning up resources using terraform)</li>
<li>How to debug when things don't go your way.</li>
</ol>
<p>I'll be describing modifications to make to existing code, rather than providing a fully complete repository, to ensure that this guide is easier to update.</p>
<p>The architecture consists of four main parts:</p>
<ol>
<li>A GraphQL server (A server running in AWS Lambda - Node v8 environment)</li>
<li>A database of some sort (either Key:Value store such as DynamoDB or traditional, like postgres/mysql)</li>
<li>A 'Server-side rendering' server (the React app running in AWS Lambda - Node v8 environment)</li>
<li>The Static resources - sitting in an S3 bucket, sent to your users via a CloudFront CDN (which also conveniently provides you with free SSL certificates!)</li>
</ol>
<p>On the payment side of things, we'll add a GraphQL resolver that talks to Stripe, but that'll be covered in a later blog post.</p>
<p>To begin, we're going to use Razzle. It's like Create-react-app, but for server-side rendering.</p>
<pre><code>npm install -g create-razzle-app

create-razzle-app my-app
cd my-app
npm start
</code></pre>
<p>Now, because we're going to be deploying this onto a serverless environment, we need to change how Razzle builds code. To do that, we're going to create a file in the project root directory called <code>razzle.config.js</code>, with the following content:</p>
<pre><code>module.exports = {
  modify: (config, { target, dev }, webpack) => {

    const appConfig = config

    if (target === 'node' &#x26;&#x26; !dev) {
      appConfig.externals = []
      appConfig.output.publicPath = `${process.env.PUBLIC_PATH}`
    }

    return appConfig
  },
}
</code></pre>
<p>Next, we'll edit the <code>src/index.js</code> file to allow us to run in AWS Lambda when we're in production, and run normally when we're developing locally.
Run <code>npm install aws-serverless-express</code> in the project root, then replace the contents of <code>src/index.js</code> with:</p>
<pre><code>import app from './server'
export let handler

if (process.env.NODE_ENV === 'production') {
  const awsServerlessExpress = require('aws-serverless-express')

  const binaryMimeTypes = [
    'application/octet-stream',
    'font/eot',
    'font/opentype',
    'font/otf',
    'image/jpeg',
    'image/png',
    'image/svg+xml',
  ]
  const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes)

  handler = (event, context, callback) => {
    awsServerlessExpress.proxy(server, event, context)
  }
} else {
  const http = require('http')

  const server = http.createServer(app)

  let currentApp = app

  server.listen(process.env.PORT || 3000, error => {
    if (error) {
      console.log(error)
    }

    console.log('🚀 started')
  })

  if (module.hot) {
    console.log('✅  Server-side HMR Enabled!')

    module.hot.accept('./server', () => {
      console.log('🔁  HMR Reloading `./server`...')
      server.removeListener('request', currentApp)
      const newApp = require('./server').default
      server.on('request', newApp)
      currentApp = newApp
    })
  }
}
</code></pre>
<p>What we've done here is dynamically import <code>aws-serverless-express</code>, as well as the <code>http</code> module. This allows us to run the same file for both production, and developing locally.</p>
<p>Lastly on the React code side of things, we'll need to edit <code>src/app.js</code>, and remove the <code>exact</code> keyword from line 9, such that it reads:</p>
<pre><code>    &#x3C;Route path="/" component={Home} />
</code></pre>
<p>(this is a requirement to have React function in AWS API Gateway correctly)</p>
<p>At this point, we can step back, write some terraform files to build our infrastructure, and eventually bask in the awesome fact that you've set up server-less server-side rendering in React.</p>
<ol>
<li>
<p>Create an infrastructure folder and enter it: <code>mkdir infrastructure &#x26;&#x26; cd infrastructure</code></p>
</li>
<li>
<p>Create a <code>main.tf</code> file, containing:</p>
<pre><code>provider "aws" {
  region = "${var.aws_region}"
}
</code></pre>
</li>
<li>
<p>Create a <code>vars.tf</code> file, containing:</p>
<pre><code>variable "name" { default = "my-app" } #name of your service
variable "aws_region" { default = "ap-southeast-2" } #ch
variable "comment" {default = "my-app resource - generated by maxrozen.com/2018/08/08/start-your-own-internet-business-with-react-part-1"}
variable "bucket_site" {default = "my-app-12345"} # IMPORTANT - set your own app name here!
output "API Gateway URL" {
  value = "${aws_api_gateway_deployment.ssr_deployment.invoke_url}"
}
output "S3_BUCKET" {
  value = "${aws_s3_bucket.site.bucket_domain_name}"
}
</code></pre>
</li>
<li>
<p>Create a <code>lambda.tf</code> file, containing:</p>
<pre><code>resource "aws_iam_role" "lambda_iam" {
  name = "lambda_iam_${var.name}"

  assume_role_policy = &#x3C;&#x3C;EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow"
    }
  ]
}
EOF
}

resource "aws_lambda_function" "lambda" {
  function_name = "lambda_${var.name}"
  role             = "${aws_iam_role.lambda_iam.arn}"
  handler       = "server.handler"
  runtime       = "nodejs8.10"
  timeout       = "300"
  memory_size   = "256" #MB. in production, this can be up to 3GB

  filename = "../build/server.zip"
  source_code_hash = "${base64sha256(file("../build/server.zip"))}"

  tags {
    Project = "${var.name}"
  }
}

resource "aws_lambda_permission" "allow_api_to_exec_lambda" {
  statement_id  = "allow_api_to_exec_lambda"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda.function_name}"
  principal     = "apigateway.amazonaws.com"
  # The /*/*/* part allows invocation from any stage, method and resource path
  # within API Gateway REST API.
  source_arn = "${aws_api_gateway_rest_api.ssr.execution_arn}/*/*/*"

}
</code></pre>
</li>
<li>
<p>Create an <code>api_gateway.tf</code> file, containing:</p>
<pre><code>resource "aws_api_gateway_rest_api" "ssr" {
  name        = "${var.name}_ssr"
  description = "${var.comment}"
}

resource "aws_api_gateway_resource" "proxy" {
  rest_api_id = "${aws_api_gateway_rest_api.ssr.id}"
  parent_id   = "${aws_api_gateway_rest_api.ssr.root_resource_id}"
  path_part   = "{proxy+}"
}

resource "aws_api_gateway_method" "proxy" {
  rest_api_id   = "${aws_api_gateway_rest_api.ssr.id}"
  resource_id   = "${aws_api_gateway_resource.proxy.id}"
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda" {
  rest_api_id = "${aws_api_gateway_rest_api.ssr.id}"
  resource_id = "${aws_api_gateway_method.proxy.resource_id}"
  http_method = "${aws_api_gateway_method.proxy.http_method}"

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "${aws_lambda_function.lambda.invoke_arn}"
}

resource "aws_api_gateway_method" "proxy_root" {
  rest_api_id   = "${aws_api_gateway_rest_api.ssr.id}"
  resource_id   = "${aws_api_gateway_rest_api.ssr.root_resource_id}"
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda_root" {
  rest_api_id = "${aws_api_gateway_rest_api.ssr.id}"
  resource_id = "${aws_api_gateway_method.proxy_root.resource_id}"
  http_method = "${aws_api_gateway_method.proxy_root.http_method}"

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "${aws_lambda_function.lambda.invoke_arn}"
}

resource "aws_api_gateway_deployment" "ssr_deployment" {
  depends_on = [
    "aws_api_gateway_integration.lambda",
    "aws_api_gateway_integration.lambda_root",
  ]

  rest_api_id = "${aws_api_gateway_rest_api.ssr.id}"
  stage_name  = "prod"
}
</code></pre>
</li>
<li>
<p>Finally, create an <code>s3.tf</code> file to store your static content, containing:</p>
<pre><code>resource "aws_s3_bucket" "site" {
    bucket = "${var.bucket_site}"
    acl = "public-read"
    website {
        index_document = "index.html"
        error_document = "404.html"
    }
    tags {
      Project = "${var.name}"
    }
    cors_rule {
      allowed_headers = ["*"]
      allowed_methods = ["GET"]
      allowed_origins = ["https://${aws_api_gateway_deployment.ssr_deployment.rest_api_id}.execute-api.${var.aws_region}.amazonaws.com"]
      expose_headers  = ["ETag"]
      max_age_seconds = 3000
    }
    force_destroy = true
    policy = &#x3C;&#x3C;EOF
{
        "Id": "bucket_policy_site",
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "bucket_policy_site_main",
            "Action": [
              "s3:GetObject"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::${var.bucket_site}/*",
            "Principal": "*"
          }
        ]
      }
EOF
}
</code></pre>
</li>
</ol>
<p>If you've made it this far, I'm genuinely impressed. You now have terraform ready to go and deploy your infrastructure.</p>
<p>We do this by entering the <code>infrastructure/</code> directory, and running <code>terraform apply</code>. Terraform will then list all of the changes it's about to make, and then at the end, will say:</p>
<pre><code>Plan: 11 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
</code></pre>
<p>Type <code>yes</code> if and only if terraform states <code>0 to destroy</code>. I will not be liable for the destruction of your production AWS resources.</p>
<p>Terraform will take a couple of minutes to create all the resources we specified, and eventually will output:</p>
<pre><code>Apply complete! Resources: 11 added, 0 changed, 0 destroyed.

Outputs:

API Gateway URL = https://hfytoyl60m.execute-api.ap-southeast-2.amazonaws.com/prod
S3_BUCKET = my-app-12345.s3.amazonaws.com
</code></pre>
<p>To actually deploy this I manually edited my <code>package.json</code> file, and replaced the <code>"build"</code> key, with:</p>
<pre><code>"build":
      "PUBLIC_PATH=https://my-app-12345.s3-ap-southeast-2.amazonaws.com/public/ razzle build &#x26;&#x26; aws s3 sync \"./build/public/\" s3://my-app-12345/public &#x26;&#x26; aws s3 sync \"./build/static/\" s3://my-app-12345/static &#x26;&#x26; cd ./build &#x26;&#x26; zip -r ./server.zip ./server.*",
</code></pre>
<p>This sets the PUBLIC_PATH (the static files like css/js) to point to the S3 bucket we created. Yours will be different. Take particular notice of the <code>s3-ap-southeast-2</code> portion of the URL, as I doubt you'll also be setting your region to Sydney, Australia.</p>
<p>This concludes Part 1: Server-side Rendering, Serverlessly.</p>
<p>Check https://maxrozen.com for future updates!</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to handle server-side rendering in React]]></title>
            <link>https://maxrozen.com/tips-for-making-server-side-rendering-in-react-easy</link>
            <guid>https://maxrozen.com/tips-for-making-server-side-rendering-in-react-easy</guid>
            <pubDate>Mon, 04 Jun 2018 05:52:00 GMT</pubDate>
            <description><![CDATA[Getting server-side rendering to work in React is quite a hassle if you don't know what you're doing. Let's make it easier.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/tips-for-making-server-side-rendering-in-react-easy">clicking here</a>.)</div><p>The first time I worked through deploying a server-side rendered React app to AWS Lambda, it took me about 5 hours of messing around with my Terraform config, and actual server-side code before I figured out what was wrong (My terraform config was for a outputting JSON from my API Gateway, rather than HTML).</p>
<p>While the easiest way to get started with server-side rendering in React is to start your project with it in mind, I realise for 95% of people this is not an option.</p>
<p>So, you have two options that will save you HOURS figuring out how to setup babel and webpack to play nicely together.</p>
<p>Either:</p>
<p><a href="https://nextjs.org/">Use Next.js</a>.</p>
<p>Next.js solves server-side rendering of React applications with a framework approach. Similar to using Gatsby. If you have a site with content that you want indexed on search engines, and you're fine with learning a new framework, then you probably want to look into Next.js</p>
<p>OR</p>
<p><a href="https://github.com/jaredpalmer/razzle">Use Razzle</a>.</p>
<p>It's basically Create-React-App, for server-side rendered Javascript.</p>
<p>It has an <a href="https://github.com/jaredpalmer/razzle/tree/master/examples">extensive list of examples</a> including: AfterJS, Elm, Firebase Functions, Inferno, JSX, Material-UI, Koa, Preact, React Native Web, Redux, Styled Components and TypeScript.</p>
<p>Once you've seen some Razzle examples, and added Razzle to your project (or just created a fresh Razzle project and copied all of your components into it), you're ready for the next steps.</p>
<p>Usually all you need to do is render some React components, and inject them as Strings into a HTML template. Easier said than done, but if you take baby steps (for example, start with rendering your CSS) you'll find it much more achievable than attempting to refactor your entire Application to be Server-side rendered in one go (See <a href="#indispensable-resources">Indispensible resources</a> below for the resources I used).</p>
<h2>Step 1: Refactor all of your CSS-modules into CSS-in-JS.</h2>
<p>Using either <a href="https://emotion.sh">Emotion</a> or Styled-Components. You should avoid global CSS if you're using Material-UI, though Styled-Components/Emotion are more forgiving if you modify existing components via global CSS.</p>
<h2>Step 2: Refactor your Redux.</h2>
<p>In my case I only needed to call createStore on the server like this:</p>
<pre><code class="language-js">import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';

import reducer from './reducers';

export default (initialState) =>
  createStore(reducer, initialState, applyMiddleware(thunkMiddleware));
</code></pre>
<p>Where the reducer was my client-side reducer code. See <a href="https://medium.freecodecamp.org/demystifying-reacts-server-side-render-de335d408fe4">this link</a> for further info.</p>
<h2>Step 3: Add Apollo/GraphQL</h2>
<p>The trick to making this work in my case was adding <code>ssrForceFetchDelay: 100</code> to my Apollo Client (This prevents Apollo from refetching GraphQL queries after the server already fetches them). Follow the tips at Resource <a href="https://dev-blog.apollodata.com/how-server-side-rendering-works-with-react-apollo-20f31b0c7348">[2]</a> below.</p>
<h2>Step 4: Deploying a bundle without having to copy your <code>node_modules/</code> folder into your AWS Lambda function</h2>
<p>This one was surprisingly easy, and at the time not particularly well documented:
Create a <code>razzle.config.js</code> file with the following contents:</p>
<pre><code class="language-javascript">module.exports = {
  modify: (config, { target, dev }, webpack) => {
    // do something to config
    const appConfig = config; // stay immutable here

    if (target === 'node' &#x26;&#x26; !dev) {
      appConfig.externals = [];
    }

    return appConfig;
  },
};
</code></pre>
<p>Razzle by default will use nodeExternals to prevent webpack from bundling your node_modules - which you need for AWS Lambda. This fixes that issue.</p>
<h2>Indispensable resources:</h2>
<ol>
<li>Getting started:
https://medium.freecodecamp.org/demystifying-reacts-server-side-render-de335d408fe4</li>
<li>Adding Apollo/GraphQL:
https://dev-blog.apollodata.com/how-server-side-rendering-works-with-react-apollo-20f31b0c7348</li>
<li>Adding Styled-Components:
https://www.styled-components.com/docs/advanced#server-side-rendering</li>
<li>Adding Material-UI
https://material-ui.com/guides/server-rendering/</li>
</ol>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to resolve 'X defined in resolvers, but not in schema' with babel-plugin-inline-import]]></title>
            <link>https://maxrozen.com/inline-import-graphql-issues</link>
            <guid>https://maxrozen.com/inline-import-graphql-issues</guid>
            <pubDate>Tue, 12 Dec 2017 18:52:00 GMT</pubDate>
            <description><![CDATA[Showing how to resolve the dreaded 'Query.X defined in resolvers, but not in schema' issue when your X is **definitely** defined in both]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/inline-import-graphql-issues">clicking here</a>.)</div><p>The inspiration for this post comes from this closed issue in
babel-plugin-inline-import
(https://github.com/Quadric/babel-plugin-inline-import/issues/1) a plugin that
my company uses in almost all of our projects.</p>
<p>Essentially, the issue is that you'll run your <code>npm run dev</code> script, decide to
modify your resolvers, or your schema, re-run the script to get your resolvers,
and boom:</p>
<p><code>'Query.X defined in resolvers, but not in schema'</code></p>
<p>The root cause here is that the babel-plugin-inline-import plugin caches your
schema.</p>
<p>The resolution is essentially to have <code>BABEL_DISABLE_CACHE=1</code> in your <code>.env</code>,
and to have two separate webpack files, one for your graphql API, and another
for your client.</p>
<p>In my graphql webpack config I have the following:</p>
<pre><code>module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, '..'),
        exclude: path.resolve(__dirname, '..', 'node_modules'),
        loader: 'babel-loader'
      },
      {
        test: /\.graphql$/,
        include: path.resolve(__dirname, '..'),
        exclude: path.resolve(__dirname, '..', 'node_modules'),
        loader: 'graphql-tag/loader'
      }
    ]
  }
</code></pre>
<p>Which will include my graphql schema inline (which I keep in my project root)</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
        <item>
            <title><![CDATA[How to style react-select with styled-components or emotion]]></title>
            <link>https://maxrozen.com/style-react-select-styled-components-emotion</link>
            <guid>https://maxrozen.com/style-react-select-styled-components-emotion</guid>
            <pubDate>Mon, 11 Dec 2017 18:52:00 GMT</pubDate>
            <description><![CDATA[Styling react-select can be annoying. Here's a quick guide on how to do it with popular CSS-in-JS libraries.]]></description>
            <content:encoded><![CDATA[<div style="margin-bottom=55px; font-style: italic;">(This is an article from MaxRozen.com. You can read the original by <a href="https://maxrozen.com/style-react-select-styled-components-emotion">clicking here</a>.)</div><p>For customisable dropdowns,
<a href="https://github.com/JedWatson/react-select">react-select</a> is one of the most
popular select dropdown pickers in Github.</p>
<p>In terms of CSS-in-JS solutions,
<a href="https://www.styled-components.com/">styled-components</a> and
<a href="https://emotion.sh/">emotion</a> are often the first options developers consider.
They use the same API, which makes it a breeze to swap between the two.</p>
<p>How to use react-select and styled-components together:</p>
<pre><code class="language-js">import React from "react";
import ReactSelect from "react-select";
import styled from 'styled-components';

const MultiSelect = styled(ReactSelect)`
	&#x26;.Select--multi  {

		.Select-value {
			display: inline-flex;
			align-items: center;
		}
	}

	&#x26; .Select-placeholder {
		font-size: smaller;
	}
`

export (props) => &#x3C;MultiSelect multi {...props} />
</code></pre>
<p>How to use react-select and emotion together:</p>
<pre><code class="language-js">import React from "react";
import ReactSelect from "react-select";
import styled from 'emotion';

const MultiSelect = styled(ReactSelect)`
	&#x26;.Select--multi  {

		.Select-value {
			display: inline-flex;
			align-items: center;
		}
	}

	&#x26; .Select-placeholder {
		font-size: smaller;
	}
`

export (props) => &#x3C;MultiSelect multi {...props} />
</code></pre>
<p>It's really that easy to go between the two libraries.</p>
<p>You can also do the exact same thing with
<a href="https://github.com/airbnb/react-dates">react-dates</a> saving you from having
those pesky global.css files several thousand lines long.</p>
]]></content:encoded>
            <author>hey@maxrozen.com (Max Rozen)</author>
        </item>
    </channel>
</rss>