<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dan Morgan • Blog Feed]]></title><description><![CDATA[Dan is a front end engineer working on design systems in Washington D.C.]]></description><link>http://github.com/dylang/node-rss</link><generator>GatsbyJS</generator><lastBuildDate>Fri, 11 Dec 2020 02:49:32 GMT</lastBuildDate><item><title><![CDATA[Automated Testing and Linting for Python Projects]]></title><description><![CDATA[A step-by-step guide to setting up Pytest and Flake8 on Mac OS and GitHub Actions for a cleaner, more stable Python 3 application codebase.]]></description><link>https://danny.codes/blog/python-automated-testing-and-linting/</link><guid isPermaLink="false">https://danny.codes/blog/python-automated-testing-and-linting/</guid><pubDate>Sun, 02 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I don&amp;#x27;t work on Python projects very often, so when I do, I usually need to start from scratch and look up the latest (read: easiest) way to setup automated tests and linting. Luckily, open-source libraries and continuous integration platforms have made the process quick and free. After testing a few different combinations, I&amp;#x27;ve found a solution that works well for me and I hope you&amp;#x27;ll find it similarly helpful.&lt;/p&gt;&lt;p&gt;First, we&amp;#x27;ll configure Pytest and Flake8 in our application. Second, we&amp;#x27;ll integrate those into our local Git workflow using Git hooks (via pre-commit). Third, we&amp;#x27;ll mirror our local testing in a GitHub Action which will run against pull requests and ensure no wonky code makes it into our main branch.&lt;/p&gt;&lt;h2 id=&quot;prerequisites&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#prerequisites&quot; aria-label=&quot;prerequisites permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Prerequisites&lt;/h2&gt;&lt;p&gt;In order to dive right in, you&amp;#x27;ll need to have Python3, a local git repo, and a remote GitHub repo. If you have those already, skip ahead to &lt;a href=&quot;#install-dependencies&quot;&gt;the next section&lt;/a&gt;.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Python 3&lt;/strong&gt;: To check whether Python 3.x is installed, run &lt;code&gt;which python3&lt;/code&gt; in terminal. If the command is not found, follow &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;the official guide&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;GitHub repo&lt;/strong&gt;: If needed, &lt;a href=&quot;https://github.com/new&quot;&gt;create a GitHub repo&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Local repo&lt;/strong&gt;: If you&amp;#x27;re starting from scratch, clone the GitHub repo created in the step above. If you already have a local project but haven&amp;#x27;t linked your remote Git repo, see the &lt;a href=&quot;https://docs.github.com/en/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line&quot;&gt;GitHub docs&lt;/a&gt; for steps.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;install-dependencies&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#install-dependencies&quot; aria-label=&quot;install dependencies permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Install Dependencies&lt;/h2&gt;&lt;p&gt;In the root of your project, create a &lt;code&gt;requirements.txt&lt;/code&gt; file and add &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;flake8&lt;/code&gt;, and &lt;code&gt;pre-commit&lt;/code&gt; project dependencies:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;####### requirements.txt #######
pytest
flake8
pre-commit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, run the following command to install with pip:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;run-test-command&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#run-test-command&quot; aria-label=&quot;run test command permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Run Test Command&lt;/h2&gt;&lt;p&gt;After installing Pytest and Flake8, create a sample test to ensure everything is working as expected. First, create &lt;code&gt;src/main.py&lt;/code&gt; and add a sample function:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# main.py
def add(a, b):
    return a + b
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, create &lt;code&gt;src/main_test.py&lt;/code&gt; and add the following test assertion:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# main_test.py
from main import add

def test_success():
    assert add(2, 2) == 4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;#x27;re now ready to run the test suite. In terminal, you should see a success message when running the following command:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  pytest
========================== test session starts ==========================
platform darwin -- Python 3.7.5, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /Users/dan/projects/python-app-testing-and-linting
collected 1 item

src/main_test.py .                                                [100%]

=========================== 1 passed in 0.01s ===========================
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before continuing, add an intentionally failing test to ensure it is also picked up by the &lt;code&gt;pytest&lt;/code&gt; command. You should now see something like following:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  pytest
========================== test session starts ==========================
platform darwin -- Python 3.7.5, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /Users/dan/Sites/experiments/python-app-testing-and-linting
collected 2 items

src/main_test.py .F                                               [100%]

=============================== FAILURES ================================
_____________________________ test_failure ______________________________

    def test_failure():
&amp;gt;       assert add(3, 2) == 4
E       assert 5 == 4
E        +  where 5 = add(3, 2)

src/main_test.py:9: AssertionError
====================== 1 failed, 1 passed in 0.06s ======================
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before going back and fixing the failing test, leave it as-is in its failing state. We&amp;#x27;ll come back to it later.&lt;/p&gt;&lt;h2 id=&quot;run-lint-command&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#run-lint-command&quot; aria-label=&quot;run lint command permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Run Lint Command&lt;/h2&gt;&lt;p&gt;Next we&amp;#x27;ll identify undesirable formatting within our code. To do so, edit your test file and remove the line breaks from in-between the sample tests and save.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# main_test.py
from main import add
def test_success():
    assert add(2, 2) == 4
def test_failure():
    assert add(3, 2) == 4

&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If your editor is automatically applying standard formatting as you save, that&amp;#x27;s great. It will make things easier going forward. That said, for this next example, humor me and bypass it temporarily. If you&amp;#x27;re using VS Code, select &lt;strong&gt;File: Save without Formatting&lt;/strong&gt; from the command palette:&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:42.857142857142854%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAByZAB/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQJf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAERIP/aAAgBAQABPyErHj//2gAMAwEAAgADAAAAEOMP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAHBABAAIBBQAAAAAAAAAAAAAAAQARMRAhQVHR/9oACAEBAAE/EBpdjBkj3RW+R41//9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Save without formatting in VS Code&quot; title=&quot;Save without formatting in VS Code&quot; src=&quot;/static/263c7f5ef1928bdb8557fc1c3ac50c93/e5166/save-without-formatting.jpg&quot; srcSet=&quot;/static/263c7f5ef1928bdb8557fc1c3ac50c93/f93b5/save-without-formatting.jpg 300w,/static/263c7f5ef1928bdb8557fc1c3ac50c93/b4294/save-without-formatting.jpg 600w,/static/263c7f5ef1928bdb8557fc1c3ac50c93/e5166/save-without-formatting.jpg 1200w,/static/263c7f5ef1928bdb8557fc1c3ac50c93/6e7c1/save-without-formatting.jpg 1750w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;Now run &lt;code&gt;flake8&lt;/code&gt; in terminal and observe the formatting issues it discovered:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  flake8
./src/main_test.py:2:1: E302 expected 2 blank lines, found 0
./src/main_test.py:4:1: E302 expected 2 blank lines, found 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instead of going back and fixing the issues, keep reading. We&amp;#x27;ll setup an automatic Git hook in the next section where they&amp;#x27;ll serve as a useful example.&lt;/p&gt;&lt;h2 id=&quot;add-commit-hook&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#add-commit-hook&quot; aria-label=&quot;add commit hook permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Add Commit Hook&lt;/h2&gt;&lt;p&gt;To automatically validate formatting before code is checked-in to version control, we&amp;#x27;ll set up a pre-commit hook (using the aptly named pre-commit package). In your project root, create &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; and add the following:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
  - repo: https://gitlab.com/pycqa/flake8
    hooks:
      - id: flake8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, run pre-commit&amp;#x27;s install script to add the hook:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  pre-commit install
pre-commit installed at .git/hooks/pre-commit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;#x27;re now ready to commit the changes in a new Git branch and in doing so, test the newly installed hook. To do so, follow the commands below:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  git checkout -b add-tests-and-linting
Switched to a new branch &amp;#x27;add-tests-and-linting&amp;#x27;

➜  git add .

➜  git status
On branch add-tests-and-linting
Changes to be committed:
  (use &amp;quot;git restore --staged &amp;lt;file&amp;gt;...&amp;quot; to unstage)
        new file:   .pre-commit-config.yaml
        new file:   requirements.txt
        new file:   src/main.py
        new file:   src/main_test.py

➜  git commit -m &amp;quot;Add test and linting configuration&amp;quot;
Check Yaml...............................................................Passed
Fix End of Files.........................................................Passed
Trim Trailing Whitespace.................................................Passed
flake8...................................................................Failed
- hook id: flake8
- exit code: 1

src/main_test.py:2:1: E302 expected 2 blank lines, found 0
src/main_test.py:4:1: E302 expected 2 blank lines, found 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you retained the intentional formatting errors from &lt;a href=&quot;#run-lint-command&quot;&gt;the previous section&lt;/a&gt;, your attempt to commit should be rejected based on the flake8 formatting check. Go back to the test file, fix the formatting (but leave the failing test), and retry the commit. This time, it should succeed as expected.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  git add .

