[{"params":{"slug":"2025-01-distribute-open-source-tools-with-homebrew-taps-a-beginners-guide"},"props":{"id":"2025-01-distribute-open-source-tools-with-homebrew-taps-a-beginners-guide.md","data":{"title":"Distribute Open-Source Tools with Homebrew Taps: A Beginner's Guide","showTitle":true,"image":"/images/post_covers/homebrew.jpg","includeLowResHero":false,"prose":true,"date":"2025-01-27T08:00:00.000Z","tags":"development brew","status":"published"},"body":"Often, when I create open-source tools or apps, it's tricky to distribute them easily. You usually\nneed to specify where to download from, and where to place the extracted files, which is a manual\nprocess that doesn't usually translate into a simple script.\n\nThat's where Homebrew comes in. It makes it easy to package your tools in a way that works on both\nmacOS and Linux and installs them for you. Homebrew taps are user-created repositories that simplify\ndistribution. Instead of asking your users to manually download and configure files, you can provide\nthem with a single command to install your tool. This seamless experience can make your app more\napproachable for users and easier for you to manage.\n\nThere are plenty of package managers – but I think Homebrew is one of the simpler ones to\npublish for, believe it or not, which aren't tied to a specific development language or platform\n(such as Node's NPM, or Dart Pub).\n\nIn this post, we'll go over how to get your own tap up and running, without having to rely on the\ncore repository, or having to fulfill the minimum requirements to have your app uploaded to it.\n\n## What are Homebrew taps?\n\nA tap is Homebrew's term for user-made repositories of software, separate from the main Homebrew\nrepository.\n\nWithout having to be verified by Homebrew's team, and without the minimum standards (your software's\npopularity, for example), you can publish your apps on your own repository, and easily point your\nusers to install using 1 or 2 lines of shell code.\n\nFor example, to use my own tap, you can either add it as a repository to Homebrew:\n\n```sh\nbrew tap chenasraf/tap\n```\n\nAnd then install a package:\n\n```sh\nbrew install sofmani\n```\n\nOr, you can directly install from the tap without adding it:\n\n```sh\nbrew install chenasraf/tap/sofmani\n```\n\nSo, how can you make this work for your own apps?\n\n## Step 1: Setting up a tap repository on GitHub\n\nFirst thing's first, you need a GitHub repository to hold your tap. The first requirement is\nproperly naming your repository.\n\nIf you prefix the name of your repository with `homebrew-`, the rest of the name becomes your tap\nname, which will be under your username.\n\nFor example, for my sample above, the repository I created was named `chenasraf/homebrew-tap`, which\nthen translates into `chenasraf/tap` for brew.\n\nI suggest a similar naming scheme. I personally don't think it's a good idea to have more than 1\nmain tap, as publishing and maintaining multiple repositories might prove to be a hassle. Also, if\nall your apps are accessible under the same tap, you can direct your users more easily and have them\ninstall with less possible commands and actions.\n\n## Step 2: Adding GitHub Actions\n\nIf you're not familiar, GitHub Actions are a way to run automatic processes on your repository, in\nthe server, which also allows you to report success or failure and block PRs or have more actions\ndone based on the results. It's often used to automate testing and releasing software on GitHub\nwhenever something is pushed to a repository or if it fills certain criteria.\n\nHomebrew supplies us with a few GitHub actions to help us with the publishing process. These, in\naddition to publishing, run some tests and pre-requisites to make sure everything is set up properly\nand that your tap will be accessible and usable without errors.\n\nThe first action is the brew test bot pipeline. This action will run some tests whenever you push to\nyour repository, making sure test run and files are formatted properly. This action also creates\nBottles for your app for various platforms. We will discuss Bottles in just a bit.\n\nTo add this action, you can paste this into `.github/workflows/test.yml`:\n\n### Test Action\n\n```yml\nname: brew test-bot\n\non:\n push:\n branches:\n - master\n pull_request:\n\njobs:\n test-bot:\n strategy:\n matrix:\n os: [ubuntu-22.04, macos-13, macos-14]\n runs-on: ${{ matrix.os }}\n steps:\n - name: Set up Homebrew\n id: set-up-homebrew\n uses: Homebrew/actions/setup-homebrew@master\n\n - name: Cache Homebrew Bundler RubyGems\n uses: actions/cache@v4\n with:\n path: ${{ steps.set-up-homebrew.outputs.gems-path }}\n key: ${{ matrix.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}\n restore-keys: ${{ matrix.os }}-rubygems-\n\n - run: brew test-bot --only-cleanup-before\n\n - run: brew test-bot --only-setup\n\n - run: brew test-bot --only-tap-syntax\n\n - run: brew test-bot --only-formulae\n if: github.event_name == 'pull_request'\n\n - name: Upload bottles as artifact\n if: always() && github.event_name == 'pull_request'\n uses: actions/upload-artifact@v4\n with:\n name: bottles_${{ matrix.os }}\n path: '*.bottle.*'\n```\n\nA brief overview of the above:\n\n- We run this job on 3 different platforms, Ubuntu 22, and macOS 13 and 14.\n- We set up Homebrew, and caching to quicken the process\n- We run some of brew's test-bot functionality, which includes making some tests and building the\n binaries for each of the matrix platforms\n\nNow every push to your repository will run the tests, and create bottles for your app. These bottles\nwill be re-used when you release the next version of your app.\n\n### Publish Action\n\nThe next action is where the magic happens. Paste the following into\n`.github/workflows/publish.yml`:\n\n```yml\nname: brew pr-pull\n\non:\n pull_request_target:\n types:\n - labeled\n\njobs:\n pr-pull:\n if: contains(github.event.pull_request.labels.*.name, 'pr-pull')\n runs-on: ubuntu-22.04\n permissions:\n contents: write\n pull-requests: write\n steps:\n - name: Set up Homebrew\n uses: Homebrew/actions/setup-homebrew@master\n\n - name: Set up git\n uses: Homebrew/actions/git-user-config@master\n\n - name: Pull bottles\n env:\n HOMEBREW_GITHUB_API_TOKEN: ${{ github.token }}\n PULL_REQUEST: ${{ github.event.pull_request.number }}\n run: brew pr-pull --debug --tap=\"$GITHUB_REPOSITORY\" \"$PULL_REQUEST\"\n\n - name: Push commits\n uses: Homebrew/actions/git-try-push@master\n with:\n token: ${{ github.token }}\n branch: master\n\n - name: Delete branch\n if: github.event.pull_request.head.repo.fork == false\n env:\n BRANCH: ${{ github.event.pull_request.head.ref }}\n run: git push --delete origin \"$BRANCH\"\n```\n\nThis action will create a release for you when your app is ready to be released.\n\nA brief overview of the above:\n\n- We set up homebrew & git\n- We pull the previously-created bottles, so we can use them in this action\n- We tell Homebrew to push new commits containing the downloaded bottles, and previous existing\n commits\n- We then delete the source branch\n\nNow, push your changes to your repository, and then create a new branch. This branch will be used to\ncreate a PR that will be the release PR for your app.\n\n## Step 3: Creating your first Formula\n\nIn Homebrew, \"Formulae\" (plural) or \"Formula\" (singular) are the actual individual app installers. A\nformula tells brew where to fetch your code from, and how to install it into the user's system.\n\nEach of your apps should have a formula. For our example, I will show you how to create a build for\na Go app – but this can be modified as needed for any other type of app.\n\nFor this, you will need a tagged release for your own app, whatever it is written in. For our\nexample, we can reference\n[sofmani v1.4.0](https://github.com/chenasraf/sofmani/releases/tag/v1.4.0).\n\nA few notes:\n\n- A downloadable release is not required – we will be instructing Homebrew to install from the\n source tar file which we will fetch.\n- A tag is not required, any ref can work, such as a branch or commit hash – but it's\n recommended to use a tag to make it easy to follow the correct version.\n\n### Creating a formula file\n\nTo create a new formula file, you can use the `brew create` command. This will create a new formula\ntemplate that you will have to modify.\n\nThe command should be run as follows:\n\n```sh\nbrew create <source tar.gz URL>\n```\n\nTo use our example from before, in our case we will use:\n\n```sh\nbrew create https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz\n```\n\nThis will start a prompt that will ask for some details about your app. Fill them up and you will be\nbrought into your shell's editor to edit the formula file. Your file should be located at\n`/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/<first-letter-of-name>/<app-name>.rb`,\nor a similar directory somewhere else depending on your Homebrew setup.\n\nTake note of your file and move it into your repository. In the state it's currently in, it is meant\nto be pushed to your fork of Homebrew, so that you can create an official PR to add it to the core\nrepository – but for our case, we are only publishing this for our own repository.\n\nYour file should be placed in the `Formula` directory of the repository. To make it easier to sort a\nlot of apps, you can divide your formulae to separate directories, where each directory is the first\nletter of the apps below it.\n\nAs an example, for our case we can use `Formula/s/sofmani.rb` or just `Formula/sofmani.rb`. I chose\nthe latter because I don't think I am close to having enough apps there to require more splitting\nup.\n\n### Updating the file\n\nNow that we have a file in place, it should look like this:\n\n```ruby\n# Documentation: https://docs.brew.sh/Formula-Cookbook\n# https://rubydoc.brew.sh/Formula\n# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!\nclass Sofmani < Formula\n desc \"Installs software from a declerative config on any system \"\n homepage \"https://chenasraf.github.io/sofmani/\"\n url \"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz\"\n sha256 \"0cb4d1f51b23f272fd8f5146e30f937e7cd9ff1275fbcc732a6f27e017c0426a\"\n license \"MIT\"\n\n # depends_on \"cmake\" => :build\n\n def install\n # Remove unrecognized options if they cause configure to fail\n # https://rubydoc.brew.sh/Formula.html#std_configure_args-instance_method\n system \"./configure\", \"--disable-silent-rules\", *std_configure_args\n # system \"cmake\", \"-S\", \".\", \"-B\", \"build\", *std_cmake_args\n end\n\n test do\n # `test do` will create, run in and delete a temporary directory.\n #\n # This test will fail and we won't accept that! For Homebrew/homebrew-core\n # this will need to be a test that verifies the functionality of the\n # software. Run the test with `brew test sofmani`. Options passed\n # to `brew install` such as `--HEAD` also need to be provided to `brew test`.\n #\n # The installed folder is not in the path, so use the entire path to any\n # executables being tested: `system bin/\"program\", \"do\", \"something\"`.\n system \"false\"\n end\nend\n```\n\nNote that yours will differ slightly, but the gist is the same.\n\nThere are 3 main sections we currently want to update.\n\n1. The `install` block\n2. The `test` block\n3. The `depends_on` block\n\n#### Install block\n\nThe `install` block will be used to install the software. This is where you tell Homebrew how to\nbuild the app from source. Since we are demonstrating a go app, we can replace the block with the\nfollowing:\n\n```ruby\ndef install\n system \"go\", \"build\", \"-buildmode\", \"exe\", \"-o\", \"sofmani\", \".\" # Build from source\n bin.install \"sofmani\" # Install the bin into the required directory\nend\n```\n\nIt's pretty straightforward, but just to make sure we understand what's going on:\n\nFirst, we use `system` to run a shell command. This would be the regular build command for your app.\nYou can run several commands here, use conditions, and do whatever else you need to get your app\nproperly built from source. You will notice I used `\".\"` for the current directory as target.\n\nSecond, we use `bin.install` which tells brew to act on the directory `bin`, and installs a binary\ninto the homebrew prefix location that it needs to be in for that directory. In our case we supplied\nit the output binary name, `\"sofmani\"`.\n\n#### Test block\n\nHere we just need to test that the software is installed and works as expected. This isn't the best\npractice but for our example we will make a short call to the app's help and check that the output\ndoesn't crash.\n\n```ruby\ntest do\n assert_match \"Usage: sofmani [options] [config_file]\", shell_output(\"#{bin}/sofmani -h\")\nend\n```\n\nThis adds a simple string match check against the help output.\n\nFore more information about adding tests and more matchers, you can refer here:\n[Formula Cookbook — Add a test to the formula](https://docs.brew.sh/Formula-Cookbook#add-a-test-to-the-formula)\n\n#### Depends on block\n\nWe also need to tell brew that if the user builds from source, they would require `go` to be present\nin the system:\n\n```rb\ndepends_on go: [:build]\n```\n\nThis tells Homebrew to require the `go` system-wide executable, which can be added either by\nHomebrew or in a different method by the user. Limiting it to `:build` makes sure that the\nrequirement is only relevant if building from source, not when downloading from bottle.\n\nFor more information about how the dependencies work (for example, requiring a specific version, or\nother steps or options) you can read more here:\n[Formula Cookbook — Check for dependencies](https://docs.brew.sh/Formula-Cookbook#check-for-dependencies)\n\n## Step 4: Releasing a new version\n\nMake sure to update any other required information, remove all the unnecessary comments from the\ncode and push your changes. Make sure you are using an alternative branch and not your default\nbranch.\n\nFor every new version of app, you should do the following steps:\n\n1. Make your changes\n1. Push to a new branch\n1. Create PR from your branch to the default branch\n\nThis is the process to start a release.\n\nOnce all the tests on your PR pass properly (push new commits with fixes if needed), all you have to\ndo is **add the `pr-pull` tag to your PR**.\n\nThis will instruct the release action (`release.yml`) to start a release process. It relies on\nprevious actions to be completed, as it will also attach bottles to the release if they were\nproperly created.\n\nWhen the action is done, you will notice your PR was closed without merging. This is working as\nexpected, and is a side-effect of the brew bot creating additional commits for you and then pushing\nthem to the target branch directly alongside your original commits, instead of merging from your\nbranch through the PR.\n\n### Bottles\n\nYou might have noticed that in the extra commit that the bot created, it added a bottles section to\nthe Formula file, which contains the pre-built binaries for your app:\n\n```ruby\nbottle do\n root_url \"https://github.com/chenasraf/homebrew-tap/releases/download/sofmani-1.4.0\"\n sha256 cellar: :any_skip_relocation, arm64_sonoma: \"4941252086eb62660b684f1210dbec8d9bfd7a343f2fca43a8cc19dee64bb38b\"\n sha256 cellar: :any_skip_relocation, ventura: \"7b7c96d587d956219f30089af88e416adfef5b039f2f844b241a766d6e8016d1\"\n sha256 cellar: :any_skip_relocation, x86_64_linux: \"39fd3e306f66b7c04a3abe0e8e5dbe2070ef9c72fdd1bdefde2a1807d22082ae\"\nend\n```\n\nBottles are simply pre-built binaries that you can install from the tap directly, without building\nfrom source.\n\nIf a user tries to install your tap on one of the supported platforms, they will have a faster\nprocess by downloading the matching binary that was \"bottled up\" during the release process.\n\nIf they can't find a proper match, or if they choose to `--build-from-source`, then they will build\nthe app from source for their system, including any necessary dependencies.\n\n## Addendum: Updating an existing app\n\nSome time has passed, and we want to update the app to version 1.5.0.\n\nFor this we need to change 2 things:\n\n1. The download URL for the tar file\n1. The SHA256 checksum for the tar file\n\n```diff\n- url \"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz\"\n- sha256 \"0cb4d1f51b23f272fd8f5146e30f937e7cd9ff1275fbcc732a6f27e017c0426a\"\n+ url \"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.5.0.tar.gz\"\n+ sha256 \"7a488daa01d999ff2e1a310ce351e3d0d3add4e18c709625b61e7b67b554b90b\"\n```\n\nTo generate a new checksum, you need to download the tar file, and get the checksum for that:\n\n```sh\ncurl -L https://github.com/chenasraf/sofmani/archive/refs/tags/v1.5.0.tar.gz | shasum -a 256\n```\n\nOnce you made these changes, you are ready to push them to a **new branch**, as before, create the\nPR, and then when the actions are completed successfully you just add the `pr-pull` tag to your PR.\n\nAnd there you go! You have released your app to the world.\n\n## Addendum: Simplifying the process by automating more of it\n\nI like to automate stuff more, so I created 2 scripts to help:\n\n1. Download the latest release, get a checksum, and update the file\n2. Create a PR with the proper branch\n\nHere are the 2 scripts I use:\n\n```bash\n#!/usr/bin/env bash\n\nif [[ -z \"$REPO_NAME\" ]]; then\n read -r -p \"Enter repository path: \" REPO_NAME\nfi\n\nif [[ -z \"$REPO_NAME\" ]]; then\n echo \"Repository path is required\"\n exit 1\nfi\n\nVERSION=$(curl -s \"https://api.github.com/repos/$REPO_NAME/releases/latest\" | jq -r .tag_name)\nURL=\"https://github.com/$REPO_NAME/archive/refs/tags/$VERSION.tar.gz\"\nAPP_NAME=$(basename \"$REPO_NAME\")\n\necho \"Version: $VERSION\"\necho \"URL: $URL\"\n\ncurl -Ls \"$URL\" -o \"$APP_NAME-$VERSION.tar.gz\"\necho -n \"SHA256: \"\nhash=$(sha256sum \"$APP_NAME-$VERSION.tar.gz\" | awk '{print $1}')\necho \"$hash\"\nrm \"$APP_NAME-$VERSION.tar.gz\"\n\nsed -i.bak \"s/sha256 \\\".*\\\"/sha256 \\\"$hash\\\"/\" \"Formula/$APP_NAME.rb\"\nrm \"Formula/$APP_NAME.rb.bak\"\n\nsed -i.bak \"s| url \\\".*\\\"| url \\\"$URL\\\"|\" \"Formula/$APP_NAME.rb\"\nrm \"Formula/$APP_NAME.rb.bak\"\n```\n\nI can either run this directly to get the interactive version or add the `REPO_NAME` variable ahead\nof time to pre-populate it and make it completely automatic.\n\n```sh\nREPO_NAME=chenasraf/sofmani ./scripts/update-hash.sh\n```\n\nAnd here is the second step of the process, which is to create the PR properly:\n\n```sh\n#!/usr/bin/env bash\n\nif [[ -z \"$REPO_NAME\" ]]; then\n read -r -p \"Enter repository path: \" REPO_NAME\nfi\n\nif [[ -z \"$REPO_NAME\" ]]; then\n echo \"Repository path is required\"\n exit 1\nfi\n\nVERSION=$(curl -s \"https://api.github.com/repos/$REPO_NAME/releases/latest\" | jq -r .tag_name)\nAPP_NAME=$(basename \"$REPO_NAME\")\n\necho \"Version: $VERSION\"\nBRANCH=\"feature/$APP_NAME-$VERSION\"\n\nif ! git switch -C \"$BRANCH\"; then\n echo \"Branch already exists, aborting\"\n exit 1\nfi\ngit add \"Formula/$APP_NAME.rb\"\ngit commit -m \"feat: update $APP_NAME to $VERSION\"\ngit push --set-upstream origin \"$BRANCH\"\n\nif gh pr create --fill; then\n open -u \"$(gh pr list --json url | jq -r '.[0].url')\"\n git switch master\nelse\n echo \"Couldn't create PR, aborting\"\n exit 1\nfi\n```\n\nAgain, I can use the variable to make life a bit easier:\n\n```sh\nREPO_NAME=chenasraf/sofmani ./scripts/create-pr.sh\n```\n\nWe can combine the 2 and automatically get the latest release pushed for any app:\n\n```sh\nREPO_NAME=chenasraf/sofmani ./scripts/update-hash.sh && ./scripts/create-pr.sh\n```\n\nHow's that for making it simple? With one line (which you can wrap in a Makefile or any similar\nchoice if you want), you can update the tap and create a PR in one step.\n\n## Conclusion\n\nWe created a tap, pushed a new app to it, and also pushed an update.\n\nNow that you have an app on your tap, you can simply instruct your users to use it instead of\ndownloading the release manually or figuring out what to do with the files.\n\nIt's also pretty widely used on macOS and so it's almost guaranteed to simplify things for a lot of\nyour potential users, or for yourself!\n\n**Share your taps with us in the comments, or if you have more tips, or questions feel free to reply\nbelow to let everyone know!**\n\n## Further Reading\n\nIt's best to learn directly from the source, so here are some resources to answer more questions,\nfor example, how to properly manage the dependencies for other types of apps, or how to make more\ncomplex install steps.\n\n- [Taps (Third-Party Repositories) — Homebrew Documentation](https://docs.brew.sh/Taps)\n- [Formula Cookbook — Homebrew Documentation](https://docs.brew.sh/Formula-Cookbook)\n- [Adding Software to Homebrew — Homebrew Documentation](https://docs.brew.sh/Adding-Software-to-Homebrew)","filePath":"src/content/post/2025-01-distribute-open-source-tools-with-homebrew-taps-a-beginners-guide.md","digest":"4086c978b01291e2","rendered":{"html":"<p>Often, when I create open-source tools or apps, it’s tricky to distribute them easily. You usually\nneed to specify where to download from, and where to place the extracted files, which is a manual\nprocess that doesn’t usually translate into a simple script.</p>\n<p>That’s where Homebrew comes in. It makes it easy to package your tools in a way that works on both\nmacOS and Linux and installs them for you. Homebrew taps are user-created repositories that simplify\ndistribution. Instead of asking your users to manually download and configure files, you can provide\nthem with a single command to install your tool. This seamless experience can make your app more\napproachable for users and easier for you to manage.</p>\n<p>There are plenty of package managers – but I think Homebrew is one of the simpler ones to\npublish for, believe it or not, which aren’t tied to a specific development language or platform\n(such as Node’s NPM, or Dart Pub).</p>\n<p>In this post, we’ll go over how to get your own tap up and running, without having to rely on the\ncore repository, or having to fulfill the minimum requirements to have your app uploaded to it.</p>\n<h2 id=\"what-are-homebrew-taps\">What are Homebrew taps?</h2>\n<p>A tap is Homebrew’s term for user-made repositories of software, separate from the main Homebrew\nrepository.</p>\n<p>Without having to be verified by Homebrew’s team, and without the minimum standards (your software’s\npopularity, for example), you can publish your apps on your own repository, and easily point your\nusers to install using 1 or 2 lines of shell code.</p>\n<p>For example, to use my own tap, you can either add it as a repository to Homebrew:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">brew</span><span style=\"color:#9ECBFF\"> tap</span><span style=\"color:#9ECBFF\"> chenasraf/tap</span></span></code></pre>\n<p>And then install a package:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">brew</span><span style=\"color:#9ECBFF\"> install</span><span style=\"color:#9ECBFF\"> sofmani</span></span></code></pre>\n<p>Or, you can directly install from the tap without adding it:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">brew</span><span style=\"color:#9ECBFF\"> install</span><span style=\"color:#9ECBFF\"> chenasraf/tap/sofmani</span></span></code></pre>\n<p>So, how can you make this work for your own apps?</p>\n<h2 id=\"step-1-setting-up-a-tap-repository-on-github\">Step 1: Setting up a tap repository on GitHub</h2>\n<p>First thing’s first, you need a GitHub repository to hold your tap. The first requirement is\nproperly naming your repository.</p>\n<p>If you prefix the name of your repository with <code>homebrew-</code>, the rest of the name becomes your tap\nname, which will be under your username.</p>\n<p>For example, for my sample above, the repository I created was named <code>chenasraf/homebrew-tap</code>, which\nthen translates into <code>chenasraf/tap</code> for brew.</p>\n<p>I suggest a similar naming scheme. I personally don’t think it’s a good idea to have more than 1\nmain tap, as publishing and maintaining multiple repositories might prove to be a hassle. Also, if\nall your apps are accessible under the same tap, you can direct your users more easily and have them\ninstall with less possible commands and actions.</p>\n<h2 id=\"step-2-adding-github-actions\">Step 2: Adding GitHub Actions</h2>\n<p>If you’re not familiar, GitHub Actions are a way to run automatic processes on your repository, in\nthe server, which also allows you to report success or failure and block PRs or have more actions\ndone based on the results. It’s often used to automate testing and releasing software on GitHub\nwhenever something is pushed to a repository or if it fills certain criteria.</p>\n<p>Homebrew supplies us with a few GitHub actions to help us with the publishing process. These, in\naddition to publishing, run some tests and pre-requisites to make sure everything is set up properly\nand that your tap will be accessible and usable without errors.</p>\n<p>The first action is the brew test bot pipeline. This action will run some tests whenever you push to\nyour repository, making sure test run and files are formatted properly. This action also creates\nBottles for your app for various platforms. We will discuss Bottles in just a bit.</p>\n<p>To add this action, you can paste this into <code>.github/workflows/test.yml</code>:</p>\n<h3 id=\"test-action\">Test Action</h3>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yml\"><code><span class=\"line\"><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew test-bot</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">on</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> push</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> branches</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#9ECBFF\">master</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> pull_request</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#85E89D\">jobs</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> test-bot</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> strategy</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> matrix</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> os</span><span style=\"color:#E1E4E8\">: [</span><span style=\"color:#9ECBFF\">ubuntu-22.04</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">macos-13</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">macos-14</span><span style=\"color:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> runs-on</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ matrix.os }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> steps</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Set up Homebrew</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> id</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">set-up-homebrew</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Homebrew/actions/setup-homebrew@master</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Cache Homebrew Bundler RubyGems</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">actions/cache@v4</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> with</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> path</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ steps.set-up-homebrew.outputs.gems-path }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> key</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ matrix.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> restore-keys</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ matrix.os }}-rubygems-</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew test-bot --only-cleanup-before</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew test-bot --only-setup</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew test-bot --only-tap-syntax</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew test-bot --only-formulae</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> if</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">github.event_name == 'pull_request'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Upload bottles as artifact</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> if</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">always() && github.event_name == 'pull_request'</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">actions/upload-artifact@v4</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> with</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">bottles_${{ matrix.os }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> path</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'*.bottle.*'</span></span></code></pre>\n<p>A brief overview of the above:</p>\n<ul>\n<li>We run this job on 3 different platforms, Ubuntu 22, and macOS 13 and 14.</li>\n<li>We set up Homebrew, and caching to quicken the process</li>\n<li>We run some of brew’s test-bot functionality, which includes making some tests and building the\nbinaries for each of the matrix platforms</li>\n</ul>\n<p>Now every push to your repository will run the tests, and create bottles for your app. These bottles\nwill be re-used when you release the next version of your app.</p>\n<h3 id=\"publish-action\">Publish Action</h3>\n<p>The next action is where the magic happens. Paste the following into\n<code>.github/workflows/publish.yml</code>:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yml\"><code><span class=\"line\"><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew pr-pull</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">on</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> pull_request_target</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> types</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#9ECBFF\">labeled</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#85E89D\">jobs</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> pr-pull</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> if</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">contains(github.event.pull_request.labels.*.name, 'pr-pull')</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> runs-on</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">ubuntu-22.04</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> permissions</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> contents</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">write</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> pull-requests</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">write</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> steps</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Set up Homebrew</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Homebrew/actions/setup-homebrew@master</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Set up git</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Homebrew/actions/git-user-config@master</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Pull bottles</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> env</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> HOMEBREW_GITHUB_API_TOKEN</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ github.token }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> PULL_REQUEST</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ github.event.pull_request.number }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">brew pr-pull --debug --tap=\"$GITHUB_REPOSITORY\" \"$PULL_REQUEST\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Push commits</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> uses</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Homebrew/actions/git-try-push@master</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> with</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> token</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ github.token }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> branch</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">master</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Delete branch</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> if</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">github.event.pull_request.head.repo.fork == false</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> env</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> BRANCH</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">${{ github.event.pull_request.head.ref }}</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> run</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">git push --delete origin \"$BRANCH\"</span></span></code></pre>\n<p>This action will create a release for you when your app is ready to be released.</p>\n<p>A brief overview of the above:</p>\n<ul>\n<li>We set up homebrew & git</li>\n<li>We pull the previously-created bottles, so we can use them in this action</li>\n<li>We tell Homebrew to push new commits containing the downloaded bottles, and previous existing\ncommits</li>\n<li>We then delete the source branch</li>\n</ul>\n<p>Now, push your changes to your repository, and then create a new branch. This branch will be used to\ncreate a PR that will be the release PR for your app.</p>\n<h2 id=\"step-3-creating-your-first-formula\">Step 3: Creating your first Formula</h2>\n<p>In Homebrew, “Formulae” (plural) or “Formula” (singular) are the actual individual app installers. A\nformula tells brew where to fetch your code from, and how to install it into the user’s system.</p>\n<p>Each of your apps should have a formula. For our example, I will show you how to create a build for\na Go app – but this can be modified as needed for any other type of app.</p>\n<p>For this, you will need a tagged release for your own app, whatever it is written in. For our\nexample, we can reference\n<a href=\"https://github.com/chenasraf/sofmani/releases/tag/v1.4.0\">sofmani v1.4.0</a>.</p>\n<p>A few notes:</p>\n<ul>\n<li>A downloadable release is not required – we will be instructing Homebrew to install from the\nsource tar file which we will fetch.</li>\n<li>A tag is not required, any ref can work, such as a branch or commit hash – but it’s\nrecommended to use a tag to make it easy to follow the correct version.</li>\n</ul>\n<h3 id=\"creating-a-formula-file\">Creating a formula file</h3>\n<p>To create a new formula file, you can use the <code>brew create</code> command. This will create a new formula\ntemplate that you will have to modify.</p>\n<p>The command should be run as follows:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">brew</span><span style=\"color:#9ECBFF\"> create</span><span style=\"color:#F97583\"> <</span><span style=\"color:#9ECBFF\">source</span><span style=\"color:#9ECBFF\"> tar.gz</span><span style=\"color:#9ECBFF\"> UR</span><span style=\"color:#E1E4E8\">L</span><span style=\"color:#F97583\">></span></span></code></pre>\n<p>To use our example from before, in our case we will use:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">brew</span><span style=\"color:#9ECBFF\"> create</span><span style=\"color:#9ECBFF\"> https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz</span></span></code></pre>\n<p>This will start a prompt that will ask for some details about your app. Fill them up and you will be\nbrought into your shell’s editor to edit the formula file. Your file should be located at\n<code>/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/<first-letter-of-name>/<app-name>.rb</code>,\nor a similar directory somewhere else depending on your Homebrew setup.</p>\n<p>Take note of your file and move it into your repository. In the state it’s currently in, it is meant\nto be pushed to your fork of Homebrew, so that you can create an official PR to add it to the core\nrepository – but for our case, we are only publishing this for our own repository.</p>\n<p>Your file should be placed in the <code>Formula</code> directory of the repository. To make it easier to sort a\nlot of apps, you can divide your formulae to separate directories, where each directory is the first\nletter of the apps below it.</p>\n<p>As an example, for our case we can use <code>Formula/s/sofmani.rb</code> or just <code>Formula/sofmani.rb</code>. I chose\nthe latter because I don’t think I am close to having enough apps there to require more splitting\nup.</p>\n<h3 id=\"updating-the-file\">Updating the file</h3>\n<p>Now that we have a file in place, it should look like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ruby\"><code><span class=\"line\"><span style=\"color:#6A737D\"># Documentation: https://docs.brew.sh/Formula-Cookbook</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"># https://rubydoc.brew.sh/Formula</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"># PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#B392F0\"> Sofmani</span><span style=\"color:#E1E4E8\"> < </span><span style=\"color:#B392F0\">Formula</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> desc </span><span style=\"color:#9ECBFF\">\"Installs software from a declerative config on any system \"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> homepage </span><span style=\"color:#9ECBFF\">\"https://chenasraf.github.io/sofmani/\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url </span><span style=\"color:#9ECBFF\">\"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> sha256 </span><span style=\"color:#9ECBFF\">\"0cb4d1f51b23f272fd8f5146e30f937e7cd9ff1275fbcc732a6f27e017c0426a\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> license </span><span style=\"color:#9ECBFF\">\"MIT\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # depends_on \"cmake\" => :build</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> def</span><span style=\"color:#B392F0\"> install</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # Remove unrecognized options if they cause configure to fail</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # https://rubydoc.brew.sh/Formula.html#std_configure_args-instance_method</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> system</span><span style=\"color:#9ECBFF\"> \"./configure\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"--disable-silent-rules\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#F97583\">*</span><span style=\"color:#E1E4E8\">std_configure_args</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # system \"cmake\", \"-S\", \".\", \"-B\", \"build\", *std_cmake_args</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> end</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> test</span><span style=\"color:#F97583\"> do</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # `test do` will create, run in and delete a temporary directory.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> #</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # This test will fail and we won't accept that! For Homebrew/homebrew-core</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # this will need to be a test that verifies the functionality of the</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # software. Run the test with `brew test sofmani`. Options passed</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # to `brew install` such as `--HEAD` also need to be provided to `brew test`.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> #</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # The installed folder is not in the path, so use the entire path to any</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> # executables being tested: `system bin/\"program\", \"do\", \"something\"`.</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> system</span><span style=\"color:#9ECBFF\"> \"false\"</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> end</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">end</span></span></code></pre>\n<p>Note that yours will differ slightly, but the gist is the same.</p>\n<p>There are 3 main sections we currently want to update.</p>\n<ol>\n<li>The <code>install</code> block</li>\n<li>The <code>test</code> block</li>\n<li>The <code>depends_on</code> block</li>\n</ol>\n<h4 id=\"install-block\">Install block</h4>\n<p>The <code>install</code> block will be used to install the software. This is where you tell Homebrew how to\nbuild the app from source. Since we are demonstrating a go app, we can replace the block with the\nfollowing:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ruby\"><code><span class=\"line\"><span style=\"color:#F97583\">def</span><span style=\"color:#B392F0\"> install</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> system</span><span style=\"color:#9ECBFF\"> \"go\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"build\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"-buildmode\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"exe\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"-o\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\"sofmani\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">\".\"</span><span style=\"color:#6A737D\"> # Build from source</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> bin.</span><span style=\"color:#B392F0\">install</span><span style=\"color:#9ECBFF\"> \"sofmani\"</span><span style=\"color:#6A737D\"> # Install the bin into the required directory</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">end</span></span></code></pre>\n<p>It’s pretty straightforward, but just to make sure we understand what’s going on:</p>\n<p>First, we use <code>system</code> to run a shell command. This would be the regular build command for your app.\nYou can run several commands here, use conditions, and do whatever else you need to get your app\nproperly built from source. You will notice I used <code>\".\"</code> for the current directory as target.</p>\n<p>Second, we use <code>bin.install</code> which tells brew to act on the directory <code>bin</code>, and installs a binary\ninto the homebrew prefix location that it needs to be in for that directory. In our case we supplied\nit the output binary name, <code>\"sofmani\"</code>.</p>\n<h4 id=\"test-block\">Test block</h4>\n<p>Here we just need to test that the software is installed and works as expected. This isn’t the best\npractice but for our example we will make a short call to the app’s help and check that the output\ndoesn’t crash.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ruby\"><code><span class=\"line\"><span style=\"color:#79B8FF\">test</span><span style=\"color:#F97583\"> do</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> assert_match </span><span style=\"color:#9ECBFF\">\"Usage: sofmani [options] [config_file]\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">shell_output</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#9ECBFF\">#{bin}</span><span style=\"color:#9ECBFF\">/sofmani -h\"</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">end</span></span></code></pre>\n<p>This adds a simple string match check against the help output.</p>\n<p>Fore more information about adding tests and more matchers, you can refer here:\n<a href=\"https://docs.brew.sh/Formula-Cookbook#add-a-test-to-the-formula\">Formula Cookbook — Add a test to the formula</a></p>\n<h4 id=\"depends-on-block\">Depends on block</h4>\n<p>We also need to tell brew that if the user builds from source, they would require <code>go</code> to be present\nin the system:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"rb\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">depends_on </span><span style=\"color:#79B8FF\">go:</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">:build</span><span style=\"color:#E1E4E8\">]</span></span></code></pre>\n<p>This tells Homebrew to require the <code>go</code> system-wide executable, which can be added either by\nHomebrew or in a different method by the user. Limiting it to <code>:build</code> makes sure that the\nrequirement is only relevant if building from source, not when downloading from bottle.</p>\n<p>For more information about how the dependencies work (for example, requiring a specific version, or\nother steps or options) you can read more here:\n<a href=\"https://docs.brew.sh/Formula-Cookbook#check-for-dependencies\">Formula Cookbook — Check for dependencies</a></p>\n<h2 id=\"step-4-releasing-a-new-version\">Step 4: Releasing a new version</h2>\n<p>Make sure to update any other required information, remove all the unnecessary comments from the\ncode and push your changes. Make sure you are using an alternative branch and not your default\nbranch.</p>\n<p>For every new version of app, you should do the following steps:</p>\n<ol>\n<li>Make your changes</li>\n<li>Push to a new branch</li>\n<li>Create PR from your branch to the default branch</li>\n</ol>\n<p>This is the process to start a release.</p>\n<p>Once all the tests on your PR pass properly (push new commits with fixes if needed), all you have to\ndo is <strong>add the <code>pr-pull</code> tag to your PR</strong>.</p>\n<p>This will instruct the release action (<code>release.yml</code>) to start a release process. It relies on\nprevious actions to be completed, as it will also attach bottles to the release if they were\nproperly created.</p>\n<p>When the action is done, you will notice your PR was closed without merging. This is working as\nexpected, and is a side-effect of the brew bot creating additional commits for you and then pushing\nthem to the target branch directly alongside your original commits, instead of merging from your\nbranch through the PR.</p>\n<h3 id=\"bottles\">Bottles</h3>\n<p>You might have noticed that in the extra commit that the bot created, it added a bottles section to\nthe Formula file, which contains the pre-built binaries for your app:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ruby\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">bottle </span><span style=\"color:#F97583\">do</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> root_url </span><span style=\"color:#9ECBFF\">\"https://github.com/chenasraf/homebrew-tap/releases/download/sofmani-1.4.0\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> sha256 </span><span style=\"color:#79B8FF\">cellar:</span><span style=\"color:#79B8FF\"> :any_skip_relocation</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">arm64_sonoma:</span><span style=\"color:#9ECBFF\"> \"4941252086eb62660b684f1210dbec8d9bfd7a343f2fca43a8cc19dee64bb38b\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> sha256 </span><span style=\"color:#79B8FF\">cellar:</span><span style=\"color:#79B8FF\"> :any_skip_relocation</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">ventura:</span><span style=\"color:#9ECBFF\"> \"7b7c96d587d956219f30089af88e416adfef5b039f2f844b241a766d6e8016d1\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> sha256 </span><span style=\"color:#79B8FF\">cellar:</span><span style=\"color:#79B8FF\"> :any_skip_relocation</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">x86_64_linux:</span><span style=\"color:#9ECBFF\"> \"39fd3e306f66b7c04a3abe0e8e5dbe2070ef9c72fdd1bdefde2a1807d22082ae\"</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">end</span></span></code></pre>\n<p>Bottles are simply pre-built binaries that you can install from the tap directly, without building\nfrom source.</p>\n<p>If a user tries to install your tap on one of the supported platforms, they will have a faster\nprocess by downloading the matching binary that was “bottled up” during the release process.</p>\n<p>If they can’t find a proper match, or if they choose to <code>--build-from-source</code>, then they will build\nthe app from source for their system, including any necessary dependencies.</p>\n<h2 id=\"addendum-updating-an-existing-app\">Addendum: Updating an existing app</h2>\n<p>Some time has passed, and we want to update the app to version 1.5.0.</p>\n<p>For this we need to change 2 things:</p>\n<ol>\n<li>The download URL for the tar file</li>\n<li>The SHA256 checksum for the tar file</li>\n</ol>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"diff\"><code><span class=\"line\"><span style=\"color:#FDAEB7\"><span style=\"user-select: none;\">-</span> url \"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.4.0.tar.gz\"</span></span>\n<span class=\"line\"><span style=\"color:#FDAEB7\"><span style=\"user-select: none;\">-</span> sha256 \"0cb4d1f51b23f272fd8f5146e30f937e7cd9ff1275fbcc732a6f27e017c0426a\"</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> url \"https://github.com/chenasraf/sofmani/archive/refs/tags/v1.5.0.tar.gz\"</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> sha256 \"7a488daa01d999ff2e1a310ce351e3d0d3add4e18c709625b61e7b67b554b90b\"</span></span></code></pre>\n<p>To generate a new checksum, you need to download the tar file, and get the checksum for that:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">curl</span><span style=\"color:#79B8FF\"> -L</span><span style=\"color:#9ECBFF\"> https://github.com/chenasraf/sofmani/archive/refs/tags/v1.5.0.tar.gz</span><span style=\"color:#F97583\"> |</span><span style=\"color:#B392F0\"> shasum</span><span style=\"color:#79B8FF\"> -a</span><span style=\"color:#79B8FF\"> 256</span></span></code></pre>\n<p>Once you made these changes, you are ready to push them to a <strong>new branch</strong>, as before, create the\nPR, and then when the actions are completed successfully you just add the <code>pr-pull</code> tag to your PR.</p>\n<p>And there you go! You have released your app to the world.</p>\n<h2 id=\"addendum-simplifying-the-process-by-automating-more-of-it\">Addendum: Simplifying the process by automating more of it</h2>\n<p>I like to automate stuff more, so I created 2 scripts to help:</p>\n<ol>\n<li>Download the latest release, get a checksum, and update the file</li>\n<li>Create a PR with the proper branch</li>\n</ol>\n<p>Here are the 2 scripts I use:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"bash\"><code><span class=\"line\"><span style=\"color:#6A737D\">#!/usr/bin/env bash</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#E1E4E8\"> [[ </span><span style=\"color:#F97583\">-z</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\"> ]]; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> read</span><span style=\"color:#79B8FF\"> -r</span><span style=\"color:#79B8FF\"> -p</span><span style=\"color:#9ECBFF\"> \"Enter repository path: \"</span><span style=\"color:#9ECBFF\"> REPO_NAME</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#E1E4E8\"> [[ </span><span style=\"color:#F97583\">-z</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\"> ]]; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> echo</span><span style=\"color:#9ECBFF\"> \"Repository path is required\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> exit</span><span style=\"color:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">VERSION</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">$(</span><span style=\"color:#B392F0\">curl</span><span style=\"color:#79B8FF\"> -s</span><span style=\"color:#9ECBFF\"> \"https://api.github.com/repos/</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">/releases/latest\"</span><span style=\"color:#F97583\"> |</span><span style=\"color:#B392F0\"> jq</span><span style=\"color:#79B8FF\"> -r</span><span style=\"color:#9ECBFF\"> .tag_name</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">URL</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">\"https://github.com/</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">/archive/refs/tags/</span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">.tar.gz\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">APP_NAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">$(</span><span style=\"color:#B392F0\">basename</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">echo</span><span style=\"color:#9ECBFF\"> \"Version: </span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">echo</span><span style=\"color:#9ECBFF\"> \"URL: </span><span style=\"color:#E1E4E8\">$URL</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\">curl</span><span style=\"color:#79B8FF\"> -Ls</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$URL</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#79B8FF\"> -o</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">-</span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">.tar.gz\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">echo</span><span style=\"color:#79B8FF\"> -n</span><span style=\"color:#9ECBFF\"> \"SHA256: \"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">hash</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">$(</span><span style=\"color:#B392F0\">sha256sum</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">-</span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">.tar.gz\"</span><span style=\"color:#F97583\"> |</span><span style=\"color:#B392F0\"> awk</span><span style=\"color:#9ECBFF\"> '{print $1}'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">echo</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$hash</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">rm</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">-</span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">.tar.gz\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\">sed</span><span style=\"color:#79B8FF\"> -i.bak</span><span style=\"color:#9ECBFF\"> \"s/sha256 </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">.*</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">/sha256 </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#E1E4E8\">$hash</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">/\"</span><span style=\"color:#9ECBFF\"> \"Formula/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">.rb\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">rm</span><span style=\"color:#9ECBFF\"> \"Formula/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">.rb.bak\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\">sed</span><span style=\"color:#79B8FF\"> -i.bak</span><span style=\"color:#9ECBFF\"> \"s| url </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">.*</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">| url </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#E1E4E8\">$URL</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">|\"</span><span style=\"color:#9ECBFF\"> \"Formula/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">.rb\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">rm</span><span style=\"color:#9ECBFF\"> \"Formula/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">.rb.bak\"</span></span></code></pre>\n<p>I can either run this directly to get the interactive version or add the <code>REPO_NAME</code> variable ahead\nof time to pre-populate it and make it completely automatic.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">REPO_NAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">chenasraf/sofmani</span><span style=\"color:#B392F0\"> ./scripts/update-hash.sh</span></span></code></pre>\n<p>And here is the second step of the process, which is to create the PR properly:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#6A737D\">#!/usr/bin/env bash</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#E1E4E8\"> [[ </span><span style=\"color:#F97583\">-z</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\"> ]]; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> read</span><span style=\"color:#79B8FF\"> -r</span><span style=\"color:#79B8FF\"> -p</span><span style=\"color:#9ECBFF\"> \"Enter repository path: \"</span><span style=\"color:#9ECBFF\"> REPO_NAME</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#E1E4E8\"> [[ </span><span style=\"color:#F97583\">-z</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\"> ]]; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> echo</span><span style=\"color:#9ECBFF\"> \"Repository path is required\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> exit</span><span style=\"color:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">VERSION</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">$(</span><span style=\"color:#B392F0\">curl</span><span style=\"color:#79B8FF\"> -s</span><span style=\"color:#9ECBFF\"> \"https://api.github.com/repos/</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">/releases/latest\"</span><span style=\"color:#F97583\"> |</span><span style=\"color:#B392F0\"> jq</span><span style=\"color:#79B8FF\"> -r</span><span style=\"color:#9ECBFF\"> .tag_name</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">APP_NAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">$(</span><span style=\"color:#B392F0\">basename</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$REPO_NAME</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">echo</span><span style=\"color:#9ECBFF\"> \"Version: </span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">BRANCH</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">\"feature/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">-</span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#F97583\"> !</span><span style=\"color:#B392F0\"> git</span><span style=\"color:#9ECBFF\"> switch</span><span style=\"color:#79B8FF\"> -C</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$BRANCH</span><span style=\"color:#9ECBFF\">\"</span><span style=\"color:#E1E4E8\">; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> echo</span><span style=\"color:#9ECBFF\"> \"Branch already exists, aborting\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> exit</span><span style=\"color:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">git</span><span style=\"color:#9ECBFF\"> add</span><span style=\"color:#9ECBFF\"> \"Formula/</span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\">.rb\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">git</span><span style=\"color:#9ECBFF\"> commit</span><span style=\"color:#79B8FF\"> -m</span><span style=\"color:#9ECBFF\"> \"feat: update </span><span style=\"color:#E1E4E8\">$APP_NAME</span><span style=\"color:#9ECBFF\"> to </span><span style=\"color:#E1E4E8\">$VERSION</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">git</span><span style=\"color:#9ECBFF\"> push</span><span style=\"color:#79B8FF\"> --set-upstream</span><span style=\"color:#9ECBFF\"> origin</span><span style=\"color:#9ECBFF\"> \"</span><span style=\"color:#E1E4E8\">$BRANCH</span><span style=\"color:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#B392F0\"> gh</span><span style=\"color:#9ECBFF\"> pr</span><span style=\"color:#9ECBFF\"> create</span><span style=\"color:#79B8FF\"> --fill</span><span style=\"color:#E1E4E8\">; </span><span style=\"color:#F97583\">then</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> open</span><span style=\"color:#79B8FF\"> -u</span><span style=\"color:#9ECBFF\"> \"$(</span><span style=\"color:#B392F0\">gh</span><span style=\"color:#9ECBFF\"> pr list </span><span style=\"color:#79B8FF\">--json</span><span style=\"color:#9ECBFF\"> url </span><span style=\"color:#F97583\">|</span><span style=\"color:#B392F0\"> jq</span><span style=\"color:#79B8FF\"> -r</span><span style=\"color:#9ECBFF\"> '.[0].url')\"</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> git</span><span style=\"color:#9ECBFF\"> switch</span><span style=\"color:#9ECBFF\"> master</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">else</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> echo</span><span style=\"color:#9ECBFF\"> \"Couldn't create PR, aborting\"</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> exit</span><span style=\"color:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">fi</span></span></code></pre>\n<p>Again, I can use the variable to make life a bit easier:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">REPO_NAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">chenasraf/sofmani</span><span style=\"color:#B392F0\"> ./scripts/create-pr.sh</span></span></code></pre>\n<p>We can combine the 2 and automatically get the latest release pushed for any app:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">REPO_NAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">chenasraf/sofmani</span><span style=\"color:#B392F0\"> ./scripts/update-hash.sh</span><span style=\"color:#E1E4E8\"> && </span><span style=\"color:#B392F0\">./scripts/create-pr.sh</span></span></code></pre>\n<p>How’s that for making it simple? With one line (which you can wrap in a Makefile or any similar\nchoice if you want), you can update the tap and create a PR in one step.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>We created a tap, pushed a new app to it, and also pushed an update.</p>\n<p>Now that you have an app on your tap, you can simply instruct your users to use it instead of\ndownloading the release manually or figuring out what to do with the files.</p>\n<p>It’s also pretty widely used on macOS and so it’s almost guaranteed to simplify things for a lot of\nyour potential users, or for yourself!</p>\n<p><strong>Share your taps with us in the comments, or if you have more tips, or questions feel free to reply\nbelow to let everyone know!</strong></p>\n<h2 id=\"further-reading\">Further Reading</h2>\n<p>It’s best to learn directly from the source, so here are some resources to answer more questions,\nfor example, how to properly manage the dependencies for other types of apps, or how to make more\ncomplex install steps.</p>\n<ul>\n<li><a href=\"https://docs.brew.sh/Taps\">Taps (Third-Party Repositories) — Homebrew Documentation</a></li>\n<li><a href=\"https://docs.brew.sh/Formula-Cookbook\">Formula Cookbook — Homebrew Documentation</a></li>\n<li><a href=\"https://docs.brew.sh/Adding-Software-to-Homebrew\">Adding Software to Homebrew — Homebrew Documentation</a></li>\n</ul>","metadata":{"headings":[{"depth":2,"slug":"what-are-homebrew-taps","text":"What are Homebrew taps?"},{"depth":2,"slug":"step-1-setting-up-a-tap-repository-on-github","text":"Step 1: Setting up a tap repository on GitHub"},{"depth":2,"slug":"step-2-adding-github-actions","text":"Step 2: Adding GitHub Actions"},{"depth":3,"slug":"test-action","text":"Test Action"},{"depth":3,"slug":"publish-action","text":"Publish Action"},{"depth":2,"slug":"step-3-creating-your-first-formula","text":"Step 3: Creating your first Formula"},{"depth":3,"slug":"creating-a-formula-file","text":"Creating a formula file"},{"depth":3,"slug":"updating-the-file","text":"Updating the file"},{"depth":4,"slug":"install-block","text":"Install block"},{"depth":4,"slug":"test-block","text":"Test block"},{"depth":4,"slug":"depends-on-block","text":"Depends on block"},{"depth":2,"slug":"step-4-releasing-a-new-version","text":"Step 4: Releasing a new version"},{"depth":3,"slug":"bottles","text":"Bottles"},{"depth":2,"slug":"addendum-updating-an-existing-app","text":"Addendum: Updating an existing app"},{"depth":2,"slug":"addendum-simplifying-the-process-by-automating-more-of-it","text":"Addendum: Simplifying the process by automating more of it"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"further-reading","text":"Further Reading"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Distribute Open-Source Tools with Homebrew Taps: A Beginner's Guide","date":"2025-01-27 08:00:00 +0000","tags":"development brew","image":"/images/post_covers/homebrew.jpg"},"imagePaths":[]}},"collection":"post","slug":"2025-01-distribute-open-source-tools-with-homebrew-taps-a-beginners-guide"},"content":{"headings":[{"depth":2,"slug":"what-are-homebrew-taps","text":"What are Homebrew taps?"},{"depth":2,"slug":"step-1-setting-up-a-tap-repository-on-github","text":"Step 1: Setting up a tap repository on GitHub"},{"depth":2,"slug":"step-2-adding-github-actions","text":"Step 2: Adding GitHub Actions"},{"depth":3,"slug":"test-action","text":"Test Action"},{"depth":3,"slug":"publish-action","text":"Publish Action"},{"depth":2,"slug":"step-3-creating-your-first-formula","text":"Step 3: Creating your first Formula"},{"depth":3,"slug":"creating-a-formula-file","text":"Creating a formula file"},{"depth":3,"slug":"updating-the-file","text":"Updating the file"},{"depth":4,"slug":"install-block","text":"Install block"},{"depth":4,"slug":"test-block","text":"Test block"},{"depth":4,"slug":"depends-on-block","text":"Depends on block"},{"depth":2,"slug":"step-4-releasing-a-new-version","text":"Step 4: Releasing a new version"},{"depth":3,"slug":"bottles","text":"Bottles"},{"depth":2,"slug":"addendum-updating-an-existing-app","text":"Addendum: Updating an existing app"},{"depth":2,"slug":"addendum-simplifying-the-process-by-automating-more-of-it","text":"Addendum: Simplifying the process by automating more of it"},{"depth":2,"slug":"conclusion","text":"Conclusion"},{"depth":2,"slug":"further-reading","text":"Further Reading"}],"remarkPluginFrontmatter":{"title":"Distribute Open-Source Tools with Homebrew Taps: A Beginner's Guide","date":"2025-01-27 08:00:00 +0000","tags":"development brew","image":"/images/post_covers/homebrew.jpg"}}},{"params":{"slug":"2024-09-pathfinding-guide-for-2d-top-view-tiles-in-godot-4-3"},"props":{"id":"2024-09-pathfinding-guide-for-2d-top-view-tiles-in-godot-4-3.md","data":{"title":"Pathfinding Guide for 2D Top-View Tiles in Godot 4.3","showTitle":true,"image":"/images/post_covers/godot-tileset-nav.jpg","includeLowResHero":false,"prose":true,"date":"2024-09-03T12:00:00.000Z","updatedDate":"2025-03-21T12:00:00.000Z","tags":"tutorial godot gamedev gdscript","status":"published"},"body":"Making your enemies run after your player character is a common task in 2D games. If you are using\nGodot and having trouble implementing pathfinding while respecting your TileSet collisions, you've\ncome to the right place!\n\nI **love** Godot. It's a really good game engine. It has its flaws, sure; but it's very flexible,\nit's designed to give the developer a lot of control, and it's still very easy to use. That said, 2D\ntop-down navigation on TileMaps has proven to be a difficult task to research and plan correctly.\n\nIn this tutorial we will learn how to implement pathfinding, in a flexible and portable way, with\navoiding obstacle tiles being the main focus point which was a pain for me.\n\n## Motivation\n\nGodot 4.3 introduced a lot of new features. Among them are `TileMapLayers` nodes, which replace the\nold `TileMap` node's layers property. This isn't actually part of the problem; they mostly work the\nsame as before.\n\nThe problem is, I was under the impression that if I give some tiles collision information, it would\nbe used by the pathfinding and be avoided by the moving actors. That is not the case!\n\nI actually got the most useful information in understanding how to use pathfinding by watching\n[this video](https://www.youtube.com/watch?v=7ZAF_fn3VOc) by\n[Coding Quests](https://www.youtube.com/@CodingQuests) on YouTube. His video provides a great basis,\nand I recommend you watch it.\n\nThis tutorial will do much of the same, but I will try to give you a more flexible way to handle\nobstacle tiles, so that it is more scalable for your game, and easier to manage and expand on.\n\n## Starting set-up\n\nFirst thing's first, we need to create some structure and understand how the nodes interact.\n\nLet's assume we have the following scene tree (I only included relevant nodes for brevity):\n\n```\nSceneBase (Node2D)\n├── GroundLayer (TileMapLayer)\n├── ActorLayer (TileMapLayer)\n│ ├── Player (CharacterBody2D)\n│ └── Enemy (CharacterBody2D)\n├── Trees (TileMapLayer)\n└── Rocks (TileMapLayer)\n```\n\nThis is the base starting point. We have a ground layer, which has tiles like grass and dirt. We\nhave an actors layer, inside it, we have two actors: the player and an enemy. We want the enemy to\nchase the player around, finding their path and avoiding blocked tiles on the way.\n\nBelow those, we have 2 more layers just so we can make sure our pathfinding is working across\nmultiple layers automatically in a more generic, less constrained way than having to manually link\nthem for each map scene.\n\n### Preparing the TileSets\n\nFor any navigable TileSets you plan to use, you will need to add navigation support to it, to allow\npathfinding to work.\n\nTo do that, first, find one of the layers that uses the desired TileSet, and expand the TileSet's\ndetails.\n\nThen, expand the \"Navigation Layers\" section, and add a layer with the bitmask of your choice (just\nleave at the default if you're unsure).\n\n\n\nOnce that's done, go to the TileSet editor. Go to the \"Paint\" tab, and select a walkable tile, such\nas grass or dirt.\n\n\n\nFrom \"Paint Properties\", under \"Navigation\", select the navigation layer you created (it should be\nthe only one).\n\n\n\nYou can start painting on a few more tiles as you see fit. Any tile you mark will be **walkable**.\n\n## Implementing pathfinding\n\nNow that the basic setup is out of the way, we can carry on. Pathfinding isn't too tricky to\nimplement in this case for just the base.\n\nFirst, let's add a child node to the enemy, of type `NavigationAgent2D`.\n\n> <ion-icon name=\"information-circle-outline\" class=\"align-middle\"></ion-icon> You may also create a\n> nav node via code, or use an `@export var` as alternate methods to get a navigation node; but\n> we'll keep it simple in this tutorial.\n\nThen, let's attach a (or modify our existing) script for the enemy.\n\n```gdscript\nextends CharacterBody2D\nclass_name Enemy\n\nvar speed := 50.0 # can be anything\n\n@onready var nav: NavigationAgent2D = $NavigationAgent2D # change if needed\n@onready var player: CharacterBody2D = %Player # change if needed\n\nfunc _ready() -> void:\n\tactor_setup.call_deferred()\n\tnav.velocity_computed.connect(_velocity_computed)\n\nfunc actor_setup():\n\t# Wait for the first physics frame so the NavigationServer can sync.\n\tawait get_tree().physics_frame\n\n\t# Now that the navigation map is no longer empty, set the movement target.\n\tset_movement_target(player.position)\n\nfunc set_movement_target(movement_target: Vector2):\n\tnav.target_position = movement_target\n\nfunc _physics_process(delta: float) -> void:\n\t_move_towards_player()\n\nfunc _move_towards_player():\n pass\n```\n\nBefore we implement the actual movement function, let's go over what we have added so far.\n\nIn the `_ready()` function, we do 2 things:\n\n1. Call the `actor_setup` function so the `nav` node is ready. We use `call_deferred()` to make it\n wait for the next frame before it is invoked. This assures us that the `nav` node is ready.\n1. We connect the `velocity_computed` signal so we can get the `nav` node's velocity for avoidance\n (if enabled).\n\nThe `actor_setup` function also does a couple of things:\n\n1. Waits for 1 physics frame to wait for the `nav` agent to be ready\n1. Sets the navigator's target position to the player position\n\n### What does avoidance do?\n\nWhat is avoidance, you ask? It is a feature of the NavigationAgent2D that allows it to avoid other\nnavigation agents. You can define a radius to circle around agents on your path, and the path will\nbe recalculated to avoid it. However, it does not relate to collision tiles or static obstacles. We\nwill handle that a bit later on.\n\n### Implementing the movement logic\n\nGetting back on topic, we will go ahead and implement the logic to move the actor using the\nnavigation agent.\n\nThis is most of the logic involving the actual navigation agent, so let's get going.\n\n```gdscript\nfunc _move_towards_player() -> void:\n\t# Update the player position\n\tset_movement_target(player.position)\n\n\t# If we're at the target, stop\n\tif nav.is_navigation_finished():\n\t\treturn\n\n\t# Get pathfinding information\n\tvar current_agent_position: Vector2 = global_position\n\tvar next_path_position: Vector2 = nav.get_next_path_position()\n\n\t# Calculate the new velocity\n\tvar new_velocity = current_agent_position.direction_to(next_path_position) * speed\n\n\t# Set correct velocity\n\tif nav.avoidance_enabled:\n\t\tnav.set_velocity(new_velocity)\n\telse:\n\t\t_velocity_computed(new_velocity)\n\n\t# Do the movement\n\tmove_and_slide()\n\nfunc _velocity_computed(safe_velocity: Vector2):\n\tvelocity = safe_velocity\n```\n\nThe comments should help you understand what we do on each section, but I want to explain a bit more\nabout the avoidance lines:\n\n```gdscript\nfunc _move_towards_player() -> void:\n\t# ...\n\t# Set correct velocity\n\tif nav.avoidance_enabled:\n\t\tnav.set_velocity(new_velocity)\n\telse:\n\t\t_velocity_computed(new_velocity)\n\t# ...\n\nfunc _velocity_computed(safe_velocity: Vector2):\n\tvelocity = safe_velocity\n```\n\nIf the nav's avoidance is enabled, we don't directly set the velocity of the actor. We instead pass\nthe calculated velocity to the nav agent itself. The agent will use it to handle the next\ncalculations in order to avoid other nav agents, and report back when it has a new \"safer\" path.\n\nWhen the `_velocity_computed` function is called (either by us or by the `velocity_computed` signal\nof the nav), we simply set the velocity of the actor. Avoidance will make sure to call the\n`velocity_computed` when necessary, which is why we don't manually set it in the case it is enabled.\n\n> <ion-icon name=\"information-circle-outline\" class=\"align-middle\"></ion-icon> It's important to\n> note that only `NavigationAgent2D` are considered for avoidance, not any obstacle. For that, just\n> continue reading.\n\n### Testing our setup\n\nThis setup should work for the initial test. Just run the game and see if it works.\n\nTo make it more visual, you can go to the navigation agent of the enemy, and check the \"Debug\"\nproperty; which will show us the paths the enemy is trying to follow. Also, in Godot's top menu,\nunder \"Debug\" -> check \"Visible Navigation\", which will highlight which tiles are used as possible\npaths and which aren't.\n\nHere's how it looks right now:\n\n\n\nOk, that works nicely. How would we go about removing obstacle tiles, though?\n\n## Obstacle tiles\n\nSome tiles, such as rocks and trees, are meant to be obstacles which block your (and others') path.\nBut unfortunately (and this is an area where Godot can definitely improve), linking the navigation\narea to tile collisions isn't possible out of the box without some code.\n\nLet's see how we need to implement that.\n\n### Preparing the TileSet\n\nOne thing we need is to set some collision on the relevant tiles.\n\nIn your TileSet's inspector, add a Physics layer, similar to how we added a Navigation layer:\n\n\n\nThen, once again, go to the TileSet panel, click \"Paint\", but this time, under \"Physics Layers\"\nchoose the layer you just added:\n\n\n\nNow when you paint tiles you will make them block your path and your enemies', if they try to walk\nthrough it.\n\nThey **will** still try to walk through these tiles, because the navigation isn't aware of the\nphysics there. This is where we do a little bit of magic.\n\nTo your ground layer, attach a new script. Call it whatever you like, I picked\n`NavigationTileLayer`.\n\nIn this script we need to do a few things:\n\n1. We need to get information about all the relevant layers (ones that contain obstacles)\n2. We need to tell the navigation system that any tiles with collision should be removed from the\n nav area\n\nIn order to do that, we'll write a little bit of code. Not much more, I promise!\n\nTo get the information on relevant layers, there are many possibilities:\n\n- Put them all in the same group, and use `get_tree().get_nodes_in_group()`\n- Find any sibling nodes with the `TileMapLayer` type\n- Use an `@export var` and manually put the relevant layers in there (less recommended)\n\nTo make it super simple, I think we'll go with the first option.\n\nFirst, select your `TileMapLayer`s in the scene. Then in the inspector pane, click the \"Node\" tab,\nthen \"Groups\".\n\nSimply add a new group with the \"+\" button:\n\n\n\nand add them to the `Obstacles` group (change the name if you want but just make sure to also change\nit in the script).\n\nThis is how it will look when added (it's your choice whether to make this group global or scene\nlocal):\n\n\n\nGreat, now let's add a function for that in our script:\n\n```gdscript\nextends TileMapLayer\n\nvar _obstacles: Array[TileMapLayer] = []\n\nfunc _ready() -> void:\n\t_get_obstacle_layers()\n\nfunc _get_obstacle_layers():\n\t# make sure the name here is the same as the group's\n\tvar layers = get_tree().get_nodes_in_group(\"Obstacles\")\n\n\tfor layer in layers:\n\t\tif layer is not TileMapLayer: continue\n\t\t_obstacles.append(layer)\n```\n\nOk, great! Now the next step is to remove navigation from collision tiles.\n\nWe will need a couple of things for that. First, we need to implement the\n`_use_tile_data_runtime_update` function.\n\n```gdscript\nfunc _use_tile_data_runtime_update(coords: Vector2i) -> bool:\n\treturn _is_used_by_obstacle(coords)\n```\n\nThis function is an override for a built-in for `TileMapLayer`. It should return a bool, which\nrepresents whether the cell at `coords` needs to have its data updated in runtime.\n\nIt makes sure we don't run excess updates on every cell and only update what we need. You probably\nnoticed I added a stub `_is_used_by_obstacle` function there, let's implement that now:\n\n```gdscript\nfunc _is_used_by_obstacle(coords: Vector2i) -> bool:\n\tfor layer in _obstacles:\n\t\tif coords in layer.get_used_cells():\n\t\t\tvar is_obstacle = layer.get_cell_tile_data(coords).get_collision_polygons_count(0) > 0\n\t\t\tif is_obstacle:\n\t\t\t\treturn true\n\treturn false\n```\n\nOur logic here is pretty simple:\n\n- Iterate over every layer in `_obstacles`\n- For each layer, check if the coords correspond to a cell\n - If there is a cell in that layer on those coords, check if it is an obstacle:\n - We do the check by getting the tile data, and retrieving the collision polygons count for it\n - If it has more than 0 (i.e., it has a collision shape), it is an obstacle\n - The `0` we pass to the function is the index of the physics layer we created on the TileSet,\n so if you had previous layers, this is what you would want to change\n - Return `true` if it is an obstacle\n - Continue otherwise\n- If no obstacles were found and we reached this point, then we return `false`\n\nIt's an efficient way to attach the information from the `TileMapLayer` into the navigation area. We\ndon't need any custom data layer or any additional setup for the tiles other than the collision\n(which we would want anyway, so our player would be blocked).\n\nNow for the last piece of the puzzle, we just need to actually set the navigation polygon on the\ntile to accommodate to the walkable state of the tile. Add a new method on the script:\n\n```gdscript\nfunc _tile_data_runtime_update(coords: Vector2i, tile_data: TileData) -> void:\n\tif not _is_used_by_obstacle(coords):\n\t\ttile_data.set_navigation_polygon(0, null)\n```\n\nThis is another built-in method in Godot that will only run on the tiles that have returned true\ninside the previous `_use_tile_data_runtime_update` method. It will only try to update the tiles\nthat are relevant, in order to save on memory. Since it will only run on tiles we already queried\nthat are not walkable, it won't try to update every tile.\n\n### Last test\n\nLet's run the game one more time - we can see that any tiles that contain collision are not\nhighlighted in the navigation debug. We can also notice our agent finding ways around the obstacles.\nSuccess!\n\n\n\n## Conclusion\n\nI believe this is a pretty flexible system. Basically, if we need to add more TileSets or tiles to\nthe same set, we just need to make sure of two things:\n\n1. The layer is grouped (or otherwise connected to the `NavigationTileLayer` script's obstacles)\n1. We have collision polygons on the required tiles\n\nOther than those, no special set up is required. This means you can essentially put the script\nanywhere, even on a completely separate node, and re-use that same node across all your maps with\nminimal effort. And if you don't want to use groups, you can find other ways to get the correct\nlayers to use.\n\nHopefully this tutorial helps you understand a bit more about the `TileMapLayer` nodes, about\n`NavigationAgent2D`, and about how to use tile data to your advantage. Personally, I had trouble\nfinding resources that properly explain how to handle 2d obstacles using pathfinding, so I thought\nit could be a nice write-up for others to learn from.\n\n**Do you have ideas for improvement on this system? Do you have any questions? Please leave a\ncomment!**","filePath":"src/content/post/2024-09-pathfinding-guide-for-2d-top-view-tiles-in-godot-4-3.md","digest":"30122fc569510249","rendered":{"html":"<p>Making your enemies run after your player character is a common task in 2D games. If you are using\nGodot and having trouble implementing pathfinding while respecting your TileSet collisions, you’ve\ncome to the right place!</p>\n<p>I <strong>love</strong> Godot. It’s a really good game engine. It has its flaws, sure; but it’s very flexible,\nit’s designed to give the developer a lot of control, and it’s still very easy to use. That said, 2D\ntop-down navigation on TileMaps has proven to be a difficult task to research and plan correctly.</p>\n<p>In this tutorial we will learn how to implement pathfinding, in a flexible and portable way, with\navoiding obstacle tiles being the main focus point which was a pain for me.</p>\n<h2 id=\"motivation\">Motivation</h2>\n<p>Godot 4.3 introduced a lot of new features. Among them are <code>TileMapLayers</code> nodes, which replace the\nold <code>TileMap</code> node’s layers property. This isn’t actually part of the problem; they mostly work the\nsame as before.</p>\n<p>The problem is, I was under the impression that if I give some tiles collision information, it would\nbe used by the pathfinding and be avoided by the moving actors. That is not the case!</p>\n<p>I actually got the most useful information in understanding how to use pathfinding by watching\n<a href=\"https://www.youtube.com/watch?v=7ZAF_fn3VOc\">this video</a> by\n<a href=\"https://www.youtube.com/@CodingQuests\">Coding Quests</a> on YouTube. His video provides a great basis,\nand I recommend you watch it.</p>\n<p>This tutorial will do much of the same, but I will try to give you a more flexible way to handle\nobstacle tiles, so that it is more scalable for your game, and easier to manage and expand on.</p>\n<h2 id=\"starting-set-up\">Starting set-up</h2>\n<p>First thing’s first, we need to create some structure and understand how the nodes interact.</p>\n<p>Let’s assume we have the following scene tree (I only included relevant nodes for brevity):</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>SceneBase (Node2D)</span></span>\n<span class=\"line\"><span>├── GroundLayer (TileMapLayer)</span></span>\n<span class=\"line\"><span>├── ActorLayer (TileMapLayer)</span></span>\n<span class=\"line\"><span>│ ├── Player (CharacterBody2D)</span></span>\n<span class=\"line\"><span>│ └── Enemy (CharacterBody2D)</span></span>\n<span class=\"line\"><span>├── Trees (TileMapLayer)</span></span>\n<span class=\"line\"><span>└── Rocks (TileMapLayer)</span></span></code></pre>\n<p>This is the base starting point. We have a ground layer, which has tiles like grass and dirt. We\nhave an actors layer, inside it, we have two actors: the player and an enemy. We want the enemy to\nchase the player around, finding their path and avoiding blocked tiles on the way.</p>\n<p>Below those, we have 2 more layers just so we can make sure our pathfinding is working across\nmultiple layers automatically in a more generic, less constrained way than having to manually link\nthem for each map scene.</p>\n<h3 id=\"preparing-the-tilesets\">Preparing the TileSets</h3>\n<p>For any navigable TileSets you plan to use, you will need to add navigation support to it, to allow\npathfinding to work.</p>\n<p>To do that, first, find one of the layers that uses the desired TileSet, and expand the TileSet’s\ndetails.</p>\n<p>Then, expand the “Navigation Layers” section, and add a layer with the bitmask of your choice (just\nleave at the default if you’re unsure).</p>\n<p><img src=\"/images/godot-navigation-tilemaps/nav-layers.png\" alt=\"Navigation layer\"></p>\n<p>Once that’s done, go to the TileSet editor. Go to the “Paint” tab, and select a walkable tile, such\nas grass or dirt.</p>\n<p><img src=\"/images/godot-navigation-tilemaps/paint-before.png\" alt=\"Paint before\"></p>\n<p>From “Paint Properties”, under “Navigation”, select the navigation layer you created (it should be\nthe only one).</p>\n<p><img src=\"/images/godot-navigation-tilemaps/paint-pick-nav.png\" alt=\"Paint pick navigation layer\"></p>\n<p>You can start painting on a few more tiles as you see fit. Any tile you mark will be <strong>walkable</strong>.</p>\n<h2 id=\"implementing-pathfinding\">Implementing pathfinding</h2>\n<p>Now that the basic setup is out of the way, we can carry on. Pathfinding isn’t too tricky to\nimplement in this case for just the base.</p>\n<p>First, let’s add a child node to the enemy, of type <code>NavigationAgent2D</code>.</p>\n<blockquote>\n<p><ion-icon name=\"information-circle-outline\" class=\"align-middle\"></ion-icon> You may also create a\nnav node via code, or use an <code>@export var</code> as alternate methods to get a navigation node; but\nwe’ll keep it simple in this tutorial.</p>\n</blockquote>\n<p>Then, let’s attach a (or modify our existing) script for the enemy.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">extends</span><span style=\"color:#B392F0\"> CharacterBody2D</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">class_name</span><span style=\"color:#B392F0\"> Enemy</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">var</span><span style=\"color:#E1E4E8\"> speed </span><span style=\"color:#F97583\">:=</span><span style=\"color:#79B8FF\"> 50.0</span><span style=\"color:#6A737D\"> # can be anything</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\">@onready</span><span style=\"color:#F97583\"> var</span><span style=\"color:#E1E4E8\"> nav: </span><span style=\"color:#B392F0\">NavigationAgent2D</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> $</span><span style=\"color:#79B8FF\">NavigationAgent2D</span><span style=\"color:#6A737D\"> # change if needed</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">@onready</span><span style=\"color:#F97583\"> var</span><span style=\"color:#E1E4E8\"> player: </span><span style=\"color:#B392F0\">CharacterBody2D</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> %</span><span style=\"color:#79B8FF\">Player</span><span style=\"color:#6A737D\"> # change if needed</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _ready</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\tactor_setup.</span><span style=\"color:#B392F0\">call_deferred</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\tnav.velocity_computed.</span><span style=\"color:#B392F0\">connect</span><span style=\"color:#E1E4E8\">(_velocity_computed)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> actor_setup</span><span style=\"color:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Wait for the first physics frame so the NavigationServer can sync.</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tawait</span><span style=\"color:#B392F0\"> get_tree</span><span style=\"color:#E1E4E8\">().physics_frame</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Now that the navigation map is no longer empty, set the movement target.</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\tset_movement_target</span><span style=\"color:#E1E4E8\">(player.position)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> set_movement_target</span><span style=\"color:#E1E4E8\">(movement_target: </span><span style=\"color:#B392F0\">Vector2</span><span style=\"color:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\tnav.target_position </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> movement_target</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _physics_process</span><span style=\"color:#E1E4E8\">(delta: </span><span style=\"color:#B392F0\">float</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\t_move_towards_player</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _move_towards_player</span><span style=\"color:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> pass</span></span></code></pre>\n<p>Before we implement the actual movement function, let’s go over what we have added so far.</p>\n<p>In the <code>_ready()</code> function, we do 2 things:</p>\n<ol>\n<li>Call the <code>actor_setup</code> function so the <code>nav</code> node is ready. We use <code>call_deferred()</code> to make it\nwait for the next frame before it is invoked. This assures us that the <code>nav</code> node is ready.</li>\n<li>We connect the <code>velocity_computed</code> signal so we can get the <code>nav</code> node’s velocity for avoidance\n(if enabled).</li>\n</ol>\n<p>The <code>actor_setup</code> function also does a couple of things:</p>\n<ol>\n<li>Waits for 1 physics frame to wait for the <code>nav</code> agent to be ready</li>\n<li>Sets the navigator’s target position to the player position</li>\n</ol>\n<h3 id=\"what-does-avoidance-do\">What does avoidance do?</h3>\n<p>What is avoidance, you ask? It is a feature of the NavigationAgent2D that allows it to avoid other\nnavigation agents. You can define a radius to circle around agents on your path, and the path will\nbe recalculated to avoid it. However, it does not relate to collision tiles or static obstacles. We\nwill handle that a bit later on.</p>\n<h3 id=\"implementing-the-movement-logic\">Implementing the movement logic</h3>\n<p>Getting back on topic, we will go ahead and implement the logic to move the actor using the\nnavigation agent.</p>\n<p>This is most of the logic involving the actual navigation agent, so let’s get going.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _move_towards_player</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Update the player position</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\tset_movement_target</span><span style=\"color:#E1E4E8\">(player.position)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# If we're at the target, stop</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tif</span><span style=\"color:#E1E4E8\"> nav.</span><span style=\"color:#B392F0\">is_navigation_finished</span><span style=\"color:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\treturn</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Get pathfinding information</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tvar</span><span style=\"color:#E1E4E8\"> current_agent_position: </span><span style=\"color:#B392F0\">Vector2</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> global_position</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tvar</span><span style=\"color:#E1E4E8\"> next_path_position: </span><span style=\"color:#B392F0\">Vector2</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> nav.</span><span style=\"color:#B392F0\">get_next_path_position</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Calculate the new velocity</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tvar</span><span style=\"color:#E1E4E8\"> new_velocity </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> current_agent_position.</span><span style=\"color:#B392F0\">direction_to</span><span style=\"color:#E1E4E8\">(next_path_position) </span><span style=\"color:#F97583\">*</span><span style=\"color:#E1E4E8\"> speed</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Set correct velocity</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tif</span><span style=\"color:#E1E4E8\"> nav.avoidance_enabled:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\tnav.</span><span style=\"color:#B392F0\">set_velocity</span><span style=\"color:#E1E4E8\">(new_velocity)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\telse</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\t\t_velocity_computed</span><span style=\"color:#E1E4E8\">(new_velocity)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Do the movement</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\tmove_and_slide</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _velocity_computed</span><span style=\"color:#E1E4E8\">(safe_velocity: </span><span style=\"color:#B392F0\">Vector2</span><span style=\"color:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\tvelocity </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> safe_velocity</span></span></code></pre>\n<p>The comments should help you understand what we do on each section, but I want to explain a bit more\nabout the avoidance lines:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _move_towards_player</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# ...</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# Set correct velocity</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tif</span><span style=\"color:#E1E4E8\"> nav.avoidance_enabled:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\tnav.</span><span style=\"color:#B392F0\">set_velocity</span><span style=\"color:#E1E4E8\">(new_velocity)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\telse</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\t\t_velocity_computed</span><span style=\"color:#E1E4E8\">(new_velocity)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _velocity_computed</span><span style=\"color:#E1E4E8\">(safe_velocity: </span><span style=\"color:#B392F0\">Vector2</span><span style=\"color:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\tvelocity </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> safe_velocity</span></span></code></pre>\n<p>If the nav’s avoidance is enabled, we don’t directly set the velocity of the actor. We instead pass\nthe calculated velocity to the nav agent itself. The agent will use it to handle the next\ncalculations in order to avoid other nav agents, and report back when it has a new “safer” path.</p>\n<p>When the <code>_velocity_computed</code> function is called (either by us or by the <code>velocity_computed</code> signal\nof the nav), we simply set the velocity of the actor. Avoidance will make sure to call the\n<code>velocity_computed</code> when necessary, which is why we don’t manually set it in the case it is enabled.</p>\n<blockquote>\n<p><ion-icon name=\"information-circle-outline\" class=\"align-middle\"></ion-icon> It’s important to\nnote that only <code>NavigationAgent2D</code> are considered for avoidance, not any obstacle. For that, just\ncontinue reading.</p>\n</blockquote>\n<h3 id=\"testing-our-setup\">Testing our setup</h3>\n<p>This setup should work for the initial test. Just run the game and see if it works.</p>\n<p>To make it more visual, you can go to the navigation agent of the enemy, and check the “Debug”\nproperty; which will show us the paths the enemy is trying to follow. Also, in Godot’s top menu,\nunder “Debug” -> check “Visible Navigation”, which will highlight which tiles are used as possible\npaths and which aren’t.</p>\n<p>Here’s how it looks right now:</p>\n<p><img src=\"/images/godot-navigation-tilemaps/test-01.png\" alt=\"First test\"></p>\n<p>Ok, that works nicely. How would we go about removing obstacle tiles, though?</p>\n<h2 id=\"obstacle-tiles\">Obstacle tiles</h2>\n<p>Some tiles, such as rocks and trees, are meant to be obstacles which block your (and others’) path.\nBut unfortunately (and this is an area where Godot can definitely improve), linking the navigation\narea to tile collisions isn’t possible out of the box without some code.</p>\n<p>Let’s see how we need to implement that.</p>\n<h3 id=\"preparing-the-tileset\">Preparing the TileSet</h3>\n<p>One thing we need is to set some collision on the relevant tiles.</p>\n<p>In your TileSet’s inspector, add a Physics layer, similar to how we added a Navigation layer:</p>\n<p><img src=\"/images/godot-navigation-tilemaps/physics-layers.png\" alt=\"Physics layers\"></p>\n<p>Then, once again, go to the TileSet panel, click “Paint”, but this time, under “Physics Layers”\nchoose the layer you just added:</p>\n<p><img src=\"/images/godot-navigation-tilemaps/paint-pick-physics.png\" alt=\"Paint pick physics layer\"></p>\n<p>Now when you paint tiles you will make them block your path and your enemies’, if they try to walk\nthrough it.</p>\n<p>They <strong>will</strong> still try to walk through these tiles, because the navigation isn’t aware of the\nphysics there. This is where we do a little bit of magic.</p>\n<p>To your ground layer, attach a new script. Call it whatever you like, I picked\n<code>NavigationTileLayer</code>.</p>\n<p>In this script we need to do a few things:</p>\n<ol>\n<li>We need to get information about all the relevant layers (ones that contain obstacles)</li>\n<li>We need to tell the navigation system that any tiles with collision should be removed from the\nnav area</li>\n</ol>\n<p>In order to do that, we’ll write a little bit of code. Not much more, I promise!</p>\n<p>To get the information on relevant layers, there are many possibilities:</p>\n<ul>\n<li>Put them all in the same group, and use <code>get_tree().get_nodes_in_group()</code></li>\n<li>Find any sibling nodes with the <code>TileMapLayer</code> type</li>\n<li>Use an <code>@export var</code> and manually put the relevant layers in there (less recommended)</li>\n</ul>\n<p>To make it super simple, I think we’ll go with the first option.</p>\n<p>First, select your <code>TileMapLayer</code>s in the scene. Then in the inspector pane, click the “Node” tab,\nthen “Groups”.</p>\n<p>Simply add a new group with the ”+” button:</p>\n<p><img src=\"/images/godot-navigation-tilemaps/create-group-before.png\" alt=\"Create group\"></p>\n<p>and add them to the <code>Obstacles</code> group (change the name if you want but just make sure to also change\nit in the script).</p>\n<p>This is how it will look when added (it’s your choice whether to make this group global or scene\nlocal):</p>\n<p><img src=\"/images/godot-navigation-tilemaps/create-group-after.png\" alt=\"Created group\"></p>\n<p>Great, now let’s add a function for that in our script:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">extends</span><span style=\"color:#B392F0\"> TileMapLayer</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">var</span><span style=\"color:#E1E4E8\"> _obstacles: </span><span style=\"color:#B392F0\">Array</span><span style=\"color:#E1E4E8\">[</span><span style=\"color:#B392F0\">TileMapLayer</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _ready</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">\t_get_obstacle_layers</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _get_obstacle_layers</span><span style=\"color:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">\t# make sure the name here is the same as the group's</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tvar</span><span style=\"color:#E1E4E8\"> layers </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> get_tree</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">get_nodes_in_group</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">\"Obstacles\"</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tfor</span><span style=\"color:#E1E4E8\"> layer </span><span style=\"color:#F97583\">in</span><span style=\"color:#E1E4E8\"> layers:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\tif</span><span style=\"color:#E1E4E8\"> layer </span><span style=\"color:#F97583\">is</span><span style=\"color:#B392F0\"> not</span><span style=\"color:#B392F0\"> TileMapLayer</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#F97583\">continue</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\t_obstacles.</span><span style=\"color:#B392F0\">append</span><span style=\"color:#E1E4E8\">(layer)</span></span></code></pre>\n<p>Ok, great! Now the next step is to remove navigation from collision tiles.</p>\n<p>We will need a couple of things for that. First, we need to implement the\n<code>_use_tile_data_runtime_update</code> function.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _use_tile_data_runtime_update</span><span style=\"color:#E1E4E8\">(coords: </span><span style=\"color:#B392F0\">Vector2i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> bool</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\treturn</span><span style=\"color:#B392F0\"> _is_used_by_obstacle</span><span style=\"color:#E1E4E8\">(coords)</span></span></code></pre>\n<p>This function is an override for a built-in for <code>TileMapLayer</code>. It should return a bool, which\nrepresents whether the cell at <code>coords</code> needs to have its data updated in runtime.</p>\n<p>It makes sure we don’t run excess updates on every cell and only update what we need. You probably\nnoticed I added a stub <code>_is_used_by_obstacle</code> function there, let’s implement that now:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _is_used_by_obstacle</span><span style=\"color:#E1E4E8\">(coords: </span><span style=\"color:#B392F0\">Vector2i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> bool</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tfor</span><span style=\"color:#E1E4E8\"> layer </span><span style=\"color:#F97583\">in</span><span style=\"color:#E1E4E8\"> _obstacles:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\tif</span><span style=\"color:#E1E4E8\"> coords </span><span style=\"color:#F97583\">in</span><span style=\"color:#E1E4E8\"> layer.</span><span style=\"color:#B392F0\">get_used_cells</span><span style=\"color:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\t\tvar</span><span style=\"color:#E1E4E8\"> is_obstacle </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> layer.</span><span style=\"color:#B392F0\">get_cell_tile_data</span><span style=\"color:#E1E4E8\">(coords).</span><span style=\"color:#B392F0\">get_collision_polygons_count</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">></span><span style=\"color:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\t\tif</span><span style=\"color:#E1E4E8\"> is_obstacle:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\t\t\t\treturn</span><span style=\"color:#79B8FF\"> true</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\treturn</span><span style=\"color:#79B8FF\"> false</span></span></code></pre>\n<p>Our logic here is pretty simple:</p>\n<ul>\n<li>Iterate over every layer in <code>_obstacles</code></li>\n<li>For each layer, check if the coords correspond to a cell\n<ul>\n<li>If there is a cell in that layer on those coords, check if it is an obstacle:\n<ul>\n<li>We do the check by getting the tile data, and retrieving the collision polygons count for it</li>\n<li>If it has more than 0 (i.e., it has a collision shape), it is an obstacle</li>\n<li>The <code>0</code> we pass to the function is the index of the physics layer we created on the TileSet,\nso if you had previous layers, this is what you would want to change</li>\n<li>Return <code>true</code> if it is an obstacle</li>\n<li>Continue otherwise</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>If no obstacles were found and we reached this point, then we return <code>false</code></li>\n</ul>\n<p>It’s an efficient way to attach the information from the <code>TileMapLayer</code> into the navigation area. We\ndon’t need any custom data layer or any additional setup for the tiles other than the collision\n(which we would want anyway, so our player would be blocked).</p>\n<p>Now for the last piece of the puzzle, we just need to actually set the navigation polygon on the\ntile to accommodate to the walkable state of the tile. Add a new method on the script:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"gdscript\"><code><span class=\"line\"><span style=\"color:#F97583\">func</span><span style=\"color:#B392F0\"> _tile_data_runtime_update</span><span style=\"color:#E1E4E8\">(coords: </span><span style=\"color:#B392F0\">Vector2i</span><span style=\"color:#E1E4E8\">, tile_data: </span><span style=\"color:#B392F0\">TileData</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">-></span><span style=\"color:#B392F0\"> void</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">\tif</span><span style=\"color:#F97583\"> not</span><span style=\"color:#B392F0\"> _is_used_by_obstacle</span><span style=\"color:#E1E4E8\">(coords):</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">\t\ttile_data.</span><span style=\"color:#B392F0\">set_navigation_polygon</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>This is another built-in method in Godot that will only run on the tiles that have returned true\ninside the previous <code>_use_tile_data_runtime_update</code> method. It will only try to update the tiles\nthat are relevant, in order to save on memory. Since it will only run on tiles we already queried\nthat are not walkable, it won’t try to update every tile.</p>\n<h3 id=\"last-test\">Last test</h3>\n<p>Let’s run the game one more time - we can see that any tiles that contain collision are not\nhighlighted in the navigation debug. We can also notice our agent finding ways around the obstacles.\nSuccess!</p>\n<p><img src=\"/images/godot-navigation-tilemaps/test-02.png\" alt=\"Last test\"></p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>I believe this is a pretty flexible system. Basically, if we need to add more TileSets or tiles to\nthe same set, we just need to make sure of two things:</p>\n<ol>\n<li>The layer is grouped (or otherwise connected to the <code>NavigationTileLayer</code> script’s obstacles)</li>\n<li>We have collision polygons on the required tiles</li>\n</ol>\n<p>Other than those, no special set up is required. This means you can essentially put the script\nanywhere, even on a completely separate node, and re-use that same node across all your maps with\nminimal effort. And if you don’t want to use groups, you can find other ways to get the correct\nlayers to use.</p>\n<p>Hopefully this tutorial helps you understand a bit more about the <code>TileMapLayer</code> nodes, about\n<code>NavigationAgent2D</code>, and about how to use tile data to your advantage. Personally, I had trouble\nfinding resources that properly explain how to handle 2d obstacles using pathfinding, so I thought\nit could be a nice write-up for others to learn from.</p>\n<p><strong>Do you have ideas for improvement on this system? Do you have any questions? Please leave a\ncomment!</strong></p>","metadata":{"headings":[{"depth":2,"slug":"motivation","text":"Motivation"},{"depth":2,"slug":"starting-set-up","text":"Starting set-up"},{"depth":3,"slug":"preparing-the-tilesets","text":"Preparing the TileSets"},{"depth":2,"slug":"implementing-pathfinding","text":"Implementing pathfinding"},{"depth":3,"slug":"what-does-avoidance-do","text":"What does avoidance do?"},{"depth":3,"slug":"implementing-the-movement-logic","text":"Implementing the movement logic"},{"depth":3,"slug":"testing-our-setup","text":"Testing our setup"},{"depth":2,"slug":"obstacle-tiles","text":"Obstacle tiles"},{"depth":3,"slug":"preparing-the-tileset","text":"Preparing the TileSet"},{"depth":3,"slug":"last-test","text":"Last test"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Pathfinding Guide for 2D Top-View Tiles in Godot 4.3","date":"2024-09-03 12:00:00 +0000","updatedDate":"2025-03-21 12:00:00 +0000","image":"/images/post_covers/godot-tileset-nav.jpg","tags":"tutorial godot gamedev gdscript"},"imagePaths":[]}},"collection":"post","slug":"2024-09-pathfinding-guide-for-2d-top-view-tiles-in-godot-4-3"},"content":{"headings":[{"depth":2,"slug":"motivation","text":"Motivation"},{"depth":2,"slug":"starting-set-up","text":"Starting set-up"},{"depth":3,"slug":"preparing-the-tilesets","text":"Preparing the TileSets"},{"depth":2,"slug":"implementing-pathfinding","text":"Implementing pathfinding"},{"depth":3,"slug":"what-does-avoidance-do","text":"What does avoidance do?"},{"depth":3,"slug":"implementing-the-movement-logic","text":"Implementing the movement logic"},{"depth":3,"slug":"testing-our-setup","text":"Testing our setup"},{"depth":2,"slug":"obstacle-tiles","text":"Obstacle tiles"},{"depth":3,"slug":"preparing-the-tileset","text":"Preparing the TileSet"},{"depth":3,"slug":"last-test","text":"Last test"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"remarkPluginFrontmatter":{"title":"Pathfinding Guide for 2D Top-View Tiles in Godot 4.3","date":"2024-09-03 12:00:00 +0000","updatedDate":"2025-03-21 12:00:00 +0000","image":"/images/post_covers/godot-tileset-nav.jpg","tags":"tutorial godot gamedev gdscript"}}},{"params":{"slug":"2024-08-create-an-automated-portfolio-using-github-and-astro"},"props":{"id":"2024-08-create-an-automated-portfolio-using-github-and-astro.md","data":{"title":"Create an Automated Portfolio Using GitHub and Astro","showTitle":true,"image":"/images/post_covers/resume-stand.jpg","imageAttribution":"<a href=\"https://www.freepik.com/free-photo/children-having-lemonade-stand_26299529.htm\">\n Image by freepik\n</a>","includeLowResHero":false,"prose":true,"date":"2024-08-27T09:00:00.000Z","tags":"development astro javascript typescript tutorial github","status":"published"},"body":"> ## I have updated the code in this article to use Astro loaders/Content Layer, and release it as a [package on NPM](http://npmjs.com/package/github-repos-astro-loader)!\n\nI have a tendency to start a lot of projects. I don't necessarily finish them all — some\nsoftware is never truly finished, sometimes they become deprecated, and sometimes, I just end up\nbored, or simply forget about them.\n\nAs a contractor/potential employee, I find it useful to redirect people to my GitHub page, where my\nprojects are most prominently displayed and easily accessible, and they can get a quick glance at\nwhat I like to do and how my code and conduct look.\n\nSince I have many rotating projects, it's becoming quite difficult to keep track of which projects\nwere recently updated, added, or removed. Not to mention, 6 pins on the GitHub profile really isn't\nthat much.\n\nIn this article, I will try to automate this process just a bit. This blog seems like the perfect\nplace to make things a bit more organized but also make sure they can be updated easily. It gives me\nmore control than GitHub, and I already have a projects page here, which would be a great target to\nreplace and put something a bit less static in.\n\nHopefully, this article will give you some nice ideas on how you can spruce up your website with\nsome semi-dynamic content, or how you can also help yourself display your featured work with a lot\nless manual labor.\n\n## The goal\n\nThis blog is an [Astro](https://astro.build/) project hosted as a static website. I truly recommend\nAstro; it's an incredibly easy, fun, and flexible way to create a website or a blog that isn't meant\nto be some kind of large web application — but I digress.\n\nWhat I'm trying to accomplish here is essentially the following:\n\n- Get all my GitHub repos\n- Filter out uninteresting ones and prominently display the best ones\n- List others by type/category/language/some other grouping\n\nNow, since this is a static website generator that I am using, there are a few limitations:\n\n- There is no \"server side\" during runtime — any processing is being done during build\n- I want the system I am creating to play nicely with the existing code\n- I want it to be easy to update, manage, add overrides to, and hide certain repositories\n\n## The plan\n\nAs of writing this, I haven't yet changed a single character of code. I first want to get a glimpse\nat what I currently have and try to see if I can come up with a plan for how to get through this.\n\nI already have a [projects](/projects) page; it's simply an index with a list of some projects. Each\nproject links to its own page, which might or might not have more information. This isn't ideal, but\nI can already see a way to improve here — GitHub holds a README, a project description,\nreleases, and links. It would be nice to implement this information somehow into the page.\n\nIn Astro, pages are created inside the `src/pages/` directory of a project. Content that can be\ngenerated for those pages using simpler formats, such as Markdown and MDX, is available inside the\n`src/content` directory. With that in mind, let's take a peek at the project-related files:\n\n```\nsrc/\n├── components\n│ ├── ProjectLinks.astro\n├── content\n│ ├── config.ts\n│ └── project\n│ ├── dart-script-runner.md\n│ ├── dungeon-paper.md\n│ ├── flutter-wheel-spinner.md\n│ ├── mudblock.md\n│ ├── simple-scaffold.md\n│ └── unaconfig.md\n└── pages\n └── projects\n ├── [...slug].astro\n └── index.astro\n```\n\nAs you can see, there are a few files that are relevant to projects.\n\n- A component that displays a project\n- An index page\n- A page for each project (`[...slug].astro`)\n- Contents for each project (the `.md` files)\n\nSo, for each repository I need to:\n\n- Generate a `.md` file\n- Update the index page to display them with new information & sorting\n\nThis will get me most of the way there. After that, it will be mostly connecting some pipes to make\nsure it's easy to reproduce this from, say, a CI environment, for easy deployment.\n\n## Getting the data\n\nWell, I can't display GitHub projects without getting their information, right?\n\nFor this, I'll need a\n[GitHub token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)\nthat works. I've already got one set up — perfect.\n\nRight now, the top of the `src/pages/[...slug].astro` file looks like this:\n\n```ts\nexport async function getStaticPaths() {\n const posts = await getCollection('project')\n return posts.map((post) => ({\n params: { slug: post.slug },\n props: post,\n }))\n}\ntype Props = CollectionEntry<'project'>\n\nconst post = Astro.props\nconst { description, links } = post.data\nconst { Content } = await post.render()\n```\n\nThe function `getStaticPaths` is a special function in Astro that is responsible for telling Astro\nwhich pages to generate, and it is using the content directory to do so. I am passing `slug` as a\nparam to the page generator, which means it can be accessed in the name's placeholder — that's\nwhat the file is called `[...slug].astro`. `[]` denotes param tokens to be replaced, and the `...`\nmeans they can have a directory path inside.\n\nThe top of the `src/pages/index.astro` file looks like this:\n\n```ts\nconst projects = await getCollection('project')\nprojects.sort((a, b) => {\n return a.data.order - b.data.order\n})\n```\n\nWhich is where we load all the projects for the index page. Note that they don't necessarily have to\ncorrelate; I can have pages without links routing to them or projects without pages to link to. This\nis not our case, but it's good to know.\n\nPractically anything can be loaded here — which is where I intend to invoke the generator of\nnew pages for the GitHub projects. So, let's work on getting that to work.\n\n### Using GitHub API to grab repositories\n\nA quick search and we can easily find the\n[GitHub API specification for repositories for a user](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-a-user).\n\nThis is where I need the API key.\n\nLet's create a new TypeScript file and put whatever logic we need there in its own box.\n\nI'll start simple and work up from there. Let's make a request to the repos API, and for each repo\nalso grab the README file.\n\n```ts\n// src/github-projects.ts\nimport { GITHUB_TOKEN } from './consts'\n\nconst GITHUB_USERNAME = 'chenasraf'\nconst headers = new Headers()\nheaders.set('Authorization', `Bearer ${GITHUB_TOKEN}`)\n\nexport async function getProjectsList(): Promise<GitHubProjectSchema[]> {\n // Fetch the data\n const response = await fetch(`https://api.github.com/users/${GITHUB_USERNAME}/repos`, { headers })\n const repos = await response.json()\n\n const projects: GitHubProjectSchema[] = []\n\n // Create a new object for each repo + get README\n for (const repo of repos) {\n const project = GitHubProjectSchema.parse({\n name: repo.name,\n url: repo.html_url,\n description: repo.description,\n stars: repo.stargazers_count,\n order: -repo.stargazers_count,\n })\n\n // Get the README.md static file\n const readmeResponse = await fetch(\n `https://raw.githubusercontent.com/${GITHUB_USERNAME}/${repo.name}/${repo.default_branch}/README.md`,\n { headers },\n )\n const readme = await readmeResponse.text()\n project.readme = readme\n projects.push(project)\n }\n\n return projects\n}\n```\n\nI'll also create a type for it:\n\n```ts\n// src/types.ts\nexport const LinkSchema = z.object({\n title: z.string(),\n href: z.string(),\n icon: z.string(),\n})\nexport type LinkSchema = z.infer<typeof LinkSchema>\n\nexport const GitHubProjectSchema = z.object({\n name: z.string(),\n title: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n stars: z.number(),\n readme: z.string().optional(),\n order: z.number(),\n links: z.array(LinkSchema),\n})\nexport type GitHubProjectSchema = z.infer<typeof GitHubProjectSchema>\n```\n\nSeems like quite a bit of code, but it's really not complex at all. Here's a rundown:\n\n1. Fetch the data from `/users/${GITHUB_USERNAME}/repos`, which gives us all the basic information\n about each repository\n1. Fetch the README from the raw file link for each repository\n1. Put it all together and push into an array\n\nNow, I'll just use that instead of the original in `index.astro`, just so I can test what it shows\nand that everything is working.\n\n```ts\nconst projects = await getProjectsList()\nprojects.sort((a, b) => {\n return a.order - b.order\n})\n\n// in the template itself:\nprojects.map((project) => (\n <code>\n <pre>{JSON.stringify(project, null, 2)}</pre>\n </code>\n))\n```\n\nI removed the previous `ProjectCard` component for now because we haven't updated it yet, and I just\nwanna make sure things are working.\n\nLet's refresh the page, and... Well, this is taking a couple of seconds, but okay, nothing major at\nall. It loaded! Wait... this is only 30 projects. Pretty sure I have more than that, even just the\npublic ones.\n\nHmm... Oh, here, I found the problem.\n\n### Handling pagination\n\nSeems like I've hit a small snag, but it's all well documented, so I can only blame my own laziness.\n\nHere is an excerpt from the GitHub API documentation for `/{user}/repos`:\n\n> `page - integer` The page number of the results to fetch. For more information, see\n> \"[Using pagination in the REST API.](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api)\"\n\nOkay, let's implement pagination to get everything:\n\n```ts\nasync function fetchRepos() {\n const repos = []\n let page = 1\n\n let response = await fetchReposPage(page)\n repos.push(...response.repos)\n\n while (response.url) {\n response = await fetchReposPage(++page)\n repos.push(...response.repos)\n }\n\n return repos\n}\n\nasync function fetchReposPage(page: number) {\n const response = await fetch(\n `https://api.github.com/users/${GITHUB_USERNAME}/repos?page=${page}&per_page=100`,\n { headers },\n )\n\n const repos = await response.json()\n const links = response.headers.get('link')\n\n let url: string | null = null\n\n if (links) {\n const next = links.split(',').find((link) => link.includes('rel=\"next\"'))\n if (next) {\n url = next.split(';', 1).toString().slice(1, -1)\n }\n }\n return { repos, url }\n}\n```\n\nI took some shortcuts — I used an iterating number instead of using the actual page link. It\nshould still work, and there is no unknown data in the actual link. I just need to make sure it\nexists before trying.\n\nAlso, I could have totally made the fetching logic recursive, which might have looked cleaner than a\n`while` loop. But I don't wanna stress over this too much. Overengineering is bleh.\n\nLet's try it now, and... it works! That didn't take me multiple iterations to get it right _at all_\n😇.\n\nWell, I have all the data I could want for now. On to the next step.\n\n## Generating Markdown files\n\nAstro consumes Markdown files, which allow me to also do some fancier rendering for each project\nrather than just showing text. You can add metadata (often called \"frontmatter\") at the top of the\nfile, like so:\n\n```md\n---\ntitle: Simple Scaffold\norder: 200\nlinks:\n - href: https://www.npmjs.com/package/simple-scaffold\n icon: logo-npm\n title: NPM\ndescription: |-\n <p class=\"mb-0\">A simple command to generate any file structure, from single components to entire app boilerplates.</p>\n <p>\n See\n <a href=\"/2019/03/simple-scaffold\">my post</a>\n detailing more, or the\n <a href=\"https://chenasraf.github.io/simple-scaffold\">documentation</a>\n /\n <a href=\"https://www.npmjs.com/package/simple-scaffold\">NPM</a>\n page to start using it!\n </p>\n---\n\n<!-- Markdown content may go here -->\n```\n\nSo, I want to generate a similar file, but populated with our new data from GitHub.\n\nI also want to be able to add custom information or add overrides, which won't be destroyed if I\nhappen to want to re-fetch repositories. I'll think about that later; for now, let's get this new\ntask done.\n\n### Creating the file\n\nWhile this is Markdown at the bottom, the top looks a lot like a YAML file.\n\nFirst thing's first: I'll move the original files to a different directory, so I don't destroy them\nright now while working. Also, I will probably want to keep some of the data there as overrides or\nadditions to what the repo info gives me.\n\nThe next step is to convert the structure I already have to YAML. I am trying to decide whether to\nuse a package or just implement a simple string file writer for this. The latter option is cool, but\nI'm sure there will be many problems with correctly escaping quotes and other YAML tokens in the\noutput, so I prefer just using a pre-made package for this. I quickly found\n[yaml](https://www.npmjs.com/package/yaml), and it seems like a perfect option for that.\n\nThis part was surprisingly simple — only a few lines:\n\n```ts\nasync function generateProjectFile({ readme, ...project }: GitHubProjectSchema) {\n const file = path.join(projectsDir, `${project.name.toLowerCase()}.md`)\n const lines = ['---']\n lines.push(yaml.stringify(project).trim())\n lines.push('---')\n if (readme) {\n lines.push(readme)\n }\n return fs.writeFile(file, lines.join('\\n'))\n}\n\nexport async function generateProjectsList() {\n const projects = await getProjectsList()\n await Promise.all(projects.map(generateProjectFile))\n}\n```\n\nThat already generates all the files for me. Success! I can now revert some of the index code back\nto load it back from the original collection:\n\n```ts\nawait generateProjectsList()\nconst projects = await getCollection('project')\nprojects.sort((a, b) => {\n return a.data.order - b.data.order\n})\n```\n\nAnd also update the correct types for Astro:\n\n```ts\n// src/content/config.ts\n\nconst project = defineCollection({\n schema: GitHubProjectSchema,\n})\n\nexport const collections = { project }\n```\n\nAnd now my files look like this:\n\n```md\n---\nname: simple-scaffold\ndescription: >-\n Generate any file structure — from single components to entire app boilerplates, with a\n single command.\nurl: https://github.com/chenasraf/simple-scaffold\nstars: 53\norder: -53\nlinks: []\n---\n\n<p align=\"center\">\n <img src=\"https://chenasraf.github.io//simple-scaffold/img/logo-lg.png\" alt=\"Logo\" />\n</p>\n<!-- Rest of contents of the README.md (too big to show here) -->\n```\n\nThis already looks pretty solid. If I just update the `ProjectCard` props and use it again, I should\nalready see a nice list of projects. Let's do that:\n\n```astro\n---\n// src/pages/projects/index.astro\n---\n<div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n {projects.map((project) => <ProjectCard project={project} />)}\n</div>\n```\n\n```astro\n---\n// src/components/ProjectCard.astro\nimport type { CollectionEntry } from 'astro:content'\nimport ProjectLinks from './ProjectLinks.astro'\ntype Props = { project: CollectionEntry<'project'> }\nconst { project } = Astro.props\nconst { title, description, links } = project.data\n---\n\n<style>\n .project-card p {\n @apply mt-0;\n @apply mb-0;\n }\n</style>\n<div\n class:list={[\n 'project-card cursor-default transition duration-300',\n 'border dark:border-none',\n 'shadow-generic hover:shadow-generic-hover',\n 'dark:shadow-generic-dark dark:hover:shadow-generic-dark-hover',\n 'rounded-2xl p-6',\n ]}\n>\n <h3 class=\"mt-0\">\n <a href={`/projects/${project.slug}`}>{title}</a>\n </h3>\n <div set:html={description} />\n <ProjectLinks links={links} />\n</div>\n```\n\nSuccess! I see my GitHub projects all nicely laid out for me on the projects page.\n\nLet's update the details page so that clicking it will lead to the actual information.\n\nThere are 2 main things to change; first, let's update the props so they reflect the new md files:\n\n```ts\n// src/pages/projects/[slug].astro\nconst post = Astro.props\nconst { name, description, links } = post.data\nconst { Content } = await post.render()\n```\n\nYou will notice the `const { Content } = await post.render()` line there at the bottom.\n\nWhen importing the md file as part of a collection, the actual contents of the md file are already\nparsed for Markdown and are returned as a component I can inject into the page.\n\nThe metadata at the top (the frontmatter) is added alongside the content as additional props for\nuse.\n\nNow we can simply plug it into the template:\n\n```astro\n---\n// src/pages/projects/[slug].astro\n// ...\n---\n<Page showTitle prose title={name} description={description ?? ''}>\n <p set:html={description} />\n <div class=\"no-prose\">\n <ProjectLinks links={links} />\n </div>\n <Prose><Content /></Prose>\n</Page>\n```\n\nWe now have a fully functioning projects list, along with a singular project page for each repo.\nGreat!\n\n## Enriching the data\n\nThe proof-of-concept is done. Now we can enrich the data a bit more, and maybe update the design. I\nalso already cached the repository responses into JSON files, so that the process can start over\nlater without having to re-fetch everything.\n\nLet's add some kind of filters to the project page. The easiest win is to add the GitHub link to the\nlinks array:\n\n```ts\nasync function getProjectsList(): Promise<GitHubProjectSchema[]> {\n // ...\n\n for (const repo of repos) {\n const project = GitHubProjectSchema.parse({\n name: repo.name,\n title: repo.name,\n url: repo.html_url,\n description: repo.description,\n stars: repo.stargazers_count,\n order: -repo.stargazers_count,\n // There 👇🏼\n links: [{ href: repo.html_url, icon: 'logo-github', title: 'GitHub' }],\n })\n }\n\n // ...\n}\n```\n\nNow, I'll put my old files in a directory called `project-overrides`. We will use this to manually\nadd override fields to any repo we choose.\n\nWe'll start by loading the corresponding file:\n\n```ts\nif (await fileExists(overridesFile)) {\n const content = await fs.readFile(overridesFile, 'utf8')\n const allLines = content.toString().split('\\n')\n const lines = allLines.slice(0, content.lastIndexOf('---')).join('\\n')\n const obj = jsYaml.parse(lines) as GitHubProjectSchema\n}\n```\n\nThen loading the overrides information there:\n\n```ts\nfor (const link of obj.links ?? []) {\n const found = project.links.findIndex((i) => i.href === link.href)\n if (found >= 0) {\n project.links.splice(found, 1, link)\n } else {\n project.links.push(link)\n }\n}\nfor (const key of ['title', 'order', 'description'] as const) {\n if (obj[key as keyof typeof obj] != null) {\n project[key as keyof typeof project] = obj[key as keyof typeof obj] as never\n }\n}\n```\n\nI wanted to avoid duplicated links, so I let the overrides take precedence if there are any\nconflicts.\n\n### Filtering out projects\n\nI also want to filter out any irrelevant projects. I'll do that by making a few checks before I push\ninto the projects array.\n\nSomething like this:\n\n```ts\nconst overridesDir = path.join(process.cwd(), 'src', 'content', 'project-overrides')\nconst projectIgnoreFile = path.join(overridesDir, '.projectignore')\nlet projectIgnore: string[] = []\n\nfs.readFile(projectIgnoreFile, 'utf8').then((content) => {\n projectIgnore = content\n .split('\\n')\n .map((i) => i.trim())\n .filter(Boolean)\n})\n\nfunction projectFilter(project: Record<string, any>): boolean {\n if (projectIgnore.includes(project.name)) {\n return false\n }\n return [\n !project.fork,\n project.stargazers_count > 0,\n //\n ].every(Boolean)\n}\n```\n\nAs you can see, I exclude:\n\n1. Project names listed in the ignore file\n1. Forks\n1. Projects without stars\n\nI can always tweak this further if I need to, but the regular filters got me most of the way, while\nI filled the gap with old (but starred) projects in the ignore file.\n\nNow, I can add it to the main repo loop, simply using `continue` to skip the current project:\n\n```ts\nasync function getProjectsList(): Promise<GitHubProjectSchema[]> {\n const repos = await fetchRepos()\n const projects: GitHubProjectSchema[] = []\n\n for (const repo of repos) {\n if (!projectFilter(repo)) {\n continue\n }\n // ...\n projects.push(project)\n }\n // ...\n}\n```\n\n### Marking featured projects\n\nWell, that's easy — I can just add a new property to `GitHubProjectSchema`, called `featured`.\nIt's a simple boolean, and when I want, I can add an override for that field; there shouldn't be\nmany of them anyway, and it's more likely they will already have overrides. Even if not, having a\nfile with 3 lines isn't that terrible.\n\nLet's update the types:\n\n```ts\n// src/types.ts\nexport const GitHubProjectSchema = z.object({\n name: z.string(),\n title: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n stars: z.number(),\n readme: z.string().optional(),\n order: z.number(),\n links: z.array(LinkSchema),\n // here:\n featured: z.boolean().optional().default(false),\n})\n```\n\nAnd add the implementation to the overrides loading:\n\n```ts\n// just add the 'featured' property to the list of overridable fields:\nfor (const key of ['title', 'order', 'description', 'featured'] as const) {\n // original logic\n}\n```\n\nAs for changing how featured projects are displayed, that's up to you. Personally, I partitioned\nthem into 2 arrays, and had the featured ones at the top with a bit of a difference in the design,\nwhile the other ones were separated to the bottom and also a bit smaller in size.\n\n## Finishing touches\n\nI have the basic structure working well. I could call it a day, but I will also update some styles\nin the new pages, re-order the information, stuff like that.\n\nThere are also some other ideas I can think of to improve this:\n\n- Adding releases links\n- Adding license\n- Smarter filtering\n- Search feature\n- Show project tags\n- Populate with project URL from the repo info\n\nBut all of these can wait for another day.\n\n## Wrapping it all up\n\nThe last thing I need is to make sure it's easy to update the project files whenever I need to make\nan update.\n\n- I have a CI that deploys the site on every push to master\n- I would like a way to trigger the changes without necessarily making an actual change to a real\n file\n\nI have a few ideas in mind. To manually trigger a rebuild, I could do one of the following:\n\n- Use manually triggered GitHub Actions\n- Commit some sort of hash file, which will change when needed, causing the build to trigger\n- Add the files to gitignore, which will make sure every GitHub Action run goes and fetches the most\n up-to-date information\n\nI think adding the files to gitignore and manually triggering GitHub Actions would solve both of my\nissues.\n\nUpdating the workflow is easy; all I have to do is add the `workflow_dispatch` event to be able to\nmanually trigger my deploy action:\n\n```yaml\n# .github/workflows/deploy.yaml\n\nname: Deploy\non:\n push:\n branches:\n - master\n workflow_dispatch:\n```\n\nAnd to add the files to gitignore:\n\n```gitignore\n# .gitignore\nsrc/content/project\n```\n\n### Uh-oh\n\nSeems there is a major snag.\n\nIn Astro, this all works pretty well in dev mode, but when using the `astro build` command, the\nnewly created files aren't noticed by the build system (actually not really, I should have noticed I\nneed an extra refresh after building all the files — it was a hint).\n\nApparently, the file list needs to be refreshed, and I don't think that's quite possible.\n\n**So... what now?**\n\nI decided simpler is better. Let's add this build step as a prebuild command, separate from Astro.\nThis will make it work.\n\nThere is one caveat, and that is we can't use any of Astro's stuff. So, if I want the types in the\nscript, I will have to replicate them (and worry about inconsistencies in the future) using my own\nzod import, not the one that is bundled with Astro:\n\n```ts\n// src/github-projects.ts\nconst LinkSchema = z.object({\n title: z.string(),\n href: z.string(),\n icon: z.string(),\n})\ntype LinkSchema = z.infer<typeof LinkSchema>\n\nconst GitHubProjectSchema = z.object({\n name: z.string(),\n title: z.string(),\n description: z.string().nullable(),\n url: z.string(),\n stars: z.number(),\n readme: z.string().optional(),\n order: z.number(),\n links: z.array(LinkSchema),\n featured: z.boolean().optional().default(false),\n})\ntype GitHubProjectSchema = z.infer<typeof GitHubProjectSchema>\n```\n\nAlso, I needed to change the way I access env vars:\n\n```ts\n// from:\nconst GITHUB_TOKEN = import.meta.env.GH_TOKEN || import.meta.env.GITHUB_TOKEN!\n// to:\nconst GITHUB_TOKEN = process.env.GH_TOKEN || process.env.GITHUB_TOKEN!\n```\n\nAnd then I created a standalone script to run this:\n\n```ts\nimport { generateProjectsList } from '../github-projects'\ngenerateProjectsList()\n```\n\nAnd made it run on pre-build in `package.json`:\n\n```json\n{\n \"scripts\": {\n \"prebuild\": \"tsx src/scripts/generate-projects.ts\"\n }\n}\n```\n\nI also removed the generate call from the astro pages.\n\nAnd now it works!\n\nI have also released this as a package. It has been updated and you can have more control over the\nfilters, and it generally works better as it integrates directly into Astro.\n[Check it out](http://npmjs.com/package/github-repos-astro-loader), or install right now using:\n\n#### npm\n\n```sh\nnpm i github-repos-astro-loader\n```\n\n#### pnpm\n\n```sh\npnpm i github-repos-astro-loader\n```\n\n### Yarn\n\n```sh\nyarn i github-repos-astro-loader\n```\n\n<br />\n<br />\n\n## Conclusion\n\nThat's it! You can take a look at my [Projects page](/projects) to see how it turned out.\n\nWhile it's not perfect, I think it's an interesting direction to take, and there are many\npossibilities for improving how and what to display. Have you done anything similar for yourself?\nHow do you make sure to keep your projects up to date? Leave a comment below telling us more.","filePath":"src/content/post/2024-08-create-an-automated-portfolio-using-github-and-astro.md","digest":"dbb8d10f18f72b31","rendered":{"html":"<blockquote>\n<h2 id=\"i-have-updated-the-code-in-this-article-to-use-astro-loaderscontent-layer-and-release-it-as-a-package-on-npm\">I have updated the code in this article to use Astro loaders/Content Layer, and release it as a <a href=\"http://npmjs.com/package/github-repos-astro-loader\">package on NPM</a>!</h2>\n</blockquote>\n<p>I have a tendency to start a lot of projects. I don’t necessarily finish them all — some\nsoftware is never truly finished, sometimes they become deprecated, and sometimes, I just end up\nbored, or simply forget about them.</p>\n<p>As a contractor/potential employee, I find it useful to redirect people to my GitHub page, where my\nprojects are most prominently displayed and easily accessible, and they can get a quick glance at\nwhat I like to do and how my code and conduct look.</p>\n<p>Since I have many rotating projects, it’s becoming quite difficult to keep track of which projects\nwere recently updated, added, or removed. Not to mention, 6 pins on the GitHub profile really isn’t\nthat much.</p>\n<p>In this article, I will try to automate this process just a bit. This blog seems like the perfect\nplace to make things a bit more organized but also make sure they can be updated easily. It gives me\nmore control than GitHub, and I already have a projects page here, which would be a great target to\nreplace and put something a bit less static in.</p>\n<p>Hopefully, this article will give you some nice ideas on how you can spruce up your website with\nsome semi-dynamic content, or how you can also help yourself display your featured work with a lot\nless manual labor.</p>\n<h2 id=\"the-goal\">The goal</h2>\n<p>This blog is an <a href=\"https://astro.build/\">Astro</a> project hosted as a static website. I truly recommend\nAstro; it’s an incredibly easy, fun, and flexible way to create a website or a blog that isn’t meant\nto be some kind of large web application — but I digress.</p>\n<p>What I’m trying to accomplish here is essentially the following:</p>\n<ul>\n<li>Get all my GitHub repos</li>\n<li>Filter out uninteresting ones and prominently display the best ones</li>\n<li>List others by type/category/language/some other grouping</li>\n</ul>\n<p>Now, since this is a static website generator that I am using, there are a few limitations:</p>\n<ul>\n<li>There is no “server side” during runtime — any processing is being done during build</li>\n<li>I want the system I am creating to play nicely with the existing code</li>\n<li>I want it to be easy to update, manage, add overrides to, and hide certain repositories</li>\n</ul>\n<h2 id=\"the-plan\">The plan</h2>\n<p>As of writing this, I haven’t yet changed a single character of code. I first want to get a glimpse\nat what I currently have and try to see if I can come up with a plan for how to get through this.</p>\n<p>I already have a <a href=\"/projects\">projects</a> page; it’s simply an index with a list of some projects. Each\nproject links to its own page, which might or might not have more information. This isn’t ideal, but\nI can already see a way to improve here — GitHub holds a README, a project description,\nreleases, and links. It would be nice to implement this information somehow into the page.</p>\n<p>In Astro, pages are created inside the <code>src/pages/</code> directory of a project. Content that can be\ngenerated for those pages using simpler formats, such as Markdown and MDX, is available inside the\n<code>src/content</code> directory. With that in mind, let’s take a peek at the project-related files:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>src/</span></span>\n<span class=\"line\"><span>├── components</span></span>\n<span class=\"line\"><span>│ ├── ProjectLinks.astro</span></span>\n<span class=\"line\"><span>├── content</span></span>\n<span class=\"line\"><span>│ ├── config.ts</span></span>\n<span class=\"line\"><span>│ └── project</span></span>\n<span class=\"line\"><span>│ ├── dart-script-runner.md</span></span>\n<span class=\"line\"><span>│ ├── dungeon-paper.md</span></span>\n<span class=\"line\"><span>│ ├── flutter-wheel-spinner.md</span></span>\n<span class=\"line\"><span>│ ├── mudblock.md</span></span>\n<span class=\"line\"><span>│ ├── simple-scaffold.md</span></span>\n<span class=\"line\"><span>│ └── unaconfig.md</span></span>\n<span class=\"line\"><span>└── pages</span></span>\n<span class=\"line\"><span> └── projects</span></span>\n<span class=\"line\"><span> ├── [...slug].astro</span></span>\n<span class=\"line\"><span> └── index.astro</span></span></code></pre>\n<p>As you can see, there are a few files that are relevant to projects.</p>\n<ul>\n<li>A component that displays a project</li>\n<li>An index page</li>\n<li>A page for each project (<code>[...slug].astro</code>)</li>\n<li>Contents for each project (the <code>.md</code> files)</li>\n</ul>\n<p>So, for each repository I need to:</p>\n<ul>\n<li>Generate a <code>.md</code> file</li>\n<li>Update the index page to display them with new information & sorting</li>\n</ul>\n<p>This will get me most of the way there. After that, it will be mostly connecting some pipes to make\nsure it’s easy to reproduce this from, say, a CI environment, for easy deployment.</p>\n<h2 id=\"getting-the-data\">Getting the data</h2>\n<p>Well, I can’t display GitHub projects without getting their information, right?</p>\n<p>For this, I’ll need a\n<a href=\"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\">GitHub token</a>\nthat works. I’ve already got one set up — perfect.</p>\n<p>Right now, the top of the <code>src/pages/[...slug].astro</code> file looks like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> getStaticPaths</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> posts</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getCollection</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'project'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> posts.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">post</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> params: { slug: post.slug },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> props: post,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> Props</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> CollectionEntry</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#9ECBFF\">'project'</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> post</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> Astro.props</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">description</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">links</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> post.data</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">Content</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> post.</span><span style=\"color:#B392F0\">render</span><span style=\"color:#E1E4E8\">()</span></span></code></pre>\n<p>The function <code>getStaticPaths</code> is a special function in Astro that is responsible for telling Astro\nwhich pages to generate, and it is using the content directory to do so. I am passing <code>slug</code> as a\nparam to the page generator, which means it can be accessed in the name’s placeholder — that’s\nwhat the file is called <code>[...slug].astro</code>. <code>[]</code> denotes param tokens to be replaced, and the <code>...</code>\nmeans they can have a directory path inside.</p>\n<p>The top of the <code>src/pages/index.astro</code> file looks like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getCollection</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'project'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">projects.</span><span style=\"color:#B392F0\">sort</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">a</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">b</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> a.data.order </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> b.data.order</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span></code></pre>\n<p>Which is where we load all the projects for the index page. Note that they don’t necessarily have to\ncorrelate; I can have pages without links routing to them or projects without pages to link to. This\nis not our case, but it’s good to know.</p>\n<p>Practically anything can be loaded here — which is where I intend to invoke the generator of\nnew pages for the GitHub projects. So, let’s work on getting that to work.</p>\n<h3 id=\"using-github-api-to-grab-repositories\">Using GitHub API to grab repositories</h3>\n<p>A quick search and we can easily find the\n<a href=\"https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-a-user\">GitHub API specification for repositories for a user</a>.</p>\n<p>This is where I need the API key.</p>\n<p>Let’s create a new TypeScript file and put whatever logic we need there in its own box.</p>\n<p>I’ll start simple and work up from there. Let’s make a request to the repos API, and for each repo\nalso grab the README file.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/github-projects.ts</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> { GITHUB_TOKEN } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> './consts'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> GITHUB_USERNAME</span><span style=\"color:#F97583\"> =</span><span style=\"color:#9ECBFF\"> 'chenasraf'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> headers</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> new</span><span style=\"color:#B392F0\"> Headers</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">headers.</span><span style=\"color:#B392F0\">set</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'Authorization'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">`Bearer ${</span><span style=\"color:#79B8FF\">GITHUB_TOKEN</span><span style=\"color:#9ECBFF\">}`</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> getProjectsList</span><span style=\"color:#E1E4E8\">()</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">GitHubProjectSchema</span><span style=\"color:#E1E4E8\">[]> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // Fetch the data</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> response</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetch</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">`https://api.github.com/users/${</span><span style=\"color:#79B8FF\">GITHUB_USERNAME</span><span style=\"color:#9ECBFF\">}/repos`</span><span style=\"color:#E1E4E8\">, { headers })</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> repos</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> response.</span><span style=\"color:#B392F0\">json</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span><span style=\"color:#E1E4E8\">[] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // Create a new object for each repo + get README</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> repo</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> repos) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> project</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> GitHubProjectSchema.</span><span style=\"color:#B392F0\">parse</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> name: repo.name,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url: repo.html_url,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> description: repo.description,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stars: repo.stargazers_count,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> order: </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\">repo.stargazers_count,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // Get the README.md static file</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> readmeResponse</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetch</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> `https://raw.githubusercontent.com/${</span><span style=\"color:#79B8FF\">GITHUB_USERNAME</span><span style=\"color:#9ECBFF\">}/${</span><span style=\"color:#E1E4E8\">repo</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#E1E4E8\">name</span><span style=\"color:#9ECBFF\">}/${</span><span style=\"color:#E1E4E8\">repo</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#E1E4E8\">default_branch</span><span style=\"color:#9ECBFF\">}/README.md`</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> { headers },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> readme</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> readmeResponse.</span><span style=\"color:#B392F0\">text</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> project.readme </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> readme</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> projects.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(project)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> projects</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>I’ll also create a type for it:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/types.ts</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> LinkSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> z.</span><span style=\"color:#B392F0\">object</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> href: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> icon: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> type</span><span style=\"color:#B392F0\"> LinkSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> z</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">infer</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#F97583\">typeof</span><span style=\"color:#E1E4E8\"> LinkSchema></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> GitHubProjectSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> z.</span><span style=\"color:#B392F0\">object</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> name: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> description: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">nullable</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stars: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> readme: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">optional</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> order: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> links: z.</span><span style=\"color:#B392F0\">array</span><span style=\"color:#E1E4E8\">(LinkSchema),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> type</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> z</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">infer</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#F97583\">typeof</span><span style=\"color:#E1E4E8\"> GitHubProjectSchema></span></span></code></pre>\n<p>Seems like quite a bit of code, but it’s really not complex at all. Here’s a rundown:</p>\n<ol>\n<li>Fetch the data from <code>/users/${GITHUB_USERNAME}/repos</code>, which gives us all the basic information\nabout each repository</li>\n<li>Fetch the README from the raw file link for each repository</li>\n<li>Put it all together and push into an array</li>\n</ol>\n<p>Now, I’ll just use that instead of the original in <code>index.astro</code>, just so I can test what it shows\nand that everything is working.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getProjectsList</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">projects.</span><span style=\"color:#B392F0\">sort</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">a</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">b</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> a.order </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> b.order</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// in the template itself:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">projects.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">project</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#B392F0\">code</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#B392F0\">pre</span><span style=\"color:#E1E4E8\">>{JSON.stringify(project, null, </span><span style=\"color:#79B8FF\">2</span><span style=\"color:#E1E4E8\">)}</span><span style=\"color:#F97583\"></</span><span style=\"color:#E1E4E8\">pre</span><span style=\"color:#F97583\">></span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> </</span><span style=\"color:#E1E4E8\">code</span><span style=\"color:#F97583\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">))</span></span></code></pre>\n<p>I removed the previous <code>ProjectCard</code> component for now because we haven’t updated it yet, and I just\nwanna make sure things are working.</p>\n<p>Let’s refresh the page, and… Well, this is taking a couple of seconds, but okay, nothing major at\nall. It loaded! Wait… this is only 30 projects. Pretty sure I have more than that, even just the\npublic ones.</p>\n<p>Hmm… Oh, here, I found the problem.</p>\n<h3 id=\"handling-pagination\">Handling pagination</h3>\n<p>Seems like I’ve hit a small snag, but it’s all well documented, so I can only blame my own laziness.</p>\n<p>Here is an excerpt from the GitHub API documentation for <code>/{user}/repos</code>:</p>\n<blockquote>\n<p><code>page - integer</code> The page number of the results to fetch. For more information, see\n“<a href=\"https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api\">Using pagination in the REST API.</a>”</p>\n</blockquote>\n<p>Okay, let’s implement pagination to get everything:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> fetchRepos</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> repos</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> let</span><span style=\"color:#E1E4E8\"> page </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> 1</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> let</span><span style=\"color:#E1E4E8\"> response </span><span style=\"color:#F97583\">=</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetchReposPage</span><span style=\"color:#E1E4E8\">(page)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> repos.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">response.repos)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> while</span><span style=\"color:#E1E4E8\"> (response.url) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> response </span><span style=\"color:#F97583\">=</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetchReposPage</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#F97583\">++</span><span style=\"color:#E1E4E8\">page)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> repos.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">response.repos)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> repos</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> fetchReposPage</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#FFAB70\">page</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> number</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> response</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetch</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> `https://api.github.com/users/${</span><span style=\"color:#79B8FF\">GITHUB_USERNAME</span><span style=\"color:#9ECBFF\">}/repos?page=${</span><span style=\"color:#E1E4E8\">page</span><span style=\"color:#9ECBFF\">}&per_page=100`</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> { headers },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> repos</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> response.</span><span style=\"color:#B392F0\">json</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> links</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> response.headers.</span><span style=\"color:#B392F0\">get</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'link'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> let</span><span style=\"color:#E1E4E8\"> url</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> string</span><span style=\"color:#F97583\"> |</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#F97583\"> =</span><span style=\"color:#79B8FF\"> null</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (links) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> next</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> links.</span><span style=\"color:#B392F0\">split</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">','</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">find</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">link</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> link.</span><span style=\"color:#B392F0\">includes</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'rel=\"next\"'</span><span style=\"color:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (next) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> next.</span><span style=\"color:#B392F0\">split</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">';'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">1</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">toString</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">slice</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">1</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#F97583\">-</span><span style=\"color:#79B8FF\">1</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> { repos, url }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>I took some shortcuts — I used an iterating number instead of using the actual page link. It\nshould still work, and there is no unknown data in the actual link. I just need to make sure it\nexists before trying.</p>\n<p>Also, I could have totally made the fetching logic recursive, which might have looked cleaner than a\n<code>while</code> loop. But I don’t wanna stress over this too much. Overengineering is bleh.</p>\n<p>Let’s try it now, and… it works! That didn’t take me multiple iterations to get it right <em>at all</em>\n😇.</p>\n<p>Well, I have all the data I could want for now. On to the next step.</p>\n<h2 id=\"generating-markdown-files\">Generating Markdown files</h2>\n<p>Astro consumes Markdown files, which allow me to also do some fancier rendering for each project\nrather than just showing text. You can add metadata (often called “frontmatter”) at the top of the\nfile, like so:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"md\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">---</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">title</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Simple Scaffold</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">order</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#79B8FF\">200</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">links</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#85E89D\">href</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">https://www.npmjs.com/package/simple-scaffold</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> icon</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">logo-npm</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> title</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">NPM</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">description</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#F97583\">|-</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> <p class=\"mb-0\">A simple command to generate any file structure, from single components to entire app boilerplates.</p></span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> <p></span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> See</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> <a href=\"/2019/03/simple-scaffold\">my post</a></span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> detailing more, or the</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> <a href=\"https://chenasraf.github.io/simple-scaffold\">documentation</a></span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> /</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> <a href=\"https://www.npmjs.com/package/simple-scaffold\">NPM</a></span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> page to start using it!</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> </p></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"><!-- Markdown content may go here --></span></span></code></pre>\n<p>So, I want to generate a similar file, but populated with our new data from GitHub.</p>\n<p>I also want to be able to add custom information or add overrides, which won’t be destroyed if I\nhappen to want to re-fetch repositories. I’ll think about that later; for now, let’s get this new\ntask done.</p>\n<h3 id=\"creating-the-file\">Creating the file</h3>\n<p>While this is Markdown at the bottom, the top looks a lot like a YAML file.</p>\n<p>First thing’s first: I’ll move the original files to a different directory, so I don’t destroy them\nright now while working. Also, I will probably want to keep some of the data there as overrides or\nadditions to what the repo info gives me.</p>\n<p>The next step is to convert the structure I already have to YAML. I am trying to decide whether to\nuse a package or just implement a simple string file writer for this. The latter option is cool, but\nI’m sure there will be many problems with correctly escaping quotes and other YAML tokens in the\noutput, so I prefer just using a pre-made package for this. I quickly found\n<a href=\"https://www.npmjs.com/package/yaml\">yaml</a>, and it seems like a perfect option for that.</p>\n<p>This part was surprisingly simple — only a few lines:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> generateProjectFile</span><span style=\"color:#E1E4E8\">({ </span><span style=\"color:#FFAB70\">readme</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#F97583\">...</span><span style=\"color:#FFAB70\">project</span><span style=\"color:#E1E4E8\"> }</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> file</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> path.</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(projectsDir, </span><span style=\"color:#9ECBFF\">`${</span><span style=\"color:#E1E4E8\">project</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#E1E4E8\">name</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#B392F0\">toLowerCase</span><span style=\"color:#9ECBFF\">()</span><span style=\"color:#9ECBFF\">}.md`</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> lines</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#9ECBFF\">'---'</span><span style=\"color:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> lines.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(yaml.</span><span style=\"color:#B392F0\">stringify</span><span style=\"color:#E1E4E8\">(project).</span><span style=\"color:#B392F0\">trim</span><span style=\"color:#E1E4E8\">())</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> lines.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'---'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (readme) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> lines.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(readme)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> fs.</span><span style=\"color:#B392F0\">writeFile</span><span style=\"color:#E1E4E8\">(file, lines.</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#79B8FF\">\\n</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> generateProjectsList</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getProjectsList</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#79B8FF\"> Promise</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">all</span><span style=\"color:#E1E4E8\">(projects.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">(generateProjectFile))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>That already generates all the files for me. Success! I can now revert some of the index code back\nto load it back from the original collection:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">await</span><span style=\"color:#B392F0\"> generateProjectsList</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getCollection</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'project'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">projects.</span><span style=\"color:#B392F0\">sort</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">a</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">b</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> a.data.order </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> b.data.order</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span></code></pre>\n<p>And also update the correct types for Astro:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/content/config.ts</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> project</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> defineCollection</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> schema: GitHubProjectSchema,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> collections</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> { project }</span></span></code></pre>\n<p>And now my files look like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"md\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">---</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">simple-scaffold</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">description</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#F97583\">>-</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> Generate any file structure &mdash; from single components to entire app boilerplates, with a</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> single command.</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">url</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">https://github.com/chenasraf/simple-scaffold</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">stars</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#79B8FF\">53</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">order</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#79B8FF\">-53</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">links</span><span style=\"color:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"><p align=\"center\"></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <img src=\"https://chenasraf.github.io//simple-scaffold/img/logo-lg.png\" alt=\"Logo\" /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></p></span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"><!-- Rest of contents of the README.md (too big to show here) --></span></span></code></pre>\n<p>This already looks pretty solid. If I just update the <code>ProjectCard</code> props and use it again, I should\nalready see a nice list of projects. Let’s do that:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"astro\"><code><span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// src/pages/projects/index.astro</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> class</span><span style=\"color:#E1E4E8\">=</span><span style=\"color:#9ECBFF\">\"grid grid-cols-1 md:grid-cols-2 gap-8\"</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {projects.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">project</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#79B8FF\">ProjectCard</span><span style=\"color:#B392F0\"> project</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{project} />)}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"astro\"><code><span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// src/components/ProjectCard.astro</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#F97583\"> type</span><span style=\"color:#E1E4E8\"> { CollectionEntry } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'astro:content'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> ProjectLinks </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> './ProjectLinks.astro'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> Props</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#FFAB70\">project</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> CollectionEntry</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#9ECBFF\">'project'</span><span style=\"color:#E1E4E8\">> }</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">project</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> Astro.props</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">title</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">description</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">links</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> project.data</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">style</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> .project-card</span><span style=\"color:#85E89D\"> p</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> @</span><span style=\"color:#79B8FF\">apply</span><span style=\"color:#79B8FF\"> mt-</span><span style=\"color:#E1E4E8\">0;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> @</span><span style=\"color:#79B8FF\">apply</span><span style=\"color:#79B8FF\"> mb-</span><span style=\"color:#E1E4E8\">0;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">style</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">div</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> class:list</span><span style=\"color:#E1E4E8\">={[</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> 'project-card cursor-default transition duration-300'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> 'border dark:border-none'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> 'shadow-generic hover:shadow-generic-hover'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> 'dark:shadow-generic-dark dark:hover:shadow-generic-dark-hover'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> 'rounded-2xl p-6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ]}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">h3</span><span style=\"color:#B392F0\"> class</span><span style=\"color:#E1E4E8\">=</span><span style=\"color:#9ECBFF\">\"mt-0\"</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">a</span><span style=\"color:#B392F0\"> href</span><span style=\"color:#E1E4E8\">={</span><span style=\"color:#9ECBFF\">`/projects/${</span><span style=\"color:#E1E4E8\">project</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#E1E4E8\">slug</span><span style=\"color:#9ECBFF\">}`</span><span style=\"color:#E1E4E8\">}>{title}</</span><span style=\"color:#85E89D\">a</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">h3</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> set:html</span><span style=\"color:#E1E4E8\">={description} /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#79B8FF\">ProjectLinks</span><span style=\"color:#B392F0\"> links</span><span style=\"color:#E1E4E8\">={links} /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<p>Success! I see my GitHub projects all nicely laid out for me on the projects page.</p>\n<p>Let’s update the details page so that clicking it will lead to the actual information.</p>\n<p>There are 2 main things to change; first, let’s update the props so they reflect the new md files:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/pages/projects/[slug].astro</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> post</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> Astro.props</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">name</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">description</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">links</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> post.data</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">Content</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> post.</span><span style=\"color:#B392F0\">render</span><span style=\"color:#E1E4E8\">()</span></span></code></pre>\n<p>You will notice the <code>const { Content } = await post.render()</code> line there at the bottom.</p>\n<p>When importing the md file as part of a collection, the actual contents of the md file are already\nparsed for Markdown and are returned as a component I can inject into the page.</p>\n<p>The metadata at the top (the frontmatter) is added alongside the content as additional props for\nuse.</p>\n<p>Now we can simply plug it into the template:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"astro\"><code><span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// src/pages/projects/[slug].astro</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// ...</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">---</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">Page</span><span style=\"color:#B392F0\"> showTitle</span><span style=\"color:#B392F0\"> prose</span><span style=\"color:#B392F0\"> title</span><span style=\"color:#E1E4E8\">={name} </span><span style=\"color:#B392F0\">description</span><span style=\"color:#E1E4E8\">={description </span><span style=\"color:#F97583\">??</span><span style=\"color:#9ECBFF\"> ''</span><span style=\"color:#E1E4E8\">}></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">p</span><span style=\"color:#B392F0\"> set:html</span><span style=\"color:#E1E4E8\">={description} /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> class</span><span style=\"color:#E1E4E8\">=</span><span style=\"color:#9ECBFF\">\"no-prose\"</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#79B8FF\">ProjectLinks</span><span style=\"color:#B392F0\"> links</span><span style=\"color:#E1E4E8\">={links} /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#79B8FF\">Prose</span><span style=\"color:#E1E4E8\">><</span><span style=\"color:#79B8FF\">Content</span><span style=\"color:#E1E4E8\"> /></</span><span style=\"color:#79B8FF\">Prose</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#79B8FF\">Page</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<p>We now have a fully functioning projects list, along with a singular project page for each repo.\nGreat!</p>\n<h2 id=\"enriching-the-data\">Enriching the data</h2>\n<p>The proof-of-concept is done. Now we can enrich the data a bit more, and maybe update the design. I\nalso already cached the repository responses into JSON files, so that the process can start over\nlater without having to re-fetch everything.</p>\n<p>Let’s add some kind of filters to the project page. The easiest win is to add the GitHub link to the\nlinks array:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> getProjectsList</span><span style=\"color:#E1E4E8\">()</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">GitHubProjectSchema</span><span style=\"color:#E1E4E8\">[]> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> repo</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> repos) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> project</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> GitHubProjectSchema.</span><span style=\"color:#B392F0\">parse</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> name: repo.name,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: repo.name,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url: repo.html_url,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> description: repo.description,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stars: repo.stargazers_count,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> order: </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\">repo.stargazers_count,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // There 👇🏼</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> links: [{ href: repo.html_url, icon: </span><span style=\"color:#9ECBFF\">'logo-github'</span><span style=\"color:#E1E4E8\">, title: </span><span style=\"color:#9ECBFF\">'GitHub'</span><span style=\"color:#E1E4E8\"> }],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now, I’ll put my old files in a directory called <code>project-overrides</code>. We will use this to manually\nadd override fields to any repo we choose.</p>\n<p>We’ll start by loading the corresponding file:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">if</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">await</span><span style=\"color:#B392F0\"> fileExists</span><span style=\"color:#E1E4E8\">(overridesFile)) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> content</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> fs.</span><span style=\"color:#B392F0\">readFile</span><span style=\"color:#E1E4E8\">(overridesFile, </span><span style=\"color:#9ECBFF\">'utf8'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> allLines</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> content.</span><span style=\"color:#B392F0\">toString</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">split</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#79B8FF\">\\n</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> lines</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> allLines.</span><span style=\"color:#B392F0\">slice</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">, content.</span><span style=\"color:#B392F0\">lastIndexOf</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'---'</span><span style=\"color:#E1E4E8\">)).</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#79B8FF\">\\n</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> obj</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> jsYaml.</span><span style=\"color:#B392F0\">parse</span><span style=\"color:#E1E4E8\">(lines) </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Then loading the overrides information there:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> link</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> obj.links </span><span style=\"color:#F97583\">??</span><span style=\"color:#E1E4E8\"> []) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> found</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> project.links.</span><span style=\"color:#B392F0\">findIndex</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> i.href </span><span style=\"color:#F97583\">===</span><span style=\"color:#E1E4E8\"> link.href)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (found </span><span style=\"color:#F97583\">>=</span><span style=\"color:#79B8FF\"> 0</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> project.links.</span><span style=\"color:#B392F0\">splice</span><span style=\"color:#E1E4E8\">(found, </span><span style=\"color:#79B8FF\">1</span><span style=\"color:#E1E4E8\">, link)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">else</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> project.links.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(link)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> key</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#9ECBFF\">'title'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'order'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'description'</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (obj[key </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> keyof</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> obj] </span><span style=\"color:#F97583\">!=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> project[key </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> keyof</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> project] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> obj[key </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> keyof</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> obj] </span><span style=\"color:#F97583\">as</span><span style=\"color:#79B8FF\"> never</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>I wanted to avoid duplicated links, so I let the overrides take precedence if there are any\nconflicts.</p>\n<h3 id=\"filtering-out-projects\">Filtering out projects</h3>\n<p>I also want to filter out any irrelevant projects. I’ll do that by making a few checks before I push\ninto the projects array.</p>\n<p>Something like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> overridesDir</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> path.</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(process.</span><span style=\"color:#B392F0\">cwd</span><span style=\"color:#E1E4E8\">(), </span><span style=\"color:#9ECBFF\">'src'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'content'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'project-overrides'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> projectIgnoreFile</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> path.</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(overridesDir, </span><span style=\"color:#9ECBFF\">'.projectignore'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">let</span><span style=\"color:#E1E4E8\"> projectIgnore</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> string</span><span style=\"color:#E1E4E8\">[] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">fs.</span><span style=\"color:#B392F0\">readFile</span><span style=\"color:#E1E4E8\">(projectIgnoreFile, </span><span style=\"color:#9ECBFF\">'utf8'</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">content</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> projectIgnore </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> content</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> .</span><span style=\"color:#B392F0\">split</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#79B8FF\">\\n</span><span style=\"color:#9ECBFF\">'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> .</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> i.</span><span style=\"color:#B392F0\">trim</span><span style=\"color:#E1E4E8\">())</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> .</span><span style=\"color:#B392F0\">filter</span><span style=\"color:#E1E4E8\">(Boolean)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">function</span><span style=\"color:#B392F0\"> projectFilter</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#FFAB70\">project</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Record</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">string</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">any</span><span style=\"color:#E1E4E8\">>)</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> boolean</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (projectIgnore.</span><span style=\"color:#B392F0\">includes</span><span style=\"color:#E1E4E8\">(project.name)) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#79B8FF\"> false</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> !</span><span style=\"color:#E1E4E8\">project.fork,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> project.stargazers_count </span><span style=\"color:#F97583\">></span><span style=\"color:#79B8FF\"> 0</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> //</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ].</span><span style=\"color:#B392F0\">every</span><span style=\"color:#E1E4E8\">(Boolean)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>As you can see, I exclude:</p>\n<ol>\n<li>Project names listed in the ignore file</li>\n<li>Forks</li>\n<li>Projects without stars</li>\n</ol>\n<p>I can always tweak this further if I need to, but the regular filters got me most of the way, while\nI filled the gap with old (but starred) projects in the ignore file.</p>\n<p>Now, I can add it to the main repo loop, simply using <code>continue</code> to skip the current project:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> getProjectsList</span><span style=\"color:#E1E4E8\">()</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">GitHubProjectSchema</span><span style=\"color:#E1E4E8\">[]> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> repos</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> fetchRepos</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> projects</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span><span style=\"color:#E1E4E8\">[] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> repo</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> repos) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">!</span><span style=\"color:#B392F0\">projectFilter</span><span style=\"color:#E1E4E8\">(repo)) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> continue</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> projects.</span><span style=\"color:#B392F0\">push</span><span style=\"color:#E1E4E8\">(project)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<h3 id=\"marking-featured-projects\">Marking featured projects</h3>\n<p>Well, that’s easy — I can just add a new property to <code>GitHubProjectSchema</code>, called <code>featured</code>.\nIt’s a simple boolean, and when I want, I can add an override for that field; there shouldn’t be\nmany of them anyway, and it’s more likely they will already have overrides. Even if not, having a\nfile with 3 lines isn’t that terrible.</p>\n<p>Let’s update the types:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/types.ts</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> GitHubProjectSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> z.</span><span style=\"color:#B392F0\">object</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> name: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> description: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">nullable</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stars: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> readme: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">optional</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> order: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> links: z.</span><span style=\"color:#B392F0\">array</span><span style=\"color:#E1E4E8\">(LinkSchema),</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // here:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> featured: z.</span><span style=\"color:#B392F0\">boolean</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">optional</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">default</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span></code></pre>\n<p>And add the implementation to the overrides loading:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// just add the 'featured' property to the list of overridable fields:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">for</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> key</span><span style=\"color:#F97583\"> of</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#9ECBFF\">'title'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'order'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'description'</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#9ECBFF\">'featured'</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // original logic</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>As for changing how featured projects are displayed, that’s up to you. Personally, I partitioned\nthem into 2 arrays, and had the featured ones at the top with a bit of a difference in the design,\nwhile the other ones were separated to the bottom and also a bit smaller in size.</p>\n<h2 id=\"finishing-touches\">Finishing touches</h2>\n<p>I have the basic structure working well. I could call it a day, but I will also update some styles\nin the new pages, re-order the information, stuff like that.</p>\n<p>There are also some other ideas I can think of to improve this:</p>\n<ul>\n<li>Adding releases links</li>\n<li>Adding license</li>\n<li>Smarter filtering</li>\n<li>Search feature</li>\n<li>Show project tags</li>\n<li>Populate with project URL from the repo info</li>\n</ul>\n<p>But all of these can wait for another day.</p>\n<h2 id=\"wrapping-it-all-up\">Wrapping it all up</h2>\n<p>The last thing I need is to make sure it’s easy to update the project files whenever I need to make\nan update.</p>\n<ul>\n<li>I have a CI that deploys the site on every push to master</li>\n<li>I would like a way to trigger the changes without necessarily making an actual change to a real\nfile</li>\n</ul>\n<p>I have a few ideas in mind. To manually trigger a rebuild, I could do one of the following:</p>\n<ul>\n<li>Use manually triggered GitHub Actions</li>\n<li>Commit some sort of hash file, which will change when needed, causing the build to trigger</li>\n<li>Add the files to gitignore, which will make sure every GitHub Action run goes and fetches the most\nup-to-date information</li>\n</ul>\n<p>I think adding the files to gitignore and manually triggering GitHub Actions would solve both of my\nissues.</p>\n<p>Updating the workflow is easy; all I have to do is add the <code>workflow_dispatch</code> event to be able to\nmanually trigger my deploy action:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\"><code><span class=\"line\"><span style=\"color:#6A737D\"># .github/workflows/deploy.yaml</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#85E89D\">name</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">Deploy</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">on</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> push</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> branches</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> - </span><span style=\"color:#9ECBFF\">master</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"> workflow_dispatch</span><span style=\"color:#E1E4E8\">:</span></span></code></pre>\n<p>And to add the files to gitignore:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span># .gitignore</span></span>\n<span class=\"line\"><span>src/content/project</span></span></code></pre>\n<h3 id=\"uh-oh\">Uh-oh</h3>\n<p>Seems there is a major snag.</p>\n<p>In Astro, this all works pretty well in dev mode, but when using the <code>astro build</code> command, the\nnewly created files aren’t noticed by the build system (actually not really, I should have noticed I\nneed an extra refresh after building all the files — it was a hint).</p>\n<p>Apparently, the file list needs to be refreshed, and I don’t think that’s quite possible.</p>\n<p><strong>So… what now?</strong></p>\n<p>I decided simpler is better. Let’s add this build step as a prebuild command, separate from Astro.\nThis will make it work.</p>\n<p>There is one caveat, and that is we can’t use any of Astro’s stuff. So, if I want the types in the\nscript, I will have to replicate them (and worry about inconsistencies in the future) using my own\nzod import, not the one that is bundled with Astro:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// src/github-projects.ts</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> LinkSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> z.</span><span style=\"color:#B392F0\">object</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> href: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> icon: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> LinkSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> z</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">infer</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#F97583\">typeof</span><span style=\"color:#E1E4E8\"> LinkSchema></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> GitHubProjectSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> z.</span><span style=\"color:#B392F0\">object</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> name: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> description: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">nullable</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> url: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stars: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> readme: z.</span><span style=\"color:#B392F0\">string</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">optional</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> order: z.</span><span style=\"color:#B392F0\">number</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> links: z.</span><span style=\"color:#B392F0\">array</span><span style=\"color:#E1E4E8\">(LinkSchema),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> featured: z.</span><span style=\"color:#B392F0\">boolean</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">optional</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">default</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> GitHubProjectSchema</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> z</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">infer</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#F97583\">typeof</span><span style=\"color:#E1E4E8\"> GitHubProjectSchema></span></span></code></pre>\n<p>Also, I needed to change the way I access env vars:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// from:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> GITHUB_TOKEN</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> import</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#79B8FF\">meta</span><span style=\"color:#E1E4E8\">.env.</span><span style=\"color:#79B8FF\">GH_TOKEN</span><span style=\"color:#F97583\"> ||</span><span style=\"color:#F97583\"> import</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#79B8FF\">meta</span><span style=\"color:#E1E4E8\">.env.</span><span style=\"color:#79B8FF\">GITHUB_TOKEN</span><span style=\"color:#F97583\">!</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// to:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> GITHUB_TOKEN</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> process.env.</span><span style=\"color:#79B8FF\">GH_TOKEN</span><span style=\"color:#F97583\"> ||</span><span style=\"color:#E1E4E8\"> process.env.</span><span style=\"color:#79B8FF\">GITHUB_TOKEN</span><span style=\"color:#F97583\">!</span></span></code></pre>\n<p>And then I created a standalone script to run this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> { generateProjectsList } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> '../github-projects'</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">generateProjectsList</span><span style=\"color:#E1E4E8\">()</span></span></code></pre>\n<p>And made it run on pre-build in <code>package.json</code>:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"json\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> \"scripts\"</span><span style=\"color:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> \"prebuild\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"tsx src/scripts/generate-projects.ts\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>I also removed the generate call from the astro pages.</p>\n<p>And now it works!</p>\n<p>I have also released this as a package. It has been updated and you can have more control over the\nfilters, and it generally works better as it integrates directly into Astro.\n<a href=\"http://npmjs.com/package/github-repos-astro-loader\">Check it out</a>, or install right now using:</p>\n<h4 id=\"npm\">npm</h4>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">npm</span><span style=\"color:#9ECBFF\"> i</span><span style=\"color:#9ECBFF\"> github-repos-astro-loader</span></span></code></pre>\n<h4 id=\"pnpm\">pnpm</h4>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">pnpm</span><span style=\"color:#9ECBFF\"> i</span><span style=\"color:#9ECBFF\"> github-repos-astro-loader</span></span></code></pre>\n<h3 id=\"yarn\">Yarn</h3>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"sh\"><code><span class=\"line\"><span style=\"color:#B392F0\">yarn</span><span style=\"color:#9ECBFF\"> i</span><span style=\"color:#9ECBFF\"> github-repos-astro-loader</span></span></code></pre>\n<br>\n<br>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>That’s it! You can take a look at my <a href=\"/projects\">Projects page</a> to see how it turned out.</p>\n<p>While it’s not perfect, I think it’s an interesting direction to take, and there are many\npossibilities for improving how and what to display. Have you done anything similar for yourself?\nHow do you make sure to keep your projects up to date? Leave a comment below telling us more.</p>","metadata":{"headings":[{"depth":2,"slug":"i-have-updated-the-code-in-this-article-to-use-astro-loaderscontent-layer-and-release-it-as-a-package-on-npm","text":"I have updated the code in this article to use Astro loaders/Content Layer, and release it as a package on NPM!"},{"depth":2,"slug":"the-goal","text":"The goal"},{"depth":2,"slug":"the-plan","text":"The plan"},{"depth":2,"slug":"getting-the-data","text":"Getting the data"},{"depth":3,"slug":"using-github-api-to-grab-repositories","text":"Using GitHub API to grab repositories"},{"depth":3,"slug":"handling-pagination","text":"Handling pagination"},{"depth":2,"slug":"generating-markdown-files","text":"Generating Markdown files"},{"depth":3,"slug":"creating-the-file","text":"Creating the file"},{"depth":2,"slug":"enriching-the-data","text":"Enriching the data"},{"depth":3,"slug":"filtering-out-projects","text":"Filtering out projects"},{"depth":3,"slug":"marking-featured-projects","text":"Marking featured projects"},{"depth":2,"slug":"finishing-touches","text":"Finishing touches"},{"depth":2,"slug":"wrapping-it-all-up","text":"Wrapping it all up"},{"depth":3,"slug":"uh-oh","text":"Uh-oh"},{"depth":4,"slug":"npm","text":"npm"},{"depth":4,"slug":"pnpm","text":"pnpm"},{"depth":3,"slug":"yarn","text":"Yarn"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Create an Automated Portfolio Using GitHub and Astro","date":"2024-08-27 12:00:00 +0300","tags":"development astro javascript typescript tutorial github","image":"/images/post_covers/resume-stand.jpg","imageAttribution":"<a href=\"https://www.freepik.com/free-photo/children-having-lemonade-stand_26299529.htm\">\n Image by freepik\n</a>"},"imagePaths":[]}},"collection":"post","slug":"2024-08-create-an-automated-portfolio-using-github-and-astro"},"content":{"headings":[{"depth":2,"slug":"i-have-updated-the-code-in-this-article-to-use-astro-loaderscontent-layer-and-release-it-as-a-package-on-npm","text":"I have updated the code in this article to use Astro loaders/Content Layer, and release it as a package on NPM!"},{"depth":2,"slug":"the-goal","text":"The goal"},{"depth":2,"slug":"the-plan","text":"The plan"},{"depth":2,"slug":"getting-the-data","text":"Getting the data"},{"depth":3,"slug":"using-github-api-to-grab-repositories","text":"Using GitHub API to grab repositories"},{"depth":3,"slug":"handling-pagination","text":"Handling pagination"},{"depth":2,"slug":"generating-markdown-files","text":"Generating Markdown files"},{"depth":3,"slug":"creating-the-file","text":"Creating the file"},{"depth":2,"slug":"enriching-the-data","text":"Enriching the data"},{"depth":3,"slug":"filtering-out-projects","text":"Filtering out projects"},{"depth":3,"slug":"marking-featured-projects","text":"Marking featured projects"},{"depth":2,"slug":"finishing-touches","text":"Finishing touches"},{"depth":2,"slug":"wrapping-it-all-up","text":"Wrapping it all up"},{"depth":3,"slug":"uh-oh","text":"Uh-oh"},{"depth":4,"slug":"npm","text":"npm"},{"depth":4,"slug":"pnpm","text":"pnpm"},{"depth":3,"slug":"yarn","text":"Yarn"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"remarkPluginFrontmatter":{"title":"Create an Automated Portfolio Using GitHub and Astro","date":"2024-08-27 12:00:00 +0300","tags":"development astro javascript typescript tutorial github","image":"/images/post_covers/resume-stand.jpg","imageAttribution":"<a href=\"https://www.freepik.com/free-photo/children-having-lemonade-stand_26299529.htm\">\n Image by freepik\n</a>"}}},{"params":{"slug":"2023-11-making-the-most-of-tailwind-css"},"props":{"id":"2023-11-making-the-most-of-tailwind-css.md","data":{"title":"Making the most of TailwindCSS","showTitle":true,"includeLowResHero":false,"prose":true,"date":"2023-11-12T23:03:03.000Z","tags":"development css tips tailwind","status":"hidden"},"body":"[TailwindCSS](https://tailwindcss.com) is a great library for easily composing styles. I have been\nusing Tailwind for a while and I absolutely love it. The bundle remains small by removing unused\nclasses, while redundant styles don't pile up almost at all, since all the classes end up global and\navailable to all. It's a very different way of writing styles and I am fond of it.\n\nWhile using Tailwind I have found several things that weren't immediately known to me from the\nstart, and which I think could benefit anyone using the library. Here is a collection of some of the\nbest tips I have learned.\n\n## Set your override colors\n\nIt may sound either dumb, or immediatlely obvious. But maybe I wasn't aware of the importance of\ncorrectly labeling your colors, shadesm and making sure they easily align with your project's design\nsystem.\n\n### Color objects\n\nFirst thing's first, to familiarize ourselves with the options, when we define colors, they can come\nas either objects, or single color strings. Example:\n\n```ts\nexport default {\n theme: {\n extend: {\n colors: {\n // single color\n blue: '#0000ff',\n // color with shades\n curious: {\n 50: '#f0f9ff',\n 100: '#e0f2fe',\n 200: '#bbe5fc',\n 300: '#7fd0fa',\n 400: '#3ab9f6',\n 500: '#11a1e6',\n 600: '#058ed9',\n 700: '#05669f',\n 800: '#095783',\n 900: '#0d496d',\n 950: '#092e48',\n },\n },\n },\n },\n}\n```\n\nNow that we know the basics, we can start talking strategy.\n\n### Color aliases\n\nWhat I usually do is always include the original colors, but also include some aliases, or\ncompletely new colors so that I can use them more easily. For example, by using `primary` and\n`secondary` colors as aliases to other colors, you can easily update your entire site's theme\nwithout going over the actual components.\n\nHere is an example of a color scheme using this method:\n\n```ts\n// get the original tailwind colors\nimport colors from 'tailwindcss/colors'\n\nexport default {\n theme: {\n extend: {\n colors: {\n ...colors,\n primary: colorScheme.indigo,\n secondary: colorScheme.purple,\n },\n },\n },\n}\n```\n\nThis way, we can use classes such as `text-primary-500`, `bg-secondary-900` and more, and easily be\nable to swap them out when needed.\n\n### The `DEFAULT` keyword\n\nAnother important tip about colors is to make use of the `DEFAULT` key in a given color\nconfiguration.\n\nSay we have the following color:\n\n```ts\nexport default {\n theme: {\n extend: {\n colors: {\n curious: {\n 50: '#f0f9ff',\n 100: '#e0f2fe',\n 200: '#bbe5fc',\n 300: '#7fd0fa',\n 400: '#3ab9f6',\n 500: '#11a1e6',\n 600: '#058ed9',\n 700: '#05669f',\n 800: '#095783',\n 900: '#0d496d',\n 950: '#092e48',\n },\n },\n },\n },\n}\n```\n\nWe are able to use classes such as `text-curious-100` to use the 100 shade. But what if we wanted a\nmain color where we don't have to specify the shade each time?\n\nIf we add a `DEFAULT` color, it will be the one used when there is no shade part given in the class.\n\nLet's add one:\n\n```ts\nconst curious = {\n // default:\n DEFAULT: '#058ed9',\n 50: '#f0f9ff',\n 100: '#e0f2fe',\n 200: '#bbe5fc',\n 300: '#7fd0fa',\n 400: '#3ab9f6',\n 500: '#11a1e6',\n 600: '#058ed9',\n 700: '#05669f',\n 800: '#095783',\n 900: '#0d496d',\n 950: '#092e48',\n}\n```\n\nNow, we can just use `text-curious` and `bg-curious` and we always get the primary version of the\ncolor.\n\n### Contrast colors\n\nAnother thing I like to do is add `fg` or `foreground` keys to my colors.\n\nThis makes it easy for me to always find a contrasting text color for any given color.\n\nFor example, if I have a primarily dark background color, and I want a color that is always visible\nagainst it, to add text or icons.\n\nI would do this (real example from this blog):\n\n```ts\nconst flamingo = {\n DEFAULT: '#fc5130',\n fg: '#ffffff',\n 50: '#fff2ed',\n 100: '#ffe1d5',\n 200: '#ffbea9',\n 300: '#fe9273',\n 400: '#fc5130',\n 500: '#fa3115',\n 600: '#eb170b',\n 700: '#c30c0b',\n 800: '#9b1116',\n 900: '#7d1114',\n 950: '#43070b',\n}\n```\n\nNow when I use `bg-flamingo`, I can always use `text-flamingo-fg` to get a valid contrasting color,\nno need for other checks or JS logic or what not.\n\n## Customize breakpoints\n\n### Mobile-first or desktop-first?\n\nTailwind is a mobile-first library. This approach leads to patterns which determine how you make\nyour designs responsive.\n\nFor example, you might design a section that turns from a horizontal flex on desktop, to a vertical\none on mobile.\n\nNormally, you would take the mobile design as the \"default\" approach, and then use a media query\nmodifier to add a desktop design, like so:\n\n```html\n<ul class=\"flex flex-col md:flex-row md:flex-wrap\">\n <li>...</li>\n</ul>\n```\n\nBut maybe you don't like that approach? Maybe you are developing a web app which is primarily used\non desktops, but you want to just make sure it's responsive enough to support smaller screens. Most\nof your overrides could be for smaller screens, and you might get designs for them first, while\nsmaller screens are an afterthought.\n\nIn that case, conside changing the way your media queries work for Tailwind. For example, I can find\nit handy in some projects, to have a setup like this:\n\n```ts\nexport default {\n theme: {\n screens: {\n '2xl': { max: '1440px' },\n xl: { max: '1280px' },\n lg: { max: '1024px' },\n md: { max: '800px' },\n sm: { max: '750px' },\n },\n },\n}\n```\n\nThis way, the above example can be rewritten like so:\n\n```html\n<ul class=\"flex flex-row flex-wrap md:flex-col\">\n <li>...</li>\n</ul>\n```\n\nThe difference in the example is subtle, but imagine using many breakpoints on one element or a few\nelements together, each using several breakpoints. It piles up!\n\nAn extension of this might be to combine `min` and `max` to only target specific screens, for\nexample:\n\n```ts\nexport default {\n theme: {\n screens: {\n mobile: { max: '760px' },\n tablet: { min: '761px', max: '1023px' },\n laptop: { min: '1024px', max: '1279px' },\n desktop: { min: '1280px' },\n },\n },\n}\n```\n\n## Add customized utilities\n\n## Use tw-merge\n\n## Add new animations\n\n## Avoid interpolated classes","filePath":"src/content/post/2023-11-making-the-most-of-tailwind-css.md","digest":"4c07151256966338","rendered":{"html":"<p><a href=\"https://tailwindcss.com\">TailwindCSS</a> is a great library for easily composing styles. I have been\nusing Tailwind for a while and I absolutely love it. The bundle remains small by removing unused\nclasses, while redundant styles don’t pile up almost at all, since all the classes end up global and\navailable to all. It’s a very different way of writing styles and I am fond of it.</p>\n<p>While using Tailwind I have found several things that weren’t immediately known to me from the\nstart, and which I think could benefit anyone using the library. Here is a collection of some of the\nbest tips I have learned.</p>\n<h2 id=\"set-your-override-colors\">Set your override colors</h2>\n<p>It may sound either dumb, or immediatlely obvious. But maybe I wasn’t aware of the importance of\ncorrectly labeling your colors, shadesm and making sure they easily align with your project’s design\nsystem.</p>\n<h3 id=\"color-objects\">Color objects</h3>\n<p>First thing’s first, to familiarize ourselves with the options, when we define colors, they can come\nas either objects, or single color strings. Example:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> theme: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> extend: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> colors: {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // single color</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> blue: </span><span style=\"color:#9ECBFF\">'#0000ff'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // color with shades</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> curious: {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 50</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#f0f9ff'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#e0f2fe'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 200</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#bbe5fc'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 300</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#7fd0fa'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 400</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#3ab9f6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 500</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#11a1e6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 600</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#058ed9'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 700</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#05669f'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 800</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#095783'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 900</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#0d496d'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 950</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#092e48'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now that we know the basics, we can start talking strategy.</p>\n<h3 id=\"color-aliases\">Color aliases</h3>\n<p>What I usually do is always include the original colors, but also include some aliases, or\ncompletely new colors so that I can use them more easily. For example, by using <code>primary</code> and\n<code>secondary</code> colors as aliases to other colors, you can easily update your entire site’s theme\nwithout going over the actual components.</p>\n<p>Here is an example of a color scheme using this method:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// get the original tailwind colors</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> colors </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'tailwindcss/colors'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> theme: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> extend: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> colors: {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> ...</span><span style=\"color:#E1E4E8\">colors,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> primary: colorScheme.indigo,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> secondary: colorScheme.purple,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>This way, we can use classes such as <code>text-primary-500</code>, <code>bg-secondary-900</code> and more, and easily be\nable to swap them out when needed.</p>\n<h3 id=\"the-default-keyword\">The <code>DEFAULT</code> keyword</h3>\n<p>Another important tip about colors is to make use of the <code>DEFAULT</code> key in a given color\nconfiguration.</p>\n<p>Say we have the following color:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> theme: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> extend: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> colors: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> curious: {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 50</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#f0f9ff'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#e0f2fe'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 200</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#bbe5fc'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 300</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#7fd0fa'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 400</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#3ab9f6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 500</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#11a1e6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 600</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#058ed9'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 700</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#05669f'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 800</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#095783'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 900</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#0d496d'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 950</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#092e48'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We are able to use classes such as <code>text-curious-100</code> to use the 100 shade. But what if we wanted a\nmain color where we don’t have to specify the shade each time?</p>\n<p>If we add a <code>DEFAULT</code> color, it will be the one used when there is no shade part given in the class.</p>\n<p>Let’s add one:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> curious</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // default:</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> DEFAULT: </span><span style=\"color:#9ECBFF\">'#058ed9'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 50</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#f0f9ff'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#e0f2fe'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 200</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#bbe5fc'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 300</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#7fd0fa'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 400</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#3ab9f6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 500</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#11a1e6'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 600</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#058ed9'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 700</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#05669f'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 800</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#095783'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 900</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#0d496d'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 950</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#092e48'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now, we can just use <code>text-curious</code> and <code>bg-curious</code> and we always get the primary version of the\ncolor.</p>\n<h3 id=\"contrast-colors\">Contrast colors</h3>\n<p>Another thing I like to do is add <code>fg</code> or <code>foreground</code> keys to my colors.</p>\n<p>This makes it easy for me to always find a contrasting text color for any given color.</p>\n<p>For example, if I have a primarily dark background color, and I want a color that is always visible\nagainst it, to add text or icons.</p>\n<p>I would do this (real example from this blog):</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> flamingo</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> DEFAULT: </span><span style=\"color:#9ECBFF\">'#fc5130'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> fg: </span><span style=\"color:#9ECBFF\">'#ffffff'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 50</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#fff2ed'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#ffe1d5'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 200</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#ffbea9'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 300</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#fe9273'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 400</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#fc5130'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 500</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#fa3115'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 600</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#eb170b'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 700</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#c30c0b'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 800</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#9b1116'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 900</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#7d1114'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 950</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'#43070b'</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now when I use <code>bg-flamingo</code>, I can always use <code>text-flamingo-fg</code> to get a valid contrasting color,\nno need for other checks or JS logic or what not.</p>\n<h2 id=\"customize-breakpoints\">Customize breakpoints</h2>\n<h3 id=\"mobile-first-or-desktop-first\">Mobile-first or desktop-first?</h3>\n<p>Tailwind is a mobile-first library. This approach leads to patterns which determine how you make\nyour designs responsive.</p>\n<p>For example, you might design a section that turns from a horizontal flex on desktop, to a vertical\none on mobile.</p>\n<p>Normally, you would take the mobile design as the “default” approach, and then use a media query\nmodifier to add a desktop design, like so:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"html\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">ul</span><span style=\"color:#B392F0\"> class</span><span style=\"color:#E1E4E8\">=</span><span style=\"color:#9ECBFF\">\"flex flex-col md:flex-row md:flex-wrap\"</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">li</span><span style=\"color:#E1E4E8\">>...</</span><span style=\"color:#85E89D\">li</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">ul</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<p>But maybe you don’t like that approach? Maybe you are developing a web app which is primarily used\non desktops, but you want to just make sure it’s responsive enough to support smaller screens. Most\nof your overrides could be for smaller screens, and you might get designs for them first, while\nsmaller screens are an afterthought.</p>\n<p>In that case, conside changing the way your media queries work for Tailwind. For example, I can find\nit handy in some projects, to have a setup like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> theme: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> screens: {</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> '2xl'</span><span style=\"color:#E1E4E8\">: { max: </span><span style=\"color:#9ECBFF\">'1440px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> xl: { max: </span><span style=\"color:#9ECBFF\">'1280px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> lg: { max: </span><span style=\"color:#9ECBFF\">'1024px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> md: { max: </span><span style=\"color:#9ECBFF\">'800px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> sm: { max: </span><span style=\"color:#9ECBFF\">'750px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>This way, the above example can be rewritten like so:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"html\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">ul</span><span style=\"color:#B392F0\"> class</span><span style=\"color:#E1E4E8\">=</span><span style=\"color:#9ECBFF\">\"flex flex-row flex-wrap md:flex-col\"</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">li</span><span style=\"color:#E1E4E8\">>...</</span><span style=\"color:#85E89D\">li</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">ul</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<p>The difference in the example is subtle, but imagine using many breakpoints on one element or a few\nelements together, each using several breakpoints. It piles up!</p>\n<p>An extension of this might be to combine <code>min</code> and <code>max</code> to only target specific screens, for\nexample:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> theme: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> screens: {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> mobile: { max: </span><span style=\"color:#9ECBFF\">'760px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> tablet: { min: </span><span style=\"color:#9ECBFF\">'761px'</span><span style=\"color:#E1E4E8\">, max: </span><span style=\"color:#9ECBFF\">'1023px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> laptop: { min: </span><span style=\"color:#9ECBFF\">'1024px'</span><span style=\"color:#E1E4E8\">, max: </span><span style=\"color:#9ECBFF\">'1279px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> desktop: { min: </span><span style=\"color:#9ECBFF\">'1280px'</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<h2 id=\"add-customized-utilities\">Add customized utilities</h2>\n<h2 id=\"use-tw-merge\">Use tw-merge</h2>\n<h2 id=\"add-new-animations\">Add new animations</h2>\n<h2 id=\"avoid-interpolated-classes\">Avoid interpolated classes</h2>","metadata":{"headings":[{"depth":2,"slug":"set-your-override-colors","text":"Set your override colors"},{"depth":3,"slug":"color-objects","text":"Color objects"},{"depth":3,"slug":"color-aliases","text":"Color aliases"},{"depth":3,"slug":"the-default-keyword","text":"The DEFAULT keyword"},{"depth":3,"slug":"contrast-colors","text":"Contrast colors"},{"depth":2,"slug":"customize-breakpoints","text":"Customize breakpoints"},{"depth":3,"slug":"mobile-first-or-desktop-first","text":"Mobile-first or desktop-first?"},{"depth":2,"slug":"add-customized-utilities","text":"Add customized utilities"},{"depth":2,"slug":"use-tw-merge","text":"Use tw-merge"},{"depth":2,"slug":"add-new-animations","text":"Add new animations"},{"depth":2,"slug":"avoid-interpolated-classes","text":"Avoid interpolated classes"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Making the most of TailwindCSS","date":"2023-11-13 01:03:03 +0200","tags":"development css tips tailwind","status":"hidden"},"imagePaths":[]}},"collection":"post","slug":"2023-11-making-the-most-of-tailwind-css"},"content":{"headings":[{"depth":2,"slug":"set-your-override-colors","text":"Set your override colors"},{"depth":3,"slug":"color-objects","text":"Color objects"},{"depth":3,"slug":"color-aliases","text":"Color aliases"},{"depth":3,"slug":"the-default-keyword","text":"The DEFAULT keyword"},{"depth":3,"slug":"contrast-colors","text":"Contrast colors"},{"depth":2,"slug":"customize-breakpoints","text":"Customize breakpoints"},{"depth":3,"slug":"mobile-first-or-desktop-first","text":"Mobile-first or desktop-first?"},{"depth":2,"slug":"add-customized-utilities","text":"Add customized utilities"},{"depth":2,"slug":"use-tw-merge","text":"Use tw-merge"},{"depth":2,"slug":"add-new-animations","text":"Add new animations"},{"depth":2,"slug":"avoid-interpolated-classes","text":"Avoid interpolated classes"}],"remarkPluginFrontmatter":{"title":"Making the most of TailwindCSS","date":"2023-11-13 01:03:03 +0200","tags":"development css tips tailwind","status":"hidden"}}},{"params":{"slug":"2023-11-html-dialogs-using-astro-and-htmx"},"props":{"id":"2023-11-html-dialogs-using-astro-and-htmx.md","data":{"title":"Build HTML5 Dialogs using Astro and HTMX","showTitle":true,"includeLowResHero":false,"prose":true,"date":"2023-11-11T00:25:47.000Z","tags":"","status":"hidden"},"body":"<!-- https://youtu.be/CYuujJvgmns?si=7dg3v2hDtrgtyvEP&t=1784 -->\n\n> \"[...] things like modals and multi-step UI — this part you're not doing in Astro.\"\n>\n> ~ Theo, on [this video](https://youtu.be/CYuujJvgmns?si=7dg3v2hDtrgtyvEP&t=1781)\n\nIs that a challenge?\n\nI'm sure it's not. And actually, for the most part, I guess you could say it's true. Astro is built\nmore for static websites with small interatcivity than for full-blown interactivity suite with user\nflows on a single page.\n\nBut it shouldn't necessarily be. Dialogs have been part of the HTML5 spec for a while, and it's not\ngoing away soon.","filePath":"src/content/post/2023-11-html-dialogs-using-astro-and-htmx.md","digest":"611f8d7b10efa9cc","rendered":{"html":"<!-- https://youtu.be/CYuujJvgmns?si=7dg3v2hDtrgtyvEP&t=1784 -->\n<blockquote>\n<p>”[…] things like modals and multi-step UI — this part you’re not doing in Astro.”</p>\n<p>~ Theo, on <a href=\"https://youtu.be/CYuujJvgmns?si=7dg3v2hDtrgtyvEP&t=1781\">this video</a></p>\n</blockquote>\n<p>Is that a challenge?</p>\n<p>I’m sure it’s not. And actually, for the most part, I guess you could say it’s true. Astro is built\nmore for static websites with small interatcivity than for full-blown interactivity suite with user\nflows on a single page.</p>\n<p>But it shouldn’t necessarily be. Dialogs have been part of the HTML5 spec for a while, and it’s not\ngoing away soon.</p>","metadata":{"headings":[],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Build HTML5 Dialogs using Astro and HTMX","date":"2023-11-11 02:25:47 +0200","tags":"","status":"hidden"},"imagePaths":[]}},"collection":"post","slug":"2023-11-html-dialogs-using-astro-and-htmx"},"content":{"headings":[],"remarkPluginFrontmatter":{"title":"Build HTML5 Dialogs using Astro and HTMX","date":"2023-11-11 02:25:47 +0200","tags":"","status":"hidden"}}},{"params":{"slug":"2023-11-create-your-own-use-optimistic-hook"},"props":{"id":"2023-11-create-your-own-use-optimistic-hook.md","data":{"title":"Create your own useOptimistic hook","showTitle":true,"image":"/images/post_covers/react-use-optimistic.jpg","imageAttribution":"Image by \n<a href=\"https://www.freepik.com/free-photo/unhappy-offended-woman-stands-with-arms-crossed-doesnt-speak-husband-joyful-bearded-man-has-dark-skin-giggles-positively-something-funny-expresses-positive-emotions-people-reaction_13580391.htm\">\n wayhomestudio\n</a>\non Freepik","includeLowResHero":false,"prose":true,"date":"2023-11-09T21:38:07.000Z","tags":"react javascript typescript tutorial development","status":"published"},"body":"Recently, I came across [this video](https://www.youtube.com/watch?v=M3mGY0pgFk0&t=279s) by Web Dev\nSimplified, which introduced me to React's new\n[useOptimistic](https://react.dev/reference/react/useOptimistic) hook.\n\nI began to wonder whether this is even needed. React has never been a library to provide more than\nbare-bones controls and hooks. The `useCallback`, `useMemo`, and `useState` hooks are most of the\nbuilt-in hooks and they provide a solid base upon which you can build pretty much any logic inside\nyour components or custom hooks.\n\nSo since when does React provide more high-level abstractions above hooks? At that point, they\nbecome even more opinionated than they already are. Not all use cases are the same - but this _is_ a\npretty generic hook, all things considered. Apparently, it is still in the experimental stage.\n\nI decided to experiment with creating this hook on my own, to understand if it was overcoming some\ninternal system limitation. I looked at various examples of existing optimistic hooks on NPM, but I\ndidn't really find one that falls in line with more modern coding styles, or they were completely\noutdated and abandoned.\n\n## Why create your own?\n\nIdeally, React should generally keep its size down as much as possible, and not increase the bundle\nwith more bloat that won't be used by everyone. I believe this hook is a very nice idea, but it is\nnot relevant for everyone, and I think if you find it does fit your needs, you don't have to wait\nfor official support - you can do it (or copy the final code here) in relatively short time, and\nfeature-complete (and added).\n\nIn addition, I thought it would make a good thought experiment and some coding practice, it can't\nhurt!\n\n## Looking at React's original implementation\n\nI decided to first take a look at React's official implementation of this. Not the source code, but\nI did want to get a sense of whether I like the consensus they came up with, or whether I feel it\nhas room for improvement.\n\nHere is a basic usage example, verbatim from\n[React's docs](https://react.dev/reference/react/useOptimistic#use):\n\n```ts\nconst [optimisticState, addOptimistic] = useOptimistic(\n state,\n // updateFn\n (currentState, optimisticValue) => {\n // merge and return new state\n // with optimistic value\n },\n)\n```\n\nSeems nice, but I think we can do better.\n\nOff the top of my head, I don't see a way to get the status of the optimistic value. Can I know when\nit's been finalized without having to maintain a separate state?\n\nAlso, there doesn't seem to be a way to roll back to the original value if an error has occurred. Or\nat least, the docs don't yet specify any. We can fix that, and provide a more \"holistic\" approach,\nwhich in my opinion doesn't leave the scope of this hook. Say I push an optimistic update, and it\nfails, how can I roll it back?\n\nWe'll try to make sense of where it should go as we go along.\n\n## Creating a hook\n\nLet's start with the signature, and make sure we have our types going for us.\n\n```ts\ntype StateUpdater<T, A = T> = (currentState: T, newState: A) => T\n\nexport function useOptimistic<T, A>(\n initial: T,\n merge?: StateUpdater<T, A>,\n): readonly [T, React.Dispatch<React.SetStateAction<T>>] {\n // ...\n}\n```\n\nWe start with the basics. An initial value, of type `T` which is generic, and an optional merge\nfunction. If it's not provided by the user, we'll just have it replace the old state with the new\nwhen an update comes. The `merge` function can have any type in the argument for the final dispatch,\nbut we assume `T` unless specified.\n\nNow we should add the state that maintains the optimistic value.\n\n```ts\nexport function useOptimistic<T>(\n initial: T,\n merge?: StateUpdater<T, A>,\n): readonly [T, React.Dispatch<React.SetStateAction<A>>] {\n const [state, setState] = useState(initial)\n return [state, setState]\n}\n```\n\nWe... have a very simple hook working right now. It doesn't really do anything special at all. Very\nbland.\n\nOk so, what's the most basic requirement? Well, for starters, it should let us merge the state if we\nprovide a `merge` function, which we haven't use yet from our arguments. Then, we should take into\naccount that updating the parent state (held in `initial`) should also update the state itself, so\nthat it will be up-to-date with the latest server value.\n\n## Creating the update functions\n\nWe'll start by adding the `useEffect` hook, that will update from the parent `initial` prop when it\nupdates.\n\n```ts\nuseEffect(() => {\n setState(initial)\n}, [initial])\n```\n\nNow, when the `initial` value is updated, the internal state will be kept up-to-date with the\nchange.\n\nWe can also implement the `merge` functionality. For that, we need to provide another implementation\nof the `setState` function, which uses it internally and adds some more logic.\n\n```ts\nconst update = useCallback(\n (value: SetStateAction<A>) => {\n const newValue = typeof value === 'function' ? (value as (value: T) => T)(state) : value\n const merged = merge ? merge(state, newValue as A) : (newValue as T)\n setState(merged)\n },\n [state],\n)\n\nreturn [state, update] // instead of [state, setState]\n```\n\nOkay, it's a little more to break down here.\n\nFirst, we have an identical callback to `setState` now, except it also runs the `merge` function\nbefore sending the final value.\n\nIn `useState`, in addition to passing a new state value, the `setState` function can be used with a\ncallback function, which takes the current state as a parameter, and should return the new state.\nThe variable `newValue` is populated with the result of that process.\n\nThen we just stack the `merge` function on top of that, if it exists.\n\nThe usage for this would still be:\n\n```ts\nconst [count, setCount] = useOptimistic(0)\n\n// option 1\nsetCount(1)\n\n// option 2\nsetCount((c) => c + 1)\n```\n\nAll the changes happen internally. Nice.\n\nThat's pretty much it as it stands right now. React's implementation is pretty similar to this, with\nmaybe a slight variation on how the change is calculated with the parent's update, to determine when\nthe value should be brought up to date.\n\nA bit anti-climactic? Well, let's add a few bells and whistles. And we can now, because we don't\nneed React to build it for us. We're strong!\n\n## What else can we add? Rolling back on failure\n\nThe first idea for improvement I have is to be able to catch an error, and roll back the value if it\nfails. Let's begin implementing this.\n\nFirst, we need to have a value to go back to. So every time the developer updates the optimistic\nstate to a new value, and an operation related to it fails, we want to provide a way to revert to\nthe original value.\n\nFor a recap, here's the entire hook as it now is, before we make changes:\n\n```ts\ntype StateUpdater<T, A = T> = (currentState: T, newState: A) => T\n\nexport function useOptimistic<T>(\n initial: T,\n merge?: StateUpdater<T, A>,\n): readonly [T, React.Dispatch<React.SetStateAction<A>>] {\n const [state, setState] = useState(initial)\n\n useEffect(() => {\n setState(initial)\n }, [initial])\n\n const update = useCallback(\n (value: SetStateAction<A>) => {\n const newValue = typeof value === 'function' ? (value as (value: T) => T)(state) : value\n const merged = merge ? merge(state, newValue as A) : (newValue as T)\n setState(merged)\n },\n [state],\n )\n\n return [state, update]\n}\n```\n\nNow, let's say the developer comes across a case like this:\n\n```tsx\nfunction MyApp({ comments }) {\n const [oComments, addComment] = useOptimistic(\n comments || [],\n (existingComments, newComment) => [...existingComments, newComment],\n )\n\n const sendComment = async (comment: Comment) => {\n addComment(comment)\n await sendCommentToApi(comment) // could fail and throw\n }\n\n return ...\n}\n```\n\nIn this case, we are sending some API request to send the comment content to the backend and make\nsure it's already there.\n\nBut if it fails, we will have to revert it to its original content manually by keeping a separate\nstate for the original value, and set it back if it failed.\n\nInstead, we can make our new hook handle this.\n\nLet's add another state that maintains the previous value:\n\n```ts\nconst [rollbackState, setRollbackState] = useState<T | null>(null)\n```\n\nAnd now that we have that, we should set this rollback value once we set a new value through the\nregular setter:\n\n```ts\nconst update = useCallback(\n (value: SetStateAction<A>) => {\n const newValue = typeof value === 'function' ? (value as (value: T) => T)(state) : value\n const merged = merge ? merge(state, newValue as A) : (newValue as T)\n setRollbackState(state)\n setState(merged)\n },\n [state],\n)\n```\n\nWe take this value, save it, and... Do nothing with it. We should probably write that logic\nsomewhere. How's this:\n\n```ts\nconst rollback = useCallback(() => {\n if (rollbackState == null) {\n // you can either return here, or throw an error, and be more strict about false rollbacks:\n // throw new Error('There is no state to roll back to')\n return\n }\n setState(rollbackState!)\n setRollbackState(null)\n}, [rollbackState])\n```\n\nAnd we can also add a `done` callback, which ditches the retained value, just for clean up.\nSomething like this:\n\n```ts\nconst done = useCallback(() => {\n setRollbackState(null)\n}, [])\n```\n\nLet's also return these in our hook:\n\n```ts\nexport function useOptimistic<T, A = T>(\n initial: T,\n merge?: StateUpdater<T, A>,\n): readonly [\n T,\n React.Dispatch<React.SetStateAction<A>>,\n {\n readonly rollback: () => void\n readonly done: () => void\n },\n] {\n // ...\n return [state, update, { rollback, done }]\n}\n```\n\nNice, right? We just revert. Now to actually use it, we can update the component:\n\n```ts\nconst [oComments, addComment, { done, rollback }] = useOptimistic(\n // ^^^^^^^^^^^^^^^^^^\n comments || [],\n (existingComments, newComment) => [...existingComments, newComment],\n)\n\nconst sendComment = async (comment: Comment) => {\n try {\n addComment(comment)\n await sendCommentToApi(comment) // could fail and throw\n } catch (e) {\n rollback()\n }\n}\n```\n\nThat's not too bad. If we fail, we use `rollback` and get back our previous value, and we never had\nto save it separately, which is good for scaling this to multiple places.\n\nBut... if you give it a go, you will notice a problem.\n\nLet's do a small test:\n\n```ts\nconst fail = async (comment: Comment) => {\n try {\n addComment(comment)\n await new Promise((res) => setTimeout(res, 1000))\n throw new Error('Oops!')\n done()\n } catch {\n rollback()\n }\n}\n```\n\nMake this run somewhere and you will realize the value reverted back to `null` instead of the old\nvalue, or simply doesn't roll back. What gives?\n\nHere is the problem: when we are using `rollback`, it uses the state variable, `rollbackState`, to\nset the value back on the main state. But here is the thing - this variable is a dependency in the\ncallback hook on `rollback`. Take a closer look:\n\n```ts\n// before:\n// const [rollbackState, setRollbackState] = useState<T | null>(null)\n//\n// after:\nconst rollbackState = useRef<T | null>(null)\n\nconst rollback = useCallback(() => {\n if (rollbackState == null) {\n return\n }\n setState(rollbackState!)\n setRollbackState(null)\n}, [rollbackState])\n// ^^^^^^^^^^^^^\n```\n\nSo why is that bad? Well, in our `fail` test above, we used `addComment` which is our `update`\nfunction with a fancy name. When the API call fails we call `rollback`. But `rollback` was created\nbound to the `rollbackState` that was there at the time of its creation. We `wait` for a promise to\nreturn, and then use the `done` or `rollback` functions, that were created **before** the\n`rollbackState` was even set away from `null` and into a value in the first place. So basically we\nare re-using the value from before we put anything in it, which means it's `null`, which is the\nvalue we roll back to when using `rollback`.\n\nHow do we fix this?\n\n## Fixing the stale value\n\nThere are probably many possible solutions, but here is my very naive one.\n\nHow do I make sure I get the correct reference to the updated value?\n\nIn reality, I don't need `rollbackState` to be an actual state. Why can I say that with confidence?\nWell, I don't need to call a re-render when it updates, as it is only there to reference a value in\nanother function when called later, and it's not displayed in the UI - meaning it has no need to\ncall for a render when I update it. Also it happens on literally every `update` call as well, so I\nam trying to uselessly mark the frame dirty for re-render twice when one time is enough.\n\nWith that in mind, I can now safely switch the `rollbackState` from a state hook to a `useRef` hook.\n\nWhy would that work? Because a `useRef` reference is always a constant object. But the `current`\nvalue inside that object can be changed. In effect, when we reference a ref value, we can say we are\nsure it is the most recent, as the ref never needs to change and no mismatches can occur.\n\nHere's how it looks after the fix:\n\n```ts\nconst update = useCallback(\n (value: SetStateAction<A>) => {\n const newValue = typeof value === 'function' ? (value as (value: T) => T)(state) : value\n const merged = merge ? merge(state, newValue as A) : (newValue as T)\n rollbackState.current = state\n setState(merged)\n },\n [state],\n)\n\nconst rollback = useCallback(() => {\n if (rollbackState.current == null) {\n return\n }\n setState(rollbackState.current!)\n rollbackState.current = null\n}, [])\n\nconst done = useCallback(() => {\n rollbackState.current = null\n}, [])\n```\n\nThis way, no matter when I call the `rollback` or `done` functions, they will always try to access\nthe same parent object. With that object, they can modify the inner property and we don't need to\nworry about the data being stale.\n\n## Simplifying usage\n\nYou might notice that now you can have a pretty redundant pattern of `try {...} catch() {...}` where\nyou would `rollback` and `done` at the end of each section. Well that's no fun, we want less code.\n\nWhy not add this to our hook?\n\n```ts\nconst run = useCallback(\n async (cb: () => Promise<unknown> | unknown) => {\n try {\n await cb()\n done()\n } catch (_) {\n rollback()\n }\n },\n [done, rollback],\n)\n```\n\nSimple yet elegant. If we export this from the hook:\n\n```ts\nreturn [state, update as React.Dispatch<React.SetStateAction<A>>, { done, rollback, run }]\n```\n\nNow it will now let us do:\n\n```ts\nconst sendComment = async (comment: Comment) => {\n run(async () => {\n addComment(comment)\n await sendCommentToApi(comment)\n })\n}\n```\n\nMuch simpler, right?\n\n## Saving the promise state\n\nOne last thing I want us to add is the ability to know when our state is loading. This way, even\nwithout using `useMutation` from\n[@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query), we can get a loading\nstate of any promise, while working directly with the `useOptimistic` hook instead.\n\nFor that we can simply add a state or a ref (decide which you want depending on your wanted DX),\nwhich sets a `pending` state to `true` and `false` when we update the state, and when we call\ndone/rollback, accordingly.\n\nHere is the full entire hook with all the things we made here:\n\n```ts\ntype StateUpdater<T, A = T> = (currentState: T, newState: A) => T\n\nexport function useOptimistic<T, A = T>(\n initial: T,\n merge?: StateUpdater<T, A>,\n): readonly [\n T,\n React.Dispatch<React.SetStateAction<A>>,\n {\n readonly done: () => void\n readonly rollback: () => void\n readonly pending: boolean\n readonly run: (promise: () => Promise<unknown> | unknown) => void\n },\n] {\n const [state, setState] = useState(initial)\n const rollbackState = useRef<T | null>(null)\n const [pending, setPending] = useState(false)\n\n useEffect(() => {\n setPending(false)\n setState(initial)\n }, [initial])\n\n const update = useCallback(\n (value: SetStateAction<A>) => {\n const newValue = typeof value === 'function' ? (value as (value: T) => T)(state) : value\n const merged = merge ? merge(state, newValue as A) : (newValue as T)\n rollbackState.current = state\n setState(merged)\n setPending(true)\n },\n [state],\n )\n\n const rollback = useCallback(() => {\n if (rollbackState.current == null) {\n // throw new Error('There is no state to roll back to')\n return\n }\n setState(rollbackState.current!)\n rollbackState.current = null\n setPending(false)\n }, [])\n\n const done = useCallback(() => {\n rollbackState.current = null\n setPending(false)\n }, [])\n\n const run = useCallback(\n async (cb: () => Promise<unknown> | unknown) => {\n try {\n await cb()\n done()\n } catch (_) {\n rollback()\n }\n },\n [done, rollback],\n )\n\n return [\n state,\n update as React.Dispatch<React.SetStateAction<A>>,\n { done, rollback, pending, run },\n ] as const\n}\n```\n\n## Where to go from here?\n\nThere are many ways that you can improve this hook, but I feel like it now reached a fairly usable\nstate which we can all build from in our own special ways.\n\nUntil React decides (or decides against) promoting `useOptimistic` to non-canary channels, maybe we\ncan all benefit from a relatively simple hook that does exactly this without having to wait.\n\nI hope you found this article useful or interesting. Feel free to ask questions or suggest\nimprovements in the comments!","filePath":"src/content/post/2023-11-create-your-own-use-optimistic-hook.md","digest":"ec6e1064d17ab547","rendered":{"html":"<p>Recently, I came across <a href=\"https://www.youtube.com/watch?v=M3mGY0pgFk0&t=279s\">this video</a> by Web Dev\nSimplified, which introduced me to React’s new\n<a href=\"https://react.dev/reference/react/useOptimistic\">useOptimistic</a> hook.</p>\n<p>I began to wonder whether this is even needed. React has never been a library to provide more than\nbare-bones controls and hooks. The <code>useCallback</code>, <code>useMemo</code>, and <code>useState</code> hooks are most of the\nbuilt-in hooks and they provide a solid base upon which you can build pretty much any logic inside\nyour components or custom hooks.</p>\n<p>So since when does React provide more high-level abstractions above hooks? At that point, they\nbecome even more opinionated than they already are. Not all use cases are the same - but this <em>is</em> a\npretty generic hook, all things considered. Apparently, it is still in the experimental stage.</p>\n<p>I decided to experiment with creating this hook on my own, to understand if it was overcoming some\ninternal system limitation. I looked at various examples of existing optimistic hooks on NPM, but I\ndidn’t really find one that falls in line with more modern coding styles, or they were completely\noutdated and abandoned.</p>\n<h2 id=\"why-create-your-own\">Why create your own?</h2>\n<p>Ideally, React should generally keep its size down as much as possible, and not increase the bundle\nwith more bloat that won’t be used by everyone. I believe this hook is a very nice idea, but it is\nnot relevant for everyone, and I think if you find it does fit your needs, you don’t have to wait\nfor official support - you can do it (or copy the final code here) in relatively short time, and\nfeature-complete (and added).</p>\n<p>In addition, I thought it would make a good thought experiment and some coding practice, it can’t\nhurt!</p>\n<h2 id=\"looking-at-reacts-original-implementation\">Looking at React’s original implementation</h2>\n<p>I decided to first take a look at React’s official implementation of this. Not the source code, but\nI did want to get a sense of whether I like the consensus they came up with, or whether I feel it\nhas room for improvement.</p>\n<p>Here is a basic usage example, verbatim from\n<a href=\"https://react.dev/reference/react/useOptimistic#use\">React’s docs</a>:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">optimisticState</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">addOptimistic</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> state,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // updateFn</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">currentState</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">optimisticValue</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // merge and return new state</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // with optimistic value</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>Seems nice, but I think we can do better.</p>\n<p>Off the top of my head, I don’t see a way to get the status of the optimistic value. Can I know when\nit’s been finalized without having to maintain a separate state?</p>\n<p>Also, there doesn’t seem to be a way to roll back to the original value if an error has occurred. Or\nat least, the docs don’t yet specify any. We can fix that, and provide a more “holistic” approach,\nwhich in my opinion doesn’t leave the scope of this hook. Say I push an optimistic update, and it\nfails, how can I roll it back?</p>\n<p>We’ll try to make sense of where it should go as we go along.</p>\n<h2 id=\"creating-a-hook\">Creating a hook</h2>\n<p>Let’s start with the signature, and make sure we have our types going for us.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">currentState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">newState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>(</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> initial</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> merge</span><span style=\"color:#F97583\">?:</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> readonly</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">>>] {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We start with the basics. An initial value, of type <code>T</code> which is generic, and an optional merge\nfunction. If it’s not provided by the user, we’ll just have it replace the old state with the new\nwhen an update comes. The <code>merge</code> function can have any type in the argument for the final dispatch,\nbut we assume <code>T</code> unless specified.</p>\n<p>Now we should add the state that maintains the optimistic value.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">>(</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> initial</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> merge</span><span style=\"color:#F97583\">?:</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> readonly</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>] {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">state</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setState</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> [state, setState]</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We… have a very simple hook working right now. It doesn’t really do anything special at all. Very\nbland.</p>\n<p>Ok so, what’s the most basic requirement? Well, for starters, it should let us merge the state if we\nprovide a <code>merge</code> function, which we haven’t use yet from our arguments. Then, we should take into\naccount that updating the parent state (held in <code>initial</code>) should also update the state itself, so\nthat it will be up-to-date with the latest server value.</p>\n<h2 id=\"creating-the-update-functions\">Creating the update functions</h2>\n<p>We’ll start by adding the <code>useEffect</code> hook, that will update from the parent <code>initial</code> prop when it\nupdates.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [initial])</span></span></code></pre>\n<p>Now, when the <code>initial</code> value is updated, the internal state will be kept up-to-date with the\nchange.</p>\n<p>We can also implement the <code>merge</code> functionality. For that, we need to provide another implementation\nof the <code>setState</code> function, which uses it internally and adds some more logic.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> update</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> newValue</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">===</span><span style=\"color:#9ECBFF\"> 'function'</span><span style=\"color:#F97583\"> ?</span><span style=\"color:#E1E4E8\"> (value </span><span style=\"color:#F97583\">as</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)(state) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> merged</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> merge </span><span style=\"color:#F97583\">?</span><span style=\"color:#B392F0\"> merge</span><span style=\"color:#E1E4E8\">(state, newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(merged)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [state],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">return</span><span style=\"color:#E1E4E8\"> [state, update] </span><span style=\"color:#6A737D\">// instead of [state, setState]</span></span></code></pre>\n<p>Okay, it’s a little more to break down here.</p>\n<p>First, we have an identical callback to <code>setState</code> now, except it also runs the <code>merge</code> function\nbefore sending the final value.</p>\n<p>In <code>useState</code>, in addition to passing a new state value, the <code>setState</code> function can be used with a\ncallback function, which takes the current state as a parameter, and should return the new state.\nThe variable <code>newValue</code> is populated with the result of that process.</p>\n<p>Then we just stack the <code>merge</code> function on top of that, if it exists.</p>\n<p>The usage for this would still be:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">count</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCount</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// option 1</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">setCount</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">1</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// option 2</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">setCount</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">c</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> c </span><span style=\"color:#F97583\">+</span><span style=\"color:#79B8FF\"> 1</span><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>All the changes happen internally. Nice.</p>\n<p>That’s pretty much it as it stands right now. React’s implementation is pretty similar to this, with\nmaybe a slight variation on how the change is calculated with the parent’s update, to determine when\nthe value should be brought up to date.</p>\n<p>A bit anti-climactic? Well, let’s add a few bells and whistles. And we can now, because we don’t\nneed React to build it for us. We’re strong!</p>\n<h2 id=\"what-else-can-we-add-rolling-back-on-failure\">What else can we add? Rolling back on failure</h2>\n<p>The first idea for improvement I have is to be able to catch an error, and roll back the value if it\nfails. Let’s begin implementing this.</p>\n<p>First, we need to have a value to go back to. So every time the developer updates the optimistic\nstate to a new value, and an operation related to it fails, we want to provide a way to revert to\nthe original value.</p>\n<p>For a recap, here’s the entire hook as it now is, before we make changes:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">currentState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">newState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">>(</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> initial</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> merge</span><span style=\"color:#F97583\">?:</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> readonly</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>] {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">state</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setState</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [initial])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> update</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> newValue</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">===</span><span style=\"color:#9ECBFF\"> 'function'</span><span style=\"color:#F97583\"> ?</span><span style=\"color:#E1E4E8\"> (value </span><span style=\"color:#F97583\">as</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)(state) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> merged</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> merge </span><span style=\"color:#F97583\">?</span><span style=\"color:#B392F0\"> merge</span><span style=\"color:#E1E4E8\">(state, newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(merged)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [state],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> [state, update]</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now, let’s say the developer comes across a case like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">function</span><span style=\"color:#B392F0\"> MyApp</span><span style=\"color:#E1E4E8\">({ </span><span style=\"color:#FFAB70\">comments</span><span style=\"color:#E1E4E8\"> }) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">oComments</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">addComment</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> comments </span><span style=\"color:#F97583\">||</span><span style=\"color:#E1E4E8\"> [],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">existingComments</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">newComment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">existingComments, newComment],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#B392F0\"> sendComment</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">comment</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Comment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> addComment</span><span style=\"color:#E1E4E8\">(comment)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> sendCommentToApi</span><span style=\"color:#E1E4E8\">(comment) </span><span style=\"color:#6A737D\">// could fail and throw</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#F97583\"> ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>In this case, we are sending some API request to send the comment content to the backend and make\nsure it’s already there.</p>\n<p>But if it fails, we will have to revert it to its original content manually by keeping a separate\nstate for the original value, and set it back if it failed.</p>\n<p>Instead, we can make our new hook handle this.</p>\n<p>Let’s add another state that maintains the previous value:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">rollbackState</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setRollbackState</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useState</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#F97583\"> |</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">>(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>And now that we have that, we should set this rollback value once we set a new value through the\nregular setter:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> update</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> newValue</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">===</span><span style=\"color:#9ECBFF\"> 'function'</span><span style=\"color:#F97583\"> ?</span><span style=\"color:#E1E4E8\"> (value </span><span style=\"color:#F97583\">as</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)(state) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> merged</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> merge </span><span style=\"color:#F97583\">?</span><span style=\"color:#B392F0\"> merge</span><span style=\"color:#E1E4E8\">(state, newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setRollbackState</span><span style=\"color:#E1E4E8\">(state)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(merged)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [state],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>We take this value, save it, and… Do nothing with it. We should probably write that logic\nsomewhere. How’s this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> rollback</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (rollbackState </span><span style=\"color:#F97583\">==</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // you can either return here, or throw an error, and be more strict about false rollbacks:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // throw new Error('There is no state to roll back to')</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(rollbackState</span><span style=\"color:#F97583\">!</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setRollbackState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [rollbackState])</span></span></code></pre>\n<p>And we can also add a <code>done</code> callback, which ditches the retained value, just for clean up.\nSomething like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> done</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setRollbackState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [])</span></span></code></pre>\n<p>Let’s also return these in our hook:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">>(</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> initial</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> merge</span><span style=\"color:#F97583\">?:</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> readonly</span><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> void</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#B392F0\"> done</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> void</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">] {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> [state, update, { rollback, done }]</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Nice, right? We just revert. Now to actually use it, we can update the component:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">oComments</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">addComment</span><span style=\"color:#E1E4E8\">, { </span><span style=\"color:#79B8FF\">done</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">rollback</span><span style=\"color:#E1E4E8\"> }] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ^^^^^^^^^^^^^^^^^^</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> comments </span><span style=\"color:#F97583\">||</span><span style=\"color:#E1E4E8\"> [],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">existingComments</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">newComment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">existingComments, newComment],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> sendComment</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">comment</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Comment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> try</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> addComment</span><span style=\"color:#E1E4E8\">(comment)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> sendCommentToApi</span><span style=\"color:#E1E4E8\">(comment) </span><span style=\"color:#6A737D\">// could fail and throw</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">catch</span><span style=\"color:#E1E4E8\"> (e) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>That’s not too bad. If we fail, we use <code>rollback</code> and get back our previous value, and we never had\nto save it separately, which is good for scaling this to multiple places.</p>\n<p>But… if you give it a go, you will notice a problem.</p>\n<p>Let’s do a small test:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> fail</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">comment</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Comment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> try</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> addComment</span><span style=\"color:#E1E4E8\">(comment)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#F97583\"> new</span><span style=\"color:#79B8FF\"> Promise</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">res</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setTimeout</span><span style=\"color:#E1E4E8\">(res, </span><span style=\"color:#79B8FF\">1000</span><span style=\"color:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> throw</span><span style=\"color:#F97583\"> new</span><span style=\"color:#B392F0\"> Error</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'Oops!'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> done</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">catch</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Make this run somewhere and you will realize the value reverted back to <code>null</code> instead of the old\nvalue, or simply doesn’t roll back. What gives?</p>\n<p>Here is the problem: when we are using <code>rollback</code>, it uses the state variable, <code>rollbackState</code>, to\nset the value back on the main state. But here is the thing - this variable is a dependency in the\ncallback hook on <code>rollback</code>. Take a closer look:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#6A737D\">// before:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// const [rollbackState, setRollbackState] = useState<T | null>(null)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">//</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// after:</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> rollbackState</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useRef</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#F97583\"> |</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">>(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> rollback</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (rollbackState </span><span style=\"color:#F97583\">==</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(rollbackState</span><span style=\"color:#F97583\">!</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setRollbackState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [rollbackState])</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// ^^^^^^^^^^^^^</span></span></code></pre>\n<p>So why is that bad? Well, in our <code>fail</code> test above, we used <code>addComment</code> which is our <code>update</code>\nfunction with a fancy name. When the API call fails we call <code>rollback</code>. But <code>rollback</code> was created\nbound to the <code>rollbackState</code> that was there at the time of its creation. We <code>wait</code> for a promise to\nreturn, and then use the <code>done</code> or <code>rollback</code> functions, that were created <strong>before</strong> the\n<code>rollbackState</code> was even set away from <code>null</code> and into a value in the first place. So basically we\nare re-using the value from before we put anything in it, which means it’s <code>null</code>, which is the\nvalue we roll back to when using <code>rollback</code>.</p>\n<p>How do we fix this?</p>\n<h2 id=\"fixing-the-stale-value\">Fixing the stale value</h2>\n<p>There are probably many possible solutions, but here is my very naive one.</p>\n<p>How do I make sure I get the correct reference to the updated value?</p>\n<p>In reality, I don’t need <code>rollbackState</code> to be an actual state. Why can I say that with confidence?\nWell, I don’t need to call a re-render when it updates, as it is only there to reference a value in\nanother function when called later, and it’s not displayed in the UI - meaning it has no need to\ncall for a render when I update it. Also it happens on literally every <code>update</code> call as well, so I\nam trying to uselessly mark the frame dirty for re-render twice when one time is enough.</p>\n<p>With that in mind, I can now safely switch the <code>rollbackState</code> from a state hook to a <code>useRef</code> hook.</p>\n<p>Why would that work? Because a <code>useRef</code> reference is always a constant object. But the <code>current</code>\nvalue inside that object can be changed. In effect, when we reference a ref value, we can say we are\nsure it is the most recent, as the ref never needs to change and no mismatches can occur.</p>\n<p>Here’s how it looks after the fix:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> update</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> newValue</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">===</span><span style=\"color:#9ECBFF\"> 'function'</span><span style=\"color:#F97583\"> ?</span><span style=\"color:#E1E4E8\"> (value </span><span style=\"color:#F97583\">as</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)(state) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> merged</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> merge </span><span style=\"color:#F97583\">?</span><span style=\"color:#B392F0\"> merge</span><span style=\"color:#E1E4E8\">(state, newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> state</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(merged)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [state],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> rollback</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (rollbackState.current </span><span style=\"color:#F97583\">==</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(rollbackState.current</span><span style=\"color:#F97583\">!</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> null</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> done</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> null</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [])</span></span></code></pre>\n<p>This way, no matter when I call the <code>rollback</code> or <code>done</code> functions, they will always try to access\nthe same parent object. With that object, they can modify the inner property and we don’t need to\nworry about the data being stale.</p>\n<h2 id=\"simplifying-usage\">Simplifying usage</h2>\n<p>You might notice that now you can have a pretty redundant pattern of <code>try {...} catch() {...}</code> where\nyou would <code>rollback</code> and <code>done</code> at the end of each section. Well that’s no fun, we want less code.</p>\n<p>Why not add this to our hook?</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> run</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#B392F0\">cb</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">unknown</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">|</span><span style=\"color:#79B8FF\"> unknown</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> try</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> cb</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> done</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">catch</span><span style=\"color:#E1E4E8\"> (_) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [done, rollback],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>Simple yet elegant. If we export this from the hook:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">return</span><span style=\"color:#E1E4E8\"> [state, update </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>, { done, rollback, run }]</span></span></code></pre>\n<p>Now it will now let us do:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> sendComment</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">comment</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> Comment</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> run</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#F97583\">async</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> addComment</span><span style=\"color:#E1E4E8\">(comment)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> sendCommentToApi</span><span style=\"color:#E1E4E8\">(comment)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Much simpler, right?</p>\n<h2 id=\"saving-the-promise-state\">Saving the promise state</h2>\n<p>One last thing I want us to add is the ability to know when our state is loading. This way, even\nwithout using <code>useMutation</code> from\n<a href=\"https://www.npmjs.com/package/@tanstack/react-query\">@tanstack/react-query</a>, we can get a loading\nstate of any promise, while working directly with the <code>useOptimistic</code> hook instead.</p>\n<p>For that we can simply add a state or a ref (decide which you want depending on your wanted DX),\nwhich sets a <code>pending</code> state to <code>true</code> and <code>false</code> when we update the state, and when we call\ndone/rollback, accordingly.</p>\n<p>Here is the full entire hook with all the things we made here:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">currentState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">newState</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> useOptimistic</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">>(</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> initial</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> merge</span><span style=\"color:#F97583\">?:</span><span style=\"color:#B392F0\"> StateUpdater</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> readonly</span><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#B392F0\"> done</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> void</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> void</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#FFAB70\"> pending</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> boolean</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> readonly</span><span style=\"color:#B392F0\"> run</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#B392F0\">promise</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">unknown</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">|</span><span style=\"color:#79B8FF\"> unknown</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> void</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">] {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">state</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setState</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> rollbackState</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useRef</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">T</span><span style=\"color:#F97583\"> |</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">>(</span><span style=\"color:#79B8FF\">null</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">pending</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setPending</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setPending</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(initial)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [initial])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> update</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> newValue</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> typeof</span><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">===</span><span style=\"color:#9ECBFF\"> 'function'</span><span style=\"color:#F97583\"> ?</span><span style=\"color:#E1E4E8\"> (value </span><span style=\"color:#F97583\">as</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">value</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)(state) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> merged</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> merge </span><span style=\"color:#F97583\">?</span><span style=\"color:#B392F0\"> merge</span><span style=\"color:#E1E4E8\">(state, newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> A</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> T</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> state</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(merged)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setPending</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">true</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [state],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> rollback</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (rollbackState.current </span><span style=\"color:#F97583\">==</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // throw new Error('There is no state to roll back to')</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(rollbackState.current</span><span style=\"color:#F97583\">!</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> null</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setPending</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> done</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rollbackState.current </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> null</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setPending</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> run</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> async</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#B392F0\">cb</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> Promise</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">unknown</span><span style=\"color:#E1E4E8\">> </span><span style=\"color:#F97583\">|</span><span style=\"color:#79B8FF\"> unknown</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> try</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> cb</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> done</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">catch</span><span style=\"color:#E1E4E8\"> (_) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> rollback</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [done, rollback],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> state,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> update </span><span style=\"color:#F97583\">as</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Dispatch</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">SetStateAction</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#B392F0\">A</span><span style=\"color:#E1E4E8\">>>,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> { done, rollback, pending, run },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ] </span><span style=\"color:#F97583\">as</span><span style=\"color:#F97583\"> const</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<h2 id=\"where-to-go-from-here\">Where to go from here?</h2>\n<p>There are many ways that you can improve this hook, but I feel like it now reached a fairly usable\nstate which we can all build from in our own special ways.</p>\n<p>Until React decides (or decides against) promoting <code>useOptimistic</code> to non-canary channels, maybe we\ncan all benefit from a relatively simple hook that does exactly this without having to wait.</p>\n<p>I hope you found this article useful or interesting. Feel free to ask questions or suggest\nimprovements in the comments!</p>","metadata":{"headings":[{"depth":2,"slug":"why-create-your-own","text":"Why create your own?"},{"depth":2,"slug":"looking-at-reacts-original-implementation","text":"Looking at React’s original implementation"},{"depth":2,"slug":"creating-a-hook","text":"Creating a hook"},{"depth":2,"slug":"creating-the-update-functions","text":"Creating the update functions"},{"depth":2,"slug":"what-else-can-we-add-rolling-back-on-failure","text":"What else can we add? Rolling back on failure"},{"depth":2,"slug":"fixing-the-stale-value","text":"Fixing the stale value"},{"depth":2,"slug":"simplifying-usage","text":"Simplifying usage"},{"depth":2,"slug":"saving-the-promise-state","text":"Saving the promise state"},{"depth":2,"slug":"where-to-go-from-here","text":"Where to go from here?"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Create your own useOptimistic hook","date":"2023-11-09 23:38:07 +0200","tags":"react javascript typescript tutorial development","image":"/images/post_covers/react-use-optimistic.jpg","imageAttribution":"Image by \n<a href=\"https://www.freepik.com/free-photo/unhappy-offended-woman-stands-with-arms-crossed-doesnt-speak-husband-joyful-bearded-man-has-dark-skin-giggles-positively-something-funny-expresses-positive-emotions-people-reaction_13580391.htm\">\n wayhomestudio\n</a>\non Freepik"},"imagePaths":[]}},"collection":"post","slug":"2023-11-create-your-own-use-optimistic-hook"},"content":{"headings":[{"depth":2,"slug":"why-create-your-own","text":"Why create your own?"},{"depth":2,"slug":"looking-at-reacts-original-implementation","text":"Looking at React’s original implementation"},{"depth":2,"slug":"creating-a-hook","text":"Creating a hook"},{"depth":2,"slug":"creating-the-update-functions","text":"Creating the update functions"},{"depth":2,"slug":"what-else-can-we-add-rolling-back-on-failure","text":"What else can we add? Rolling back on failure"},{"depth":2,"slug":"fixing-the-stale-value","text":"Fixing the stale value"},{"depth":2,"slug":"simplifying-usage","text":"Simplifying usage"},{"depth":2,"slug":"saving-the-promise-state","text":"Saving the promise state"},{"depth":2,"slug":"where-to-go-from-here","text":"Where to go from here?"}],"remarkPluginFrontmatter":{"title":"Create your own useOptimistic hook","date":"2023-11-09 23:38:07 +0200","tags":"react javascript typescript tutorial development","image":"/images/post_covers/react-use-optimistic.jpg","imageAttribution":"Image by \n<a href=\"https://www.freepik.com/free-photo/unhappy-offended-woman-stands-with-arms-crossed-doesnt-speak-husband-joyful-bearded-man-has-dark-skin-giggles-positively-something-funny-expresses-positive-emotions-people-reaction_13580391.htm\">\n wayhomestudio\n</a>\non Freepik"}}},{"params":{"slug":"2023-11-why-tooling-is-important"},"props":{"id":"2023-11-why-tooling-is-important.md","data":{"title":"Why tooling is important","showTitle":true,"image":"/images/post_covers/tooling.jpg","imageAttribution":"Background by \n<a \n href=\"https://www.freepik.com/free-vector/game-landscape-with-tropical-plants_9886200.htm#query=cartoon%20jungle&position=0&from_view=keyword&track=ais\">\n valadzionak_volha\n</a>\non Freepik","includeLowResHero":false,"prose":true,"date":"2023-11-05T21:37:25.000Z","tags":"development tool","status":"hidden"},"body":"<!-- I am seriously lazy. It sometimes affects my life, and not in a great way. But one thing I know for\nsure, is that is serves me extremely well in my work as a Full Stack™ developer. Let me\nexplain why.\n\nSoftware takes time. No change is 0 seconds to make. With that in mind, I still prefer to spend as\nlittle time doing as much as possible, in order to min-max my effort, get something done (and well),\nand go back to whatever it is I want to do otherwise in my life, or another task.\n\nThis has caused me to spend countless hours tweaking my setup. Writing one-off functions that are\ntoo-specific to actually be used, or some that are actually useful. Writing a script that I imagined\nwould be run every week or so and ends up being touched maybe twice.\n\nAll of these are failures. Or are they? Every file I made, every second I \"wasted\" ended up giving\nme more than taking in the end: The lessons I learned making those mistakes, finding out whether my\nsolution worked or not, discovering bugs and fixing along the way, writing bugs in that same script\nand learning to avoid them later on.\n\nBut more importantly than all of those - is the macro understanding of what's important to spend\ntime on, what's not, and what time I can spend to avoid spending more of it later.\n\n## Tooling is sometimes under-used, or ignored\n\nWhenever I have to dabble with Python, I learn the same painful lesson. The tooling sucks. And it's\nnot until you have something you truly hate, to remind you of what you love and what you're missing\nright now.\n\nInstalling packages, making sure you have up to date dependencies, these are things you will\ninevitably do from time to time. And it's important that you have as little active time working on\nusing those tools as possible. Ideally, you hit <kbd>Enter</kbd> and go do something else entirely.\n\nThese things should be trivial, and yet they're not. Why? If one language has it down, others can\nfollow suit. Yet, Python is stuck somewhere in 2012 with the quality of the package ecosystem when\nyou actually need to get a project working where you aren't the first developer on it. You will\nstruggle, you will install the wrong stuff, others won't install at all because it's not listed\nproperly, versioning is willy-nilly and people don't lock versions for stuff. It's so frustrating.\n\nIt might be the actual developers making those mistakes, and there might be better ways to do these\nthings. But after trying it out several times myself, I realized the user is always stupid, and the\ntool should always be right. That's why it's on a computer. If I make a calculator, for example, I\nshould make it auto-close parenthesis if the user forgot. The same goes for tools. While there is a\nlot to be said for learning to develop the hard way, it shouldn't be the same approach when using\nbuild tools. They should just work, and let you get to your actual code fast. But for some reason,\nit's not trivial to do these simple things, and you end up doing devops engineering just to get your\nwork project to run locally without error.\n\n## DX is a buzzword, and it's the only one I like\n\nDeveloper experience, commonly abbreviated DX (in contrast to user experience, or UX), has been a\nterm floating around the internet in the last few years.\n\nSome people say it's a fancy word for making things clear, and some say it is the most important\ntask in the entire world. Neither of them are entirely right or entirely wrong. Like with\neverything, the truth is subjective, and lands somewhere in the middle.\n\nPersonally, I think that more than a buzzword, and more than just meaning \"make stuff easy to use\nfor devs\". There is a philosophy that stands behind it. And somewhere in that philosophy, is the\nunderstanding that in order to be a good tool, you need not to only be easy to use and clear, but\nyou have to design your tool for building up success.\n\nWhat does that mean?\n\nIt means you need to care for the way you design your user's (the dev's) experience using that tool.\nOn top of making easy abstractions for your functions, I believe you need to embrace making it\nactually friendly, and let your tool remember that while it's powerful, a person has to use it.\n\nErrors need to be extremely clear, and indicate where the problem is. Things should be color coded.\nTell me what you're doing. Enforce sensible defaults, and let us easily change them when needed.\n\nIt's like in the chase of being un-opinionated, sometimes, things are made with so many variations,\nnone can be fully accepted and the fighting will go on between them forever. It's okay to embrace\nthe user's wishes and try to find ways to make them happen. It's okay to maybe add some default that\n99% of the users would use anyway, instead of requiring 99% of users to make mistakes in order to\nreach that value on their own. This isn't programming - there shouldn't be a skill for using a basic\ntool that installs stuff, or generates some files. It should be some dry knowledge, that is somewhat\nuniversal, that is easy to apply and remember.\n\n## Not just build tools\n-->\n\n## Good tools can be a learning tool","filePath":"src/content/post/2023-11-why-tooling-is-important.md","digest":"fa59555e315c735b","rendered":{"html":"<!-- I am seriously lazy. It sometimes affects my life, and not in a great way. But one thing I know for\nsure, is that is serves me extremely well in my work as a Full Stack™ developer. Let me\nexplain why.\n\nSoftware takes time. No change is 0 seconds to make. With that in mind, I still prefer to spend as\nlittle time doing as much as possible, in order to min-max my effort, get something done (and well),\nand go back to whatever it is I want to do otherwise in my life, or another task.\n\nThis has caused me to spend countless hours tweaking my setup. Writing one-off functions that are\ntoo-specific to actually be used, or some that are actually useful. Writing a script that I imagined\nwould be run every week or so and ends up being touched maybe twice.\n\nAll of these are failures. Or are they? Every file I made, every second I \"wasted\" ended up giving\nme more than taking in the end: The lessons I learned making those mistakes, finding out whether my\nsolution worked or not, discovering bugs and fixing along the way, writing bugs in that same script\nand learning to avoid them later on.\n\nBut more importantly than all of those - is the macro understanding of what's important to spend\ntime on, what's not, and what time I can spend to avoid spending more of it later.\n\n## Tooling is sometimes under-used, or ignored\n\nWhenever I have to dabble with Python, I learn the same painful lesson. The tooling sucks. And it's\nnot until you have something you truly hate, to remind you of what you love and what you're missing\nright now.\n\nInstalling packages, making sure you have up to date dependencies, these are things you will\ninevitably do from time to time. And it's important that you have as little active time working on\nusing those tools as possible. Ideally, you hit <kbd>Enter</kbd> and go do something else entirely.\n\nThese things should be trivial, and yet they're not. Why? If one language has it down, others can\nfollow suit. Yet, Python is stuck somewhere in 2012 with the quality of the package ecosystem when\nyou actually need to get a project working where you aren't the first developer on it. You will\nstruggle, you will install the wrong stuff, others won't install at all because it's not listed\nproperly, versioning is willy-nilly and people don't lock versions for stuff. It's so frustrating.\n\nIt might be the actual developers making those mistakes, and there might be better ways to do these\nthings. But after trying it out several times myself, I realized the user is always stupid, and the\ntool should always be right. That's why it's on a computer. If I make a calculator, for example, I\nshould make it auto-close parenthesis if the user forgot. The same goes for tools. While there is a\nlot to be said for learning to develop the hard way, it shouldn't be the same approach when using\nbuild tools. They should just work, and let you get to your actual code fast. But for some reason,\nit's not trivial to do these simple things, and you end up doing devops engineering just to get your\nwork project to run locally without error.\n\n## DX is a buzzword, and it's the only one I like\n\nDeveloper experience, commonly abbreviated DX (in contrast to user experience, or UX), has been a\nterm floating around the internet in the last few years.\n\nSome people say it's a fancy word for making things clear, and some say it is the most important\ntask in the entire world. Neither of them are entirely right or entirely wrong. Like with\neverything, the truth is subjective, and lands somewhere in the middle.\n\nPersonally, I think that more than a buzzword, and more than just meaning \"make stuff easy to use\nfor devs\". There is a philosophy that stands behind it. And somewhere in that philosophy, is the\nunderstanding that in order to be a good tool, you need not to only be easy to use and clear, but\nyou have to design your tool for building up success.\n\nWhat does that mean?\n\nIt means you need to care for the way you design your user's (the dev's) experience using that tool.\nOn top of making easy abstractions for your functions, I believe you need to embrace making it\nactually friendly, and let your tool remember that while it's powerful, a person has to use it.\n\nErrors need to be extremely clear, and indicate where the problem is. Things should be color coded.\nTell me what you're doing. Enforce sensible defaults, and let us easily change them when needed.\n\nIt's like in the chase of being un-opinionated, sometimes, things are made with so many variations,\nnone can be fully accepted and the fighting will go on between them forever. It's okay to embrace\nthe user's wishes and try to find ways to make them happen. It's okay to maybe add some default that\n99% of the users would use anyway, instead of requiring 99% of users to make mistakes in order to\nreach that value on their own. This isn't programming - there shouldn't be a skill for using a basic\ntool that installs stuff, or generates some files. It should be some dry knowledge, that is somewhat\nuniversal, that is easy to apply and remember.\n\n## Not just build tools\n-->\n<h2 id=\"good-tools-can-be-a-learning-tool\">Good tools can be a learning tool</h2>","metadata":{"headings":[{"depth":2,"slug":"good-tools-can-be-a-learning-tool","text":"Good tools can be a learning tool"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Why tooling is important","date":"2023-11-05 23:37:25 +0200","tags":"development tool","status":"hidden","image":"/images/post_covers/tooling.jpg","imageAttribution":"Background by \n<a \n href=\"https://www.freepik.com/free-vector/game-landscape-with-tropical-plants_9886200.htm#query=cartoon%20jungle&position=0&from_view=keyword&track=ais\">\n valadzionak_volha\n</a>\non Freepik"},"imagePaths":[]}},"collection":"post","slug":"2023-11-why-tooling-is-important"},"content":{"headings":[{"depth":2,"slug":"good-tools-can-be-a-learning-tool","text":"Good tools can be a learning tool"}],"remarkPluginFrontmatter":{"title":"Why tooling is important","date":"2023-11-05 23:37:25 +0200","tags":"development tool","status":"hidden","image":"/images/post_covers/tooling.jpg","imageAttribution":"Background by \n<a \n href=\"https://www.freepik.com/free-vector/game-landscape-with-tropical-plants_9886200.htm#query=cartoon%20jungle&position=0&from_view=keyword&track=ais\">\n valadzionak_volha\n</a>\non Freepik"}}},{"params":{"slug":"2023-11-the-simplicity-of-astro"},"props":{"id":"2023-11-the-simplicity-of-astro.md","data":{"title":"The simplicity of Astro","showTitle":true,"includeLowResHero":false,"prose":true,"date":"2023-11-04T22:15:52.000Z","tags":"","status":"hidden"},"body":"Recently I've decided to undertake redesigning this website. After years of using Jekyll to serve\nstatic websites, I have been looking for a change. A more modern way to serve my static sites, which\nwill make it easier and more predictable to host sites for my projects, this blog, and any future\nendeavors on the web which might not require a full-blown app or server.\n\n[Jekyll](https://jekyllrb.com/) is great - there is community support with years of questions and\nanswers to look up, many plugins for various uses, and themes to make your site look nice and\npretty. That said, it has begun to show its age. It's becoming hard to incorporate newer libraries\nsuch as [Tailwind](https://tailwindcss.com), it's not very fun to generate dynamic content using\nliquid templates, it's up to you to handle your styling composition and scopes, and the whole build\nsystem is just a bit outdated, requiring old-school manual work where automation is ripe in other\nweb frameworks.\n\nThat is not to downplay Jekyll - I still love it, and have used it for several years exactly because\nit was relatively easy to set up, it was easy at the time to get a theme running and customize it a\nbit. Using Markdown files to generate content on your site is simply a brilliant idea. Who wants a\ngiant tree of `<span>`s and `<div>`s for a simple blog article? This also opens up the possibility\nto use many more applications for writing the posts themselves, as Markdown is exportale to from\nmany applications.\n\nOn top of all that, GitHub works with it out-of-the-box, so it's incredibly simple to spin up a\nrepository, and be able to place some dynamic code for some static output. It's great!\n\n## Do less to create more\n\nAnd still, it was time to move on. I wanted to spend less time finding out where I was missing some\nliquid syntax. I wanted to use Prettier to format stuff, and I wanted templated sections to be\nsyntax highlighted. I have become spoiled because of React, and using JSX would also be a bonus to\nme.\n\nI began questioning, why can't I just use React to spin up a static website? No SSR with smart\ncaching (love you, [Next.js](https://nextjs.org)), no single page application where I need various\nhacks to get routing to properly work. Just let me write React-like stuff and get some HTML with JS\nprops in them. That's really all I'm asking.\n\n## And Astro Delivered\n\nI have heard about it a while ago from listening to some frontend related podcasts. I didn't really\nget the point of it, till I decided to give it a go one evening. And it's just what I was looking\nfor.\n\nIt's a simple, easy to support and understand template syntax, which gives me both the freedom of\nusing TS/JS to develop my logic, and the composable nature of React, while still delivering an\nincredibly fast static web application which **just works**.\n\nLong-gone are the days of formatters jumbling up every time I add a `{%if}` to my template.\nLong-gone are the days of including whole page sections, and needing to override global variables\njust to get a semblance of the ability to re-use the same code easily with different data, without\nall the hacking.\n\n## What is simple?\n\nWhen I first tried Astro, I was astounded at how little I needed to learn to get going.\n\nThe routing is a breeze. Creating collections of data was incredibly easy. Adding complex dynamic\npermalinks, using logic-filled templates, it's all just simple Node-like code that you put on top of\nthe page you're handling.\n\nBut it goes beyond this. Astro gives you just enough to get the information you need from the\ncontext of the page, and spit it out in various forms.\n\nWhen you create a list of pages that represent posts, you don't do it in a magic \"\\_posts\"\ndirectory - you define that yourself. And it's just an array you export from a function, with the\ndata for the params of the page URL - that's it. Here's how generating posts look for a blog like\nthis:\n\n```ts\nexport async function getStaticPaths() {\n const pages = await getCollection('page')\n return pages.map((page) => ({\n params: { slug: page.slug },\n props: page,\n }))\n}\ntype Props = CollectionEntry<'post'>\nconst page = Astro.props\nconst { Content } = await page.render()\n```\n\nYep... That's it. You get an array of posts. You put the URL params and the props it gets. That's\nit.\n\nI have implemented a tag directory, a projects directory, and pagination within a small amount of\nhours, totalling less than a work day.\n\n**The Astro team heavily invested time in making sure developers don't need to write a lot of\nboilerplate.**\n\nSure, the templates are usable only for Astro. But the content in those templates is almost\nbare-bones TS, and the methods for configuring and getting data of various types from all your\ngenerated pages is abstracted so simply that you barely use more than 1 function to get all your\nstuff done for an entire page collection.\n\nComparing to Jekyll, I remember wanting to add an automatic tags collection system, which generates\na tags list, and a list of posts for each tag. This required me to go and find out how to build\nplugins for Jekyll, so I can hook into the build-step, and get that code to operate while I run the\ndev server.\n\nIn contrast, when using Astro it is not only implied you define how your routes and information gets\nbuilt, it's a requirement for setting it up - there is no inherent \"\\_posts\" directory which is the\nmain entry point for blogs, and there is not much magic going on. You write, in the page that lays\nout the post template, exactly what pages you want to have.\n\nThen showing them all on the front page is even easier, like so:\n\n```ts\nconst allPosts = (await getCollection('post')).sort(\n (a, b) => b.data.date.valueOf() - a.data.date.valueOf(),\n)\nconst posts = allPosts.slice(0, PAGE_SIZE)\n```\n\nAnd you have a paginated list of posts. It's so easy! After this, you look over them in JSX. No\nliquid, no Handlebars, just JSX, which is already so widely supported.\n\n```tsx\n<div>\n {posts.map((post) => (\n <article>...</article>\n ))}\n</div>\n```\n\n## It's the philosophy\n\nEverything in Astro is just as simple. Scoping your CSS is no longer your concern - just add a\n`<style>` element, put whatever you need in it (transpilation supported!), and you treat it like a\nwhole page. You don't worry about class names, you re-use them as you want and not worry about\nconflicts in other places on the same page. They are auto scoped.\n\nIt really is a symptom of the best disease. I don't want to work hard. I want to write simple HTML\nor Markdown, and when I need just a tiny bit of logic, I can just put it there, and it took care of\nthe annoying stuff for me.","filePath":"src/content/post/2023-11-the-simplicity-of-astro.md","digest":"8b74f88bbc585da8","rendered":{"html":"<p>Recently I’ve decided to undertake redesigning this website. After years of using Jekyll to serve\nstatic websites, I have been looking for a change. A more modern way to serve my static sites, which\nwill make it easier and more predictable to host sites for my projects, this blog, and any future\nendeavors on the web which might not require a full-blown app or server.</p>\n<p><a href=\"https://jekyllrb.com/\">Jekyll</a> is great - there is community support with years of questions and\nanswers to look up, many plugins for various uses, and themes to make your site look nice and\npretty. That said, it has begun to show its age. It’s becoming hard to incorporate newer libraries\nsuch as <a href=\"https://tailwindcss.com\">Tailwind</a>, it’s not very fun to generate dynamic content using\nliquid templates, it’s up to you to handle your styling composition and scopes, and the whole build\nsystem is just a bit outdated, requiring old-school manual work where automation is ripe in other\nweb frameworks.</p>\n<p>That is not to downplay Jekyll - I still love it, and have used it for several years exactly because\nit was relatively easy to set up, it was easy at the time to get a theme running and customize it a\nbit. Using Markdown files to generate content on your site is simply a brilliant idea. Who wants a\ngiant tree of <code><span></code>s and <code><div></code>s for a simple blog article? This also opens up the possibility\nto use many more applications for writing the posts themselves, as Markdown is exportale to from\nmany applications.</p>\n<p>On top of all that, GitHub works with it out-of-the-box, so it’s incredibly simple to spin up a\nrepository, and be able to place some dynamic code for some static output. It’s great!</p>\n<h2 id=\"do-less-to-create-more\">Do less to create more</h2>\n<p>And still, it was time to move on. I wanted to spend less time finding out where I was missing some\nliquid syntax. I wanted to use Prettier to format stuff, and I wanted templated sections to be\nsyntax highlighted. I have become spoiled because of React, and using JSX would also be a bonus to\nme.</p>\n<p>I began questioning, why can’t I just use React to spin up a static website? No SSR with smart\ncaching (love you, <a href=\"https://nextjs.org\">Next.js</a>), no single page application where I need various\nhacks to get routing to properly work. Just let me write React-like stuff and get some HTML with JS\nprops in them. That’s really all I’m asking.</p>\n<h2 id=\"and-astro-delivered\">And Astro Delivered</h2>\n<p>I have heard about it a while ago from listening to some frontend related podcasts. I didn’t really\nget the point of it, till I decided to give it a go one evening. And it’s just what I was looking\nfor.</p>\n<p>It’s a simple, easy to support and understand template syntax, which gives me both the freedom of\nusing TS/JS to develop my logic, and the composable nature of React, while still delivering an\nincredibly fast static web application which <strong>just works</strong>.</p>\n<p>Long-gone are the days of formatters jumbling up every time I add a <code>{%if}</code> to my template.\nLong-gone are the days of including whole page sections, and needing to override global variables\njust to get a semblance of the ability to re-use the same code easily with different data, without\nall the hacking.</p>\n<h2 id=\"what-is-simple\">What is simple?</h2>\n<p>When I first tried Astro, I was astounded at how little I needed to learn to get going.</p>\n<p>The routing is a breeze. Creating collections of data was incredibly easy. Adding complex dynamic\npermalinks, using logic-filled templates, it’s all just simple Node-like code that you put on top of\nthe page you’re handling.</p>\n<p>But it goes beyond this. Astro gives you just enough to get the information you need from the\ncontext of the page, and spit it out in various forms.</p>\n<p>When you create a list of pages that represent posts, you don’t do it in a magic “_posts”\ndirectory - you define that yourself. And it’s just an array you export from a function, with the\ndata for the params of the page URL - that’s it. Here’s how generating posts look for a blog like\nthis:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> async</span><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> getStaticPaths</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> pages</span><span style=\"color:#F97583\"> =</span><span style=\"color:#F97583\"> await</span><span style=\"color:#B392F0\"> getCollection</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'page'</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> pages.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">page</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> params: { slug: page.slug },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> props: page,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">type</span><span style=\"color:#B392F0\"> Props</span><span style=\"color:#F97583\"> =</span><span style=\"color:#B392F0\"> CollectionEntry</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#9ECBFF\">'post'</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> page</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> Astro.props</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">Content</span><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">=</span><span style=\"color:#F97583\"> await</span><span style=\"color:#E1E4E8\"> page.</span><span style=\"color:#B392F0\">render</span><span style=\"color:#E1E4E8\">()</span></span></code></pre>\n<p>Yep… That’s it. You get an array of posts. You put the URL params and the props it gets. That’s\nit.</p>\n<p>I have implemented a tag directory, a projects directory, and pagination within a small amount of\nhours, totalling less than a work day.</p>\n<p><strong>The Astro team heavily invested time in making sure developers don’t need to write a lot of\nboilerplate.</strong></p>\n<p>Sure, the templates are usable only for Astro. But the content in those templates is almost\nbare-bones TS, and the methods for configuring and getting data of various types from all your\ngenerated pages is abstracted so simply that you barely use more than 1 function to get all your\nstuff done for an entire page collection.</p>\n<p>Comparing to Jekyll, I remember wanting to add an automatic tags collection system, which generates\na tags list, and a list of posts for each tag. This required me to go and find out how to build\nplugins for Jekyll, so I can hook into the build-step, and get that code to operate while I run the\ndev server.</p>\n<p>In contrast, when using Astro it is not only implied you define how your routes and information gets\nbuilt, it’s a requirement for setting it up - there is no inherent “_posts” directory which is the\nmain entry point for blogs, and there is not much magic going on. You write, in the page that lays\nout the post template, exactly what pages you want to have.</p>\n<p>Then showing them all on the front page is even easier, like so:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ts\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> allPosts</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#F97583\">await</span><span style=\"color:#B392F0\"> getCollection</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'post'</span><span style=\"color:#E1E4E8\">)).</span><span style=\"color:#B392F0\">sort</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">a</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">b</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> b.data.date.</span><span style=\"color:#B392F0\">valueOf</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> a.data.date.</span><span style=\"color:#B392F0\">valueOf</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> posts</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> allPosts.</span><span style=\"color:#B392F0\">slice</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">PAGE_SIZE</span><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>And you have a paginated list of posts. It’s so easy! After this, you look over them in JSX. No\nliquid, no Handlebars, just JSX, which is already so widely supported.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"><</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {posts.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">post</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">article</span><span style=\"color:#E1E4E8\">>...</</span><span style=\"color:#85E89D\">article</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ))}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"></</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span></code></pre>\n<h2 id=\"its-the-philosophy\">It’s the philosophy</h2>\n<p>Everything in Astro is just as simple. Scoping your CSS is no longer your concern - just add a\n<code><style></code> element, put whatever you need in it (transpilation supported!), and you treat it like a\nwhole page. You don’t worry about class names, you re-use them as you want and not worry about\nconflicts in other places on the same page. They are auto scoped.</p>\n<p>It really is a symptom of the best disease. I don’t want to work hard. I want to write simple HTML\nor Markdown, and when I need just a tiny bit of logic, I can just put it there, and it took care of\nthe annoying stuff for me.</p>","metadata":{"headings":[{"depth":2,"slug":"do-less-to-create-more","text":"Do less to create more"},{"depth":2,"slug":"and-astro-delivered","text":"And Astro Delivered"},{"depth":2,"slug":"what-is-simple","text":"What is simple?"},{"depth":2,"slug":"its-the-philosophy","text":"It’s the philosophy"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"The simplicity of Astro","date":"2023-11-05 00:15:52 +0200","tags":"","status":"hidden"},"imagePaths":[]}},"collection":"post","slug":"2023-11-the-simplicity-of-astro"},"content":{"headings":[{"depth":2,"slug":"do-less-to-create-more","text":"Do less to create more"},{"depth":2,"slug":"and-astro-delivered","text":"And Astro Delivered"},{"depth":2,"slug":"what-is-simple","text":"What is simple?"},{"depth":2,"slug":"its-the-philosophy","text":"It’s the philosophy"}],"remarkPluginFrontmatter":{"title":"The simplicity of Astro","date":"2023-11-05 00:15:52 +0200","tags":"","status":"hidden"}}},{"params":{"slug":"2022-08-react-using-use-effect-effectively"},"props":{"id":"2022-08-react-using-use-effect-effectively.md","data":{"title":"React: Using useEffect Effectively","showTitle":true,"image":"/images/post_covers/react-use-effect.jpg","includeLowResHero":false,"prose":true,"date":"2022-08-18T21:00:00.000Z","tags":"react tutorial javascript typescript development","status":"published"},"body":"Whether you're new to React or you are recently migrating from class components (this late?), you\nmight not be familiar with how `useEffect` works.\n\nActually, you might be using it still from other existing (or copied 😉) code, but have no idea (or\njust a small idea) what's actually happening.\n\nIn this post we'll dive in to this very flexible hook, some example, and some real, common\nuse-cases.\n\n<!-- more -->\n\n## What is `React.useEffect`?\n\nThis is a React hook that lets you introduce **side-effects** into your components.\n\nHere is some example code of a common `useEffect` hook:\n\n```tsx\nconst MyComponent = () => {\n const [posts, setPosts] = React.useState([])\n\n // This will fire only once when the component mounts\n React.useEffect(() => {\n PostsAPI.getPostsList().then((data) => setPosts(data))\n }, [])\n return <div>{JSON.stringify(posts)}</div>\n}\n```\n\n### What are side effects?\n\nWell, side effects are a general programming term, under the larger term \"effects\". Effects have a\nwide meaning, and side-effects are a subset of that. The short version of the definition of a\nside-effect is \"a function (or piece of code) that introduces changes to any value that is not\ninside its scope\" - whether input arguments, or outside scoped variables (or global variables).\n\nIn React components, that mostly means any function inside a component that introduces a change that\nis outside the scope of rendering itself.\n\nAre you calling an outside API? You are introducing an effect. Saving a value to the state?\nIntroduces an effect. _As long as it's outside the scope of rendering the component itself, it's a\nside-effect._\n\nSide-effects **are not bad per-se** - but introducing them needs to be done with intent, and with\ncaution.\n\n### React side-effects example\n\n`React.useEffect` is a hook that lets you wrap side-effects in a safe way so that you can avoid\nunnecessary renders, and use the necessary ones correctly. Consider the following code:\n\n```tsx\nconst MyComponent = () => {\n const [count, setCount] = React.useState(0)\n\n CounterAPI.getCount().then((data) => setCount(data))\n\n return <div>{count}</div>\n}\n```\n\nIf you haven't noticed th the problem at first glance - don't worry.\n\nWhat happened here is we implicitly introduced a side effect right inside our rendering.\n\nAs soon as the component renders, the `CounterAPI` will get called, return something, and `setCount`\nwill trigger a state change, which causes a render.\n\nThen immediately, the new render runs, and another API call is made... This will go on forever, so\nReact (or your browser) will crash in some way, killing the page.\n\nThis is why we need to guard against side-effects. Here is a simple change to fix this:\n\n```tsx\nconst MyComponent = () => {\n const [count, setCount] = React.useState(0)\n React.useEffect(() => {\n // add this line\n CounterAPI.getCount().then((data) => setCount(data))\n }, []) // and this line\n return <div>{count}</div>\n}\n```\n\nThis will mean our API only calls once.\n\n### A replacement for lifecycle methods\n\nIf you have ever used class-based components in React, you might be familiar with the term. In these\ntypes of components, sometimes we would need to fire events when the component mounts for the first\ntime, or when one of the props changes, to sync our state properly with our business-logic.\n\nThis is the same effect from before, but in the old\\* method:\n\n```tsx\nclass MyComponent extends React.Component {\n render() {\n // return ...\n }\n\n componentDidMount() {\n MyAPI.getResults().then((data) => this.setState({ apiData: data }))\n }\n}\n```\n\n<p style=\"font-size: 0.8em\">\n* It's not deprecated, and React will support it for a while to come. But class-based components\nare generally considered to be outdated, a bit less easy to predict sometimes - especially for\nnewcomers, and generally too verbose for sometimes really simple code.\n</p>\n\nThe `useEffect` hook is also the de-facto replacement to the lifecycle methods, which are\nunavailable when you are using function components.\n\nLet's dive into how it's used.\n\n## Structure of the `useEffect` hook\n\nA `useEffect` call takes 2 arguments.\n\n```tsx\nReact.useEffect(\n () => {\n // the side-effect code\n\n // Optionally return a clean-up function\n return () => doCleanup()\n },\n [\n /* dependencies */\n ],\n)\n```\n\nThe first is a function to run at... \"some time\". The time in which to run this effect differs on\nyour use case, and your usage of the 2nd argument. We will talk about how it works shortly.\n\nThe second is optional - but in my opinion, very required - the dependency array. Like many React\nhooks ([see my post about `useMemo` and `useCallback`][memo-hooks]), it is a list of changes that\nthe component listens to, only running the effect after changes to the dependencies were detected.\n\nIn the case of `useEffect`, the effect always runs at least once, even if that means all the values\nit depends on might be `undefined`. So wait, how does this work?\n\n## Thinking \"Reactive\"\n\nThis is a bit confusing. We are listening to changes now - okay. I will elaborate a bit.\n\nIf in the past we used to rely on the framework to call specific methods in different lifecycle\ntimes, now we are going to change our way of thinking a bit. This method introduced many problems,\nespecially because developers were often introducing side-effects where they shouldn't be possible,\nintroducing bugs and other problems.\n\nMany hooks now rely on dependency arrays to define when they are being re-built. Our way of thinking\nwill be similar - our function will run when changes are made, and they're always made at least\nonce - in the mounting phase.\n\nIf we want to fire code once when our component is mounted, we can introduce an effect with 0\ndependencies. This causes it to only run once, as no changes will ever be introduced to this very\nconstant array. This causes the hook to behave exactly as `componentDidMount` did.\n\n### Dependencies: `[] !== undefined`\n\nIt's important to note that supplying **no array** in the dependency argument, and supplying an\n**empty array** are 2 different things! Try it out yourself to see the difference.\n\n```tsx\nconst MyComponent = () => {\n const [count, setCount] = React.useState(0)\n\n React.useEffect(() => console.log('no array'))\n React.useEffect(() => console.log('empty array'), [])\n\n return <div onClick={() => setCount((prev) => ++prev)}>{count}</div>\n}\n```\n\nRunning a component with this code will show different results.\n\nImmediately, we will see this in the log:\n\n```\nno array\nempty array\n```\n\nNothing special yet. Let's click the `div` and see what happens.\n\n```\nno array\n```\n\nAhh... What's this? It ran again? But we supplied no dependencies?\n\nSupplying no value at all means it will update **every time the component renders**, not just once.\nThe dependency needs to be an array for any comparison to happen. If it's `undefined`, comparison is\nskipped and the effect runs every time.\n\n## How can we use this now?\n\nLet's review the basics we learned about this hook.\n\n1. It runs at least once\n2. It then runs:\n 1. every time its dependency array contents change, or\n 2. if there is no array, after every render\n\n### Component mounting\n\nThe simplest effect we learned about is commonly referred to as a mount effect. It runs once since\nyou supply no dependencies.\n\nLet's say we are viewing a list of posts.\n\n```tsx\nconst PostList = () => {\n const [posts, setPosts] = React.useState([])\n const [page, setPage] = React.useState(0)\n\n React.useEffect(() => {\n setLoading(true)\n PostsAPI.getPosts({ page: page }).then((data) => {\n setPosts(data.posts)\n setLoading(false)\n })\n }, [])\n\n return (\n <div>\n {loading\n ? 'Loading...'\n : posts.forEach((post, i) => {\n return <div key={post.id}>{post.title}</div>\n })}\n </div>\n )\n}\n```\n\nThis will fetch the posts once, and once only.\n\nThen we have more cases, where we decide what to supply, and we can make our component react to\nchanges in either the props or change, and introduce the effects we need.\n\nLet's consider an extension to this use-case to test with.\n\n### Reacting to a state change\n\nWe will add a page selector, which lets us change which page of the posts we are viewing. For the\nsake of simplicity, the count per page is implicit and hard-coded in the backend.\n\n```diff\n const PostList = () => {\n const [posts, setPosts] = React.useState([])\n const [loading, setLoading] = React.useState(true)\n+ const [page, setPage] = React.useState(0)\n\n React.useEffect(() => {\n setLoading(true)\n PostsAPI.getPosts({ page: page }).then((data) => {\n setPosts(data.posts)\n setLoading(false)\n })\n- }, [])\n+ }, [page])\n\n return (\n <div>\n+ <span onClick={() => setPage(prev => ++prev)}>Next page »</span>\n {loading\n ? 'Loading...'\n : posts.forEach((post, i) => {\n return <div key={post.id}>{post.title}</div>\n })}\n </div>\n )\n }\n```\n\nAs you can see, we supplied our `page` as a dependency for the API call. This means that once the\ncomponent mounts, and then every time this count changes afterwards, React will run our effect\nagain, fetching the posts from the API. Neat!\n\n### The cleanup function, timeouts and intervals\n\nAnother great use-case is to run some logic on a fixed interval, or after a timeout. Let's say we\nwant to increase a counter every second.\n\n```tsx\nconst Counter = () => {\n const [count, setCount] = React.useState(0)\n\n React.useEffect(() => {\n setInterval(() => {\n setCount((prev) => ++prev)\n }, 1000)\n }, [])\n\n return <div>{count}</div>\n}\n```\n\nThis works great! Every second, we get a new count with a larger number.\n\nBut this introduces a bug: navigate to another route, or get this component removed in some way. You\nwill notice the interval keeps running!\n\nIn a case like this, we want to make sure our interval or timeout stops when the component is no\nlonger mounted, in order to prevent a memory leak.\n\nThis is where **cleanup functions** come in handy. A `useEffect` call can also accept a function as\na result of its callback.\n\nThat function will run once one of the dependencies change, before the \"new\" effect runs. It will\nalso run once the component **unmounts**, i.e., is being removed from the tree in some way. These\ncases are the same phase that calls the cleanup function, with the only difference being whether a\nnew effect will replace the discarded one, or will the effect end its life there.\n\nThis allows us to do things like cancel timers, API requests, or make some other cleanup to make\nsure nothing happens once it's no longer necessary. React class-component users will recognize this\nas being a similar use-case for using `componentWillUnmount` lifecycle method. Let's see how we can\nfix our previous example:\n\n```diff\n const Counter = () => {\n const [count, setCount] = React.useState(0)\n\n React.useEffect(() => {\n- setInterval(() => {\n+ const id = setInterval(() => {\n setCount((prev) => ++prev)\n }, 1000)\n+ return () => clearInterval(id)\n }, [])\n\n return <div>{count}</div>\n }\n```\n\nAs you may or may not already know, `setInterval` and `setTimeout` return an id token that we can\nlater use to cancel it. By supplying it as a return function to `useEffect`, we are telling it to\ndestroy our timer every time it needs to. If it is supposed to create a new one due to dependency\nchange, it will still create it and our code will still work.\n\n### External listeners\n\nThe last prominent use-case we will show here is using external listeners inside components.\n\nThis is similar to our previous use-case, where we have a function with a side-effect that needs to\nbe cleaned up. In fact it's pretty much exactly the same. Let's say we want to display the window\nresolution in our component (or make some condition depend on a value from there). Let's use our\nprevious knowledge to make a component that works for this.\n\n```tsx\nconst WindowSize = () => {\n const [width, setWidth] = React.useState(0)\n const [height, setHeight] = React.useState(0)\n\n React.useEffect(() => {\n const listener = () => {\n setWidth(window.innerWidth)\n setHeight(window.innerHeight)\n }\n window.addEventListener('resize', listener)\n return () => window.removeEventListener('resize', listener)\n }, [])\n\n return (\n <div>\n Width: {width}px\n <br />\n Height: {height}px\n </div>\n )\n}\n```\n\nNow we have an effect that listens to window changes, and makes sure to update the state with it.\nAnother success! And as you can see, it's not much different than the interval/timeout example.\n\n## Conclusion\n\nI hope that now you might see a bit more clearly how this hook is useful, and how to actually put it\nto use properly.\n\nExperiment with using `useEffect` in different cases, and experiment with thinking more reactively\nabout changes - making dependencies change things as they need, instead of always chaining function\ncalls whenever you change values just to replicate the same behavior.\n\n<!-- -->\n\n[memo-hooks]: /2022/08/how-to-use-react-memoization-hooks-for-increased-performance\n\n<!-- -->","filePath":"src/content/post/2022-08-react-using-use-effect-effectively.md","digest":"f63958b8ca44c51d","rendered":{"html":"<p>Whether you’re new to React or you are recently migrating from class components (this late?), you\nmight not be familiar with how <code>useEffect</code> works.</p>\n<p>Actually, you might be using it still from other existing (or copied 😉) code, but have no idea (or\njust a small idea) what’s actually happening.</p>\n<p>In this post we’ll dive in to this very flexible hook, some example, and some real, common\nuse-cases.</p>\n<!-- more -->\n<h2 id=\"what-is-reactuseeffect\">What is <code>React.useEffect</code>?</h2>\n<p>This is a React hook that lets you introduce <strong>side-effects</strong> into your components.</p>\n<p>Here is some example code of a common <code>useEffect</code> hook:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> MyComponent</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">posts</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setPosts</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">([])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // This will fire only once when the component mounts</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> PostsAPI.</span><span style=\"color:#B392F0\">getPostsList</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">data</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setPosts</span><span style=\"color:#E1E4E8\">(data))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">>{</span><span style=\"color:#79B8FF\">JSON</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">stringify</span><span style=\"color:#E1E4E8\">(posts)}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<h3 id=\"what-are-side-effects\">What are side effects?</h3>\n<p>Well, side effects are a general programming term, under the larger term “effects”. Effects have a\nwide meaning, and side-effects are a subset of that. The short version of the definition of a\nside-effect is “a function (or piece of code) that introduces changes to any value that is not\ninside its scope” - whether input arguments, or outside scoped variables (or global variables).</p>\n<p>In React components, that mostly means any function inside a component that introduces a change that\nis outside the scope of rendering itself.</p>\n<p>Are you calling an outside API? You are introducing an effect. Saving a value to the state?\nIntroduces an effect. <em>As long as it’s outside the scope of rendering the component itself, it’s a\nside-effect.</em></p>\n<p>Side-effects <strong>are not bad per-se</strong> - but introducing them needs to be done with intent, and with\ncaution.</p>\n<h3 id=\"react-side-effects-example\">React side-effects example</h3>\n<p><code>React.useEffect</code> is a hook that lets you wrap side-effects in a safe way so that you can avoid\nunnecessary renders, and use the necessary ones correctly. Consider the following code:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> MyComponent</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">count</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCount</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> CounterAPI.</span><span style=\"color:#B392F0\">getCount</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">data</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setCount</span><span style=\"color:#E1E4E8\">(data))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">>{count}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>If you haven’t noticed th the problem at first glance - don’t worry.</p>\n<p>What happened here is we implicitly introduced a side effect right inside our rendering.</p>\n<p>As soon as the component renders, the <code>CounterAPI</code> will get called, return something, and <code>setCount</code>\nwill trigger a state change, which causes a render.</p>\n<p>Then immediately, the new render runs, and another API call is made… This will go on forever, so\nReact (or your browser) will crash in some way, killing the page.</p>\n<p>This is why we need to guard against side-effects. Here is a simple change to fix this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> MyComponent</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">count</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCount</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // add this line</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> CounterAPI.</span><span style=\"color:#B392F0\">getCount</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">data</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setCount</span><span style=\"color:#E1E4E8\">(data))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, []) </span><span style=\"color:#6A737D\">// and this line</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">>{count}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>This will mean our API only calls once.</p>\n<h3 id=\"a-replacement-for-lifecycle-methods\">A replacement for lifecycle methods</h3>\n<p>If you have ever used class-based components in React, you might be familiar with the term. In these\ntypes of components, sometimes we would need to fire events when the component mounts for the first\ntime, or when one of the props changes, to sync our state properly with our business-logic.</p>\n<p>This is the same effect from before, but in the old* method:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#B392F0\"> MyComponent</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">Component</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> render</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // return ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> componentDidMount</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> MyAPI.</span><span style=\"color:#B392F0\">getResults</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">data</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">setState</span><span style=\"color:#E1E4E8\">({ apiData: data }))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p style=\"font-size: 0.8em\">\n* It's not deprecated, and React will support it for a while to come. But class-based components\nare generally considered to be outdated, a bit less easy to predict sometimes - especially for\nnewcomers, and generally too verbose for sometimes really simple code.\n</p>\n<p>The <code>useEffect</code> hook is also the de-facto replacement to the lifecycle methods, which are\nunavailable when you are using function components.</p>\n<p>Let’s dive into how it’s used.</p>\n<h2 id=\"structure-of-the-useeffect-hook\">Structure of the <code>useEffect</code> hook</h2>\n<p>A <code>useEffect</code> call takes 2 arguments.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // the side-effect code</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // Optionally return a clean-up function</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> doCleanup</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> /* dependencies */</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">)</span></span></code></pre>\n<p>The first is a function to run at… “some time”. The time in which to run this effect differs on\nyour use case, and your usage of the 2nd argument. We will talk about how it works shortly.</p>\n<p>The second is optional - but in my opinion, very required - the dependency array. Like many React\nhooks (<a href=\"/2022/08/how-to-use-react-memoization-hooks-for-increased-performance\">see my post about <code>useMemo</code> and <code>useCallback</code></a>), it is a list of changes that\nthe component listens to, only running the effect after changes to the dependencies were detected.</p>\n<p>In the case of <code>useEffect</code>, the effect always runs at least once, even if that means all the values\nit depends on might be <code>undefined</code>. So wait, how does this work?</p>\n<h2 id=\"thinking-reactive\">Thinking “Reactive”</h2>\n<p>This is a bit confusing. We are listening to changes now - okay. I will elaborate a bit.</p>\n<p>If in the past we used to rely on the framework to call specific methods in different lifecycle\ntimes, now we are going to change our way of thinking a bit. This method introduced many problems,\nespecially because developers were often introducing side-effects where they shouldn’t be possible,\nintroducing bugs and other problems.</p>\n<p>Many hooks now rely on dependency arrays to define when they are being re-built. Our way of thinking\nwill be similar - our function will run when changes are made, and they’re always made at least\nonce - in the mounting phase.</p>\n<p>If we want to fire code once when our component is mounted, we can introduce an effect with 0\ndependencies. This causes it to only run once, as no changes will ever be introduced to this very\nconstant array. This causes the hook to behave exactly as <code>componentDidMount</code> did.</p>\n<h3 id=\"dependencies---undefined\">Dependencies: <code>[] !== undefined</code></h3>\n<p>It’s important to note that supplying <strong>no array</strong> in the dependency argument, and supplying an\n<strong>empty array</strong> are 2 different things! Try it out yourself to see the difference.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> MyComponent</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">count</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCount</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> console.</span><span style=\"color:#B392F0\">log</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'no array'</span><span style=\"color:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> console.</span><span style=\"color:#B392F0\">log</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'empty array'</span><span style=\"color:#E1E4E8\">), [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{() </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setCount</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#F97583\"> ++</span><span style=\"color:#E1E4E8\">prev)}>{count}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Running a component with this code will show different results.</p>\n<p>Immediately, we will see this in the log:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>no array</span></span>\n<span class=\"line\"><span>empty array</span></span></code></pre>\n<p>Nothing special yet. Let’s click the <code>div</code> and see what happens.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>no array</span></span></code></pre>\n<p>Ahh… What’s this? It ran again? But we supplied no dependencies?</p>\n<p>Supplying no value at all means it will update <strong>every time the component renders</strong>, not just once.\nThe dependency needs to be an array for any comparison to happen. If it’s <code>undefined</code>, comparison is\nskipped and the effect runs every time.</p>\n<h2 id=\"how-can-we-use-this-now\">How can we use this now?</h2>\n<p>Let’s review the basics we learned about this hook.</p>\n<ol>\n<li>It runs at least once</li>\n<li>It then runs:\n<ol>\n<li>every time its dependency array contents change, or</li>\n<li>if there is no array, after every render</li>\n</ol>\n</li>\n</ol>\n<h3 id=\"component-mounting\">Component mounting</h3>\n<p>The simplest effect we learned about is commonly referred to as a mount effect. It runs once since\nyou supply no dependencies.</p>\n<p>Let’s say we are viewing a list of posts.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> PostList</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">posts</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setPosts</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">([])</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">page</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setPage</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setLoading</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">true</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> PostsAPI.</span><span style=\"color:#B392F0\">getPosts</span><span style=\"color:#E1E4E8\">({ page: page }).</span><span style=\"color:#B392F0\">then</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">data</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setPosts</span><span style=\"color:#E1E4E8\">(data.posts)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setLoading</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">false</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {loading</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> ?</span><span style=\"color:#9ECBFF\"> 'Loading...'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> :</span><span style=\"color:#E1E4E8\"> posts.</span><span style=\"color:#B392F0\">forEach</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">post</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> key</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{post.id}>{post.title}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>This will fetch the posts once, and once only.</p>\n<p>Then we have more cases, where we decide what to supply, and we can make our component react to\nchanges in either the props or change, and introduce the effects we need.</p>\n<p>Let’s consider an extension to this use-case to test with.</p>\n<h3 id=\"reacting-to-a-state-change\">Reacting to a state change</h3>\n<p>We will add a page selector, which lets us change which page of the posts we are viewing. For the\nsake of simplicity, the count per page is implicit and hard-coded in the backend.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"diff\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"> const PostList = () => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> const [posts, setPosts] = React.useState([])</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> const [loading, setLoading] = React.useState(true)</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> const [page, setPage] = React.useState(0)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.useEffect(() => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> setLoading(true)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> PostsAPI.getPosts({ page: page }).then((data) => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> setPosts(data.posts)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> setLoading(false)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })</span></span>\n<span class=\"line\"><span style=\"color:#FDAEB7\"><span style=\"user-select: none;\">-</span> }, [])</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> }, [page])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> return (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <div></span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> <span onClick={() => setPage(prev => ++prev)}>Next page &raquo;</span></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> {loading</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ? 'Loading...'</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> : posts.forEach((post, i) => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> return <div key={post.id}>{post.title}</div></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> })}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </div></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span></code></pre>\n<p>As you can see, we supplied our <code>page</code> as a dependency for the API call. This means that once the\ncomponent mounts, and then every time this count changes afterwards, React will run our effect\nagain, fetching the posts from the API. Neat!</p>\n<h3 id=\"the-cleanup-function-timeouts-and-intervals\">The cleanup function, timeouts and intervals</h3>\n<p>Another great use-case is to run some logic on a fixed interval, or after a timeout. Let’s say we\nwant to increase a counter every second.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> Counter</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">count</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCount</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setInterval</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setCount</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#F97583\"> ++</span><span style=\"color:#E1E4E8\">prev)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, </span><span style=\"color:#79B8FF\">1000</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">>{count}</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>This works great! Every second, we get a new count with a larger number.</p>\n<p>But this introduces a bug: navigate to another route, or get this component removed in some way. You\nwill notice the interval keeps running!</p>\n<p>In a case like this, we want to make sure our interval or timeout stops when the component is no\nlonger mounted, in order to prevent a memory leak.</p>\n<p>This is where <strong>cleanup functions</strong> come in handy. A <code>useEffect</code> call can also accept a function as\na result of its callback.</p>\n<p>That function will run once one of the dependencies change, before the “new” effect runs. It will\nalso run once the component <strong>unmounts</strong>, i.e., is being removed from the tree in some way. These\ncases are the same phase that calls the cleanup function, with the only difference being whether a\nnew effect will replace the discarded one, or will the effect end its life there.</p>\n<p>This allows us to do things like cancel timers, API requests, or make some other cleanup to make\nsure nothing happens once it’s no longer necessary. React class-component users will recognize this\nas being a similar use-case for using <code>componentWillUnmount</code> lifecycle method. Let’s see how we can\nfix our previous example:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"diff\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"> const Counter = () => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> const [count, setCount] = React.useState(0)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.useEffect(() => {</span></span>\n<span class=\"line\"><span style=\"color:#FDAEB7\"><span style=\"user-select: none;\">-</span> setInterval(() => {</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> const id = setInterval(() => {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> setCount((prev) => ++prev)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, 1000)</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\"><span style=\"user-select: none;\">+</span> return () => clearInterval(id)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> return <div>{count}</div></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span></code></pre>\n<p>As you may or may not already know, <code>setInterval</code> and <code>setTimeout</code> return an id token that we can\nlater use to cancel it. By supplying it as a return function to <code>useEffect</code>, we are telling it to\ndestroy our timer every time it needs to. If it is supposed to create a new one due to dependency\nchange, it will still create it and our code will still work.</p>\n<h3 id=\"external-listeners\">External listeners</h3>\n<p>The last prominent use-case we will show here is using external listeners inside components.</p>\n<p>This is similar to our previous use-case, where we have a function with a side-effect that needs to\nbe cleaned up. In fact it’s pretty much exactly the same. Let’s say we want to display the window\nresolution in our component (or make some condition depend on a value from there). Let’s use our\nprevious knowledge to make a component that works for this.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> WindowSize</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">width</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setWidth</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">height</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setHeight</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useEffect</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#B392F0\"> listener</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setWidth</span><span style=\"color:#E1E4E8\">(window.innerWidth)</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setHeight</span><span style=\"color:#E1E4E8\">(window.innerHeight)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> window.</span><span style=\"color:#B392F0\">addEventListener</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'resize'</span><span style=\"color:#E1E4E8\">, listener)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> window.</span><span style=\"color:#B392F0\">removeEventListener</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'resize'</span><span style=\"color:#E1E4E8\">, listener)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Width: {width}px</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Height: {height}px</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now we have an effect that listens to window changes, and makes sure to update the state with it.\nAnother success! And as you can see, it’s not much different than the interval/timeout example.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>I hope that now you might see a bit more clearly how this hook is useful, and how to actually put it\nto use properly.</p>\n<p>Experiment with using <code>useEffect</code> in different cases, and experiment with thinking more reactively\nabout changes - making dependencies change things as they need, instead of always chaining function\ncalls whenever you change values just to replicate the same behavior.</p>\n<!-- -->\n<!-- -->","metadata":{"headings":[{"depth":2,"slug":"what-is-reactuseeffect","text":"What is React.useEffect?"},{"depth":3,"slug":"what-are-side-effects","text":"What are side effects?"},{"depth":3,"slug":"react-side-effects-example","text":"React side-effects example"},{"depth":3,"slug":"a-replacement-for-lifecycle-methods","text":"A replacement for lifecycle methods"},{"depth":2,"slug":"structure-of-the-useeffect-hook","text":"Structure of the useEffect hook"},{"depth":2,"slug":"thinking-reactive","text":"Thinking “Reactive”"},{"depth":3,"slug":"dependencies---undefined","text":"Dependencies: [] !== undefined"},{"depth":2,"slug":"how-can-we-use-this-now","text":"How can we use this now?"},{"depth":3,"slug":"component-mounting","text":"Component mounting"},{"depth":3,"slug":"reacting-to-a-state-change","text":"Reacting to a state change"},{"depth":3,"slug":"the-cleanup-function-timeouts-and-intervals","text":"The cleanup function, timeouts and intervals"},{"depth":3,"slug":"external-listeners","text":"External listeners"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"React: Using useEffect Effectively","date":"2022-08-19 00:00:00 +0300","tags":"react tutorial javascript typescript development","image":"/images/post_covers/react-use-effect.jpg"},"imagePaths":[]}},"collection":"post","slug":"2022-08-react-using-use-effect-effectively"},"content":{"headings":[{"depth":2,"slug":"what-is-reactuseeffect","text":"What is React.useEffect?"},{"depth":3,"slug":"what-are-side-effects","text":"What are side effects?"},{"depth":3,"slug":"react-side-effects-example","text":"React side-effects example"},{"depth":3,"slug":"a-replacement-for-lifecycle-methods","text":"A replacement for lifecycle methods"},{"depth":2,"slug":"structure-of-the-useeffect-hook","text":"Structure of the useEffect hook"},{"depth":2,"slug":"thinking-reactive","text":"Thinking “Reactive”"},{"depth":3,"slug":"dependencies---undefined","text":"Dependencies: [] !== undefined"},{"depth":2,"slug":"how-can-we-use-this-now","text":"How can we use this now?"},{"depth":3,"slug":"component-mounting","text":"Component mounting"},{"depth":3,"slug":"reacting-to-a-state-change","text":"Reacting to a state change"},{"depth":3,"slug":"the-cleanup-function-timeouts-and-intervals","text":"The cleanup function, timeouts and intervals"},{"depth":3,"slug":"external-listeners","text":"External listeners"},{"depth":2,"slug":"conclusion","text":"Conclusion"}],"remarkPluginFrontmatter":{"title":"React: Using useEffect Effectively","date":"2022-08-19 00:00:00 +0300","tags":"react tutorial javascript typescript development","image":"/images/post_covers/react-use-effect.jpg"}}},{"params":{"slug":"2022-08-how-to-use-react-memoization-hooks-for-increased-performance"},"props":{"id":"2022-08-how-to-use-react-memoization-hooks-for-increased-performance.md","data":{"title":"How to use React memoization hooks for increased performance","showTitle":true,"image":"/images/post_covers/react-use-memo.jpg","includeLowResHero":false,"prose":true,"date":"2022-08-10T13:41:15.000Z","tags":"tutorial development react javascript typescript","status":"published"},"body":"As React apps grow bigger and more complex, performance becomes more and more of a problem. As\ncomponents become larger and contain more and more sub-components, rendering becomes slow and turns\ninto a bottleneck.\n\nHow do we tackle this? If you haven't used `useMemo` and `useCallback`, we can start with those.\n\n<!-- more -->\n\nIn this tutorial, we will take a look at how these 2 very easy and handy callbacks work, and why\nthey are so useful. In fact, these days my eyes get sore when I don't see them used. So let's dive\nin to what they do.\n\n<hr class=\"center\" />\n\n## React.useMemo\n\nThis React hook's one goal is to save a value for later use, and not re-calculating it on the spot.\n\nLet's take an example of some expensive logic that runs in our render function:\n\n```tsx\nconst ExpensiveComponent: React.FC = (props) => {\n const [list, setList] = React.useState([])\n const [counter, setCounter] = React.useState(0)\n const multiplied = list.map((i) => (i * 972 + 1000) / 5213).join(', ')\n\n function addRandom() {\n setList((prev) => [...prev, Math.floor(Math.random() * 10000)])\n }\n\n function increaseCounter() {\n setCounter((prev) => ++prev)\n }\n\n return (\n <div>\n Counter: {counter}\n <br />\n Multiplied: {multiplied}\n <br />\n <button onClick={addRandom}>Add Random</button>\n <button onClick={increaseCounter}>Increase Counter</button>\n </div>\n )\n}\n```\n\nDoesn't seem very problematic, but take a look at the `multiplied` variable. The logic in this\nexample isn't too bad, but imagine working with a giant list of special objects. This mapping alone\ncould be a performance problem, especially if it's looped in a parent component.\n\nIn this case, there is another state hook - `counter`. When `setCounter` is called, `multiplied`\nwill be calculated all over again, wasting previous resources, even when an update in that case is\nnot needed as these variables are independent of each-other.\n\nThat's where `useMemo` comes in hand (read the\n[official docs](https://reactjs.org/docs/hooks-reference.html#usememo)).\n\nYou can use this hook to save the value, and retrieve the same object until a re-calculation is\nneeded.\n\nHere's how it's used, the only line we need to change is the `multiplied` definition:\n\n```tsx\nconst multiplied = React.useMemo(() => {\n console.log('recalculating multiplied:', list)\n return list.map((i) => (i * 972 + 1000) / 5213).join(', ')\n}, [list])\n```\n\nThe `useMemo` hook takes 2 arguments:\n\n1. The `create` function - used to return the calculated value of the variable we want to eventually\n use\n2. A list of dependencies. The dependency list is used to determine **when** a new value should be\n calculated - i.e, when to run the `create` function again.\n\nWe added a `console.log` call here just to note when a new value is being calculated.\n\nAnd with those changes, we can try our component again (here is the updated code just in case):\n\n```tsx\nconst ExpensiveComponent: React.FC = (props) => {\n const [list, setList] = React.useState([])\n const [counter, setCounter] = React.useState(0)\n const multiplied = React.useMemo(() => {\n console.log('recalculating multiplied:', list)\n return list.map((i) => (i * 972 + 1000) / 5213).join(', ')\n }, [list])\n\n function addRandom() {\n setList((prev) => [...prev, Math.floor(Math.random() * 10000)])\n }\n\n function increaseCounter() {\n setCounter((prev) => ++prev)\n }\n\n return (\n <div>\n Counter: {counter}\n <br />\n Multiplied: {multiplied}\n <br />\n <button onClick={addRandom}>Add Random</button>\n <button onClick={increaseCounter}>Increase Counter</button>\n </div>\n )\n}\n```\n\nIf you now change the counter by using the \"Increase Counter\" button, you will see our `console.log`\ncall is not being invoked again until we use the other button for \"Add Random\".\n\n## React.useCallback\n\nNow we have the other hook - `useCallback` (read the\n[official docs](https://reactjs.org/docs/hooks-reference.html#usecallback)).\n\nThis works exactly like the `useMemo` hook - except it is for functions instead of variable values.\n\nWe can take our button functions, and wrap each in this hook to make sure our function reference\nonly changes when needed.\n\n```tsx\nconst ExpensiveComponent: React.FC = (props) => {\n const [list, setList] = React.useState([])\n const [counter, setCounter] = React.useState(0)\n const multiplied = React.useMemo(\n () => list.map((i) => (i * 972 + 1000) / 5213).join(', '),\n [list],\n )\n const addRandom = React.useCallback(\n () => setList((prev) => [...prev, Math.floor(Math.random() * 10000)]),\n [setList],\n )\n const increaseCounter = React.useCallback(() => setCounter((prev) => ++prev), [setCounter])\n\n return (\n <div>\n Counter: {counter}\n <br />\n Multiplied: {multiplied}\n <br />\n <button onClick={addRandom}>Add Random</button>\n <button onClick={increaseCounter}>Increase Counter</button>\n </div>\n )\n}\n```\n\nNow both our variables and functions are memoized and will only change reference when their\ndependencies dictate they do.\n\n## Caveats\n\nUsing these hooks don't come without their share of problems.\n\n1. Consider whether this actually improves performance or not in your specific case. If your state\n is pretty regularly changed and these memoizations have to run pretty often, their performance\n increases might be outweighed by the performance costs of actually running the memoization logic.\n\n2. Dependency checking and generating can be expensive. Be careful what you put in the dependency\n lists, and if needed, make some more memoization and map your objects and lists in deterministic\n ways so that they are statically inspectable easily. Also avoid using expensive methods such as\n `JSON.stringify` to create those memoizations or dependencies, as it might also be too expensive\n to be worth the trouble and might make things worse.\n\n## Other things to consider\n\nYou might want to make sure your project uses lint rules that enforce exhaustive dependencies, as\nthey make tracking these things much more easy.\n\nIn some cases, you might want to add ignore comments in very specific places, but it makes it very\nclear that this part is built that way intentionally and prevents more confusion about when to\nupdate the dependencies.\n\n---\n\nHopefully you find this useful. There are many other hooks to learn about, but these 2 are very\nuseful and often ignored, so I thought it would be good to highlight them. If you are interested you\nmight want to look up `useRef` and how it differs from `useMemo`, or maybe I'll make another part\nabout it in the future. Who knows?","filePath":"src/content/post/2022-08-how-to-use-react-memoization-hooks-for-increased-performance.md","digest":"3b6a0e52c382297a","rendered":{"html":"<p>As React apps grow bigger and more complex, performance becomes more and more of a problem. As\ncomponents become larger and contain more and more sub-components, rendering becomes slow and turns\ninto a bottleneck.</p>\n<p>How do we tackle this? If you haven’t used <code>useMemo</code> and <code>useCallback</code>, we can start with those.</p>\n<!-- more -->\n<p>In this tutorial, we will take a look at how these 2 very easy and handy callbacks work, and why\nthey are so useful. In fact, these days my eyes get sore when I don’t see them used. So let’s dive\nin to what they do.</p>\n<hr class=\"center\">\n<h2 id=\"reactusememo\">React.useMemo</h2>\n<p>This React hook’s one goal is to save a value for later use, and not re-calculating it on the spot.</p>\n<p>Let’s take an example of some expensive logic that runs in our render function:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> ExpensiveComponent</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">FC</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">props</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">list</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setList</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">([])</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">counter</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCounter</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> multiplied</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> list.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (i </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 972</span><span style=\"color:#F97583\"> +</span><span style=\"color:#79B8FF\"> 1000</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 5213</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">', '</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> addRandom</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setList</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">prev, Math.</span><span style=\"color:#B392F0\">floor</span><span style=\"color:#E1E4E8\">(Math.</span><span style=\"color:#B392F0\">random</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 10000</span><span style=\"color:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> increaseCounter</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setCounter</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#F97583\"> ++</span><span style=\"color:#E1E4E8\">prev)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Counter: {counter}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Multiplied: {multiplied}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{addRandom}>Add Random</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{increaseCounter}>Increase Counter</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Doesn’t seem very problematic, but take a look at the <code>multiplied</code> variable. The logic in this\nexample isn’t too bad, but imagine working with a giant list of special objects. This mapping alone\ncould be a performance problem, especially if it’s looped in a parent component.</p>\n<p>In this case, there is another state hook - <code>counter</code>. When <code>setCounter</code> is called, <code>multiplied</code>\nwill be calculated all over again, wasting previous resources, even when an update in that case is\nnot needed as these variables are independent of each-other.</p>\n<p>That’s where <code>useMemo</code> comes in hand (read the\n<a href=\"https://reactjs.org/docs/hooks-reference.html#usememo\">official docs</a>).</p>\n<p>You can use this hook to save the value, and retrieve the same object until a re-calculation is\nneeded.</p>\n<p>Here’s how it’s used, the only line we need to change is the <code>multiplied</code> definition:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#79B8FF\"> multiplied</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useMemo</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> console.</span><span style=\"color:#B392F0\">log</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'recalculating multiplied:'</span><span style=\"color:#E1E4E8\">, list)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> list.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (i </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 972</span><span style=\"color:#F97583\"> +</span><span style=\"color:#79B8FF\"> 1000</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 5213</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">', '</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}, [list])</span></span></code></pre>\n<p>The <code>useMemo</code> hook takes 2 arguments:</p>\n<ol>\n<li>The <code>create</code> function - used to return the calculated value of the variable we want to eventually\nuse</li>\n<li>A list of dependencies. The dependency list is used to determine <strong>when</strong> a new value should be\ncalculated - i.e, when to run the <code>create</code> function again.</li>\n</ol>\n<p>We added a <code>console.log</code> call here just to note when a new value is being calculated.</p>\n<p>And with those changes, we can try our component again (here is the updated code just in case):</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> ExpensiveComponent</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">FC</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">props</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">list</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setList</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">([])</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">counter</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCounter</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> multiplied</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useMemo</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> console.</span><span style=\"color:#B392F0\">log</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">'recalculating multiplied:'</span><span style=\"color:#E1E4E8\">, list)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> list.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (i </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 972</span><span style=\"color:#F97583\"> +</span><span style=\"color:#79B8FF\"> 1000</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 5213</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">', '</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }, [list])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> addRandom</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setList</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">prev, Math.</span><span style=\"color:#B392F0\">floor</span><span style=\"color:#E1E4E8\">(Math.</span><span style=\"color:#B392F0\">random</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 10000</span><span style=\"color:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> function</span><span style=\"color:#B392F0\"> increaseCounter</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setCounter</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#F97583\"> ++</span><span style=\"color:#E1E4E8\">prev)</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Counter: {counter}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Multiplied: {multiplied}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{addRandom}>Add Random</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{increaseCounter}>Increase Counter</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>If you now change the counter by using the “Increase Counter” button, you will see our <code>console.log</code>\ncall is not being invoked again until we use the other button for “Add Random”.</p>\n<h2 id=\"reactusecallback\">React.useCallback</h2>\n<p>Now we have the other hook - <code>useCallback</code> (read the\n<a href=\"https://reactjs.org/docs/hooks-reference.html#usecallback\">official docs</a>).</p>\n<p>This works exactly like the <code>useMemo</code> hook - except it is for functions instead of variable values.</p>\n<p>We can take our button functions, and wrap each in this hook to make sure our function reference\nonly changes when needed.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">const</span><span style=\"color:#B392F0\"> ExpensiveComponent</span><span style=\"color:#F97583\">:</span><span style=\"color:#B392F0\"> React</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">FC</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">props</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">list</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setList</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">([])</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">counter</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">setCounter</span><span style=\"color:#E1E4E8\">] </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useState</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> multiplied</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useMemo</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> list.</span><span style=\"color:#B392F0\">map</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">i</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> (i </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 972</span><span style=\"color:#F97583\"> +</span><span style=\"color:#79B8FF\"> 1000</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 5213</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">join</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#9ECBFF\">', '</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [list],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> addRandom</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useCallback</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> () </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setList</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#F97583\">...</span><span style=\"color:#E1E4E8\">prev, Math.</span><span style=\"color:#B392F0\">floor</span><span style=\"color:#E1E4E8\">(Math.</span><span style=\"color:#B392F0\">random</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 10000</span><span style=\"color:#E1E4E8\">)]),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> [setList],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> increaseCounter</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> React.</span><span style=\"color:#B392F0\">useCallback</span><span style=\"color:#E1E4E8\">(() </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setCounter</span><span style=\"color:#E1E4E8\">((</span><span style=\"color:#FFAB70\">prev</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#F97583\"> ++</span><span style=\"color:#E1E4E8\">prev), [setCounter])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Counter: {counter}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> Multiplied: {multiplied}</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">br</span><span style=\"color:#E1E4E8\"> /></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{addRandom}>Add Random</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">button</span><span style=\"color:#B392F0\"> onClick</span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\">{increaseCounter}>Increase Counter</</span><span style=\"color:#85E89D\">button</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> </</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now both our variables and functions are memoized and will only change reference when their\ndependencies dictate they do.</p>\n<h2 id=\"caveats\">Caveats</h2>\n<p>Using these hooks don’t come without their share of problems.</p>\n<ol>\n<li>\n<p>Consider whether this actually improves performance or not in your specific case. If your state\nis pretty regularly changed and these memoizations have to run pretty often, their performance\nincreases might be outweighed by the performance costs of actually running the memoization logic.</p>\n</li>\n<li>\n<p>Dependency checking and generating can be expensive. Be careful what you put in the dependency\nlists, and if needed, make some more memoization and map your objects and lists in deterministic\nways so that they are statically inspectable easily. Also avoid using expensive methods such as\n<code>JSON.stringify</code> to create those memoizations or dependencies, as it might also be too expensive\nto be worth the trouble and might make things worse.</p>\n</li>\n</ol>\n<h2 id=\"other-things-to-consider\">Other things to consider</h2>\n<p>You might want to make sure your project uses lint rules that enforce exhaustive dependencies, as\nthey make tracking these things much more easy.</p>\n<p>In some cases, you might want to add ignore comments in very specific places, but it makes it very\nclear that this part is built that way intentionally and prevents more confusion about when to\nupdate the dependencies.</p>\n<hr>\n<p>Hopefully you find this useful. There are many other hooks to learn about, but these 2 are very\nuseful and often ignored, so I thought it would be good to highlight them. If you are interested you\nmight want to look up <code>useRef</code> and how it differs from <code>useMemo</code>, or maybe I’ll make another part\nabout it in the future. Who knows?</p>","metadata":{"headings":[{"depth":2,"slug":"reactusememo","text":"React.useMemo"},{"depth":2,"slug":"reactusecallback","text":"React.useCallback"},{"depth":2,"slug":"caveats","text":"Caveats"},{"depth":2,"slug":"other-things-to-consider","text":"Other things to consider"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"How to use React memoization hooks for increased performance","date":"2022-08-10 16:41:15 +0300","tags":"tutorial development react javascript typescript","image":"/images/post_covers/react-use-memo.jpg"},"imagePaths":[]}},"collection":"post","slug":"2022-08-how-to-use-react-memoization-hooks-for-increased-performance"},"content":{"headings":[{"depth":2,"slug":"reactusememo","text":"React.useMemo"},{"depth":2,"slug":"reactusecallback","text":"React.useCallback"},{"depth":2,"slug":"caveats","text":"Caveats"},{"depth":2,"slug":"other-things-to-consider","text":"Other things to consider"}],"remarkPluginFrontmatter":{"title":"How to use React memoization hooks for increased performance","date":"2022-08-10 16:41:15 +0300","tags":"tutorial development react javascript typescript","image":"/images/post_covers/react-use-memo.jpg"}}},{"params":{"slug":"2022-05-the-5-steps-of-completion-learning-to-break-coding-problems-down"},"props":{"id":"2022-05-the-5-steps-of-completion-learning-to-break-coding-problems-down.md","data":{"title":"The 5 Steps of Completion: Learning to break coding problems down","showTitle":true,"image":"/images/post_covers/steps-to-completion.jpg","includeLowResHero":false,"prose":true,"date":"2022-05-01T17:00:00.000Z","tags":"development","status":"published"},"body":"When developers first start their journey, sometimes syntax is easy. You've memorized a few\ncommands, classes, and method names, and you can make simple applications quite easily. Also, there\nis plenty of docs and tutorials online - as well as Stack Overflow - surely copying-and-pasting your\nway through would be enough, right?\n\nLet's try to answer the question: how to start approaching problem solving for any given task?\n\n<!-- more -->\n\nWell, the real problem most **actually** face on a day-to-day basis when trying to program is not\ndetermining which lines of *if*s and _for_ loops go where. The problem starts at... well, the\nproblem. Where to start? How to approach a solution to a given task or problem you are facing?\n\n> Some people, when confronted with a problem, think \"I know, I'll use regular expressions.\" Now\n> they have two problems.\n>\n> ~ Jamie Zawinski\n\n## Learning to put theory to practice\n\nIn my time developing I tried hard to learn to recognize the most important skills a developer has\nin their tool set - and one of the biggest hurdles for starting developers - is the ability to know\n_how_ to face _any_ problem - at least, in the context of development.\n\nWhen an up-coming developer gets a new task – either from someone else (a boss, a colleague)\nor that they themselves define (for challenge, for proof of concept, for learning) - they often get\nstuck at the first hurdle - \"where do I start?\"\n\nNow that you can create simple sentences – you have no idea where to begin when trying to\nwrite entire paragraphs or essays. How does it my \"hello world\" actually do what it should? How do I\ntake what I know and combine the parts so they work with each other?\n\nHere, I will try to give you some thought experiments and tools so you can be self-sufficient when\nit comes to starting a new coding task, whether it is adding to existing code or beginning from\nscratch.\n\nWe will start very simple, and then try to apply the same ideas to more complex tasks later on. So\nwhile it may seem very stupid and superficial at first, it will come together soon enough.\n\n## Defining a \"problem\"\n\nA **problem** in our context is any **task** that has been given, however large or small.\n\nFor example, here are some small task examples:\n\n- Add a \"Contact\" button to the home page that links to the contact page [design attached]\n- Create a program that calculates the area of a rectangle\n\nAnd here are some larger tasks:\n\n- Create a snake game\n- Create a to-do list application\n\nAll tasks are completely unique, and yet somewhat alike. How do we approach each of these from a\ndevelopment perspective?\n\n## The methodology - The 5 Steps of Completion\n\nLet's start introducing our frame of thought. We need to define the context of our task, so we can\nask the proper questions right after.\n\n### 1. Ask Questions\n\nLet's take a basic example to start.\n\n**Given story: Add a \"Contact\" button to the home page that links to the contact page**\n\nAsk yourself the following questions:\n\n1. What is the current **state**? (i.e., from what point do I start?) \n This won't become a task later, but will be important for you to picture a \"delta\" of sorts,\n which draws a path in your mind from A (what there is) to B (what you want achieve).\n\n2. What does the user expect to **see** change?\n\n3. What **interactions** can the user make with the change?\n\n### 2. Give Answers\n\nLet's attempt to answer these questions for this case, one by one.\n\n1. **Q:** What is the current state?\n\n **A:** The user can see a website, with various sections, images and links. In the home page,\n there is no button that links to the \"Contact\" page.\n\n2. **Q:** What does the user expect to see change?\n\n **A:** The user should see a new button with the text \"Contact\"\n\n3. **Q:** What interactions can the user make with the change?\n\n **A:** The user should be able to click the button. Clicking it will point to the \"Contact\" page.\n\nWe didn't learn a lot of new information by breaking down these questions. But we did gain\nseparation which is critical. Now we have a **list of tasks**, instead of **one task**.\n\nWhy is that helpful?\n\n### 3. Break it down\n\nLarge and small tasks alike – even the smallest of addition to your code, can be broken down\neven further. It is crucial that we learn to break things down as developers, because then we can\ncreate **actionable items** which we can start **working** on.\n\nThe example above might be a bit trivial, but it gets the point across - we can break everything\ndown further, into infinity. Really, this has no limit. But we should stop somewhere...\n\nSo what next?\n\n### 4. Create Actionables\n\nNow what we want to do is take the answers from above, and compile them into a list of actions. The\nfirst question is redundant as it describes what is going on right now. So we will list the rest and\ntry to create action items from them:\n\n- **The user should see a new button with the text \"Contact\"**\n 1. Show a button on the page\n 2. Have the text say \"Contact\"\n 3. Design the button to fit with the design\n- **The user should be able to click the button. Clicking it will point to the \"Contact\" page.**\n 1. Attach a click \"handler\" to the button\n 2. Make it redirect the page to the desired URL\n\nWe are making some progress. We now have a list of tasks to work with. See how 2 small questions\nalready turned into 5 tasks, and that is for a tiny problem.\n\n### 5. Solve It\n\nNow that we have tasks - we can get crackin'.\n\nFor the sake of this let's assume we are developers that are relatively new to web development. What\nwould we do to approach this task?\n\n#### When in doubt, look it up\n\nGoogle, Duck Duck Go, Brave, whatever. What do you choose? Your preference. What would we search for\nhere exactly?\n\nThe trick is to be as **broad as possible**, and when the answers don't help, get more specific from\nthere. This is different from debugging - where you should be as specific as possible and search for\nexact matches. But I digress. Oh, and always prepend/append the programming/hypertext language you\nare referencing.\n\nThe action item is \"Show a button on the page\". So let's try see: **how do we display a button in\nHTML?** and **how do I set its text?** Now that we have these simple questions, we can remove some\nexcess words and we remain with **2 great keywords** to search which should get us starting on the\ntopic, at the most general sense:\n\n- [html button](https://google.com/search?q=html+button)\n\nNow to answer the next burning question - **how to make the button fit with the design?**\n\nWe have another tricky task here. \nSay our design looks something like this:\n\n\n\nHere, if we are unfamiliar with everything, breaking down even further can really help. Let's look\nat what defines the style of this button:\n\n- It has a gradient fill/background\n- It has white text\n- It has rounded corners\n- It has a shadow behind it\n\nWe will want to search for more specific things, which will also be our broken down tasks; such as:\n\n- [css gradient fill](https://google.com/search?q=css+gradient+fill)\n- [css text color](https://google.com/search?q=css+rounded+box)\n- [css rounded box](https://google.com/search?q=css+rounded+box)\n- [css shadow](https://google.com/search?q=css+shadow)\n\nTry some more simple task ideas, and practice breaking down tasks into chunks that you can\ndefinitely complete. As tasks become more complex, they will sometimes become harder. But sometimes\nyou will find that they don't, really — they just become **longer**, and **larger**, which may\nseem daunting, but all big tasks can sometimes be broken up into many, many tiny tasks, which will\nbe easy to actually get done.\n\n#### Any progress is important: No task is too small\n\nOne of the important concepts behind this approach is that tasks should be broken down until you can\npicture in your mind the entire task's execution, to some extent. It's not really a rule to follow,\nbut more of a guiding line. Tasks that you can picture yourself doing almost fully, you can probably\ncomplete. And if the smallest task is still beyond your knowledge, then you try to break it down\nfurther, or must go out and seek that knowledge.\n\nWhich is to say - in order to make your tasks **useful**, you must make them **doable**. Once you\ncan tick a checkbox that says \"I'm done\", no matter the size of the contribution to the total pool\nof tasks, you still made some **progress**. That progress piles up slowly, and soon you will see\nyour projects come to life.\n\n#### The next steps\n\nPractice makes perfect - so whatever side project you have, apply this new knowledge to it. Take\nyour tasks, ask the questions, and turn them into smaller ones.\n\nIn the next part of this series, we will take a look at some more complex examples - abstract, large\nideas that need to become a coded reality. For the meantime, a good grasp of simple tasks breakdown\nwill shape your minds properly for the next difficulty that lies ahead.","filePath":"src/content/post/2022-05-the-5-steps-of-completion-learning-to-break-coding-problems-down.md","digest":"d8635791eacd4eae","rendered":{"html":"<p>When developers first start their journey, sometimes syntax is easy. You’ve memorized a few\ncommands, classes, and method names, and you can make simple applications quite easily. Also, there\nis plenty of docs and tutorials online - as well as Stack Overflow - surely copying-and-pasting your\nway through would be enough, right?</p>\n<p>Let’s try to answer the question: how to start approaching problem solving for any given task?</p>\n<!-- more -->\n<p>Well, the real problem most <strong>actually</strong> face on a day-to-day basis when trying to program is not\ndetermining which lines of <em>if</em>s and <em>for</em> loops go where. The problem starts at… well, the\nproblem. Where to start? How to approach a solution to a given task or problem you are facing?</p>\n<blockquote>\n<p>Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now\nthey have two problems.</p>\n<p>~ Jamie Zawinski</p>\n</blockquote>\n<h2 id=\"learning-to-put-theory-to-practice\">Learning to put theory to practice</h2>\n<p>In my time developing I tried hard to learn to recognize the most important skills a developer has\nin their tool set - and one of the biggest hurdles for starting developers - is the ability to know\n<em>how</em> to face <em>any</em> problem - at least, in the context of development.</p>\n<p>When an up-coming developer gets a new task – either from someone else (a boss, a colleague)\nor that they themselves define (for challenge, for proof of concept, for learning) - they often get\nstuck at the first hurdle - “where do I start?”</p>\n<p>Now that you can create simple sentences – you have no idea where to begin when trying to\nwrite entire paragraphs or essays. How does it my “hello world” actually do what it should? How do I\ntake what I know and combine the parts so they work with each other?</p>\n<p>Here, I will try to give you some thought experiments and tools so you can be self-sufficient when\nit comes to starting a new coding task, whether it is adding to existing code or beginning from\nscratch.</p>\n<p>We will start very simple, and then try to apply the same ideas to more complex tasks later on. So\nwhile it may seem very stupid and superficial at first, it will come together soon enough.</p>\n<h2 id=\"defining-a-problem\">Defining a “problem”</h2>\n<p>A <strong>problem</strong> in our context is any <strong>task</strong> that has been given, however large or small.</p>\n<p>For example, here are some small task examples:</p>\n<ul>\n<li>Add a “Contact” button to the home page that links to the contact page [design attached]</li>\n<li>Create a program that calculates the area of a rectangle</li>\n</ul>\n<p>And here are some larger tasks:</p>\n<ul>\n<li>Create a snake game</li>\n<li>Create a to-do list application</li>\n</ul>\n<p>All tasks are completely unique, and yet somewhat alike. How do we approach each of these from a\ndevelopment perspective?</p>\n<h2 id=\"the-methodology---the-5-steps-of-completion\">The methodology - The 5 Steps of Completion</h2>\n<p>Let’s start introducing our frame of thought. We need to define the context of our task, so we can\nask the proper questions right after.</p>\n<h3 id=\"1-ask-questions\">1. Ask Questions</h3>\n<p>Let’s take a basic example to start.</p>\n<p><strong>Given story: Add a “Contact” button to the home page that links to the contact page</strong></p>\n<p>Ask yourself the following questions:</p>\n<ol>\n<li>\n<p>What is the current <strong>state</strong>? (i.e., from what point do I start?)<br>\nThis won’t become a task later, but will be important for you to picture a “delta” of sorts,\nwhich draws a path in your mind from A (what there is) to B (what you want achieve).</p>\n</li>\n<li>\n<p>What does the user expect to <strong>see</strong> change?</p>\n</li>\n<li>\n<p>What <strong>interactions</strong> can the user make with the change?</p>\n</li>\n</ol>\n<h3 id=\"2-give-answers\">2. Give Answers</h3>\n<p>Let’s attempt to answer these questions for this case, one by one.</p>\n<ol>\n<li>\n<p><strong>Q:</strong> What is the current state?</p>\n<p><strong>A:</strong> The user can see a website, with various sections, images and links. In the home page,\nthere is no button that links to the “Contact” page.</p>\n</li>\n<li>\n<p><strong>Q:</strong> What does the user expect to see change?</p>\n<p><strong>A:</strong> The user should see a new button with the text “Contact”</p>\n</li>\n<li>\n<p><strong>Q:</strong> What interactions can the user make with the change?</p>\n<p><strong>A:</strong> The user should be able to click the button. Clicking it will point to the “Contact” page.</p>\n</li>\n</ol>\n<p>We didn’t learn a lot of new information by breaking down these questions. But we did gain\nseparation which is critical. Now we have a <strong>list of tasks</strong>, instead of <strong>one task</strong>.</p>\n<p>Why is that helpful?</p>\n<h3 id=\"3-break-it-down\">3. Break it down</h3>\n<p>Large and small tasks alike – even the smallest of addition to your code, can be broken down\neven further. It is crucial that we learn to break things down as developers, because then we can\ncreate <strong>actionable items</strong> which we can start <strong>working</strong> on.</p>\n<p>The example above might be a bit trivial, but it gets the point across - we can break everything\ndown further, into infinity. Really, this has no limit. But we should stop somewhere…</p>\n<p>So what next?</p>\n<h3 id=\"4-create-actionables\">4. Create Actionables</h3>\n<p>Now what we want to do is take the answers from above, and compile them into a list of actions. The\nfirst question is redundant as it describes what is going on right now. So we will list the rest and\ntry to create action items from them:</p>\n<ul>\n<li><strong>The user should see a new button with the text “Contact”</strong>\n<ol>\n<li>Show a button on the page</li>\n<li>Have the text say “Contact”</li>\n<li>Design the button to fit with the design</li>\n</ol>\n</li>\n<li><strong>The user should be able to click the button. Clicking it will point to the “Contact” page.</strong>\n<ol>\n<li>Attach a click “handler” to the button</li>\n<li>Make it redirect the page to the desired URL</li>\n</ol>\n</li>\n</ul>\n<p>We are making some progress. We now have a list of tasks to work with. See how 2 small questions\nalready turned into 5 tasks, and that is for a tiny problem.</p>\n<h3 id=\"5-solve-it\">5. Solve It</h3>\n<p>Now that we have tasks - we can get crackin’.</p>\n<p>For the sake of this let’s assume we are developers that are relatively new to web development. What\nwould we do to approach this task?</p>\n<h4 id=\"when-in-doubt-look-it-up\">When in doubt, look it up</h4>\n<p>Google, Duck Duck Go, Brave, whatever. What do you choose? Your preference. What would we search for\nhere exactly?</p>\n<p>The trick is to be as <strong>broad as possible</strong>, and when the answers don’t help, get more specific from\nthere. This is different from debugging - where you should be as specific as possible and search for\nexact matches. But I digress. Oh, and always prepend/append the programming/hypertext language you\nare referencing.</p>\n<p>The action item is “Show a button on the page”. So let’s try see: <strong>how do we display a button in\nHTML?</strong> and <strong>how do I set its text?</strong> Now that we have these simple questions, we can remove some\nexcess words and we remain with <strong>2 great keywords</strong> to search which should get us starting on the\ntopic, at the most general sense:</p>\n<ul>\n<li><a href=\"https://google.com/search?q=html+button\">html button</a></li>\n</ul>\n<p>Now to answer the next burning question - <strong>how to make the button fit with the design?</strong></p>\n<p>We have another tricky task here.<br>\nSay our design looks something like this:</p>\n<p><img src=\"/images/breakdown-problems/button-example.png\" alt=\"button example\"></p>\n<p>Here, if we are unfamiliar with everything, breaking down even further can really help. Let’s look\nat what defines the style of this button:</p>\n<ul>\n<li>It has a gradient fill/background</li>\n<li>It has white text</li>\n<li>It has rounded corners</li>\n<li>It has a shadow behind it</li>\n</ul>\n<p>We will want to search for more specific things, which will also be our broken down tasks; such as:</p>\n<ul>\n<li><a href=\"https://google.com/search?q=css+gradient+fill\">css gradient fill</a></li>\n<li><a href=\"https://google.com/search?q=css+rounded+box\">css text color</a></li>\n<li><a href=\"https://google.com/search?q=css+rounded+box\">css rounded box</a></li>\n<li><a href=\"https://google.com/search?q=css+shadow\">css shadow</a></li>\n</ul>\n<p>Try some more simple task ideas, and practice breaking down tasks into chunks that you can\ndefinitely complete. As tasks become more complex, they will sometimes become harder. But sometimes\nyou will find that they don’t, really — they just become <strong>longer</strong>, and <strong>larger</strong>, which may\nseem daunting, but all big tasks can sometimes be broken up into many, many tiny tasks, which will\nbe easy to actually get done.</p>\n<h4 id=\"any-progress-is-important-no-task-is-too-small\">Any progress is important: No task is too small</h4>\n<p>One of the important concepts behind this approach is that tasks should be broken down until you can\npicture in your mind the entire task’s execution, to some extent. It’s not really a rule to follow,\nbut more of a guiding line. Tasks that you can picture yourself doing almost fully, you can probably\ncomplete. And if the smallest task is still beyond your knowledge, then you try to break it down\nfurther, or must go out and seek that knowledge.</p>\n<p>Which is to say - in order to make your tasks <strong>useful</strong>, you must make them <strong>doable</strong>. Once you\ncan tick a checkbox that says “I’m done”, no matter the size of the contribution to the total pool\nof tasks, you still made some <strong>progress</strong>. That progress piles up slowly, and soon you will see\nyour projects come to life.</p>\n<h4 id=\"the-next-steps\">The next steps</h4>\n<p>Practice makes perfect - so whatever side project you have, apply this new knowledge to it. Take\nyour tasks, ask the questions, and turn them into smaller ones.</p>\n<p>In the next part of this series, we will take a look at some more complex examples - abstract, large\nideas that need to become a coded reality. For the meantime, a good grasp of simple tasks breakdown\nwill shape your minds properly for the next difficulty that lies ahead.</p>","metadata":{"headings":[{"depth":2,"slug":"learning-to-put-theory-to-practice","text":"Learning to put theory to practice"},{"depth":2,"slug":"defining-a-problem","text":"Defining a “problem”"},{"depth":2,"slug":"the-methodology---the-5-steps-of-completion","text":"The methodology - The 5 Steps of Completion"},{"depth":3,"slug":"1-ask-questions","text":"1. Ask Questions"},{"depth":3,"slug":"2-give-answers","text":"2. Give Answers"},{"depth":3,"slug":"3-break-it-down","text":"3. Break it down"},{"depth":3,"slug":"4-create-actionables","text":"4. Create Actionables"},{"depth":3,"slug":"5-solve-it","text":"5. Solve It"},{"depth":4,"slug":"when-in-doubt-look-it-up","text":"When in doubt, look it up"},{"depth":4,"slug":"any-progress-is-important-no-task-is-too-small","text":"Any progress is important: No task is too small"},{"depth":4,"slug":"the-next-steps","text":"The next steps"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"The 5 Steps of Completion: Learning to break coding problems down","date":"2022-05-01 20:00:00 +0300","categories":"tutorials development","tags":"development","image":"/images/post_covers/steps-to-completion.jpg"},"imagePaths":[]}},"collection":"post","slug":"2022-05-the-5-steps-of-completion-learning-to-break-coding-problems-down"},"content":{"headings":[{"depth":2,"slug":"learning-to-put-theory-to-practice","text":"Learning to put theory to practice"},{"depth":2,"slug":"defining-a-problem","text":"Defining a “problem”"},{"depth":2,"slug":"the-methodology---the-5-steps-of-completion","text":"The methodology - The 5 Steps of Completion"},{"depth":3,"slug":"1-ask-questions","text":"1. Ask Questions"},{"depth":3,"slug":"2-give-answers","text":"2. Give Answers"},{"depth":3,"slug":"3-break-it-down","text":"3. Break it down"},{"depth":3,"slug":"4-create-actionables","text":"4. Create Actionables"},{"depth":3,"slug":"5-solve-it","text":"5. Solve It"},{"depth":4,"slug":"when-in-doubt-look-it-up","text":"When in doubt, look it up"},{"depth":4,"slug":"any-progress-is-important-no-task-is-too-small","text":"Any progress is important: No task is too small"},{"depth":4,"slug":"the-next-steps","text":"The next steps"}],"remarkPluginFrontmatter":{"title":"The 5 Steps of Completion: Learning to break coding problems down","date":"2022-05-01 20:00:00 +0300","categories":"tutorials development","tags":"development","image":"/images/post_covers/steps-to-completion.jpg"}}},{"params":{"slug":"2022-04-placeholder-photo"},"props":{"id":"2022-04-placeholder-photo.md","data":{"title":"Placeholder.photo: An easy way to add mockup images to your UI and web development workflow","showTitle":true,"image":"/images/post_covers/placeholder-photo.jpg","includeLowResHero":false,"prose":true,"date":"2022-04-21T12:55:03.000Z","tags":"project development tool","status":"published"},"body":"When prototyping UI designs or code, we often need images to put in places when we have no finalized\nimages in the design, or in order to test out different sizes and colors when only some are\nprovided.\n\nI find myself needing to find an image to use when developing websites and apps often, and finding a\nmock image to upload is time consuming - not to mention leaves possibly unwanted files in your code\nor design.\n\nSo what do we do?\n\nEnter: [placeholder.photo][1].\n\n<!-- more -->\n\n## What is placeholder.photo?\n\n[Placeholder.photo][1] is a service that lets you use placeholder images anywhere, saving you the\nhassle of creating images in the required sizes for temporary use-cases.\n\nIt is a tool that gives you an image URL to use anywhere, from any web or other origin.\n\n## Why would I need this?\n\nSay you are working on implementing a website design in HTML/CSS and more languages of your\nchoosing.\n\nWhen confronted with the need to use an image, for, say, an image that exists on the design -\nusually I would put it in the appropriate asset location in the project.\n\nBut what if any of these is true...\n\n1. The image represents **user uploaded content**, so the size is variable?\n2. The image is not finalized and ready for use in the design?\n3. The image does not exist at all and a placeholder box is given in the design docs?\n\n## How does placeholder.photo help me solve this?\n\nNow instead of having to go ahead and either find images of appropriate sizes, making sure they are\nuploaded to an accessible location online, or in the case of updates to the spec, now having to\nupdate said images...\n\nYou can just take a URL from [placeholder.photo][1].\n\n<div class=\"img center img-sz-none\" markdown=\"1\">\n\n\n\n</div>\n\nThe preview tool lets you decide how the image looks by letting you customize size, background and\ntext colors, the text content and font size.\n\nBut really, you can just customize the URL yourself and use the images as necessary without having\nto rely on it.\n\nA URL like this:\n\n```text\nhttps://placeholder.photo/img/255?bg_color=fb9e00&fg_color=ffffff&text=My+Text\n```\n\nWill generate this image:\n\n<div class=\"img center img-sz-none\" markdown=\"1\">\n\n\n\n</div>\n\nWhen generating from the tool, you can easily copy the image URL you made using the copy button:\n\n<div class=\"img center img-sz-none\" markdown=\"1\">\n\n\n\n</div>\n\nOr drag & drop to a supporting tool:\n\n<div class=\"img center img-sz-md\" markdown=\"1\">\n\n\n\n</div>\n\nAnd that's it! Enjoy easier time coding & designing.\n\n<div class=\"center\" markdown=\"1\">\n\n\n\n\n\n\n</div>\n\n[1]: https://placeholder.photo","filePath":"src/content/post/2022-04-placeholder-photo.md","digest":"080f3c4a2df75e81","rendered":{"html":"<p>When prototyping UI designs or code, we often need images to put in places when we have no finalized\nimages in the design, or in order to test out different sizes and colors when only some are\nprovided.</p>\n<p>I find myself needing to find an image to use when developing websites and apps often, and finding a\nmock image to upload is time consuming - not to mention leaves possibly unwanted files in your code\nor design.</p>\n<p>So what do we do?</p>\n<p>Enter: <a href=\"https://placeholder.photo\">placeholder.photo</a>.</p>\n<!-- more -->\n<h2 id=\"what-is-placeholderphoto\">What is placeholder.photo?</h2>\n<p><a href=\"https://placeholder.photo\">Placeholder.photo</a> is a service that lets you use placeholder images anywhere, saving you the\nhassle of creating images in the required sizes for temporary use-cases.</p>\n<p>It is a tool that gives you an image URL to use anywhere, from any web or other origin.</p>\n<h2 id=\"why-would-i-need-this\">Why would I need this?</h2>\n<p>Say you are working on implementing a website design in HTML/CSS and more languages of your\nchoosing.</p>\n<p>When confronted with the need to use an image, for, say, an image that exists on the design -\nusually I would put it in the appropriate asset location in the project.</p>\n<p>But what if any of these is true…</p>\n<ol>\n<li>The image represents <strong>user uploaded content</strong>, so the size is variable?</li>\n<li>The image is not finalized and ready for use in the design?</li>\n<li>The image does not exist at all and a placeholder box is given in the design docs?</li>\n</ol>\n<h2 id=\"how-does-placeholderphoto-help-me-solve-this\">How does placeholder.photo help me solve this?</h2>\n<p>Now instead of having to go ahead and either find images of appropriate sizes, making sure they are\nuploaded to an accessible location online, or in the case of updates to the spec, now having to\nupdate said images…</p>\n<p>You can just take a URL from <a href=\"https://placeholder.photo\">placeholder.photo</a>.</p>\n<div class=\"img center img-sz-none\" markdown=\"1\">\n<p><img src=\"/images/placeholder-photo/placeholder-photo-tool-preview.png\" alt=\"The placeholder.photo custom tool\"></p>\n</div>\n<p>The preview tool lets you decide how the image looks by letting you customize size, background and\ntext colors, the text content and font size.</p>\n<p>But really, you can just customize the URL yourself and use the images as necessary without having\nto rely on it.</p>\n<p>A URL like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"text\"><code><span class=\"line\"><span>https://placeholder.photo/img/255?bg_color=fb9e00&fg_color=ffffff&text=My+Text</span></span></code></pre>\n<p>Will generate this image:</p>\n<div class=\"img center img-sz-none\" markdown=\"1\">\n<p><img src=\"https://placeholder.photo/img/255?bg_color=fb9e00&fg_color=ffffff&text=My+Text\" alt=\"Preview\"></p>\n</div>\n<p>When generating from the tool, you can easily copy the image URL you made using the copy button:</p>\n<div class=\"img center img-sz-none\" markdown=\"1\">\n<p><img src=\"/images/placeholder-photo/placeholder-photo-copy-preview.png\" alt=\"Copy button example\"></p>\n</div>\n<p>Or drag & drop to a supporting tool:</p>\n<div class=\"img center img-sz-md\" markdown=\"1\">\n<p><img src=\"/images/placeholder-photo/placeholder-photo-drag-n-drop-preview.gif\" alt=\"Drag and drop example\"></p>\n</div>\n<p>And that’s it! Enjoy easier time coding & designing.</p>\n<div class=\"center\" markdown=\"1\">\n<p><img src=\"https://placeholder.photo/img/100x48?bg_color=68bc00&fg_color=ffffff&text=Thank&font_size=20\" alt=\"Thank\">\n<img src=\"https://placeholder.photo/img/100x48?bg_color=7b64ff&fg_color=ffffff&text=You%21&font_size=20\" alt=\"you\"></p>\n<p><img src=\"https://placeholder.photo/img/208x48?bg_color=d33115&fg_color=ffffff&text=casraf.dev&font_size=20\" alt=\"casraf.dev\"></p>\n</div>","metadata":{"headings":[{"depth":2,"slug":"what-is-placeholderphoto","text":"What is placeholder.photo?"},{"depth":2,"slug":"why-would-i-need-this","text":"Why would I need this?"},{"depth":2,"slug":"how-does-placeholderphoto-help-me-solve-this","text":"How does placeholder.photo help me solve this?"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Placeholder.photo: An easy way to add mockup images to your UI and web development workflow","date":"2022-04-21 15:55:03 +0300","excerpt_separator":"<!-- more -->","tags":"project development tool","image":"/images/post_covers/placeholder-photo.jpg"},"imagePaths":[]}},"collection":"post","slug":"2022-04-placeholder-photo"},"content":{"headings":[{"depth":2,"slug":"what-is-placeholderphoto","text":"What is placeholder.photo?"},{"depth":2,"slug":"why-would-i-need-this","text":"Why would I need this?"},{"depth":2,"slug":"how-does-placeholderphoto-help-me-solve-this","text":"How does placeholder.photo help me solve this?"}],"remarkPluginFrontmatter":{"title":"Placeholder.photo: An easy way to add mockup images to your UI and web development workflow","date":"2022-04-21 15:55:03 +0300","excerpt_separator":"<!-- more -->","tags":"project development tool","image":"/images/post_covers/placeholder-photo.jpg"}}},{"params":{"slug":"2019-03-flutter-tutorial-creating-a-wheel-spinner-widget"},"props":{"id":"2019-03-flutter-tutorial-creating-a-wheel-spinner-widget.md","data":{"title":"Flutter Tutorial - Creating a Wheel Spinner Widget","showTitle":true,"image":"/images/post_covers/wheel-spinner.jpg","includeLowResHero":false,"prose":true,"date":"2019-03-22T10:00:00.000Z","tags":"tutorial flutter dart widget development project","status":"published"},"body":"[Flutter][flutter] is an amazing framework for app development, and gaining popularity every second.\nBeing component and composition based, and with the fact that it includes many built-in widgets to\nwork with, it's really easy to create beautiful widgets, either simple or complex.\n\nToday we'll be learning how to create a real-world case widget for updating a numeric value - a\npitch-knob-like control. Like the ones you can usually find in a studio. It will also support\nletting the finger go to keep rolling in the same direction, and using the velocity it was left\nwith.\n\n<!-- more -->\n\n### In this tutorial, we will:\n\n1. Create a stateful widget\n2. Use `GestureDetector` to detect scroll gestures for us, and update the callbacks appropriately.\n\n**This tutorial assumes you have some experience with Flutter and understand basic Widget\ncomposition and basic state management.**\n\n## Step 1: Create the Widget\n\nWe'll start basic. We have one base stateful widget, which will hold everything. In this example,\nI've already added some parameters we will need to accept:\n\n```dart\nclass WheelSpinner extends StatefulWidget {\n final double max;\n final double min;\n final double value;\n final Function(double value)? onSlideUpdate;\n final Function(double value)? onSlideDone;\n\n WheelSpinner({\n required this.value,\n this.max = double.infinity,\n this.min = double.negativeInfinity,\n this.onSlideDone,\n this.onSlideUpdate,\n });\n\n @override\n _WheelSpinnerState createState() => _WheelSpinnerState();\n}\n\nclass _WheelSpinnerState extends State<WheelSpinner> {\n late double value;\n\n @override\n void initState() {\n value = widget.value;\n super.initState();\n }\n\n @override\n Widget build(BuildContext context) {\n return Container(child: Text(value.toString()));\n }\n}\n```\n\nAs you can see, we are accepting a `min`, `max`, and current `value` to use with the widget. We are\nalso accepting `callback`s for when updating (while sliding) and when done (when finger is let go\nand the \"fling\" duration is over), which we will call once we update the actual value.\n\n### Let's give it some shape\n\nRight now our widget is completely empty, except for that little `Text` widget to see our current\nvalue, so let's do some styling. For this example, We will be creating a rounded, tall box, which\nwill contain some separation lines, and maybe some shade or gradient.\n\nLet's update our build method:\n\n```dart\n@override\nWidget build(BuildContext context) {\n double shadowOffset = 0.2;\n return Container(\n width: 60,\n height: 100,\n decoration: BoxDecoration(\n gradient: LinearGradient(\n begin: Alignment.topCenter,\n end: Alignment.bottomCenter,\n stops: [0.0, shadowOffset, 1.0 - shadowOffset, 1.0],\n colors: [\n Colors.grey[350],\n Colors.grey[50],\n Colors.grey[50],\n Colors.grey[350]\n ],\n ),\n border: Border.all(\n width: 1,\n style: BorderStyle.solid,\n color: Colors.grey[600],\n ),\n borderRadius: BorderRadius.circular(3.5),\n ),\n );\n}\n```\n\nWe should see something like this:\n\n<div class=\"max-w-sm mx-auto\">\n\n\n\n</div>\n\nStill no divider lines, though. Let's say we want to divvy up our box to 10 segments, so that each\ntime one of them goes outside the bounding box, we increase or decrease the value by 1. In this\nexample we create a `Stack`, with 11 lines (10 for each division + 1 extra for the scroll effect)\ngoing from top to bottom (note the `lineTopPos` function that gets the correct `y` value):\n\n```dart\n@override\nWidget build(BuildContext context) {\n // ...\n decoration: // ...,\n child: Container(\n child: Stack(\n children: List<Widget>.generate(\n 11,\n (i) {\n var top = lineTopPos(value, i);\n return Positioned.fromRect(\n rect: Rect.fromLTWH(0.0, top, 60, 0),\n child: Divider(\n color: Colors.grey[600],\n ),\n );\n },\n ).toList(),\n ),\n ),\n\n // ...\n}\n\ndouble lineTopPos(double value, int i) {\n double valueFraction = (value.ceil() - value) * 10.0;\n double indexedTop = 10.0 * i;\n double top = indexedTop + valueFraction;\n return top;\n}\n```\n\nNote the line that sets `valueFraction`. We take our `value.ceil()` and reduce the current value.\nThis always gives us a number between `0.0` and `1.0` that tells us how much of the _next_ segment\nto show. In reality, whenever we update `value`, we will always consider the small fraction we are\nscrolling into, which means we don't jump by 1 every time, which will cause the lines to represent\nthe value correctly, and also smoothly move as we input our scroll.\n\nNow, we have something like this:\n\n<div class=\"max-w-sm mx-auto\">\n\n\n\n</div>\n\nAnd now that it's all nice and pretty, let's start handling the logic.\n\n## Step 2 - Detecting gestures and updating the value\n\nWe can now wrap our widget with a `GestureDetector`. This is a built-in widget that lets you capture\nand use scroll, tap and multi-tap gestures on the child widget, and its decendants (that last part\ndepends on the `behavior` parameter).\n\n```dart\n//...\nGestureDetector(\n onVerticalDragStart: onDragStart,\n onVerticalDragUpdate: onDragUpdate,\n onVerticalDragEnd: onDragDone,\n child: /* our widget */\n),\n//...\n```\n\nAnd of course, we need to actually define `onDragStart`, `onDragUpdate` and `onDragDone`.\n\n#### 1. onDragStart\n\nWe'll start by capturing on what `value` and position the finger first started dragging. For that,\nwe will save them in our state:\n\n```dart\nOffset dragStartOffset;\ndouble dragStartValue;\n\n// ...\n\nvoid onDragStart(DragStartDetails details) {\n setState(() {\n dragStartOffset = details.globalPosition;\n dragStartValue = value;\n });\n}\n```\n\n<div></div>\n\n#### 2. onDragUpdate\n\nOn every update, aka when the finger slides up and down, we want to take the distance between the\noriginal start point, and use that to calculate our new value. If the finger scrolled up an amount\nequivalent to 10 separator lines, we increase/decrease by 10 accordingly. Of course, these numbers\nwill be much smaller since we are updating a double, on a subpixel basis.\n\n```dart\nvoid onDragUpdate(DragUpdateDetails details) {\n var newValue = clamp(\n dragStartValue - (details.globalPosition - dragStartOffset).dy / 20.0,\n widget.min,\n widget.max);\n setState(() {\n value = newValue;\n });\n if (widget.onSlideUpdate != null) {\n widget.onSlideUpdate(value);\n }\n}\n```\n\nWe set the new value to use the `dragStartValue` and decrease by the distance of the scroll so far,\ndivided by 20 to scale appropriately with the separator lines. Then we update using the callback, if\nthat's relevant.\n\n_Note:_ the `clamp` method is a just a convenience method to keep a number between 2 boundaries.\nHere is a basic implementation:\n\n```dart\ndouble clamp<T extends num>(T number, T low, T high) =>\n max(low * 1.0, min(number * 1.0, high * 1.0));\n```\n\n#### 3. Testing the current widget\n\nWe can already test out our widget - we still can't \"fling\" the finger, but we can drag our finger\nup and down to see the value updating.\n\nLet's add a Text widget to our parent `build` method, to see the value from the state that's calling\nit. Here is an example of calling our widget:\n\n```dart\nclass _MyHomePageState extends State<MyHomePage> {\n //...\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n appBar: AppBar(\n title: Text(widget.title),\n ),\n body: Center(\n child: Column(\n mainAxisSize: MainAxisSize.min,\n children: <Widget>[\n WheelSpinner(\n value: _counter.toDouble(),\n min: 0.0,\n max: 100.0,\n onSlideUpdate: (val) => setState(() {\n _counter = val;\n }),\n ),\n Padding(\n padding: const EdgeInsets.only(top: 16.0),\n child: Text(_counter.toStringAsFixed(2), textScaleFactor: 2.0,),\n ),\n ],\n ),\n ),\n );\n }\n // ...\n}\n```\n\nNow it should work and look like this:\n\n<div class=\"max-w-sm mx-auto\">\n\n\n\n</div>\n\n#### 4. onDragDone\n\nOur last piece is also the most fun. Here we will start handling our \"fling\" physics. We need 2\nthings here:\n\n1. Use the velocity of the letting-go gesture to figure out how much to add/reduce from the value\n2. Dampen this value slowly to create an eased roll effect\n\nLuckily, animations are super useful in a case like this, for applying curves to a path between 2\nnumber values - we don't need to calculate the damping ourselves.\n\nSo first thing's first, we need to define and create a new `AnimationController`, and\n`Animation<double>`, and let's also set their initial value in our `initState` method:\n\n```dart\nAnimationController flingController;\nAnimation<double> flingAnimation;\nvoid Function() currentFlingListener;\n\n@override\ninitState() {\n flingAnimation = AlwaysStoppedAnimation(0.0);\n flingController = AnimationController(vsync: this);\n // ...\n}\n```\n\nAlso, since we use animations now, we will want to mixin `SingleTickerProviderStateMixin`, which\nwill manage a ticker for us, for the animation to use:\n\n```dart\nclass _WheelSpinnerState extends State<WheelSpinner>\n with SingleTickerProviderStateMixin {\n // ...\n}\n```\n\nThen, we can start with our new method:\n\n```dart\nvoid onDragDone(DragEndDetails details) {\n setState(() {\n dragStartOffset = null;\n });\n double velocity = details.primaryVelocity;\n if (velocity.abs() == 0) {\n if (widget.onSlideDone != null) {\n widget.onSlideDone(value);\n }\n return;\n }\n}\n```\n\nIn the above lines we simply reset the drag start offset, as it's no longer relevant, now that the\nfinger was let go. Then we get the velocity of the drag, and if it's 0, we return early and submit\nour callback, `onSlideDone` with the latest value.\n\nNow, we can proceed with handling the \"fling\".\n\nWe'll start by saving the value that was set when we first let go of the finger.\n\n```dart\nvoid onDragDone(DragEndDetails details) {\n // ...\n double originalValue = value;\n // ...\n}\n```\n\nNow, we want to add a listener to our animation value. We will generate a listener based on the\nvalue that was let go at, because we will need it to calculate the updated value:\n\n```dart\nflingListener(double originalValue) {\n return () {\n double newValue =\n clamp(originalValue - flingAnimation.value, widget.min, widget.max);\n if (newValue != value) {\n setState(() {\n value = newValue;\n });\n if (flingAnimation.value == flingController.upperBound) {\n if (widget.onSlideDone != null) {\n widget.onSlideDone(value);\n }\n } else {\n if (widget.onSlideUpdate != null) {\n widget.onSlideUpdate(value);\n }\n }\n }\n };\n}\n```\n\nIn the line:\n\n```dart\ndouble newValue =\n clamp(originalValue - flingAnimation.value, widget.min, widget.max);\n```\n\nYou can see we use the animation value and simply decrease it from the original value. Depending on\nthe fling direction, this will either continue up or down along with the animation value.\n\nNow we can set our listener to that function once we call it. Saving it to an instance variable will\nallow us to remove the listener on dispose, as we will have a reference to the same listener\nfunction.\n\n```dart\n // ...\n currentFlingListener = flingListener(originalValue);\n // ...\n```\n\nThen we start a `Tween` animation: we start at `0.0`, and end at `velocity`. Whether the velocity is\npositive or negative, the math will work to reach the final number we want.\n\nWe set the `curve` as we want (in this case, `Curves.decelerate`), and attach the `parent` animation\ncontroller; attach the listener, and finally when we're done, we can use `forward()` to start\nanimating.\n\n```dart\n flingController.duration = Duration(milliseconds: velocity.abs().toInt());\n flingAnimation =\n Tween(begin: 0.0, end: velocity / 100).animate(CurvedAnimation(\n curve: Curves.decelerate,\n parent: flingController,\n ))\n ..addListener(currentFlingListener);\n flingController\n ..reset()\n ..forward();\n}\n```\n\nWe added `reset()` just before `forward()`, to make sure no previous animations are lingering for\nsome odd reason. In fact, let's add a `stop()` calls to `onDragStart` and `onDragUpdate`, as well,\nand also reset the animation itself:\n\n```dart\nvoid onDragStart(DragStartDetails details) {\n flingController.stop();\n flingAnimation = AlwaysStoppedAnimation(0.0);\n // ...\n}\n```\n\n```dart\nvoid onDragUpdate(DragUpdateDetails details) {\n flingController.stop();\n flingAnimation = AlwaysStoppedAnimation(0.0);\n // ...\n}\n```\n\nAnd now that it's all out of the way, our widget should be fully working:\n\n<div class=\"max-w-sm mx-auto\">\n\n\n\n</div>\n\n### Done!\n\nThis should be pretty much it! You can of course style it and expand on it, but I've already made\nthis a package, so any improvements you have, or if you just want to use it, head over to [the\npackage on Dart Pub][pub-package], or feel free to contribute at [the source on GitHub][gh-package].\n\nThe source of the example app used for this tutorial [right here][gh-tut], if you want to take a\nlook and compare.\n\nFeel free to ask questions, provide feedback or correct my mistakes in the comments - I'm sure there\nare some.\n\n[flutter]: https://flutter.dev\n[gh-package]: https://github.com/chenasraf/wheel_spinner\n[pub-package]: https://pub.dartlang.org/packages/wheel_spinner\n[gh-tut]: https://github.com/chenasraf/wheel_spinner_tutorial","filePath":"src/content/post/2019-03-flutter-tutorial-creating-a-wheel-spinner-widget.md","digest":"1e0b3c222a728baf","rendered":{"html":"<p><a href=\"https://flutter.dev\">Flutter</a> is an amazing framework for app development, and gaining popularity every second.\nBeing component and composition based, and with the fact that it includes many built-in widgets to\nwork with, it’s really easy to create beautiful widgets, either simple or complex.</p>\n<p>Today we’ll be learning how to create a real-world case widget for updating a numeric value - a\npitch-knob-like control. Like the ones you can usually find in a studio. It will also support\nletting the finger go to keep rolling in the same direction, and using the velocity it was left\nwith.</p>\n<!-- more -->\n<h3 id=\"in-this-tutorial-we-will\">In this tutorial, we will:</h3>\n<ol>\n<li>Create a stateful widget</li>\n<li>Use <code>GestureDetector</code> to detect scroll gestures for us, and update the callbacks appropriately.</li>\n</ol>\n<p><strong>This tutorial assumes you have some experience with Flutter and understand basic Widget\ncomposition and basic state management.</strong></p>\n<h2 id=\"step-1-create-the-widget\">Step 1: Create the Widget</h2>\n<p>We’ll start basic. We have one base stateful widget, which will hold everything. In this example,\nI’ve already added some parameters we will need to accept:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#79B8FF\"> WheelSpinner</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#79B8FF\"> StatefulWidget</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> final</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> max;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> final</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> min;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> final</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> value;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> final</span><span style=\"color:#79B8FF\"> Function</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> value)</span><span style=\"color:#F97583\">?</span><span style=\"color:#E1E4E8\"> onSlideUpdate;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> final</span><span style=\"color:#79B8FF\"> Function</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> value)</span><span style=\"color:#F97583\">?</span><span style=\"color:#E1E4E8\"> onSlideDone;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> WheelSpinner</span><span style=\"color:#E1E4E8\">({</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> required</span><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.value,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.max </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\">.infinity,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.min </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\">.negativeInfinity,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.onSlideDone,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">.onSlideUpdate,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> @override</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> _WheelSpinnerState</span><span style=\"color:#B392F0\"> createState</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">=></span><span style=\"color:#79B8FF\"> _WheelSpinnerState</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#79B8FF\"> _WheelSpinnerState</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#79B8FF\"> State</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">WheelSpinner</span><span style=\"color:#E1E4E8\">> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> late</span><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> value;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> @override</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> void</span><span style=\"color:#B392F0\"> initState</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> widget.value;</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> super</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">initState</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\"> @override</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Widget</span><span style=\"color:#B392F0\"> build</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">BuildContext</span><span style=\"color:#E1E4E8\"> context) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#79B8FF\"> Container</span><span style=\"color:#E1E4E8\">(child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Text</span><span style=\"color:#E1E4E8\">(value.</span><span style=\"color:#B392F0\">toString</span><span style=\"color:#E1E4E8\">()));</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>As you can see, we are accepting a <code>min</code>, <code>max</code>, and current <code>value</code> to use with the widget. We are\nalso accepting <code>callback</code>s for when updating (while sliding) and when done (when finger is let go\nand the “fling” duration is over), which we will call once we update the actual value.</p>\n<h3 id=\"lets-give-it-some-shape\">Let’s give it some shape</h3>\n<p>Right now our widget is completely empty, except for that little <code>Text</code> widget to see our current\nvalue, so let’s do some styling. For this example, We will be creating a rounded, tall box, which\nwill contain some separation lines, and maybe some shade or gradient.</p>\n<p>Let’s update our build method:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">@override</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">Widget</span><span style=\"color:#B392F0\"> build</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">BuildContext</span><span style=\"color:#E1E4E8\"> context) {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> shadowOffset </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> 0.2</span><span style=\"color:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#79B8FF\"> Container</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> width</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 60</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> height</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> decoration</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> BoxDecoration</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> gradient</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> LinearGradient</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> begin</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Alignment</span><span style=\"color:#E1E4E8\">.topCenter,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> end</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Alignment</span><span style=\"color:#E1E4E8\">.bottomCenter,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> stops</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> [</span><span style=\"color:#79B8FF\">0.0</span><span style=\"color:#E1E4E8\">, shadowOffset, </span><span style=\"color:#79B8FF\">1.0</span><span style=\"color:#F97583\"> -</span><span style=\"color:#E1E4E8\"> shadowOffset, </span><span style=\"color:#79B8FF\">1.0</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> colors</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">350</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">50</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">50</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">350</span><span style=\"color:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> border</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Border</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">all</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> width</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 1</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> style</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> BorderStyle</span><span style=\"color:#E1E4E8\">.solid,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> color</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">600</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> borderRadius</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> BorderRadius</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">circular</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">3.5</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> );</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We should see something like this:</p>\n<div class=\"max-w-sm mx-auto\">\n<p><img src=\"/images/wheel-spinner-tutorial/scr01.png\" alt=\"Widget - without lines\"></p>\n</div>\n<p>Still no divider lines, though. Let’s say we want to divvy up our box to 10 segments, so that each\ntime one of them goes outside the bounding box, we increase or decrease the value by 1. In this\nexample we create a <code>Stack</code>, with 11 lines (10 for each division + 1 extra for the scroll effect)\ngoing from top to bottom (note the <code>lineTopPos</code> function that gets the correct <code>y</code> value):</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">@override</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">Widget</span><span style=\"color:#B392F0\"> build</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">BuildContext</span><span style=\"color:#E1E4E8\"> context) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> decoration</span><span style=\"color:#F97583\">:</span><span style=\"color:#6A737D\"> // ...,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Container</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Stack</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> children</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> List</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">Widget</span><span style=\"color:#E1E4E8\">>.</span><span style=\"color:#B392F0\">generate</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> 11</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> (i) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> var</span><span style=\"color:#E1E4E8\"> top </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> lineTopPos</span><span style=\"color:#E1E4E8\">(value, i);</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#79B8FF\"> Positioned</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">fromRect</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> rect</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Rect</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">fromLTWH</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0.0</span><span style=\"color:#E1E4E8\">, top, </span><span style=\"color:#79B8FF\">60</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">0</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Divider</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> color</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Colors</span><span style=\"color:#E1E4E8\">.grey[</span><span style=\"color:#79B8FF\">600</span><span style=\"color:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> );</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ).</span><span style=\"color:#B392F0\">toList</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">double</span><span style=\"color:#B392F0\"> lineTopPos</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> value, </span><span style=\"color:#79B8FF\">int</span><span style=\"color:#E1E4E8\"> i) {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> valueFraction </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> (value.</span><span style=\"color:#B392F0\">ceil</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> value) </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 10.0</span><span style=\"color:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> indexedTop </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> 10.0</span><span style=\"color:#F97583\"> *</span><span style=\"color:#E1E4E8\"> i;</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> top </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> indexedTop </span><span style=\"color:#F97583\">+</span><span style=\"color:#E1E4E8\"> valueFraction;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> top;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Note the line that sets <code>valueFraction</code>. We take our <code>value.ceil()</code> and reduce the current value.\nThis always gives us a number between <code>0.0</code> and <code>1.0</code> that tells us how much of the <em>next</em> segment\nto show. In reality, whenever we update <code>value</code>, we will always consider the small fraction we are\nscrolling into, which means we don’t jump by 1 every time, which will cause the lines to represent\nthe value correctly, and also smoothly move as we input our scroll.</p>\n<p>Now, we have something like this:</p>\n<div class=\"max-w-sm mx-auto\">\n<p><img src=\"/images/wheel-spinner-tutorial/scr02.png\" alt=\"Widget - with lines\"></p>\n</div>\n<p>And now that it’s all nice and pretty, let’s start handling the logic.</p>\n<h2 id=\"step-2---detecting-gestures-and-updating-the-value\">Step 2 - Detecting gestures and updating the value</h2>\n<p>We can now wrap our widget with a <code>GestureDetector</code>. This is a built-in widget that lets you capture\nand use scroll, tap and multi-tap gestures on the child widget, and its decendants (that last part\ndepends on the <code>behavior</code> parameter).</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#6A737D\">//...</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">GestureDetector</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> onVerticalDragStart</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> onDragStart,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> onVerticalDragUpdate</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> onDragUpdate,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> onVerticalDragEnd</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> onDragDone,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#6A737D\"> /* our widget */</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\">//...</span></span></code></pre>\n<p>And of course, we need to actually define <code>onDragStart</code>, <code>onDragUpdate</code> and <code>onDragDone</code>.</p>\n<h4 id=\"1-ondragstart\">1. onDragStart</h4>\n<p>We’ll start by capturing on what <code>value</code> and position the finger first started dragging. For that,\nwe will save them in our state:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#79B8FF\">Offset</span><span style=\"color:#E1E4E8\"> dragStartOffset;</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> dragStartValue;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D\">// ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragStart</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragStartDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> dragStartOffset </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> details.globalPosition;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> dragStartValue </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> value;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<div></div>\n<h4 id=\"2-ondragupdate\">2. onDragUpdate</h4>\n<p>On every update, aka when the finger slides up and down, we want to take the distance between the\noriginal start point, and use that to calculate our new value. If the finger scrolled up an amount\nequivalent to 10 separator lines, we increase/decrease by 10 accordingly. Of course, these numbers\nwill be much smaller since we are updating a double, on a subpixel basis.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragUpdate</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragUpdateDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> var</span><span style=\"color:#E1E4E8\"> newValue </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> clamp</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> dragStartValue </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> (details.globalPosition </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> dragStartOffset).dy </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 20.0</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.min,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.max);</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> newValue;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (widget.onSlideUpdate </span><span style=\"color:#F97583\">!=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.</span><span style=\"color:#B392F0\">onSlideUpdate</span><span style=\"color:#E1E4E8\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We set the new value to use the <code>dragStartValue</code> and decrease by the distance of the scroll so far,\ndivided by 20 to scale appropriately with the separator lines. Then we update using the callback, if\nthat’s relevant.</p>\n<p><em>Note:</em> the <code>clamp</code> method is a just a convenience method to keep a number between 2 boundaries.\nHere is a basic implementation:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#79B8FF\">double</span><span style=\"color:#B392F0\"> clamp</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">T</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#79B8FF\"> num</span><span style=\"color:#E1E4E8\">>(</span><span style=\"color:#79B8FF\">T</span><span style=\"color:#E1E4E8\"> number, </span><span style=\"color:#79B8FF\">T</span><span style=\"color:#E1E4E8\"> low, </span><span style=\"color:#79B8FF\">T</span><span style=\"color:#E1E4E8\"> high) </span><span style=\"color:#F97583\">=></span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> max</span><span style=\"color:#E1E4E8\">(low </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 1.0</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#B392F0\">min</span><span style=\"color:#E1E4E8\">(number </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 1.0</span><span style=\"color:#E1E4E8\">, high </span><span style=\"color:#F97583\">*</span><span style=\"color:#79B8FF\"> 1.0</span><span style=\"color:#E1E4E8\">));</span></span></code></pre>\n<h4 id=\"3-testing-the-current-widget\">3. Testing the current widget</h4>\n<p>We can already test out our widget - we still can’t “fling” the finger, but we can drag our finger\nup and down to see the value updating.</p>\n<p>Let’s add a Text widget to our parent <code>build</code> method, to see the value from the state that’s calling\nit. Here is an example of calling our widget:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#79B8FF\"> _MyHomePageState</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#79B8FF\"> State</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">MyHomePage</span><span style=\"color:#E1E4E8\">> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> //...</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> @override</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Widget</span><span style=\"color:#B392F0\"> build</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">BuildContext</span><span style=\"color:#E1E4E8\"> context) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#79B8FF\"> Scaffold</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> appBar</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> AppBar</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> title</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Text</span><span style=\"color:#E1E4E8\">(widget.title),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> body</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Center</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Column</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> mainAxisSize</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> MainAxisSize</span><span style=\"color:#E1E4E8\">.min,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> children</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> <</span><span style=\"color:#79B8FF\">Widget</span><span style=\"color:#F97583\">></span><span style=\"color:#E1E4E8\">[</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> WheelSpinner</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> value</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> _counter.</span><span style=\"color:#B392F0\">toDouble</span><span style=\"color:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> min</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 0.0</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> max</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 100.0</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> onSlideUpdate</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> (val) </span><span style=\"color:#F97583\">=></span><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> _counter </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> val;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Padding</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> padding</span><span style=\"color:#F97583\">:</span><span style=\"color:#F97583\"> const</span><span style=\"color:#79B8FF\"> EdgeInsets</span><span style=\"color:#E1E4E8\">.</span><span style=\"color:#B392F0\">only</span><span style=\"color:#E1E4E8\">(top</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 16.0</span><span style=\"color:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> child</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Text</span><span style=\"color:#E1E4E8\">(_counter.</span><span style=\"color:#B392F0\">toStringAsFixed</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">2</span><span style=\"color:#E1E4E8\">), textScaleFactor</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 2.0</span><span style=\"color:#E1E4E8\">,),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ],</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ),</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> );</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now it should work and look like this:</p>\n<div class=\"max-w-sm mx-auto\">\n<p><img src=\"/images/wheel-spinner-tutorial/scr04.gif\" alt=\"App example - animation without fling\"></p>\n</div>\n<h4 id=\"4-ondragdone\">4. onDragDone</h4>\n<p>Our last piece is also the most fun. Here we will start handling our “fling” physics. We need 2\nthings here:</p>\n<ol>\n<li>Use the velocity of the letting-go gesture to figure out how much to add/reduce from the value</li>\n<li>Dampen this value slowly to create an eased roll effect</li>\n</ol>\n<p>Luckily, animations are super useful in a case like this, for applying curves to a path between 2\nnumber values - we don’t need to calculate the damping ourselves.</p>\n<p>So first thing’s first, we need to define and create a new <code>AnimationController</code>, and\n<code>Animation<double></code>, and let’s also set their initial value in our <code>initState</code> method:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#79B8FF\">AnimationController</span><span style=\"color:#E1E4E8\"> flingController;</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">Animation</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\">> flingAnimation;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#79B8FF\"> Function</span><span style=\"color:#E1E4E8\">() currentFlingListener;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">@override</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">initState</span><span style=\"color:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingAnimation </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> AlwaysStoppedAnimation</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0.0</span><span style=\"color:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingController </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> AnimationController</span><span style=\"color:#E1E4E8\">(vsync</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> this</span><span style=\"color:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Also, since we use animations now, we will want to mixin <code>SingleTickerProviderStateMixin</code>, which\nwill manage a ticker for us, for the animation to use:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">class</span><span style=\"color:#79B8FF\"> _WheelSpinnerState</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#79B8FF\"> State</span><span style=\"color:#E1E4E8\"><</span><span style=\"color:#79B8FF\">WheelSpinner</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> with</span><span style=\"color:#79B8FF\"> SingleTickerProviderStateMixin</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Then, we can start with our new method:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragDone</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragEndDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> dragStartOffset </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> velocity </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> details.primaryVelocity;</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (velocity.</span><span style=\"color:#B392F0\">abs</span><span style=\"color:#E1E4E8\">() </span><span style=\"color:#F97583\">==</span><span style=\"color:#79B8FF\"> 0</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (widget.onSlideDone </span><span style=\"color:#F97583\">!=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.</span><span style=\"color:#B392F0\">onSlideDone</span><span style=\"color:#E1E4E8\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>In the above lines we simply reset the drag start offset, as it’s no longer relevant, now that the\nfinger was let go. Then we get the velocity of the drag, and if it’s 0, we return early and submit\nour callback, <code>onSlideDone</code> with the latest value.</p>\n<p>Now, we can proceed with handling the “fling”.</p>\n<p>We’ll start by saving the value that was set when we first let go of the finger.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragDone</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragEndDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> originalValue </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> value;</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>Now, we want to add a listener to our animation value. We will generate a listener based on the\nvalue that was let go at, because we will need it to calculate the updated value:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#B392F0\">flingListener</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> originalValue) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> () {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> double</span><span style=\"color:#E1E4E8\"> newValue </span><span style=\"color:#F97583\">=</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> clamp</span><span style=\"color:#E1E4E8\">(originalValue </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> flingAnimation.value, widget.min, widget.max);</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (newValue </span><span style=\"color:#F97583\">!=</span><span style=\"color:#E1E4E8\"> value) {</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> setState</span><span style=\"color:#E1E4E8\">(() {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> value </span><span style=\"color:#F97583\">=</span><span style=\"color:#E1E4E8\"> newValue;</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (flingAnimation.value </span><span style=\"color:#F97583\">==</span><span style=\"color:#E1E4E8\"> flingController.upperBound) {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (widget.onSlideDone </span><span style=\"color:#F97583\">!=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.</span><span style=\"color:#B392F0\">onSlideDone</span><span style=\"color:#E1E4E8\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> } </span><span style=\"color:#F97583\">else</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> if</span><span style=\"color:#E1E4E8\"> (widget.onSlideUpdate </span><span style=\"color:#F97583\">!=</span><span style=\"color:#79B8FF\"> null</span><span style=\"color:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> widget.</span><span style=\"color:#B392F0\">onSlideUpdate</span><span style=\"color:#E1E4E8\">(value);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> };</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>In the line:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#79B8FF\">double</span><span style=\"color:#E1E4E8\"> newValue </span><span style=\"color:#F97583\">=</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\"> clamp</span><span style=\"color:#E1E4E8\">(originalValue </span><span style=\"color:#F97583\">-</span><span style=\"color:#E1E4E8\"> flingAnimation.value, widget.min, widget.max);</span></span></code></pre>\n<p>You can see we use the animation value and simply decrease it from the original value. Depending on\nthe fling direction, this will either continue up or down along with the animation value.</p>\n<p>Now we can set our listener to that function once we call it. Saving it to an instance variable will\nallow us to remove the listener on dispose, as we will have a reference to the same listener\nfunction.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> currentFlingListener </span><span style=\"color:#F97583\">=</span><span style=\"color:#B392F0\"> flingListener</span><span style=\"color:#E1E4E8\">(originalValue);</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span></code></pre>\n<p>Then we start a <code>Tween</code> animation: we start at <code>0.0</code>, and end at <code>velocity</code>. Whether the velocity is\npositive or negative, the math will work to reach the final number we want.</p>\n<p>We set the <code>curve</code> as we want (in this case, <code>Curves.decelerate</code>), and attach the <code>parent</code> animation\ncontroller; attach the listener, and finally when we’re done, we can use <code>forward()</code> to start\nanimating.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#E1E4E8\"> flingController.duration </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> Duration</span><span style=\"color:#E1E4E8\">(milliseconds</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> velocity.</span><span style=\"color:#B392F0\">abs</span><span style=\"color:#E1E4E8\">().</span><span style=\"color:#B392F0\">toInt</span><span style=\"color:#E1E4E8\">());</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingAnimation </span><span style=\"color:#F97583\">=</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> Tween</span><span style=\"color:#E1E4E8\">(begin</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> 0.0</span><span style=\"color:#E1E4E8\">, end</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> velocity </span><span style=\"color:#F97583\">/</span><span style=\"color:#79B8FF\"> 100</span><span style=\"color:#E1E4E8\">).</span><span style=\"color:#B392F0\">animate</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">CurvedAnimation</span><span style=\"color:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> curve</span><span style=\"color:#F97583\">:</span><span style=\"color:#79B8FF\"> Curves</span><span style=\"color:#E1E4E8\">.decelerate,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> parent</span><span style=\"color:#F97583\">:</span><span style=\"color:#E1E4E8\"> flingController,</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ))</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ..</span><span style=\"color:#B392F0\">addListener</span><span style=\"color:#E1E4E8\">(currentFlingListener);</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingController</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ..</span><span style=\"color:#B392F0\">reset</span><span style=\"color:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> ..</span><span style=\"color:#B392F0\">forward</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>We added <code>reset()</code> just before <code>forward()</code>, to make sure no previous animations are lingering for\nsome odd reason. In fact, let’s add a <code>stop()</code> calls to <code>onDragStart</code> and <code>onDragUpdate</code>, as well,\nand also reset the animation itself:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragStart</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragStartDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingController.</span><span style=\"color:#B392F0\">stop</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingAnimation </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> AlwaysStoppedAnimation</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0.0</span><span style=\"color:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"dart\"><code><span class=\"line\"><span style=\"color:#F97583\">void</span><span style=\"color:#B392F0\"> onDragUpdate</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">DragUpdateDetails</span><span style=\"color:#E1E4E8\"> details) {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingController.</span><span style=\"color:#B392F0\">stop</span><span style=\"color:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> flingAnimation </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> AlwaysStoppedAnimation</span><span style=\"color:#E1E4E8\">(</span><span style=\"color:#79B8FF\">0.0</span><span style=\"color:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> // ...</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>And now that it’s all out of the way, our widget should be fully working:</p>\n<div class=\"max-w-sm mx-auto\">\n<p><img src=\"/images/wheel-spinner-tutorial/scr05.gif\" alt=\"App example - animation with fling\"></p>\n</div>\n<h3 id=\"done\">Done!</h3>\n<p>This should be pretty much it! You can of course style it and expand on it, but I’ve already made\nthis a package, so any improvements you have, or if you just want to use it, head over to <a href=\"https://pub.dartlang.org/packages/wheel_spinner\">the\npackage on Dart Pub</a>, or feel free to contribute at <a href=\"https://github.com/chenasraf/wheel_spinner\">the source on GitHub</a>.</p>\n<p>The source of the example app used for this tutorial <a href=\"https://github.com/chenasraf/wheel_spinner_tutorial\">right here</a>, if you want to take a\nlook and compare.</p>\n<p>Feel free to ask questions, provide feedback or correct my mistakes in the comments - I’m sure there\nare some.</p>","metadata":{"headings":[{"depth":3,"slug":"in-this-tutorial-we-will","text":"In this tutorial, we will:"},{"depth":2,"slug":"step-1-create-the-widget","text":"Step 1: Create the Widget"},{"depth":3,"slug":"lets-give-it-some-shape","text":"Let’s give it some shape"},{"depth":2,"slug":"step-2---detecting-gestures-and-updating-the-value","text":"Step 2 - Detecting gestures and updating the value"},{"depth":4,"slug":"1-ondragstart","text":"1. onDragStart"},{"depth":4,"slug":"2-ondragupdate","text":"2. onDragUpdate"},{"depth":4,"slug":"3-testing-the-current-widget","text":"3. Testing the current widget"},{"depth":4,"slug":"4-ondragdone","text":"4. onDragDone"},{"depth":3,"slug":"done","text":"Done!"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Flutter Tutorial - Creating a Wheel Spinner Widget","date":"2019-03-22 12:00:00 +0200","excerpt_separator":"<!-- more -->","categories":"tutorials flutter development","tags":"tutorial flutter dart widget development project","image":"/images/post_covers/wheel-spinner.jpg"},"imagePaths":[]}},"collection":"post","slug":"2019-03-flutter-tutorial-creating-a-wheel-spinner-widget"},"content":{"headings":[{"depth":3,"slug":"in-this-tutorial-we-will","text":"In this tutorial, we will:"},{"depth":2,"slug":"step-1-create-the-widget","text":"Step 1: Create the Widget"},{"depth":3,"slug":"lets-give-it-some-shape","text":"Let’s give it some shape"},{"depth":2,"slug":"step-2---detecting-gestures-and-updating-the-value","text":"Step 2 - Detecting gestures and updating the value"},{"depth":4,"slug":"1-ondragstart","text":"1. onDragStart"},{"depth":4,"slug":"2-ondragupdate","text":"2. onDragUpdate"},{"depth":4,"slug":"3-testing-the-current-widget","text":"3. Testing the current widget"},{"depth":4,"slug":"4-ondragdone","text":"4. onDragDone"},{"depth":3,"slug":"done","text":"Done!"}],"remarkPluginFrontmatter":{"title":"Flutter Tutorial - Creating a Wheel Spinner Widget","date":"2019-03-22 12:00:00 +0200","excerpt_separator":"<!-- more -->","categories":"tutorials flutter development","tags":"tutorial flutter dart widget development project","image":"/images/post_covers/wheel-spinner.jpg"}}},{"params":{"slug":"2019-03-simple-scaffold"},"props":{"id":"2019-03-simple-scaffold.md","data":{"title":"Simple Scaffold: Generating multiple files for faster coding","showTitle":true,"image":"/images/post_covers/simple-scaffold.jpg","includeLowResHero":false,"prose":true,"date":"2019-03-06T13:43:40.000Z","tags":"project node typescript javascript development tool","status":"published"},"body":"As most people who have been working a lot in front-end development, I will acknowledge that no\nmatter what framework, library set or tools you work with, you end up replicating a lot of files,\nespecially with modern component-based frameworks, such as React, Angular and Vue.js.\n\nThey usually grow to be very different, but they almost always start with the same some sort of\nbase-skeleton, which gets _built-upon_ later. A lot of pipeline code written, and **time wasted**.\n\n<!-- more -->\n\n## What have we got?\n\nThere are some existing solutions to these problems, but they usually are not very flexible, or are\ntied to one library or implementation. Let's take a look at a few examples.\n\n- _IDE snippets_, such as VS Code's easy snippet extensions/configurations are very helpful.\n Basically, you can either use configuration JSON files to add snippets to your code quickly, or\n use extensions that simply fill these up for you.\n\n There are nice benefits to this, as you can start typing the 'prefix' of the snippet and hit\n <kbd>Tab</kbd> quickly to add small snippets of code. You can also predefine stopping points,\n which you can navigate using <kbd>Tab</kbd> after inserting the snippet, and insert arguments,\n method names, etc.\n\n These are nice, but only for small snippets. When you have to create an entire component, made by\n multiple files, you're stuck with the option to create multiple files yourself, name them\n appropriately, and paste different snippets on each file.\n\n- _Some NPM libraries_ provide ways to scaffold files based on templates, but from what I've seen\n\n - they are tied to specific task runners, libraries or frameworks\n - they are difficult to set up and customize\n - combining your entire stack will be difficult\n\n## Enter: Simple Scaffold\n\n[Read the Simple Scaffold documentation][simple-scaffold]\n\nI was frustrated with this, and all I wanted was to have different sets of files I could generate,\nsimply by copying them at predefined directory structures, and fill them with some variable data.\n\nFor instance: I create a lot of React components at work. We have recently moved to a single-file\ncomponent structure for starting components; but we have different types of components that we want\nto generate based on context, like different file contents for page containers versus general-use\ncomponents.\n\nBeing fed up with my options, I made this small NPM package, that does just this, in a very easy and\nquick way to setup. Simply put your files wherever you want, and either use the CLI tool or the\npackage as an import - and you're good to go.\n\n## Example: Jekyll Posts\n\nCreating Jekyll posts is very simple! However I still had a little bit missing - I have to manually\nadd the date every time? Oh, man. What about the file identifier name, and title? My\n`excerpt_separator`?\n\nSo I just made the simplest script in Ruby (to fit in with the Jekyll theme) to run the Simple\nScaffold CLI with some custom parameters.\n\n```ruby\n#!/usr/bin/env ruby\nrequire \"json\"\n\nSCAFFOLD_DIR = \"scaffold_templates\"\n*name = ARGV\n\ndata = { dateFmt: \"yyyy-MM-dd\", fullDateFmt: \"yyyy-MM-dd HH:mm:ss XX\" }\n\nputs `\n npx simple-scaffold@latest \"#{name.join(\" \")}\" \\\n --templates #{SCAFFOLD_DIR}/**/* \\\n --output _drafts \\\n -w true \\\n -s false \\\n -d '#{JSON.dump(data)}'\n`\nputs 'Done'\n```\n\nLet's run by this real quick:\n\n- _Lines 1-7_ - setting up locals such as template directory base, and some variables to pass to the\n templates.\n- _Lines 9-16_ - We pass the parameters via a shell call (back-ticks in ruby), and immediately run\n using npx.\n\nLocals are passed to [Handlebars][handlebars], and can be used both in file/directory names and file\ncontents.\n\nNow all I had to do is create a place for my templates, and add a template inside:\n\n```text\n- scaffold_templates/\n -- {{now dateFmt}}-{{kebabCase name}}.md\n```\n\nAnd fill it up with some basic post:\n\n```yaml\n---\nlayout: post\ntitle: '{{ startCase name }}'\ndate: { { now fullDateFmt } }\nexcerpt_separator: <!-- more -->\ncategories:\n---\nPost content goes here\n```\n\nAnd voila! Running the script, along with a name:\n\n```shell\n./scaffold.rb \"Billy Jean is not my lover\"\n```\n\nGenerates the following file structure:\n\n```text\n- _posts/\n -- 2019-03-06-billy-jean-is-not-my-lover.markdown\n```\n\nWith the following content:\n\n```yaml\n---\nlayout: post\ntitle: 'Billy Jean is not my lover'\ndate: 2019-03-06 16:43:38 +0200\nexcerpt_separator: <!-- more -->\ncategories:\n---\nPost content goes here\n```\n\n## You can do more\n\nTake the same approach and think what you could create generators for with 0 effort. React or Vue\ncomponents? You can bundle each in a folder with all the imports, exports, base props, etc all lined\nup with your component name.\n\nAdd a script to your `package.json`, a couple of files, and you're good to go:\n\n**package.json**\n\n```json\n{\n \"gen:component\": \"npx simple-scaffold@latest -t scaffold_templates/react-component -o src/components -s true -w true '{\\\"className\\\": \\\"myClassName\\\",\\\"author\\\": \\\"Chen Asraf\\\"}'\"\n}\n```\n\n**`scaffold_templates/react-component/{{pascalCase name}}.tsx`**\n\n```tsx\n/**\n * Author: {{ author }}\n * Date: {{ now \"yyyy-MM-dd\" }}\n */\nimport React from 'react'\nimport { ComponentProps } from 'utils/types'\n\nexport interface {{pascalCase name}}Props extends ComponentProps {\n className?: string\n}\n\nexport default {{camelCase name}}: React.FC<{{pascalCase name}}Props> = (props) => {\n return (\n <div className=\"{{className}}\">{{camelCase name}} Component</div>\n )\n}\n```\n\n**`scaffold_templates/react-component/index.ts`**\n\n```tsx\nexport * from './{{pascalCase name}}'\n```\n\nRunning `npm run gen:component MyComponent` will quickly generate your new components:\n\n**`src/components/MyComponent/MyComponent.tsx`**\n\n```tsx\n/**\n * Author: Chen Asraf\n * Date: 2022-08-10\n */\nimport React from 'react'\nimport { ComponentProps } from 'utils/types'\n\nexport interface MyComponentProps extends ComponentProps {\n className?: string\n}\n\nexport default MyComponent: React.FC<MyComponentProps> = (props) => {\n return (\n <div className=\"myClassName\">MyComponent Component</div>\n )\n}\n```\n\n**`src/components/MyComponent/index.ts`**\n\n```tsx\nexport * from './MyComponent'\n```\n\nWhy stop there? **Create entire app boilerplates** using the same method - create your boilerplate\napp, add all your libraries, and replace your app name with `{{name}}`. That's it! You can run this\npackage with input from any local files and output them in (or as) your next project.\n\nCheck out the documentation by clicking [this link][simple-scaffold]!\n\n[handlebars]: https://handlebarsjs.com\n[simple-scaffold]: https://casraf.dev/simple-scaffold","filePath":"src/content/post/2019-03-simple-scaffold.md","digest":"93ee64ecb52f4d28","rendered":{"html":"<p>As most people who have been working a lot in front-end development, I will acknowledge that no\nmatter what framework, library set or tools you work with, you end up replicating a lot of files,\nespecially with modern component-based frameworks, such as React, Angular and Vue.js.</p>\n<p>They usually grow to be very different, but they almost always start with the same some sort of\nbase-skeleton, which gets <em>built-upon</em> later. A lot of pipeline code written, and <strong>time wasted</strong>.</p>\n<!-- more -->\n<h2 id=\"what-have-we-got\">What have we got?</h2>\n<p>There are some existing solutions to these problems, but they usually are not very flexible, or are\ntied to one library or implementation. Let’s take a look at a few examples.</p>\n<ul>\n<li>\n<p><em>IDE snippets</em>, such as VS Code’s easy snippet extensions/configurations are very helpful.\nBasically, you can either use configuration JSON files to add snippets to your code quickly, or\nuse extensions that simply fill these up for you.</p>\n<p>There are nice benefits to this, as you can start typing the ‘prefix’ of the snippet and hit\n<kbd>Tab</kbd> quickly to add small snippets of code. You can also predefine stopping points,\nwhich you can navigate using <kbd>Tab</kbd> after inserting the snippet, and insert arguments,\nmethod names, etc.</p>\n<p>These are nice, but only for small snippets. When you have to create an entire component, made by\nmultiple files, you’re stuck with the option to create multiple files yourself, name them\nappropriately, and paste different snippets on each file.</p>\n</li>\n<li>\n<p><em>Some NPM libraries</em> provide ways to scaffold files based on templates, but from what I’ve seen</p>\n<ul>\n<li>they are tied to specific task runners, libraries or frameworks</li>\n<li>they are difficult to set up and customize</li>\n<li>combining your entire stack will be difficult</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"enter-simple-scaffold\">Enter: Simple Scaffold</h2>\n<p><a href=\"https://casraf.dev/simple-scaffold\">Read the Simple Scaffold documentation</a></p>\n<p>I was frustrated with this, and all I wanted was to have different sets of files I could generate,\nsimply by copying them at predefined directory structures, and fill them with some variable data.</p>\n<p>For instance: I create a lot of React components at work. We have recently moved to a single-file\ncomponent structure for starting components; but we have different types of components that we want\nto generate based on context, like different file contents for page containers versus general-use\ncomponents.</p>\n<p>Being fed up with my options, I made this small NPM package, that does just this, in a very easy and\nquick way to setup. Simply put your files wherever you want, and either use the CLI tool or the\npackage as an import - and you’re good to go.</p>\n<h2 id=\"example-jekyll-posts\">Example: Jekyll Posts</h2>\n<p>Creating Jekyll posts is very simple! However I still had a little bit missing - I have to manually\nadd the date every time? Oh, man. What about the file identifier name, and title? My\n<code>excerpt_separator</code>?</p>\n<p>So I just made the simplest script in Ruby (to fit in with the Jekyll theme) to run the Simple\nScaffold CLI with some custom parameters.</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"ruby\"><code><span class=\"line\"><span style=\"color:#6A737D\">#!/usr/bin/env ruby</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">require</span><span style=\"color:#9ECBFF\"> \"json\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">SCAFFOLD_DIR</span><span style=\"color:#F97583\"> =</span><span style=\"color:#9ECBFF\"> \"scaffold_templates\"</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">*</span><span style=\"color:#E1E4E8\">name </span><span style=\"color:#F97583\">=</span><span style=\"color:#79B8FF\"> ARGV</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#FFAB70\">data</span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> { </span><span style=\"color:#79B8FF\">dateFmt:</span><span style=\"color:#9ECBFF\"> \"yyyy-MM-dd\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">fullDateFmt:</span><span style=\"color:#9ECBFF\"> \"yyyy-MM-dd HH:mm:ss XX\"</span><span style=\"color:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">puts</span><span style=\"color:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> npx simple-scaffold@latest \"</span><span style=\"color:#9ECBFF\">#{name.</span><span style=\"color:#B392F0\">join</span><span style=\"color:#9ECBFF\">(</span><span style=\"color:#9ECBFF\">\" \"</span><span style=\"color:#9ECBFF\">)}</span><span style=\"color:#9ECBFF\">\" \\</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> --templates </span><span style=\"color:#9ECBFF\">#{</span><span style=\"color:#79B8FF\">SCAFFOLD_DIR</span><span style=\"color:#9ECBFF\">}</span><span style=\"color:#9ECBFF\">/**/* \\</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> --output _drafts \\</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> -w true \\</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> -s false \\</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\"> -d '</span><span style=\"color:#9ECBFF\">#{</span><span style=\"color:#79B8FF\">JSON</span><span style=\"color:#9ECBFF\">.</span><span style=\"color:#B392F0\">dump</span><span style=\"color:#9ECBFF\">(data)}</span><span style=\"color:#9ECBFF\">'</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\">`</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">puts</span><span style=\"color:#9ECBFF\"> 'Done'</span></span></code></pre>\n<p>Let’s run by this real quick:</p>\n<ul>\n<li><em>Lines 1-7</em> - setting up locals such as template directory base, and some variables to pass to the\ntemplates.</li>\n<li><em>Lines 9-16</em> - We pass the parameters via a shell call (back-ticks in ruby), and immediately run\nusing npx.</li>\n</ul>\n<p>Locals are passed to <a href=\"https://handlebarsjs.com\">Handlebars</a>, and can be used both in file/directory names and file\ncontents.</p>\n<p>Now all I had to do is create a place for my templates, and add a template inside:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"text\"><code><span class=\"line\"><span>- scaffold_templates/</span></span>\n<span class=\"line\"><span> -- {{now dateFmt}}-{{kebabCase name}}.md</span></span></code></pre>\n<p>And fill it up with some basic post:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\"><code><span class=\"line\"><span style=\"color:#B392F0\">---</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">layout</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">post</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">title</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'{{ startCase name }}'</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">date</span><span style=\"color:#E1E4E8\">: { { </span><span style=\"color:#9ECBFF\">now fullDateFmt</span><span style=\"color:#E1E4E8\"> } }</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">excerpt_separator</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\"><!-- more --></span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">categories</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">---</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\">Post content goes here</span></span></code></pre>\n<p>And voila! Running the script, along with a name:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"shell\"><code><span class=\"line\"><span style=\"color:#B392F0\">./scaffold.rb</span><span style=\"color:#9ECBFF\"> \"Billy Jean is not my lover\"</span></span></code></pre>\n<p>Generates the following file structure:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"text\"><code><span class=\"line\"><span>- _posts/</span></span>\n<span class=\"line\"><span> -- 2019-03-06-billy-jean-is-not-my-lover.markdown</span></span></code></pre>\n<p>With the following content:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"yaml\"><code><span class=\"line\"><span style=\"color:#B392F0\">---</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">layout</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">post</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">title</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">'Billy Jean is not my lover'</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">date</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">2019-03-06 16:43:38 +0200</span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">excerpt_separator</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\"><!-- more --></span></span>\n<span class=\"line\"><span style=\"color:#85E89D\">categories</span><span style=\"color:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#B392F0\">---</span></span>\n<span class=\"line\"><span style=\"color:#9ECBFF\">Post content goes here</span></span></code></pre>\n<h2 id=\"you-can-do-more\">You can do more</h2>\n<p>Take the same approach and think what you could create generators for with 0 effort. React or Vue\ncomponents? You can bundle each in a folder with all the imports, exports, base props, etc all lined\nup with your component name.</p>\n<p>Add a script to your <code>package.json</code>, a couple of files, and you’re good to go:</p>\n<p><strong>package.json</strong></p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"json\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\"> \"gen:component\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"npx simple-scaffold@latest -t scaffold_templates/react-component -o src/components -s true -w true '{</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">className</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">: </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">myClassName</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">,</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">author</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">: </span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">Chen Asraf</span><span style=\"color:#79B8FF\">\\\"</span><span style=\"color:#9ECBFF\">}'\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p><strong><code>scaffold_templates/react-component/{{pascalCase name}}.tsx</code></strong></p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#6A737D\">/**</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> * Author: {{ author }}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> * Date: {{ now \"yyyy-MM-dd\" }}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> */</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> React </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'react'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> { ComponentProps } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'utils/types'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> interface</span><span style=\"color:#E1E4E8\"> {{pascalCase name}}Props extends ComponentProps {</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> className</span><span style=\"color:#F97583\">?:</span><span style=\"color:#E1E4E8\"> string</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> {{camelCase name}}: React.</span><span style=\"color:#79B8FF\">FC</span><span style=\"color:#F97583\"><</span><span style=\"color:#E1E4E8\">{{pascalCase name}}Props</span><span style=\"color:#F97583\">></span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">props</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> className</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">\"{{className}}\"</span><span style=\"color:#E1E4E8\">>{{camelCase name}} Component</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p><strong><code>scaffold_templates/react-component/index.ts</code></strong></p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#79B8FF\"> *</span><span style=\"color:#F97583\"> from</span><span style=\"color:#9ECBFF\"> './{{pascalCase name}}'</span></span></code></pre>\n<p>Running <code>npm run gen:component MyComponent</code> will quickly generate your new components:</p>\n<p><strong><code>src/components/MyComponent/MyComponent.tsx</code></strong></p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#6A737D\">/**</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> * Author: Chen Asraf</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> * Date: 2022-08-10</span></span>\n<span class=\"line\"><span style=\"color:#6A737D\"> */</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> React </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'react'</span></span>\n<span class=\"line\"><span style=\"color:#F97583\">import</span><span style=\"color:#E1E4E8\"> { ComponentProps } </span><span style=\"color:#F97583\">from</span><span style=\"color:#9ECBFF\"> 'utils/types'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> interface</span><span style=\"color:#B392F0\"> MyComponentProps</span><span style=\"color:#F97583\"> extends</span><span style=\"color:#B392F0\"> ComponentProps</span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#FFAB70\"> className</span><span style=\"color:#F97583\">?:</span><span style=\"color:#79B8FF\"> string</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#F97583\"> default</span><span style=\"color:#E1E4E8\"> MyComponent: React.</span><span style=\"color:#79B8FF\">FC</span><span style=\"color:#F97583\"><</span><span style=\"color:#E1E4E8\">MyComponentProps</span><span style=\"color:#F97583\">></span><span style=\"color:#F97583\"> =</span><span style=\"color:#E1E4E8\"> (</span><span style=\"color:#FFAB70\">props</span><span style=\"color:#E1E4E8\">) </span><span style=\"color:#F97583\">=></span><span style=\"color:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#F97583\"> return</span><span style=\"color:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> <</span><span style=\"color:#85E89D\">div</span><span style=\"color:#B392F0\"> className</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">\"myClassName\"</span><span style=\"color:#E1E4E8\">>MyComponent Component</</span><span style=\"color:#85E89D\">div</span><span style=\"color:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\"> )</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p><strong><code>src/components/MyComponent/index.ts</code></strong></p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"tsx\"><code><span class=\"line\"><span style=\"color:#F97583\">export</span><span style=\"color:#79B8FF\"> *</span><span style=\"color:#F97583\"> from</span><span style=\"color:#9ECBFF\"> './MyComponent'</span></span></code></pre>\n<p>Why stop there? <strong>Create entire app boilerplates</strong> using the same method - create your boilerplate\napp, add all your libraries, and replace your app name with <code>{{name}}</code>. That’s it! You can run this\npackage with input from any local files and output them in (or as) your next project.</p>\n<p>Check out the documentation by clicking <a href=\"https://casraf.dev/simple-scaffold\">this link</a>!</p>","metadata":{"headings":[{"depth":2,"slug":"what-have-we-got","text":"What have we got?"},{"depth":2,"slug":"enter-simple-scaffold","text":"Enter: Simple Scaffold"},{"depth":2,"slug":"example-jekyll-posts","text":"Example: Jekyll Posts"},{"depth":2,"slug":"you-can-do-more","text":"You can do more"}],"localImagePaths":[],"remoteImagePaths":[],"frontmatter":{"title":"Simple Scaffold: Generating multiple files for faster coding","date":"2019-03-06 15:43:40 +0200","excerpt_separator":"<!-- more -->","tags":"project node typescript javascript development tool","image":"/images/post_covers/simple-scaffold.jpg"},"imagePaths":[]}},"collection":"post","slug":"2019-03-simple-scaffold"},"content":{"headings":[{"depth":2,"slug":"what-have-we-got","text":"What have we got?"},{"depth":2,"slug":"enter-simple-scaffold","text":"Enter: Simple Scaffold"},{"depth":2,"slug":"example-jekyll-posts","text":"Example: Jekyll Posts"},{"depth":2,"slug":"you-can-do-more","text":"You can do more"}],"remarkPluginFrontmatter":{"title":"Simple Scaffold: Generating multiple files for faster coding","date":"2019-03-06 15:43:40 +0200","excerpt_separator":"<!-- more -->","tags":"project node typescript javascript development tool","image":"/images/post_covers/simple-scaffold.jpg"}}}]