➜  git status
On branch add-tests-and-linting
Changes to be committed:
  (use &amp;quot;git restore --staged &amp;lt;file&amp;gt;...&amp;quot; to unstage)
        new file:   .pre-commit-config.yaml
        new file:   requirements.txt
        new file:   src/main.py
        new file:   src/main_test.py

➜  git commit -m &amp;quot;Add test and linting configuration&amp;quot;
Check Yaml...............................................................Passed
Fix End of Files.........................................................Passed
Trim Trailing Whitespace.................................................Passed
flake8...................................................................Passed
[add-tests-and-linting 66ebd44] Add test and linting configuration
 4 files changed, 28 insertions(+)
 create mode 100644 .pre-commit-config.yaml
 create mode 100644 requirements.txt
 create mode 100644 src/main.py
 create mode 100644 src/main_test.py
&lt;/code&gt;&lt;/pre&gt;&lt;highlight&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you&amp;#x27;re adding this to an existing project, &lt;a href=&quot;https://pre-commit.com/#4-optional-run-against-all-the-files&quot;&gt;pre-commit&amp;#x27;s docs recommend&lt;/a&gt; running &lt;code&gt;pre-commit run --all-files&lt;/code&gt; to run lints against existing files as a baseline. By default, it will only run against the files updated in a commit.&lt;/p&gt;&lt;/highlight&gt;&lt;p&gt;Before setting up the GitHub Action in the next section, push the newly created branch and open a pull request:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  git push origin add-tests-and-linting
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (7/7), 853 bytes | 853.00 KiB/s, done.
Total 7 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for &amp;#x27;add-tests-and-linting&amp;#x27; on GitHub by visiting:
remote:      https://github.com/xdmorgan/python-app-testing-and-linting/pull/new/add-tests-and-linting
remote:
To https://github.com/xdmorgan/python-app-testing-and-linting.git
 * [new branch]      add-tests-and-linting -&amp;gt; add-tests-and-linting
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:93.60986547085201%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAATABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAEEBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHoXbUwN4QUD//EABkQAAMAAwAAAAAAAAAAAAAAAAADFAERIP/aAAgBAQABBQKVRKkkT1rB/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFxAAAwEAAAAAAAAAAAAAAAAAADKhIP/aAAgBAQAGPwJRaLd//8QAGhAAAgIDAAAAAAAAAAAAAAAAAAER8CAhMf/aAAgBAQABPyG5l7FzELCJ0xK4j//aAAwDAQACAAMAAAAQjzcA/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPxAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPxAf/8QAHhABAAICAgMBAAAAAAAAAAAAAQARITGB8EFRcdH/2gAIAQEAAT8QVW2c/wBmLPR9lme7mBYuj1cofM1HcQEBHCQ+hDc//9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Create pull request screen&quot; title=&quot;Create pull request screen&quot; src=&quot;/static/7edc80ab31ac503b9d1fc8797fbbd5c3/e5166/create-pull-request.jpg&quot; srcSet=&quot;/static/7edc80ab31ac503b9d1fc8797fbbd5c3/f93b5/create-pull-request.jpg 300w,/static/7edc80ab31ac503b9d1fc8797fbbd5c3/b4294/create-pull-request.jpg 600w,/static/7edc80ab31ac503b9d1fc8797fbbd5c3/e5166/create-pull-request.jpg 1200w,/static/7edc80ab31ac503b9d1fc8797fbbd5c3/b0376/create-pull-request.jpg 1784w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;h2 id=&quot;add-github-action&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#add-github-action&quot; aria-label=&quot;add github action permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Add GitHub Action&lt;/h2&gt;&lt;p&gt;Now that everything is working as expected locally, we&amp;#x27;ll add a slightly modified version of this &lt;a href=&quot;https://github.com/actions/starter-workflows/blob/948df6a3d0b8c4d80d97154eaa4bcbc157cff432/ci/python-app.yml&quot;&gt;community GitHub action&lt;/a&gt; to run the test suite and linting in a continous integration environment. To get started, create &lt;code&gt;.github/workflows/python-app.yaml&lt;/code&gt; with the following contents:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Python application
on:
  push:
    branches: [master]
  pull_request:
    branches: [master]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
      - name: Lint with flake8
        run: |
          # stop the build if there are Python syntax errors or undefined names
          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
          flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
      - name: Test with pytest
        run: |
          pytest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once the workflow file is created, commit it, and push the branch to update the open pull request in GitHub. Upon refresh, there should now be a check that was started automatically.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:98.20627802690582%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAECBAX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHoTssYG8VkJB//xAAXEAADAQAAAAAAAAAAAAAAAAAAAxQg/9oACAEBAAEFApkkqSNO/wD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AR//xAAXEAADAQAAAAAAAAAAAAAAAAAAIDKh/9oACAEBAAY/ApIJ1//EABwQAAICAgMAAAAAAAAAAAAAAAABEBEh8EGR4f/aAAgBAQABPyG7n2y70zSxjXLWRR//2gAMAwEAAgADAAAAECMPAP/EABURAQEAAAAAAAAAAAAAAAAAABEg/9oACAEDAQE/ECP/xAAVEQEBAAAAAAAAAAAAAAAAAAARIP/aAAgBAgEBPxBj/8QAHhABAAIBBAMAAAAAAAAAAAAAAQARIRBBwfBhcYH/2gAIAQEAAT8QbtY2ujA2V9LmPUcwA0OIxDxMltIK3X3p/9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Build failure&quot; title=&quot;Build failure&quot; src=&quot;/static/b28d4ffb9739d7bacdd96c52e4295664/e5166/build-failure.jpg&quot; srcSet=&quot;/static/b28d4ffb9739d7bacdd96c52e4295664/f93b5/build-failure.jpg 300w,/static/b28d4ffb9739d7bacdd96c52e4295664/b4294/build-failure.jpg 600w,/static/b28d4ffb9739d7bacdd96c52e4295664/e5166/build-failure.jpg 1200w,/static/b28d4ffb9739d7bacdd96c52e4295664/b0376/build-failure.jpg 1784w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;Depending on how you look at it, this is either a disappointing or desirable outcome, all checks have failed. Click &lt;strong&gt;Details&lt;/strong&gt; and check the output in the Actions tab.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:69.49579831932773%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAQFAwb/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAABdi3ebTYXD//EABwQAAEDBQAAAAAAAAAAAAAAAAADBBMBAhASNP/aAAgBAQABBQJzTRGVQmvHvPj/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAbEAACAgMBAAAAAAAAAAAAAAAAATGhEBFxkv/aAAgBAQAGPwJta8k0TQ+rP//EAB0QAAIBBAMAAAAAAAAAAAAAAAABERAhMZFRYfD/2gAIAQEAAT8hszuVhC9hqdfQ8jkcTT//2gAMAwEAAgADAAAAEIz/AP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EAB0QAQABBAMBAAAAAAAAAAAAAAEAETFRYSGR8NH/2gAIAQEAAT8QQmUxAlWm4iuQ38ZpveJ4mEdivRLz/9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Failing checks&quot; title=&quot;Failing checks&quot; src=&quot;/static/5a4ee57744f4eb931a250038d4aa9474/e5166/failing-checks.jpg&quot; srcSet=&quot;/static/5a4ee57744f4eb931a250038d4aa9474/f93b5/failing-checks.jpg 300w,/static/5a4ee57744f4eb931a250038d4aa9474/b4294/failing-checks.jpg 600w,/static/5a4ee57744f4eb931a250038d4aa9474/e5166/failing-checks.jpg 1200w,/static/5a4ee57744f4eb931a250038d4aa9474/d9c39/failing-checks.jpg 1800w,/static/5a4ee57744f4eb931a250038d4aa9474/1c0b3/failing-checks.jpg 2380w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;If you&amp;#x27;re following along, you should see the failing test we added earlier. Remove or fix it locally then commit and push the changes to kick-off a new build.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:92.50559284116332%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAATABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAECAwX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB69r2TJsWAAn/xAAZEAADAAMAAAAAAAAAAAAAAAAAAxMRICH/2gAIAQEAAQUCmsmsks7nX//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BH//EABkQAAIDAQAAAAAAAAAAAAAAAAKRACAyM//aAAgBAQAGPwLAqcxUwKv/AP/EABoQAQACAwEAAAAAAAAAAAAAAAAB8REgMXH/2gAIAQEAAT8hhioqOx4OaQ//2gAMAwEAAgADAAAAEMcIAf/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QH//EAB4QAQACAgIDAQAAAAAAAAAAAAEAESExQVGRofDB/9oACAEBAAE/EK1CnGCz7T8ln0eoNZQWUpvzcL0IZ4iZcR6g3vfc/9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Passing checks&quot; title=&quot;Passing checks&quot; src=&quot;/static/84cff8cd64777c0549448f6ac7b92f99/e5166/passing-checks.jpg&quot; srcSet=&quot;/static/84cff8cd64777c0549448f6ac7b92f99/f93b5/passing-checks.jpg 300w,/static/84cff8cd64777c0549448f6ac7b92f99/b4294/passing-checks.jpg 600w,/static/84cff8cd64777c0549448f6ac7b92f99/e5166/passing-checks.jpg 1200w,/static/84cff8cd64777c0549448f6ac7b92f99/2347c/passing-checks.jpg 1788w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;This second time around, all checks should pass. With that, everything is set up both locally and remotely. There&amp;#x27;s nothing left to do but smash that merge button and go home for the day. Our work here is done.&lt;/p&gt;&lt;h2 id=&quot;tldr-link-to-code&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#tldr-link-to-code&quot; aria-label=&quot;tldr link to code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;TL;DR: Link to Code&lt;/h2&gt;&lt;p&gt;In a hurry? Want to use the end result as a starting point for your next project? See &lt;a href=&quot;https://github.com/xdmorgan/python-app-testing-and-linting&quot;&gt;xdmorgan/python-app-testing-and-linting&lt;/a&gt; on GitHub for everything covered above.&lt;/p&gt;&lt;p&gt;As I continue to adjust my workflow, I&amp;#x27;ll update this periodically. In the meantime, if you find any problems or know of a better way, please feel free to create an issue or open a PR against the demo repo linked above. To suggest an edit to this article, use the link in the sidebar.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Good Reads: April 18, 2020]]></title><description><![CDATA[A collection of my favorite articles, videos, and other resources from the past week.]]></description><link>https://danny.codes/blog/weekly-roundup-2020-04-18/</link><guid isPermaLink="false">https://danny.codes/blog/weekly-roundup-2020-04-18/</guid><pubDate>Sat, 18 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Since my last update, the brief glimpse of sunshine we were enjoying in the capital has quickly passed and cold rainy weather has returned to the &lt;abbr title=&quot;D.C., Maryland, and Virginia&quot;&gt;DMV&lt;/abbr&gt;. With most people stuck indoors for a couple of months now, there is a ton of great content being produced and more time than usual to consume it. That said, this one is a bumper edition—Best to make a pot of coffee ☕️&lt;/p&gt;&lt;h2 id=&quot;top-highlight&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#top-highlight&quot; aria-label=&quot;top highlight permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Top Highlight&lt;/h2&gt;&lt;p&gt;Josh Comeau&amp;#x27;s article, &lt;a href=&quot;https://joshwcomeau.com/gatsby/a-static-future/&quot;&gt;A Static Future&lt;/a&gt;, does a great job explaining the power of modern static site generators and clarifies some of the strengths and weaknesses of popular libraries like &lt;a href=&quot;https://www.gatsbyjs.org/&quot;&gt;Gatsby&lt;/a&gt; and &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;. It makes for a great introduction if you&amp;#x27;re new to the topic&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:69.63470319634703%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAcZLYQf/xAAXEAADAQAAAAAAAAAAAAAAAAABEBEA/9oACAEBAAEFAqWNV//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABkQAQEBAAMAAAAAAAAAAAAAAAEAESFBUf/aAAgBAQABPyHTud9hcnzIhv/aAAwDAQACAAMAAAAQoy//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAaEAEAAwEBAQAAAAAAAAAAAAABABEhgZGx/9oACAEBAAE/EFiCS4xonlR2i+QBL8ihN8JUT//Z&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;A static future&quot; title=&quot;A static future&quot; src=&quot;/static/d7c8063f71c03e05f1cf6016877f9091/e5166/a-static-future.jpg&quot; srcSet=&quot;/static/d7c8063f71c03e05f1cf6016877f9091/f93b5/a-static-future.jpg 300w,/static/d7c8063f71c03e05f1cf6016877f9091/b4294/a-static-future.jpg 600w,/static/d7c8063f71c03e05f1cf6016877f9091/e5166/a-static-future.jpg 1200w,/static/d7c8063f71c03e05f1cf6016877f9091/d9c39/a-static-future.jpg 1800w,/static/d7c8063f71c03e05f1cf6016877f9091/df51d/a-static-future.jpg 2400w,/static/d7c8063f71c03e05f1cf6016877f9091/60dcf/a-static-future.jpg 2628w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;h2 id=&quot;design-systems&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#design-systems&quot; aria-label=&quot;design systems permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Design Systems&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://aneventapart.com/news/post/what-has-changed-and-wheres-it-going-aea-video&quot;&gt;“What Has Changed and Where’s it Going?” by Dave Rupert—An Event Apart video&lt;/a&gt;&lt;/strong&gt;: A close runner up for my favorite piece of content this week, Dave&amp;#x27;s a great speaker and his presentation at An Event Apart here in DC is no exception. He discusses how advancements in browser support have made redundant some previously popular polyfills and support libraries in an effort to slim down page weights whose growth outpaces data limits and connection speeds.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://visly.app/blog/layout-isolated-components&quot;&gt;Layout-isolated Components&lt;/a&gt;&lt;/strong&gt;: Facebook&amp;#x27;s former &amp;#x27;flexbox guy&amp;#x27;, Emil Sjölander, argues against the use of several properties whose effects extend outside the bounds of a reusable component resulting in unpredictable and inconsistent behavior in different situations.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;css&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#css&quot; aria-label=&quot;css permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSS&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ishadeed.com/article/new-facebook-css/&quot;&gt;CSS Findings From the New Facebook Design&lt;/a&gt;:&lt;/strong&gt; In-depth analysis of techniques used to build the new Facebook UI&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://blog.animaapp.com/reverse-engineering-whatsapp-webs-css-9239293009f4&quot;&gt;Reverse Engineering WhatsApp Web’s CSS&lt;/a&gt;:&lt;/strong&gt; Breakdown of some of the techniques used to build WhatsApp web and an accompanying &lt;a href=&quot;https://codepen.io/elad2412/pen/wvaNyBv&quot;&gt;Codepen rebuild&lt;/a&gt; as reference.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ethanmarcotte.com/wrote/css-grid-without-max-width/&quot;&gt;Constrained CSS grids without max-width&lt;/a&gt;:&lt;/strong&gt; Clever technique for contstraining grids with variable bookend-columns instead of a &lt;code&gt;max-width&lt;/code&gt; setting.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.chenhuijing.com/blog/css-for-i18n&quot;&gt;CSS for internationalisation&lt;/a&gt;:&lt;/strong&gt; Amazing deep-dive into HTML and CSS&amp;#x27;s i18n-friendly properties (e.g. dir, lang, logical-properties, writing-mode)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://jdan.github.io/98.css/#intro&quot;&gt;98.css&lt;/a&gt;:&lt;/strong&gt; Who doesn&amp;#x27;t love a bit of nostalgia?&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;javascript&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#javascript&quot; aria-label=&quot;javascript permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;JavaScript&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://egghead.io/playlists/introduction-to-next-js-9-9c01&quot;&gt;Introduction to Next.js 9&lt;/a&gt;&lt;/strong&gt;: This Egghead course was a great way to jump back in after using Gatsby on a handful of recent projects. &lt;a href=&quot;https://nextjs.org/blog/next-9-3&quot;&gt;Recently updated&lt;/a&gt; to empower SSG workflows, my favorite part about Next has always been the way the maintainers put careful consideration into API design to simplify complex functionality into easy-to-use solutions like filename-based dynamic routes 🤯 (Egghead subscription required)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=qeCncjPWD1A&quot;&gt;Learn ES Modules in Node with Myles Borins&lt;/a&gt;:&lt;/strong&gt; Insightful walkthrough of Node&amp;#x27;s recently added ES Module support including self-referential imports and export maps.&lt;/li&gt;&lt;/ul&gt;&lt;highlight&gt;&lt;p&gt;&lt;strong&gt;Speaking of Gatsby:&lt;/strong&gt; This site is one of those I recently built with Gatsby, it uses a custom theme (&lt;code&gt;gatsby-theme-xdmorgan&lt;/code&gt;) to aggregate and configure popular Gatsby plugins (like a &lt;a href=&quot;https://kentcdodds.com/blog/concerning-toolkits&quot;&gt;toolkit dependency&lt;/a&gt;). This isolates dependency management, allows drop-in reuse on side-projects, and provides quickstart support for TypeScript, Sass, MDX, and other opinionated nice-to-haves. Check it out on &lt;a href=&quot;https://www.npmjs.com/package/gatsby-theme-xdmorgan&quot;&gt;npm&lt;/a&gt; and &lt;a href=&quot;https://github.com/xdmorgan/gatsby-theme-xdmorgan&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/highlight&gt;&lt;h2 id=&quot;tools&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#tools&quot; aria-label=&quot;tools permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Tools&lt;/h2&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:61.13095238095239%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAMBAgQF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAesvTCVGi//EABwQAAAGAwAAAAAAAAAAAAAAAAABAgMRMRITIv/aAAgBAQABBQI20DBE6mxYnoq//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAHhAAAAUFAQAAAAAAAAAAAAAAAAIQEZEBAxMhMoH/2gAIAQEABj8C1aLXwNiLA4LCMn//xAAbEAADAAIDAAAAAAAAAAAAAAAAARExQWGx8f/aAAgBAQABPyFboAoWXeB44cR1YZU6GA//2gAMAwEAAgADAAAAEH/P/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQMBAT8Qp//EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAECAQE/EIf/xAAcEAEAAgIDAQAAAAAAAAAAAAABABEhUTFBkfD/2gAIAQEAAT8QCyV6o7kY0HkGMVfXUCBKoXE7OQwgAUBP/9k=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Framer Web beta&quot; title=&quot;Framer Web beta&quot; src=&quot;/static/e73b890744bf06da49fec773fdad3111/e5166/framer-web-beta.jpg&quot; srcSet=&quot;/static/e73b890744bf06da49fec773fdad3111/f93b5/framer-web-beta.jpg 300w,/static/e73b890744bf06da49fec773fdad3111/b4294/framer-web-beta.jpg 600w,/static/e73b890744bf06da49fec773fdad3111/e5166/framer-web-beta.jpg 1200w,/static/e73b890744bf06da49fec773fdad3111/d9c39/framer-web-beta.jpg 1800w,/static/e73b890744bf06da49fec773fdad3111/df51d/framer-web-beta.jpg 2400w,/static/e73b890744bf06da49fec773fdad3111/1b92a/framer-web-beta.jpg 3360w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;This week an invite for the &lt;a href=&quot;https://www.framer.com/web/&quot;&gt;Framer Web beta&lt;/a&gt; arrived in my inbox 📬 Being real-time and web-based, it will draw obvious comparisons with &lt;a href=&quot;https://www.figma.com/&quot;&gt;Figma&lt;/a&gt;. As a code-backed prototyping tool, I&amp;#x27;m interested to see how it can be integrated into a design systems component library workflow, more on that soon!&lt;/p&gt;&lt;h2 id=&quot;architecture&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#architecture&quot; aria-label=&quot;architecture permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Architecture&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.sitepen.com/blog/architecture-spotlight-event-sourcing/&quot;&gt;Architecture Spotlight: Event Sourcing&lt;/a&gt;&lt;/strong&gt;: Simple explanation of the event sourcing pattern and how the storage of mutation events (instead of their outcomes) can result in an easily audited historical timeline of changes to saved data.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;performance&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#performance&quot; aria-label=&quot;performance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Performance&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://thetrevorharmon.com/blog/how-to-prepare-and-use-variable-fonts-on-the-web&quot;&gt;How to Prepare and Use Variable Fonts on the Web&lt;/a&gt;:&lt;/strong&gt; Step-by-step guide to subsetting and providing fallbacks when migrating to variable fonts. Given this site also uses &lt;a href=&quot;https://rsms.me/inter/&quot;&gt;Inter&lt;/a&gt;, I should probably get on that.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://cloudfour.com/thinks/responsive-images-the-simple-way/&quot;&gt;Responsive Images the Simple Way&lt;/a&gt;&lt;/strong&gt;: CloudFour have a few helpful guides to responsive images, this one concisely explains &lt;code&gt;srcset&lt;/code&gt; and &lt;code&gt;sizes&lt;/code&gt; attributes and how/when to use them with &lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;picture /&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;conferences&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#conferences&quot; aria-label=&quot;conferences permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conferences&lt;/h2&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:771px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:29.442282749675748%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB0ElEQVQY0z3MbU9ScRgG8LNF9gDsiAgcXCWtZeKAAlKrFZyo4dNRzgiSEo8ankA8JFG51erUcNXW8iP0DfoUba7ZTJSNfFes1UYfoP3fsPvqZFsvfruvF9d1c109F2HuDsNk8cNk/eegNfDf4c4gzPYwOvizOGB0DvHnwLuHccR1HkeFQZicob+owxWGzX2hzVkdQ0jdrkC7/wpq8cW+QqkKJfd4/+YKOsYSS9DKL5HL69Aevkbs0gyeBovQQipKQRWKX6FyKA/PiVibszgDiMSnoagVyJk80tllJG8VkJnTkJ4tYjy5gKGIjJtGnlaWMZqcw6m+y5jql3C9bxQJ76TxMEWZARmCI9DmPL1L5HIskt02T077ouEudfF54s0FslmLZO+8R4LjAfGWspErZOcfUY+gk/O4TsdOVsntWSOht0rdvnWYx963ubhvB1JwG3F/w/AFMW8DK9km1io/sTLTxPPSD6zmvuPtsxZU+RvePGlhXW8hJ32FeLqBEd+esdvDSKYJn/SLuNWpd+xDNcvigQ0W9W6xaP8mS17ZZnfkOpsd32XpqzWWEmtsIVFnN6I1pkzssvnJOpOGP7PImU0mDnxiorERB7fYNe/H338A/JfqxbX+4L8AAAAASUVORK5CYII=&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Jamstack virtual&quot; title=&quot;Jamstack virtual&quot; src=&quot;/static/8744039290e48a4babe332d65c75032c/5d030/jamstack.png&quot; srcSet=&quot;/static/8744039290e48a4babe332d65c75032c/5a46d/jamstack.png 300w,/static/8744039290e48a4babe332d65c75032c/0a47e/jamstack.png 600w,/static/8744039290e48a4babe332d65c75032c/5d030/jamstack.png 771w&quot; sizes=&quot;(max-width: 771px) 100vw, 771px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.netlify.com/blog/2020/04/16/announcing-jamstack-conf-virtual-schedule-and-registration/&quot;&gt;Jamstack Conf Virtual&lt;/a&gt;:&lt;/strong&gt; Organized by &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;, the next Jamstack conf is going free virtual, see you there 👋&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;sources&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#sources&quot; aria-label=&quot;sources permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Sources&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://sidebar.io/&quot;&gt;Sidebar&lt;/a&gt;:&lt;/strong&gt; Daily newsletter&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;http://esnextnews.com/&quot;&gt;ESNext News&lt;/a&gt;:&lt;/strong&gt; Weekly newsletter&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;child-my-0 my-4x md:my-8x&quot;&gt;&lt;p&gt;&lt;img src=&quot;/edfce48303c44aa21829f4c72d8602bf/asm.gif&quot; alt=&quot;Allan Saint-Maximin&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;highlight&gt;&lt;p&gt;Thanks for reading! Hopefully the next time I write one of these it will be &lt;a href=&quot;https://www.forbes.com/sites/harrydecosemo/2020/04/15/newcastle-united-fans-reaching-for-cans-as-takeover-gets-closer/#523a94d026aa&quot;&gt;#cans&lt;/a&gt; on tyneside. 🤞&lt;/p&gt;&lt;/highlight&gt;</content:encoded></item><item><title><![CDATA[Good Reads: April 11, 2020]]></title><description><![CDATA[A collection of my favorite articles, videos, and other resources from the past week]]></description><link>https://danny.codes/blog/weekly-roundup-2020-04-11/</link><guid isPermaLink="false">https://danny.codes/blog/weekly-roundup-2020-04-11/</guid><pubDate>Sat, 11 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;something-different&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#something-different&quot; aria-label=&quot;something different permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Something Different&lt;/h2&gt;&lt;p&gt;It&amp;#x27;s a beautiful spring Saturday here in Washington D.C. and as the warmer weather thankfully arrives in the mid-atlantic, chances are you&amp;#x27;re also experiencing something very different than your usual routine due to the COVID-19 pandemic. Wherever you are, whenever you&amp;#x27;re reading this, I hope you and your loved ones are safe and well.&lt;/p&gt;&lt;p&gt;While I would usually be watching Newcastle United or playing pick-up soccer on a Saturday morning, neither of those are options today, so as they say: &amp;quot;when life gives you lemons, catch up on your bookmarked reading list.&amp;quot;&lt;/p&gt;&lt;p&gt;Though I&amp;#x27;m not always able to keep up, I subscribe to a handful of newsletters—some of which are linked below—in an attempt to stay current in topics of interest: UX, UI design, and software development. Unfortunately though, once I&amp;#x27;m done reading and the browser tab is closed, I soon find it difficult to retrieve that great content when the topic it discusses later comes up in conversation.&lt;/p&gt;&lt;highlight&gt;&lt;p&gt;This actually happened just this past week with an amazing demo of &lt;a href=&quot;https://vimeo.com/242899818&quot;&gt;Facebook&amp;#x27;s internal design tooling&lt;/a&gt; from a talk given at a Sketch meetup a while back. Thankfully, one of my coworkers was able to find it from the vague description I was able to muster from memory—They&amp;#x27;ve got skills.&lt;/p&gt;&lt;/highlight&gt;&lt;p&gt;So, in an effort to make these things easier to find in the future, I&amp;#x27;m going to start publishing roundups of 5-10 resources (articles, conference talks, GitHub repos) that caught my eye. If you find yourself enjoying the content, these roundup posts are conveniently published along with the rest of my blog posts in my &lt;a href=&quot;/rss.xml&quot;&gt;site&amp;#x27;s dedicated RSS feed&lt;/a&gt; so smash that subscribe button in your favorite RSS reader, if you&amp;#x27;re into that sort of thing.&lt;/p&gt;&lt;p&gt;With that said, let&amp;#x27;s see what we got goin&amp;#x27; on this week 👀&lt;/p&gt;&lt;h2 id=&quot;top-highlight&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#top-highlight&quot; aria-label=&quot;top highlight permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Top Highlight&lt;/h2&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:75.53516819571865%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAEDAgT/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB71qZAZZ//8QAGhABAQADAQEAAAAAAAAAAAAAAwIBBBMAEv/aAAgBAQABBQLiOCgde/OJzf2dAXM62EnN/wD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAcEAACAgIDAAAAAAAAAAAAAAAAAQJBEjERISL/2gAIAQEABj8CycVo8xizqFGLdGxcOj//xAAaEAEAAgMBAAAAAAAAAAAAAAABABEhMUFh/9oACAEBAAE/IUwcAqwkQfIQCCKrfJ2XDrVDa3nuI5YPE//aAAwDAQACAAMAAAAQKO//xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8QmK//xAAVEQEBAAAAAAAAAAAAAAAAAAAAAf/aAAgBAgEBPxBK/8QAGhABAQADAQEAAAAAAAAAAAAAAREAITFBYf/aAAgBAQABPxCt5joE249SbtWcxX8Che1wW0HIB4ezGdQd5qSw9V79y3OJ0evzP//Z&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Beyond Purple&quot; title=&quot;Beyond Purple&quot; src=&quot;/static/271cd0d866255f768179911bb53749d8/e5166/beyond-purple.jpg&quot; srcSet=&quot;/static/271cd0d866255f768179911bb53749d8/f93b5/beyond-purple.jpg 300w,/static/271cd0d866255f768179911bb53749d8/b4294/beyond-purple.jpg 600w,/static/271cd0d866255f768179911bb53749d8/e5166/beyond-purple.jpg 1200w,/static/271cd0d866255f768179911bb53749d8/d9c39/beyond-purple.jpg 1800w,/static/271cd0d866255f768179911bb53749d8/df51d/beyond-purple.jpg 2400w,/static/271cd0d866255f768179911bb53749d8/742c0/beyond-purple.jpg 2616w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;p&gt;If I had to pick a highlight this week, it would be &lt;strong&gt;Twitch&amp;#x27;s &lt;a href=&quot;https://blog.twitch.tv/en/2019/12/03/beyond-purple&quot;&gt;Beyond Purple&lt;/a&gt; design system case study&lt;/strong&gt;. I currently work as a design system maintainer (or &lt;a href=&quot;https://daverupert.com/2020/01/the-web-is-industrialized-and-i-helped-industrialize-it/&quot;&gt;gardener&lt;/a&gt;, whichever you prefer) so the elegance of their tokens implementation and their customizable creator colors really resonated with me.&lt;/p&gt;&lt;h2 id=&quot;articles&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#articles&quot; aria-label=&quot;articles permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Articles&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks&quot;&gt;The State Reducer Pattern with React Hooks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://uxdesign.cc/button-design-user-interface-components-series-85243b6736c7&quot;&gt;Button Design — UI component series&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://badootech.badoo.com/design-tokens-beyond-colors-typography-and-spacing-ad7c98f4f228&quot;&gt;Design Tokens beyond colors, typography, and spacing.&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.alwaystwisted.com/articles/creating-utility-classes-with-design-tokens-using-sass&quot;&gt;Creating Utility Classes with Design Tokens using Sass&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;highlight&gt;&lt;p&gt;This morning, I serendipitously came across Stu Robson&amp;#x27;s post describing how to use design tokens to generate utility classes. Great minds think alike obviously, I also maintain an open-source library, Skeletor, so you can &lt;a href=&quot;https://github.com/xdmorgan/skeletor/tree/master/packages/css&quot;&gt;generate utility classes for your own system tokens&lt;/a&gt;. While the documentation is still a work in-progress, the best practical example of the library&amp;#x27;s use is this site which is &lt;a href=&quot;https://github.com/xdmorgan/personal-site/blob/master/skeletor.yml&quot;&gt;also open-source&lt;/a&gt;.&lt;/p&gt;&lt;/highlight&gt;&lt;h2 id=&quot;videos&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#videos&quot; aria-label=&quot;videos permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Videos&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/resources/watch-build-your-first-app-contentful-new-app-framework/&quot;&gt;How to build your first app with Contentful&amp;#x27;s new App Framework&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://frontendmasters.com/courses/hardcore-js-patterns/&quot;&gt;Hardcore Functional Programming in JavaScript, v2&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://frontendmasters.com/courses/hardcore-js-patterns/&quot;&gt;Hardcore Functional Architecture Patterns in JavaScript&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=3QqnhDI7VFA&quot;&gt;Pastry Chef Attempts to Make Gourmet Cadbury Creme Eggs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;highlight&gt;&lt;p&gt;This week, I&amp;#x27;m working through Brian Lonsdorf&amp;#x27;s new Front End Master&amp;#x27;s Hardcore Functional Programming course and already looking forward to the the advanced patterns course next. For an introduction to functional concepts in JavaScript, I would highly recommend Will Sentance&amp;#x27;s &lt;a href=&quot;https://frontendmasters.com/teachers/will-sentance/&quot;&gt;Hard Parts: Functional JS Foundations&lt;/a&gt; also on Front End masters (FEM subscription required).&lt;/p&gt;&lt;/highlight&gt;&lt;h2 id=&quot;references--sources&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#references--sources&quot; aria-label=&quot;references  sources permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;References &amp;amp; Sources&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://kentcdodds.com/&quot;&gt;Kent Dodd&amp;#x27;s newsletter&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;http://news.design.systems/&quot;&gt;Design System News newsletter&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Rendering Lists with React]]></title><description><![CDATA[How to build a simple, reusable, and design-agnostic React list rendering component to simplify working with collections of data.]]></description><link>https://danny.codes/blog/rendering-lists-with-react/</link><guid isPermaLink="false">https://danny.codes/blog/rendering-lists-with-react/</guid><pubDate>Tue, 24 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;lets-get-it&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#lets-get-it&quot; aria-label=&quot;lets get it permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Let&amp;#x27;s Get It&lt;/h2&gt;&lt;p&gt;In this post, we&amp;#x27;ll be building a declarative React list rendering component inspired by &lt;a href=&quot;https://ant.design/components/list/&quot;&gt;Ant Design&amp;#x27;s List&lt;/a&gt; and &lt;a href=&quot;https://reactnative.dev/docs/flatlist&quot;&gt;React Native&amp;#x27;s FlatList&lt;/a&gt; to use in situations where you may regularly reach for &lt;code&gt;array.map()&lt;/code&gt;. While JavaScript&amp;#x27;s array methods are amazing modern additions to the language—they improve readability and foster immutable data transformation—they&amp;#x27;re fairly limited when it comes to UI rendering, but we can use them as a foundation for something better!&lt;/p&gt;&lt;p&gt;Over the next few examples, we&amp;#x27;ll refactor an &lt;code&gt;array.map()&lt;/code&gt; example to use a newly created &lt;code&gt;ListView&lt;/code&gt;, then start to introduce functionality designed to cut boilerplate required to implement common patterns for organizing collections of data.&lt;/p&gt;&lt;h3 id=&quot;list-and-listitem&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#list-and-listitem&quot; aria-label=&quot;list and listitem permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;List and List.Item&lt;/h3&gt;&lt;p&gt;Before we get started, we&amp;#x27;ll create a couple unstyled &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;List.Item&lt;/code&gt; components to use in the rest of the examples. To follow along interactively, see this &lt;a href=&quot;https://codesandbox.io/s/rendering-lists-with-react-2y0v8&quot;&gt;CodeSandbox Project&lt;/a&gt; and feel free to fork and customize/style the examples as we go then &lt;a href=&quot;https://twitter.com/xdanmorgan&quot;&gt;share them with me on Twitter&lt;/a&gt;&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;

// The list container
const List = ({ children }) =&amp;gt; &amp;lt;ul&amp;gt;{children}&amp;lt;/ul&amp;gt;

// An individual list item with a controlled checkbox
const Item = ({ children, ...props }) =&amp;gt; (
  &amp;lt;li&amp;gt;
    &amp;lt;label&amp;gt;
      &amp;lt;input type=&amp;quot;checkbox&amp;quot; {...props} /&amp;gt; {children}
    &amp;lt;/label&amp;gt;
  &amp;lt;/li&amp;gt;
)

// Named export of List and static access of List.Item
List.Item = Item
export { List }
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;hardcoded-list-items&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#hardcoded-list-items&quot; aria-label=&quot;hardcoded list items permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Hardcoded List Items&lt;/h3&gt;&lt;p&gt;Now we have &lt;code&gt;&amp;lt;List /&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;List.Item /&amp;gt;&lt;/code&gt; created, we can use them to create a basic to-do list.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { List } from &amp;#x27;./list&amp;#x27;

export default function Demo() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h2&amp;gt;Daily objectives&amp;lt;/h2&amp;gt;
      &amp;lt;List&amp;gt;
        &amp;lt;List.Item checked&amp;gt;Make some coffee&amp;lt;/List.Item&amp;gt;
        &amp;lt;List.Item checked&amp;gt;Write blog post&amp;lt;/List.Item&amp;gt;
        &amp;lt;List.Item checked={false}&amp;gt;Profit&amp;lt;/List.Item&amp;gt;
      &amp;lt;/List&amp;gt;
    &amp;lt;/main&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;using-arraymap&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#using-arraymap&quot; aria-label=&quot;using arraymap permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Using Array.map()&lt;/h3&gt;&lt;p&gt;As our list continues to grow, we&amp;#x27;ll update to use a simple data structure then start rendering a variable amount of items with &lt;code&gt;array.map()&lt;/code&gt;—Don&amp;#x27;t forget the key prop!&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { List } from &amp;#x27;./list&amp;#x27;

/*
items = [
  { &amp;quot;id&amp;quot;: 2, &amp;quot;title&amp;quot;: &amp;quot;Make some coffee&amp;quot;, &amp;quot;complete&amp;quot;: true }
  { &amp;quot;id&amp;quot;: 1, &amp;quot;title&amp;quot;: &amp;quot;Write blog post&amp;quot;, &amp;quot;complete&amp;quot;: true }
  { &amp;quot;id&amp;quot;: 3, &amp;quot;title&amp;quot;: &amp;quot;Profit&amp;quot;, &amp;quot;complete&amp;quot;: false }
]
*/

export default function Demo({ items = [] }) {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h2&amp;gt;Daily objectives&amp;lt;/h2&amp;gt;
      &amp;lt;List&amp;gt;
        {items.map(item =&amp;gt; (
          &amp;lt;List.Item key={item.id} checked={item.complete}&amp;gt;
            {item.title}
          &amp;lt;/List.Item&amp;gt;
        ))}
      &amp;lt;/List&amp;gt;
    &amp;lt;/main&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;new-challenger-approaches&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#new-challenger-approaches&quot; aria-label=&quot;new challenger approaches permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;New Challenger Approaches&lt;/h2&gt;&lt;p&gt;So far, so good. The example above is a valid and standard approach in most React codebases. Over time, there will be other features we want to add to this list, such as filtering, sorting, and pagination. As the project continues to grow, the boilerplate supporting these patterns will get fragmented and inconsistent, unless we have a way to centralize them.&lt;/p&gt;&lt;p&gt;Let&amp;#x27;s start our alternative approach by creating &lt;code&gt;ListView&lt;/code&gt; to be functionally equivalent to &lt;code&gt;array.map()&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// list-view.js
import React from &amp;#x27;react&amp;#x27;

export const ListView = ({ items = [], render = () =&amp;gt; {} }) =&amp;gt; (
  &amp;lt;&amp;gt;{items.map(render)}&amp;lt;/&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can now refactor our application code to replace our &lt;code&gt;List.Item&lt;/code&gt; rendering&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { ListView } from &amp;#x27;./list-view&amp;#x27;
import { List } from &amp;#x27;./list&amp;#x27;

export default function Demo({ items = [] }) {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h2&amp;gt;Daily objectives&amp;lt;/h2&amp;gt;
      &amp;lt;List&amp;gt;
        &amp;lt;ListView
          items={items}
          render={item =&amp;gt; (
            &amp;lt;List.Item key={item.id} checked={item.complete}&amp;gt;
              {item.title}
            &amp;lt;/List.Item&amp;gt;
          )}
        /&amp;gt;
      &amp;lt;/List&amp;gt;
    &amp;lt;/main&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;improving-the-api&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#improving-the-api&quot; aria-label=&quot;improving the api permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Improving the API&lt;/h2&gt;&lt;p&gt;With the foundation in place, let&amp;#x27;s add a few more props and, in doing so, add support for many more use-cases.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;

export const ListView = ({
  items = [],
  render = () =&amp;gt; {},
  as: Wrapper = React.Fragment,
  container: Container = React.Fragment,
  before = null,
  after = null,
}) =&amp;gt; (
  &amp;lt;Wrapper&amp;gt;
    {before ? before() : null}
    &amp;lt;Container&amp;gt;{items.map(render)}&amp;lt;/Container&amp;gt;
    {after ? after() : null}
  &amp;lt;/Wrapper&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A quick note on what the newly added props do before we update the example code. In each case, when these optional props are omitted, they leave behind no trace of their existence rendering either &lt;code&gt;null&lt;/code&gt; or a &lt;code&gt;Fragment&lt;/code&gt; accordingly.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;as&lt;/strong&gt;: Render the whole thing in a wrapper element.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;container&lt;/strong&gt;: Render an element around the list body.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;before&lt;/strong&gt;: Render a block of content before the list body.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;after&lt;/strong&gt;: Render a block of content after the list body.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { ListView } from &amp;#x27;./list-view&amp;#x27;
import { List } from &amp;#x27;./list&amp;#x27;

export default function Demo({ items = [] }) {
  return (
    &amp;lt;ListView
      as=&amp;quot;main&amp;quot;
      items={items}
      container={List}
      before={() =&amp;gt; (
        &amp;lt;header&amp;gt;
          &amp;lt;h1&amp;gt;Daily objectives&amp;lt;/h1&amp;gt;
        &amp;lt;/header&amp;gt;
      )}
      render={(item, idx) =&amp;gt; (
        &amp;lt;List.Item key={item.id} checked={item.complete}&amp;gt;
          {item.title}
        &amp;lt;/List.Item&amp;gt;
      )}
      after={() =&amp;gt; (
        &amp;lt;footer&amp;gt;Complete: {items.filter(item =&amp;gt; item.complete).length}&amp;lt;/footer&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;composition&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#composition&quot; aria-label=&quot;composition permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Composition&lt;/h2&gt;&lt;p&gt;At this point, the ListView itself is fully functional and gives us a nice declarative API to render the original example entirely. Are we done? Not nearly. Now we&amp;#x27;re ready to really get started! With a flexible foundation in place, we can now write a series of thin wrapper components to implement custom functionality or styles.&lt;/p&gt;&lt;h3 id=&quot;introducing-pagination&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#introducing-pagination&quot; aria-label=&quot;introducing pagination permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Introducing Pagination&lt;/h3&gt;&lt;p&gt;As our list length grows, one of the first additions we&amp;#x27;ll want to make is the introduction of pagination. To support a traditional pagination pattern, we&amp;#x27;ll wrap the &lt;code&gt;ListView&lt;/code&gt; with a specialized &lt;code&gt;PaginatedListView&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// paginated-list-view.js
import React from &amp;#x27;react&amp;#x27;
import { ListView } from &amp;#x27;./list-view&amp;#x27;

// Pagination Hook
export function usePagination(total, perPage, initial) {
  const [current, setPage] = React.useState(initial)
  const numPages =
    total % perPage ? Math.floor(total / perPage) + 1 : total / perPage
  const onPrevious = () =&amp;gt; setPage(Math.max(1, current - 1))
  const onNext = () =&amp;gt; setPage(Math.min(current + 1, numPages))
  return { current, onPrevious, onNext, numPages, setPage }
}

// Intercept list items, extract the items for or current page
// then render a standard ListView
export function PaginatedListView({
  items,
  perPage = -1,
  currentPage = 1,
  ...props
}) {
  const offset = (currentPage - 1) * perPage
  const paginated = items.slice(offset, offset + perPage)
  return &amp;lt;ListView {...props} items={paginated} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;highlight&gt;&lt;p&gt;&lt;strong&gt;Sidenote:&lt;/strong&gt; If you&amp;#x27;re new to React Hooks, this recently updated free course from Kent Dodds &amp;amp; Egghead &lt;a href=&quot;https://egghead.io/lessons/react-create-reusable-custom-hooks&quot;&gt;The Beginner&amp;#x27;s Guide to React&lt;/a&gt; is an excellent introduction to the topic.&lt;/p&gt;&lt;/highlight&gt;&lt;p&gt;To implement this in our example code, we&amp;#x27;ll need to add the &lt;code&gt;usePagination()&lt;/code&gt; Hook, update the component name from &lt;code&gt;ListView&lt;/code&gt; to &lt;code&gt;PaginatedListView&lt;/code&gt;, then add two new props: &lt;code&gt;currentPage&lt;/code&gt; and &lt;code&gt;perPage&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { PaginatedListView, usePagination } from &amp;#x27;./paginated-list-view&amp;#x27;
import { List } from &amp;#x27;./list&amp;#x27;

export default function Demo({ items = [], perPage = 6 }) {
  const { current, onPrevious, onNext, numPages } = usePagination(
    items.length,
    perPage,
    1
  )
  return (
    &amp;lt;PaginatedListView
      currentPage={current}
      perPage={perPage}
      as=&amp;quot;main&amp;quot;
      items={items}
      container={List}
      before={() =&amp;gt; (
        &amp;lt;header&amp;gt;
          &amp;lt;h1&amp;gt;Daily objectives&amp;lt;/h1&amp;gt;
        &amp;lt;/header&amp;gt;
      )}
      render={(item, idx) =&amp;gt; (
        &amp;lt;List.Item key={item.id} checked={item.complete}&amp;gt;
          {item.title}
        &amp;lt;/List.Item&amp;gt;
      )}
      after={() =&amp;gt; (
        &amp;lt;footer&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;button onClick={onPrevious} disabled={current &amp;lt;= 1}&amp;gt;
              Previous
            &amp;lt;/button&amp;gt;
            &amp;lt;button onClick={onNext} disabled={current &amp;gt;= numPages}&amp;gt;
              Next
            &amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;Complete: {items.filter(item =&amp;gt; item.complete).length}&amp;lt;/div&amp;gt;
        &amp;lt;/footer&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;render-a-table-instead&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#render-a-table-instead&quot; aria-label=&quot;render a table instead permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Render a Table Instead?&lt;/h3&gt;&lt;p&gt;We sure can. Let&amp;#x27;s swap out all the HTML in our example and semantically render a table without modifying anything below the surface and keep the pagination free.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React from &amp;#x27;react&amp;#x27;
import { PaginatedListView, usePagination } from &amp;#x27;./paginated-list-view&amp;#x27;

export default function Demo({ items = [], perPage = 6 }) {
  const { current, onPrevious, onNext, numPages } = usePagination(
    items.length,
    perPage,
    1
  )
  return (
    &amp;lt;PaginatedListView
      currentPage={current}
      perPage={perPage}
      as=&amp;quot;table&amp;quot;
      items={items}
      container=&amp;quot;tbody&amp;quot;
      before={() =&amp;gt; (
        &amp;lt;thead&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;Done&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
      )}
      render={item =&amp;gt; (
        &amp;lt;tr key={item.id}&amp;gt;
          &amp;lt;td&amp;gt;
            &amp;lt;input
              type=&amp;quot;checkbox&amp;quot;
              onChange={() =&amp;gt; {}}
              checked={item.complete}
              id={`item_${item.id}`}
            /&amp;gt;
          &amp;lt;/td&amp;gt;
          &amp;lt;td&amp;gt;
            &amp;lt;label htmlFor={`item_${item.id}`}&amp;gt;{item.title}&amp;lt;/label&amp;gt;
          &amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
      )}
      after={() =&amp;gt; (
        &amp;lt;tfoot&amp;gt;
          &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;{items.filter(item =&amp;gt; item.complete).length}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;
              &amp;lt;button onClick={onPrevious} disabled={current &amp;lt;= 1}&amp;gt;
                Previous
              &amp;lt;/button&amp;gt;
              &amp;lt;button onClick={onNext} disabled={current &amp;gt;= numPages}&amp;gt;
                Next
              &amp;lt;/button&amp;gt;
            &amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
        &amp;lt;/tfoot&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;further-improvement&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#further-improvement&quot; aria-label=&quot;further improvement permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Further Improvement&lt;/h2&gt;&lt;p&gt;To expand upon the examples above, here&amp;#x27;s a few features that can be implemented in just few lines in new composing components without modifying the underlying ListView at all:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Add list sorting and filtering by completion status using header controls&lt;/li&gt;&lt;li&gt;Create an alternate pagination pattern which uses an IntersectionOberserver-wrapped footer to support infinite scrolling&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Really, the possibilities are endless. Feel free to fork the &lt;a href=&quot;https://codesandbox.io/s/rendering-lists-with-react-2y0v8&quot;&gt;CodeSandbox Project&lt;/a&gt; or give it a shot in your next side-project and let me know what you find!&lt;/p&gt;&lt;p&gt;I hope the examples above help to illustrate the value of a generic list rendering component and the power of React as a tool to standardize UI patterns irrespective of DOM elements rendered on screen. &lt;/p&gt;&lt;p&gt;For more information, be sure to check out &lt;a href=&quot;https://ant.design/components/list/&quot;&gt;Ant Design&amp;#x27;s List&lt;/a&gt; and &lt;a href=&quot;https://reactnative.dev/docs/flatlist&quot;&gt;React Native&amp;#x27;s FlatList&lt;/a&gt; excellent examples too.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Mailchimp Subscriber Interest Groups with V3 API]]></title><description><![CDATA[How to find then use interest group IDs to refine list campaign targeting and boost engagement. Examples provided for Mailchimp V3 PHP and Node APIs.]]></description><link>https://danny.codes/blog/mailchimp-api-interests/</link><guid isPermaLink="false">https://danny.codes/blog/mailchimp-api-interests/</guid><pubDate>Thu, 22 Aug 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A client recently asked me to update their PHP Mailchimp integration to support targeting subscribers based on interests. To my surprise, locating the interest ID was more involved than updating the existing API requests. &lt;/p&gt;&lt;p&gt;Before we get started, we&amp;#x27;ll need an API key which we&amp;#x27;ll use to access Mailchimp&amp;#x27;s API playground and authenticate API calls. If you already have an API key, skip ahead to the &lt;a href=&quot;#finding-the-interest-id&quot;&gt;Finding the Interest ID&lt;/a&gt; section below.&lt;/p&gt;&lt;h2 id=&quot;creating-an-api-key&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#creating-an-api-key&quot; aria-label=&quot;creating an api key permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Creating an API Key&lt;/h2&gt;&lt;p&gt;In order to create new users using we&amp;#x27;ll first need to create an API key with access to our account. Sign into Mailchimp then go to the &lt;a href=&quot;https://us4.admin.mailchimp.com/account/api/&quot;&gt;Account Settings &amp;gt; Extras &amp;gt; API Keys&lt;/a&gt; page then &lt;strong&gt;Create a Key&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:73.4447539461467%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAABuUlEQVQ4y6VU207cMBD1r/arKsQntOUV8RmwD32ohJBYdnNfx3Hi3E/nTLKwq0IpxdLRjO3xeHzmJGaz2aAsS/Rdh2Ho0bat2AGdzgf0fa+omwYcN7/u8eXqGj+3W7h0r+uMU0icoTPjZYzjiGmanu20WsaN44BGLrKSpJPD82ncCtO2AbYokKcJSmvx2WEq7xFnGaI0RSyo61rgUVWV2iDV0G9kvQwBvVTx14RegraSMHUOiXAZpGImapoaITCZA19R+wqVgHxy7tVfeGc81+ibRiaPD/dw9iCkthrknF2qk4SHIl/WSgtrC/V5SZrEut/LmTzP9AJNuLm7xeXFV/z4/k0PxNEOSbyTA3shefwQf2yaoTyOXFEyQSgIqxQ45nl+tq/5pzF8hWETMmlGkedS3R5xHEmFkSbXYGnCMcFycD6bE9PaKNKnstk9PeJwyJVY8tK2jdj6DFzz3qEoslf3l5gAwxv4dvL1cuukOBXsgnGNnd6EodKPXwElwc9nmY//J2wvot3vnoS3WDqbIBIeU+GUUpjmP/l6D4bk8+fgRNiEClmq9B3RadLTTr5b4Vsb553994S/ATR5kx5js6TrAAAAAElFTkSuQmCC&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Mailchimp API Playground: Enter API token&quot; title=&quot;Mailchimp API Playground: Enter API token&quot; src=&quot;/static/0f1c4b10290c02772e24074dbf342814/c1b63/create-api-key.png&quot; srcSet=&quot;/static/0f1c4b10290c02772e24074dbf342814/5a46d/create-api-key.png 300w,/static/0f1c4b10290c02772e24074dbf342814/0a47e/create-api-key.png 600w,/static/0f1c4b10290c02772e24074dbf342814/c1b63/create-api-key.png 1200w,/static/0f1c4b10290c02772e24074dbf342814/d61c2/create-api-key.png 1800w,/static/0f1c4b10290c02772e24074dbf342814/b5c21/create-api-key.png 2154w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;h2 id=&quot;finding-the-interest-id&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#finding-the-interest-id&quot; aria-label=&quot;finding the interest id permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Finding the Interest ID&lt;/h2&gt;&lt;p&gt;Now that we have an API key, we need to find the interest ID. To do that, we&amp;#x27;ll need to use the &lt;a href=&quot;https://us1.api.mailchimp.com/playground/&quot;&gt;Mailchimp API Playground&lt;/a&gt;. Using the API Key we just created, log into the API Playground and follow the steps below to navigate through the ensuing screens to find the desired interest.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:71.84643510054845%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAABbElEQVQ4y51Ti5KDIAz0/z+t39A6fdn6ALVVAUXYS7hyY3tO78HMTiLgumuSZLPZ4HzOUFUV6rpGlmXI8xxXQlEUAUIINE2D2+0WwDnfXUMyjiOstZjnGc45TNMU8pZeTNMUx+ORPngO8XA44HQ6oes68OL7r0iwWN77EJVSqMoS+fWKkiIrZETFUsogYm0lTLIEr15pFHULUTdo2yYoGoYBPaPvA9hFFLHEukKtUckagpRIKVCyKlIYSInc0Hm8uxQSFC7J4gH/R0P/diJbjvYcbTv/3d6SaJUQTEq5vHfIhYRo7+j0iLs26M0EZWeY2aEdFHJy8KPCGGdKLUVLskYiYEwMqiLvGSJW5OAt4bMF/0DYfMZ/LL9W/ZVkefZW4dpXuZkvl0voR56SPymMF7jHuHG9d9jv90h3O2y32zCWPA1xsn7dNvwCt42i6loqhjaGnmkk3Wcex/VPlrnvzKPCmqqqJ/tVcf/G8gcqH0j6CrPw3QAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Mailchimp API Playground: Enter API token&quot; title=&quot;Mailchimp API Playground: Enter API token&quot; src=&quot;/static/52d67953b2424d6210ff29709f800efb/c1b63/api-playground.png&quot; srcSet=&quot;/static/52d67953b2424d6210ff29709f800efb/5a46d/api-playground.png 300w,/static/52d67953b2424d6210ff29709f800efb/0a47e/api-playground.png 600w,/static/52d67953b2424d6210ff29709f800efb/c1b63/api-playground.png 1200w,/static/52d67953b2424d6210ff29709f800efb/d61c2/api-playground.png 1800w,/static/52d67953b2424d6210ff29709f800efb/ade5e/api-playground.png 2188w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;figcaption&gt;Sign in to your account, it should look something like this.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;First, select &lt;code&gt;Lists&lt;/code&gt; then use the Subresources dropdown to select &lt;code&gt;interest-categories&lt;/code&gt;. Find the relevant category in the list then use the Subresources dropdown again to access &lt;code&gt;interests&lt;/code&gt;. You should now be presented with the list of relevant interests, click the name of the one we want an ID for and you should arrive on the screen above with all the information we&amp;#x27;ll need.&lt;/p&gt;&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1200px&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom:71.5587967183227%;position:relative;bottom:0;left:0;background-image:url(&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAABKklEQVQ4y61R2Y6DMAzM/38cH1FRtVyF3AmX12NoH1Cr3RWNNLITJ5PxWBVFQffbjfq+J2sM1VVFXdtS0zTU7ugfD7LWSh3QDHNAxe/quiblU6Y8zZTnmRLHkEfSMXPMZLnmeJ+kvpAfJwqcH9e6ri8o6zwZ/t0YhnNbzmchRtmjbvdz6z0N2lAMgTznQOR7gffzvH2ktB6krRu3jTYhvaru1HLbZVmKBWgF+Va/73cqul6vdLlc5O2TVNGJBV+7rhMPMQNEJlzhgvT/yZdPGIaBHjwwEGmtxQLlYDQPIKckfiSOkL8sy6+E75byMZEPUcgA+DBN058UAsePlXNW5EIV1P2n5XdgQideQNUZohchDAXhU91pQoweLSN+Q6UQQuE4jt9ReJbgiB8ol0kgWKT1GgAAAABJRU5ErkJggg==&amp;#x27;);background-size:cover;display:block&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Mailchimp API Playground: Interest detail page&quot; title=&quot;Mailchimp API Playground: Interest detail page&quot; src=&quot;/static/b3e2f3ade3ea14d78f5a7e53f58a2585/c1b63/interest-id-page.png&quot; srcSet=&quot;/static/b3e2f3ade3ea14d78f5a7e53f58a2585/5a46d/interest-id-page.png 300w,/static/b3e2f3ade3ea14d78f5a7e53f58a2585/0a47e/interest-id-page.png 600w,/static/b3e2f3ade3ea14d78f5a7e53f58a2585/c1b63/interest-id-page.png 1200w,/static/b3e2f3ade3ea14d78f5a7e53f58a2585/d61c2/interest-id-page.png 1800w,/static/b3e2f3ade3ea14d78f5a7e53f58a2585/b5c8e/interest-id-page.png 2194w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0&quot; loading=&quot;lazy&quot;/&gt;
    &lt;/span&gt;&lt;/p&gt;&lt;h2 id=&quot;usage-in-api-requests&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#usage-in-api-requests&quot; aria-label=&quot;usage in api requests permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Usage in API Requests&lt;/h2&gt;&lt;h3 id=&quot;php-example&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#php-example&quot; aria-label=&quot;php example permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;PHP Example&lt;/h3&gt;&lt;p&gt;Download this &lt;a href=&quot;https://github.com/drewm/mailchimp-api/blob/master/src/MailChimp.php&quot;&gt;Mailchimp API wrapper&lt;/a&gt; class, and save it next to our script so it can be found by the &lt;code&gt;include(./Mailchimp.php)&lt;/code&gt; call on line 7 below.&lt;/p&gt;&lt;p&gt;Next, we need the API Token created in the first section, as well as the List ID and Interest ID from the second. Replace &lt;code&gt;API_TOKEN&lt;/code&gt;, &lt;code&gt;LIST_ID&lt;/code&gt;, and &lt;code&gt;INTEREST_ID&lt;/code&gt; in the constant definitions on lines 2–4.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Define API config constants
define(&amp;#x27;MAILCHIMP_API&amp;#x27;, &amp;#x27;API_TOKEN&amp;#x27;);
define(&amp;#x27;MAILCHIMP_LIST&amp;#x27;, &amp;#x27;LIST_ID&amp;#x27;);
define(&amp;#x27;MAILCHIMP_INTEREST&amp;#x27;, &amp;#x27;INTEREST_ID&amp;#x27;);

// Include the API Wrapper Class
include(&amp;#x27;./Mailchimp.php&amp;#x27;);

use \DrewM\Mailchimp\Mailchimp;

// Instantiate class
$Mailchimp = new Mailchimp(MAILCHIMP_API);

// grab email address from request
$email = $_REQUEST[&amp;#x27;email&amp;#x27;];
$result = &amp;#x27;&amp;#x27;;

// if there&amp;#x27;s an address make the request to mailchimp, store the response
if ($email != &amp;#x27;&amp;#x27;) {
  $result = $Mailchimp-&amp;gt;post(&amp;quot;lists/&amp;quot; . MAILCHIMP_LIST . &amp;quot;/members&amp;quot;, array(
    &amp;#x27;email_address&amp;#x27; =&amp;gt; $email,
    &amp;#x27;status&amp;#x27; =&amp;gt; &amp;#x27;subscribed&amp;#x27;,
    &amp;#x27;interests&amp;#x27; =&amp;gt; array(MAILCHIMP_INTEREST =&amp;gt; true),
  ));
}

// return status
echo json_encode($result);
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;nodejs-example&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#nodejs-example&quot; aria-label=&quot;nodejs example permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Node.js Example&lt;/h3&gt;&lt;p&gt;Since writing the original version of this post, I was asked to add the same functionality to a Gatsby site hosted on Netlify. After obtaining necessary API information described above, see below for a JavaScript example or &lt;a href=&quot;https://github.com/xdmorgan/netlify-functions/tree/master/functions/mailchimp-subscribe&quot;&gt;see here&lt;/a&gt; for a ready-to-use Node serverless function.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Node SDK
const Mailchimp = require(&amp;#x27;mailchimp-api-v3&amp;#x27;)

// Create MC API instance
const mc_api = new Mailchimp(MAILCHIMP_API_KEY)

// Call subscribe func
const response = await create_subscriber({
  email: &amp;#x27;example@email.com&amp;#x27;, 
  list_id: LIST_ID, 
  status: &amp;#x27;subscribed&amp;#x27;, 
  interests: [INTEREST_ID]
});

// Create new subscriber
async function create_subscriber({ 
  email, 
  list_id, 
  status, 
  interests, 
  merge_fields = {} 
}, api) {
  const path = `/lists/${list_id}/members`
  let [statusCode, error] = [null, null]

  try {
    await api.post({ 
      path, 
      body: {
        email_address: email,
        status,
        merge_fields,
        ...format_interests(interests),
      } 
    })
  } catch (e) {
    statusCode = e.status
    error = e.message
  }

  return {
    created: !error,
    error_response: { statusCode, error },
    success_response: {
      email,
      subscribed: true,
      existing_member: false,
      profile_updated: false,
    },
  }
}

// convert from String[] to interest object of `{ [id]: status }` shape
function format_interests(arr = []) {
  return { interests: arr.reduce((acc, cur) =&amp;gt; ({ ...acc, [cur]: true }), {}) }
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;references--resources&quot; style=&quot;position:relative&quot;&gt;&lt;a href=&quot;#references--resources&quot; aria-label=&quot;references  resources permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;References &amp;amp; Resources&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/drewm/mailchimp-api&quot;&gt;Mailchimp API PHP Class&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/37311116/can-you-get-a-mailchimp-interest-group-id-without-using-the-api&quot;&gt;StackOverflow: Finding Mailchimp List IDs&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content:encoded></item></channel></rss>